diff --git a/src/feature/auth/application/auth-query.ts b/src/feature/auth/application/auth-query.ts index c5f1f1c..784a1c9 100644 --- a/src/feature/auth/application/auth-query.ts +++ b/src/feature/auth/application/auth-query.ts @@ -1,6 +1,7 @@ import { useMutation } from '@tanstack/react-query'; import { useGuardContext } from '@/feature/shared/context/use-gaurd-context'; import { UsecaseContext } from '@/feature/shared/context/usecase-context'; +import { createErrorMessage } from '@/feature/shared/error/create-error-message'; import { useRouteNavigation } from '@/routes/use-route-navigation'; import type { UserRole } from '../domain/user-role'; @@ -26,12 +27,14 @@ export const useSignIn = ({ onSuccess: (response) => { if (response.type === 'success') { toMain(); - } else { - if (response.code === 'AUTH_002' || response.code === 'GEN_004') { - setResponseMessage('아이디 또는 비밀번호가 일치하지 않습니다.'); - return; - } + return; } + setResponseMessage( + createErrorMessage( + response.code, + '로그인에 실패했습니다. 다시 시도해 주세요.' + ) + ); }, }); @@ -67,13 +70,14 @@ export const useSignUp = ({ onSuccess: (response) => { if (response.type === 'success') { toMain(); - } else { - if (response.code === 'USER_003') { - setResponseMessage('이미 존재하는 이메일입니다.'); - return; - } - setResponseMessage('회원가입에 실패했습니다. 다시 시도해주세요.'); + return; } + setResponseMessage( + createErrorMessage( + response.code, + '회원가입에 실패했습니다. 다시 시도해 주세요.' + ) + ); }, }); diff --git a/src/feature/shared/error/create-error-message.ts b/src/feature/shared/error/create-error-message.ts new file mode 100644 index 0000000..2c88284 --- /dev/null +++ b/src/feature/shared/error/create-error-message.ts @@ -0,0 +1,43 @@ +const ERROR_MESSAGES: Record = { + // Auth + AUTH_001: '인증 헤더가 없거나 형식이 올바르지 않습니다.', + AUTH_002: '이메일 또는 비밀번호가 일치하지 않습니다.', + AUTH_003: '리프레시 토큰이 유효하지 않습니다.', + AUTH_004: '액세스 토큰이 유효하지 않습니다.', + AUTH_005: '토큰 정보가 일치하지 않습니다.', + AUTH_006: 'OAuth 인증에 실패했습니다.', + AUTH_007: '이 작업을 수행할 권한이 없습니다.', + // User + USER_001: '이미 사용 중인 이메일입니다.', + USER_002: '이미 사용 중인 아이디입니다.', + USER_004: '사용자를 찾을 수 없습니다.', + USER_006: '이메일 인증 코드가 유효하지 않거나 만료되었습니다.', + USER_007: '이메일 인증에 실패했습니다.', + // General + GEN_002: '지원하지 않는 기능입니다.', + GEN_003: '잘못된 요청입니다.', + GEN_004: '필수 항목을 모두 입력해 주세요.', + GEN_005: '요청 형식이 올바르지 않습니다.', + // Store + STORE_001: '가게를 찾을 수 없습니다.', + // Event + EVENT_001: '이벤트를 찾을 수 없습니다.', + // Application + APPLICATION_001: '이 신청에 접근할 권한이 없습니다.', + APPLICATION_002: '신청 내역을 찾을 수 없습니다.', + APPLICATION_003: '이미 제출된 신청입니다.', + // Deposit + DEPOSIT_001: '예치금이 부족합니다.', + DEPOSIT_002: '예치금 정보를 찾을 수 없습니다.', + // S3 + S3_001: '파일 업로드 URL 생성에 실패했습니다.', +}; + +const FALLBACK_MESSAGE = '오류가 발생했습니다. 다시 시도해 주세요.'; + +export const createErrorMessage = ( + code: string, + fallback = FALLBACK_MESSAGE +): string => { + return ERROR_MESSAGES[code] ?? fallback; +}; diff --git a/src/mocks/application/resolvers.ts b/src/mocks/application/resolvers.ts index 2947606..5491c63 100644 --- a/src/mocks/application/resolvers.ts +++ b/src/mocks/application/resolvers.ts @@ -38,15 +38,30 @@ export const applicationResolver: ApplicationResolver = { const { eventId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const event = mockEvents.find((e) => e.id === eventId); if (!event) { - return HttpResponse.json({ message: 'Event not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'EVENT_001', message: 'Event not found' }, + { status: 404 } + ); } const url = new URL(request.url); @@ -75,28 +90,49 @@ export const applicationResolver: ApplicationResolver = { const { eventId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'REVIEWER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const event = mockEvents.find((e) => e.id === eventId); if (!event) { - return HttpResponse.json({ message: 'Event not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'EVENT_001', message: 'Event not found' }, + { status: 404 } + ); } // 이미 마감된 이벤트 if (!event.isActive) { return HttpResponse.json( - { message: 'Event already closed' }, + { code: 'GEN_003', message: 'Event is already closed' }, { status: 400 } ); } // 이미 신청한 이벤트 → ALREADY_APPLIED_EVENT_ID 사용 if (eventId === ALREADY_APPLIED_EVENT_ID) { - return HttpResponse.json({ message: 'Already applied' }, { status: 409 }); + return HttpResponse.json( + { + code: 'APPLICATION_003', + message: 'You have already applied for this event', + }, + { status: 409 } + ); } return HttpResponse.json(null, { status: 200 }); @@ -106,10 +142,22 @@ export const applicationResolver: ApplicationResolver = { getMyApplications: ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'REVIEWER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } return HttpResponse.json({ @@ -126,20 +174,32 @@ export const applicationResolver: ApplicationResolver = { const { applicationId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } const app = mockApplications.find((a) => a.id === applicationId); if (!app) { return HttpResponse.json( - { message: 'Application not found' }, + { code: 'APPLICATION_002', message: 'Application not found' }, { status: 404 } ); } // 해당 신청의 신청자가 아닌 경우 시뮬레이션: OWNER가 접근하면 403 if (role === 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'APPLICATION_001', + message: 'You are not allowed to access this application', + }, + { status: 403 } + ); } return HttpResponse.json(null, { status: 200 }); @@ -150,13 +210,19 @@ export const applicationResolver: ApplicationResolver = { const { applicationId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } const app = mockApplications.find((a) => a.id === applicationId); if (!app) { return HttpResponse.json( - { message: 'Application not found' }, + { code: 'APPLICATION_002', message: 'Application not found' }, { status: 404 } ); } @@ -164,14 +230,23 @@ export const applicationResolver: ApplicationResolver = { // 이미 인증을 제출한 신청 → ALREADY_SUBMITTED_APP_ID 사용 if (applicationId === ALREADY_SUBMITTED_APP_ID) { return HttpResponse.json( - { message: 'Review already submitted' }, + { + code: 'APPLICATION_003', + message: 'Review has already been submitted for this application', + }, { status: 409 } ); } // 해당 신청의 신청자가 아닌 경우: OWNER가 접근하면 403 if (role === 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'APPLICATION_001', + message: 'You are not allowed to access this application', + }, + { status: 403 } + ); } return HttpResponse.json(null, { status: 200 }); @@ -182,16 +257,28 @@ export const applicationResolver: ApplicationResolver = { const { applicationId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const app = mockApplications.find((a) => a.id === applicationId); if (!app) { return HttpResponse.json( - { message: 'Application not found' }, + { code: 'APPLICATION_002', message: 'Application not found' }, { status: 404 } ); } diff --git a/src/mocks/auth/resolvers.ts b/src/mocks/auth/resolvers.ts index a1bf214..9df21da 100644 --- a/src/mocks/auth/resolvers.ts +++ b/src/mocks/auth/resolvers.ts @@ -13,7 +13,7 @@ export const authResolver: AuthResolver = { if (!body.role || !body.username || !body.email || !body.password) { return HttpResponse.json( - { message: 'Required field missing' }, + { code: 'GEN_004', message: 'Required fields are missing or invalid' }, { status: 400 } ); } @@ -21,7 +21,7 @@ export const authResolver: AuthResolver = { // 중복 이메일 if (body.email === 'duplicate@example.com') { return HttpResponse.json( - { message: 'Email already exists' }, + { code: 'USER_001', message: 'This email is already registered' }, { status: 409 } ); } @@ -29,7 +29,10 @@ export const authResolver: AuthResolver = { // 리뷰어: 이메일 인증 코드 만료 시뮬레이션 (email에 +fail 포함) if (body.role === 'REVIEWER' && body.email.includes('+fail')) { return HttpResponse.json( - { message: 'Email verification code expired or invalid' }, + { + code: 'USER_006', + message: 'Email verification code is invalid or expired', + }, { status: 400 } ); } @@ -37,7 +40,7 @@ export const authResolver: AuthResolver = { // 사장님: 사전 설정된 비밀번호 불일치 if (body.role === 'OWNER' && body.password === 'wrong-owner-secret') { return HttpResponse.json( - { message: 'Owner password mismatch' }, + { code: 'AUTH_002', message: 'Invalid credentials' }, { status: 401 } ); } @@ -51,7 +54,7 @@ export const authResolver: AuthResolver = { if (!body.role || !body.mail || !body.password) { return HttpResponse.json( - { message: 'Required field missing' }, + { code: 'GEN_004', message: 'Required fields are missing or invalid' }, { status: 400 } ); } @@ -62,7 +65,7 @@ export const authResolver: AuthResolver = { body.password === 'wrongpassword' ) { return HttpResponse.json( - { message: 'Invalid credentials' }, + { code: 'AUTH_002', message: 'Invalid credentials' }, { status: 401 } ); } diff --git a/src/mocks/deposit/resolvers.ts b/src/mocks/deposit/resolvers.ts index 34ca9aa..9ed975d 100644 --- a/src/mocks/deposit/resolvers.ts +++ b/src/mocks/deposit/resolvers.ts @@ -13,16 +13,28 @@ export const depositResolver: DepositResolver = { getDeposit: ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } // 예치금 정보가 없는 사장님 시뮬레이션 (NO_DEPOSIT_OWNER_TOKEN 사용) if (request.headers.get('Authorization') === NO_DEPOSIT_OWNER_TOKEN) { return HttpResponse.json( - { message: 'Deposit not found' }, + { code: 'DEPOSIT_002', message: 'Deposit record not found' }, { status: 404 } ); } @@ -33,15 +45,30 @@ export const depositResolver: DepositResolver = { chargeDeposit: async ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const body = (await request.json()) as DepositRequest; if (body.amount == null || body.amount <= 0) { - return HttpResponse.json({ message: 'Invalid amount' }, { status: 400 }); + return HttpResponse.json( + { code: 'GEN_003', message: 'Amount must be a positive number' }, + { status: 400 } + ); } return HttpResponse.json( diff --git a/src/mocks/event/resolvers.ts b/src/mocks/event/resolvers.ts index ae068e6..0d049c4 100644 --- a/src/mocks/event/resolvers.ts +++ b/src/mocks/event/resolvers.ts @@ -19,16 +19,28 @@ export const eventResolver: EventResolver = { createEvent: async ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const body = (await request.json()) as EventCreateRequest; if (!body.title || !body.condition || body.reward == null) { return HttpResponse.json( - { message: 'Required field missing' }, + { code: 'GEN_004', message: 'Required fields are missing or invalid' }, { status: 400 } ); } @@ -36,7 +48,7 @@ export const eventResolver: EventResolver = { // reward >= INSUFFICIENT_DEPOSIT_REWARD → 예치금 부족 if (body.reward >= INSUFFICIENT_DEPOSIT_REWARD) { return HttpResponse.json( - { message: 'Insufficient deposit' }, + { code: 'DEPOSIT_001', message: 'Deposit balance is insufficient' }, { status: 400 } ); } @@ -56,10 +68,22 @@ export const eventResolver: EventResolver = { getOwnerEvents: ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } return HttpResponse.json({ events: mockEvents }); @@ -69,15 +93,30 @@ export const eventResolver: EventResolver = { const { eventId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const event = mockEvents.find((e) => e.id === eventId); if (!event) { - return HttpResponse.json({ message: 'Event not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'EVENT_001', message: 'Event not found' }, + { status: 404 } + ); } const body = (await request.json()) as EventPatchRequest; @@ -85,7 +124,7 @@ export const eventResolver: EventResolver = { // isActive: false로 이미 마감된 이벤트를 다시 마감하려 할 때 if (body.isActive === false && !event.isActive) { return HttpResponse.json( - { message: 'Event already closed' }, + { code: 'GEN_003', message: 'Event is already closed' }, { status: 400 } ); } @@ -97,15 +136,30 @@ export const eventResolver: EventResolver = { const { eventId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const event = mockEvents.find((e) => e.id === eventId); if (!event) { - return HttpResponse.json({ message: 'Event not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'EVENT_001', message: 'Event not found' }, + { status: 404 } + ); } return HttpResponse.json(null, { status: 200 }); diff --git a/src/mocks/s3/resolvers.ts b/src/mocks/s3/resolvers.ts index 6eae789..b28dcd9 100644 --- a/src/mocks/s3/resolvers.ts +++ b/src/mocks/s3/resolvers.ts @@ -12,13 +12,19 @@ export const s3Resolver: S3Resolver = { getPresignedUrl: async ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } const body = (await request.json()) as S3UploadRequest; if (!body.fileName || !body.fileType) { return HttpResponse.json( - { message: 'Required field missing' }, + { code: 'GEN_004', message: 'Required fields are missing or invalid' }, { status: 400 } ); } diff --git a/src/mocks/store/resolvers.ts b/src/mocks/store/resolvers.ts index 9f9b367..1b4011d 100644 --- a/src/mocks/store/resolvers.ts +++ b/src/mocks/store/resolvers.ts @@ -29,16 +29,28 @@ export const storeResolver: StoreResolver = { createStore: async ({ request }) => { const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const body = (await request.json()) as StoreCreateRequest; if (!body.name || !body.address || !body.category) { return HttpResponse.json( - { message: 'Required field missing' }, + { code: 'GEN_004', message: 'Required fields are missing or invalid' }, { status: 400 } ); } @@ -80,15 +92,30 @@ export const storeResolver: StoreResolver = { const { storeId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const store = mockStores.find((s) => s.id === storeId); if (!store) { - return HttpResponse.json({ message: 'Store not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'STORE_001', message: 'Store not found' }, + { status: 404 } + ); } const body = (await request.json()) as StorePatchRequest; @@ -99,15 +126,30 @@ export const storeResolver: StoreResolver = { const { storeId } = params; const role = getRole(request); if (!role) { - return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 }); + return HttpResponse.json( + { + code: 'AUTH_001', + message: 'Authorization header is missing or malformed', + }, + { status: 401 } + ); } if (role !== 'OWNER') { - return HttpResponse.json({ message: 'Forbidden' }, { status: 403 }); + return HttpResponse.json( + { + code: 'AUTH_007', + message: 'You do not have permission to perform this action', + }, + { status: 401 } + ); } const store = mockStores.find((s) => s.id === storeId); if (!store) { - return HttpResponse.json({ message: 'Store not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'STORE_001', message: 'Store not found' }, + { status: 404 } + ); } return HttpResponse.json(null, { status: 200 }); @@ -117,7 +159,10 @@ export const storeResolver: StoreResolver = { const { storeId } = params; const store = mockStores.find((s) => s.id === storeId); if (!store) { - return HttpResponse.json({ message: 'Store not found' }, { status: 404 }); + return HttpResponse.json( + { code: 'STORE_001', message: 'Store not found' }, + { status: 404 } + ); } return HttpResponse.json({ events: store.events });