Error Codes (전체 카탈로그)
SPEC #003 §2-9 표준 ErrorResponse. backend GlobalExceptionHandler.
ErrorResponse 구조
{
"success": false,
"error": {
"code": "AUTH_INVALID_CREDENTIALS",
"message": "이메일 또는 비밀번호가 일치하지 않습니다.",
"details": [
{ "field": "email", "code": "INVALID_FORMAT", "message": "..." }
]
}
}details[] 는 validation 실패에서만.
Auth 에러 (status 401)
| Code | 의미 |
|---|---|
AUTH_INVALID_CREDENTIALS | 이메일 / 비밀번호 불일치 |
AUTH_ACCOUNT_NOT_FOUND | 계정 없음 (보안상 INVALID_CREDENTIALS 와 동일 메시지) |
AUTH_ACCOUNT_SUSPENDED | 정지된 계정 (#003 후속) |
AUTH_REFRESH_EXPIRED | refresh token 만료 |
AUTH_REFRESH_REVOKED | refresh token 회수됨 (rotation race) |
AUTH_TOKEN_INVALID | JWT 형식 / 서명 불일치 |
AUTH_TOKEN_EXPIRED | access 만료 (BFF refresh 트리거) |
업로드 티켓(SPEC #177 · status 401) —
UPLOAD_TICKET_INVALID:POST /api/v1/uploads/files의X-Upload-Ticket이 무효·만료(기본 10분)·위조. 이 경로는 permitAll 이고 티켓이 곧 인증이라 세션 401 과 의미가 다르다(로그아웃 유발 금지 — 재시도 시 새 티켓을 발급받으면 된다). FE 매핑 “업로드 세션이 만료되었습니다. 다시 시도해 주세요.”(음원·CM·CS 티켓 첨부 공통).
Account & Auth Emails (status 400) — SPEC #145
| Code | 의미 |
|---|---|
ACCOUNT_TOKEN_INVALID | 계정 설정·비밀번호 재설정·이메일 변경 토큰 무효·만료·이미 사용됨 |
EMAIL_ALREADY_IN_USE | 이메일 변경 요청/전환 시 새 이메일이 다른 계정에서 사용 중 |
비밀번호 재설정 요청(
password-reset/request)은 계정 존재 여부와 무관하게 동일 응답(존재 은닉 — 계정 열거 방지).
Impersonation (status 401 / 403)
| Code | 의미 |
|---|---|
INDEPENDENT_HQ_IMPERSONATION_FORBIDDEN (403) | INDEPENDENT 본사 대상 — 발급(POST /admin/hq/{id}/impersonate) 단계 차단 |
AUTH_HQ_SUSPENDED (403) | SUSPENDED 본사 대상 — exchange 단계에서 HqSuspendedException 로 차단 |
EXCHANGE_TOKEN_NOT_FOUND (401) | 존재하지 않는·위변조된 교환 토큰 |
EXCHANGE_TOKEN_EXPIRED (401) | 교환 토큰 60s 초과 |
EXCHANGE_TOKEN_CONSUMED (401) | 교환 토큰 재사용 |
OPERATOR 외 호출은 도메인 코드가 아니라 Spring Security role 매처가 403 으로 차단한다(별도 코드 없음).
Validation (status 400)
| Code | 의미 |
|---|---|
VALIDATION_FAILED | request body / param validation 실패. details[] 에 field 별 |
INVALID_LEGAL_VERSION | 약관 version 이 활성 버전과 불일치 |
LEGAL_DOC_INVALID_EFFECTIVE_AT | 게시 effectiveAt 이 과거(>60s) — 발효 시각은 현재 이후여야 함 (#033). FE 게시 다이얼로그가 사전 거부 + 인라인 danger 매핑 |
INVALID_BUSINESS_NUMBER | 사업자번호 형식 오류 (#013) |
INVALID_ENUM_VALUE | enum 값 잘못 |
METADATA_PARSE_FAILED | 음원 업로드/교체 파일이 MP3 가 아니거나 ID3 태그가 손상돼 파싱 실패 (#041, uploadMusic·replaceMusicFile). 서버 메모리 ID3v2.4 재기록 단계에서 거부 |
MUSIC_INVALID_FIELD | 음원 업로드/교체 필드 위반 — title 누락·빈 파일·20MB 초과·musicSource 누락/오값 (#041·#059) |
LIBRARY_TYPE_MISMATCH | 라이브러리에 음원 추가 시 음원 musicSource(AI/TRUST) ≠ 라이브러리 libraryType (#059, addLibraryMusic). 전체 reject — 위반 musicId 를 error.fields.violatingMusicIds(콤마 구분)에 노출 |
RECORDING_UNSUPPORTED_FORMAT | (즉시방송 녹음 슬라이스) 점장 녹음 업로드(recordStoreBroadcast) 파일 MIME 이 허용 3종(audio/webm·audio/mp4·audio/mpeg) 밖. FE 매핑 “지원하지 않는 녹음 형식이에요. 브라우저를 최신으로 업데이트한 뒤 다시 녹음해주세요.” |
RECORDING_INVALID_FIELD | (즉시방송 녹음 슬라이스) 점장 녹음 업로드 필드 위반 — 빈 파일·10MB 초과·durationSeconds 1recordStoreBroadcast). FE 매핑 “녹음이 올바르지 않아요. (빈 파일·10MB 초과·길이 1 |
BROADCAST_TEMPLATE_LIMIT_EXCEEDED | (자주쓰는방송 슬라이스, status 409) 점장 템플릿 생성(createBroadcastTemplate) 시 매장당 활성 템플릿이 상한(20개)에 도달. FE 매핑 “템플릿은 최대 20개까지 저장할 수 있어요. 쓰지 않는 템플릿을 지운 뒤 다시 시도해주세요.” + 목록 total>=20 이면 [+ 새 템플릿] 사전 비활성 |
BROADCAST_TEMPLATE_NOT_FOUND | (자주쓰는방송 슬라이스, status 404) 점장 템플릿 수정(updateBroadcastTemplate)·삭제(deleteBroadcastTemplate) 대상이 본인 매장 활성 템플릿이 아님(미존재·삭제·재삭제·타 매장 — 소유 은닉). FE 매핑 “템플릿을 찾을 수 없어요. 이미 삭제됐을 수 있어요.” |
DISPATCH_INVALID_TARGET | (송출 슬라이스) 안내방송 송출(dispatchHqTtsAnnouncement) target=STORES 인데 storeIds 가 생략(null). 빈 배열은 minItems:1 검증 400(이 코드 아님)·타 본사/미존재 매장 혼입은 404 TTS_ANNOUNCEMENT_NOT_FOUND(전체 은닉). FE MVP 는 target: "ALL" 고정이라 방어적 매핑(“송출 대상이 올바르지 않습니다.”) |
TICKET_ATTACHMENT_INVALID_FIELD | (CS 티켓 첨부, BE PR #324) 첨부 업로드 파일 위반 — 빈 파일·10MB 초과. 4 채널 공통(uploadTicketAttachment·uploadHqTicketAttachment·uploadHqStoreTicketAttachment·uploadStoreTicketAttachment). FE 는 클라 사전 검증(크기·MIME)으로 대부분 미리 거르고, 남은 건 “파일을 확인해 주세요. 빈 파일이거나 10MB를 넘습니다.” 로 매핑 |
TICKET_ATTACHMENT_UNSUPPORTED_TYPE | (CS 티켓 첨부, BE PR #324) 허용 MIME 5종(image/png·image/jpeg·image/gif·image/webp·application/pdf) 밖. 매직바이트까지 검증하므로 확장자·선언 MIME 만 바꾼 파일도 이 코드로 거절된다(클라 accept·file.type 검사는 우회 가능한 보조 가드). FE 매핑 “지원하지 않는 형식입니다. 이미지(PNG·JPG·GIF·WEBP) 또는 PDF 만 올릴 수 있습니다.” |
Conflict (status 409)
| Code | 의미 |
|---|---|
DUPLICATE_EMAIL | 같은 이메일 OperatorAccount 존재 (409). SPEC #184 — 매장 생성 경로에서는 매장 생성도 함께 롤백된다(D9 — 반쪽 상태 금지). FE 는 “매장은 등록됨” 류 안내를 띄우지 말고 이메일 필드 에러로 재제출을 유도한다. 메시지는 입력 이메일을 담지 않는 고정 문구다(m6 — 임의 이메일의 409/201 관측으로 플랫폼 계정을 열거하지 못하게. 상세는 INFO 로그) |
HQ_INVALID_STATUS_TRANSITION | HqStatus 전이 불가 — 허용 외 출발 상태(이미 같은 상태 등). suspend/reactivate (#018) |
STORE_INVALID_STATUS_TRANSITION | StoreStatus 전이 불가 — 허용 외 출발 상태(같은 상태·역행). suspend(ACTIVE|INACTIVE→SUSPENDED)/reactivate(SUSPENDED→ACTIVE) (#037). FE 매장 상세 다이얼로그 인라인 매핑 |
STORE_ALREADY_CLOSED | 이미 폐점된 매장 재폐점 시도 — closed_at IS NULL 가드 affected 0 (#039). 폐점 매장 suspend/reactivate(WHERE AND closed_at IS NULL)도 동일 409. FE 폐점 다이얼로그 인라인 매핑(“이미 폐점된 매장입니다”) |
DISPATCH_SLOT_OCCUPIED | (점장 예약방송 슬라이스, #109) 점장 예약 송출(sendStoreBroadcast + scheduledAt) 시 같은 매장에 본사가 예약(SCHEDULED, source=HQ)한 동일 시각이 이미 존재 — store_id + status='SCHEDULED' + scheduled_at 일치 + announcement.source='HQ' row 존재면 409. 정확한 시각(Instant) 충돌만 차단(5분 슬롯 그리드·반복 차단은 본사 예약 슬롯 모델 후속 — 20-policy §3-3 풀구현은 별도 대형 SPEC). 점장발 SCHEDULED(자기 예약)·즉시 송출(scheduledAt 없음)은 검사 대상 아님. FE 매핑 “본사 방송이 이미 예약된 시각입니다. 다른 시각을 선택해 주세요.” |
DEVICE_LIMIT_EXCEEDED | (SPEC #178) 매장 기기 등록(registerStoreDevice) 시 그 매장의 활성 기기가 상한(4대 · StoreDevice.DEFAULT_DEVICE_LIMIT)에 도달. 이미 등록된 기기의 재등록(멱등 경로)은 상한 판정 대상이 아니다 — 신규 등록만 거절된다. 서버는 오래된 기기를 자동 회수하지 않는다(오프라인과 폐기를 구분할 수 없어 재생 중인 기기를 뺏는다) → 점장이 기기 목록에서 안 쓰는 기기를 회수한 뒤 재시도한다. FE(useStoreDevice)는 이 코드를 limitExceeded 로 올려 player 상단 warn 배너 + [기기 정리하기](기기 관리 모달)로 유도하고, 음악은 매장 단위로 정상 재생된다(기기별 재생목록만 못 씀) |
TICKET_INVALID_STATUS_TRANSITION | TicketStatus 전이 불가 — 상태머신 외 전이 시도. PATCH /tickets/{id}/status (#027). FE 클라 가드로 1차 차단 |
TICKET_ATTACHMENT_LIMIT_EXCEEDED | (CS 티켓 첨부, BE PR #324) 티켓당 첨부 상한(5개) 초과. FE 는 목록이 5개면 [파일 첨부] 컨트롤을 미리 잠그고(불필요한 왕복 차단), race 로 뚫린 경우 “첨부는 문의당 5개까지 올릴 수 있습니다.” 로 매핑 |
MUSIC_TAG_OPTION_DUPLICATE | (#132) 같은 타입(GENRE/MOOD) 내 동일 value 중복 — music_tag_option unique(type,value) 위반. createMusicTagOption·updateMusicTagOption(value 변경 시). FE /settings/music-options 추가/수정 폼 inline 매핑(“이미 같은 값의 옵션이 있습니다”) |
STORE_NAME_DUPLICATE | 같은 본사 산하 동일 매장명 (정책 후속) |
CONSENT_DUPLICATE_VERSION | 같은 version 중복 동의 시도 |
TERMS_ACTIVE_RACE | partial unique race (활성 약관 중복 INSERT) — #033 partial-unique 드롭으로 deprecated, version-unique race 로 대체 |
LEGAL_DOC_DUPLICATE_VERSION | 동일 version 약관/개인정보 중복 게시 (uq_*_version). FE 게시 다이얼로그 인라인 매핑 |
LEGAL_DOC_ACTIVATION_CONFLICT | 동시 게시 race — 재시도 안내 |
LEGAL_DOC_NOT_SCHEDULED | 예약 취소(DELETE /admin/{terms,privacy-policy}/{id}, #038) 대상이 SCHEDULED·미발효(effective_at > now)가 아님 — 이미 발효(ACTIVE)·대체(SUPERSEDED)되어 취소 불가. FE 인라인 danger 매핑(“이미 발효되었거나 대체되어 취소할 수 없습니다”) |
Authorization (status 403)
| Code | 의미 |
|---|---|
FORBIDDEN | role / scope 부족 |
ACCESS_DENIED | Spring Security 의 AccessDeniedException |
INDEPENDENT_HQ_SUSPENSION_FORBIDDEN | INDEPENDENT 가상 본사 정지/복구 시도 — 전이 불가 (#018) |
AUTH_STORE_SUSPENDED | 소속 매장이 SUSPENDED 인 STORE_MANAGER 의 인증 차단 (login·refresh·impersonation exchange). HQ AUTH_HQ_SUSPENDED 미러 (#037) |
AUTH_STORE_CLOSED | 소속 매장이 폐점(closed_at != null)된 STORE_MANAGER 의 인증 영구 차단 (login·refresh). AuthService.verifyStoreNotSuspended 확장 — 세션 revoke 만으론 access TTL 내 잔여 노출이 남아 인증단에서도 차단 (#039) |
PASSWORD_CHANGE_REQUIRED | passwordMustChange=true 인 계정이 임시 비번을 변경하기 전 보호 endpoint 호출 (#022). PasswordChangeEnforcementFilter 가 JWT 인증 직후 차단. allowlist (허용·미차단): POST /api/v1/auth/change-password · GET /api/v1/auth/me · POST /api/v1/auth/logout · POST /api/v1/auth/refresh (+ OPTIONS preflight). access token claim pmc 로 판정(요청당 DB 조회 0). 임퍼소네이션 토큰은 pmc=false 강제 → 무영향 |
UPLOAD_PURPOSE_FORBIDDEN | (SPEC #177) 업로드 티켓 발급 시 role↔purpose 불일치 — OPERATOR 는 MUSIC_*·TICKET_ATTACHMENT_ADMIN, HQ_MANAGER 는 COMMERCIAL_*·TICKET_ATTACHMENT_HQ·TICKET_ATTACHMENT_HQ_STORE, STORE_MANAGER 는 TICKET_ATTACHMENT_STORE 만 허용. 첨부 화면 FE 매핑 “첨부를 올릴 권한이 없습니다.” |
IMPERSONATION_FORBIDDEN_ACTION | (SPEC #150) 임퍼소네이션 세션(impersonatedBy claim 보유)으로 계정 주인 본인만 수행해야 하는 민감 작업 호출 — 비밀번호 변경(POST /api/v1/auth/change-password)·이메일 변경 요청. 운영사가 본사 신원을 대신 쓰는 동안 자격증명 자체를 바꾸지 못하게 차단. 일반(비임퍼소네이션) 세션은 무영향 |
Server / Network (status 5xx · 502)
| Code | 의미 |
|---|---|
INTERNAL_ERROR | 처리 안 된 예외 (production 메시지 마스킹) |
BACKEND_ERROR | (BFF only) backend 5xx — session 유지 |
BACKEND_UNREACHABLE | (BFF only) 네트워크 도달 실패 |
TTS_SYNTHESIS_FAILED | (#061, status 502) Typecast TTS 합성 외부 실패(타임아웃·polling 실패·다운로드 실패 등). 외부 장애를 5xx 로 격리하지 않고 502 로 매핑(createHqTtsAnnouncement·점장 previewStoreBroadcast·#134 runTtsSmokeTest). FE 매핑 “합성에 실패했습니다. 잠시 후 다시 시도해주세요.”(smoke-test 는 Typecast 연동 상태 확인 안내) |
UPLOAD_NOT_CONFIGURED | (SPEC #177, status 503) UPLOAD_TICKET_SECRET env 미설정 상태에서 업로드 티켓 발급·검증(fail-lazy — 부팅·다른 기능은 정상). 증상이 “큰 파일만 실패” 라 일반 5xx 문구로 뭉개면 원인 추적이 불가능하다. CS 첨부 FE 매핑 “큰 파일 업로드가 아직 준비되지 않았습니다(서버 설정 필요). 4MB 이하 파일은 지금도 올릴 수 있습니다.” — 지금 할 수 있는 일까지 안내한다 |
TTS_TOKEN_NOT_CONFIGURED | (#061, status 503) TYPECAST_API_TOKEN env 미설정 상태에서 합성 호출. 부팅은 성공(StorageProperties 패턴)하나 합성 시 503. FE 매핑 “TTS 가 설정되지 않았습니다. 관리자에게 문의해주세요.”(#134 smoke-test 는 Render env TYPECAST_API_TOKEN 확인 으로 운영자 안내) |
Not Found (status 404)
| Code | 의미 |
|---|---|
HQ_NOT_FOUND | hqId 존재 X |
STORE_NOT_FOUND | storeId 존재 X |
TERMS_NOT_PUBLISHED | 현재 유효본 TermsDocument 없음 (effective_at <= now 없음) |
PRIVACY_NOT_PUBLISHED | 현재 유효본 PrivacyPolicy 없음 |
LEGAL_DOC_NOT_FOUND | by-id 약관/개인정보 게시본 미존재 (GET·DELETE /admin/{terms,privacy-policy}/{id}, #033·#038). 예약 취소 시 FE 매핑 “이미 삭제된 예약본입니다” |
ACCOUNT_NOT_FOUND | accountId 존재 X |
TICKET_NOT_FOUND | ticketId 존재 X (#027) — FE /tickets/{id} 에서 notFound(). 본사 #086(hq_id 불일치·미존재 은닉, 상세·댓글·상태/우선순위 변경) · 점장 #112(store_id 불일치·미존재 은닉, /store/support/{id} 상세·댓글) 도 동일 코드로 통일(타 본사/타 매장 티켓 id 존재 흘림 차단). 점장 FE 는 “문의를 찾을 수 없습니다.” inline Banner |
TICKET_ATTACHMENT_NOT_FOUND | (CS 티켓 첨부, BE PR #324) 첨부 다운로드 대상이 그 티켓의 첨부가 아님 — 미존재·타 티켓 첨부·스토리지 blob 누락을 모두 이 코드로 수렴(존재 은닉). FE 매핑 “첨부를 찾을 수 없습니다. 목록을 새로고침해 주세요.” |
MUSIC_NOT_FOUND | 음원 id 존재 X 또는 soft-deleted (#041·#042). 파일 교체(replaceMusicFile)·상세 조회(getMusic)·삭제(deleteMusic) 대상 미존재. #042 는 soft-deleted·미존재·재삭제를 모두 이 코드로 통일(존재 은닉) |
MUSIC_TAG_OPTION_NOT_FOUND | (#132) 음원 태그 옵션 id 미존재. updateMusicTagOption·deleteMusicTagOption 대상 부재. FE /settings/music-options inline 매핑(“대상 옵션을 찾을 수 없습니다. 목록을 새로고침해주세요”) |
TTS_ANNOUNCEMENT_NOT_FOUND | (#061) 안내방송 id 존재 X·soft-deleted·타 본사(hqId 불일치 — 존재 은닉). 상세(getHqTtsAnnouncement)·삭제(deleteHqTtsAnnouncement)·송출(dispatchHqTtsAnnouncement)·송출 이력(#065 listHqTtsAnnouncementDispatches, STORE_BROADCAST 출처도 포함) 대상 미존재. FE 삭제는 404 를 “이미 삭제됨” 으로 흡수, 송출·이력은 “삭제되었거나 존재하지 않는 안내방송입니다.” |
STORE_DEVICE_NOT_FOUND | (SPEC #178) 매장 기기 대상 작업(renameStoreDevice·revokeStoreDevice·getStoreDevicePlaylist·setStoreDevicePlaylist)의 기기가 본인 매장의 살아있는 기기가 아님 — 미존재·이미 회수(soft-delete)됨·타 매장 기기를 모두 이 코드로 수렴(존재 은닉). FE /store/devices 매핑 “이미 정리된 기기입니다. 목록을 새로 불러옵니다.” + 목록 invalidate 로 화면과 서버를 맞춘다. ⚠️ 재생 경로(큐·재생 보고·방송 pending/ack)는 이 코드를 쓰지 않는다 — 거기서는 잘못된 deviceId 가 에러가 아니라 조용한 매장 단위 폴백이다(기기를 회수해도 그 PC 의 재생은 멈추면 안 된다) |
DISPATCH_NOT_FOUND | (송출 슬라이스) 점장 ack(ackStoreAnnouncement) 실패. ack 은 원자 조건부 UPDATE(WHERE status='PENDING')라 이미 PLAYED·중복 ack·본인 매장 아님·미존재 dispatchId 가 모두 이 코드 404(상태·존재 은닉). 첫 ack(PENDING)만 204. 효과는 멱등(상태 불변)이나 HTTP 응답은 404 — “204 멱등”이 아니다. FE player 는 404 를 “이미 소비됨”으로 보고 정상 음악 복귀 |
Rate Limited (status 429)
비용·부하가 큰 endpoint 그룹에 인메모리 토큰버킷(per-instance, principal+그룹 키) rate limit 을 건다. 한도를 넘기면 429 RATE_LIMITED + Retry-After(초) 헤더로 클라이언트에 재시도 대기 시간을 알린다. 버킷은 분당 한도만큼 토큰을 채우는 단순 모델(1분 윈도) — 멀티 인스턴스에서는 인스턴스별 독립 버킷이라 실효 한도가 인스턴스 수배가 될 수 있다(현 단일 인스턴스 전제).
| Code | 의미 |
|---|---|
RATE_LIMITED | 해당 그룹의 분당 한도 초과. Retry-After 헤더(초)만큼 대기 후 재시도. FE 매핑 “요청이 많아 잠시 후 다시 시도해 주세요.” |
그룹·한도:
| 그룹 | 분당 한도 | 대상 | env |
|---|---|---|---|
LOGIN | 짧은 시간 반복 실패 제한 | 로그인(POST /api/v1/auth/login) — 크리덴셜 스터핑·brute-force 완화 (SPEC #149). 초과 시 429 RATE_LIMITED + Retry-After. BFF 는 이 응답을 자격증명 401 분기보다 먼저 잡아 그대로 surface(메시지 미덮어씀). 계정 단위 하드 락아웃 아님(과거 “5회 실패 15분 잠금” 서술은 정정됨) | — |
SYNTHESIS | 10/분 | TTS 합성(안내방송 생성·재합성·점장 미리듣기·smoke-test) — Typecast 외부 비용·쿼터 보호 | — |
DISPATCH | 30/분 | 안내방송 송출(dispatchHqTtsAnnouncement) — fan-out row 생성 부하 보호 | — |
HQ_CONTROL | 30/분 | 본사 제어 액션 — 송출 취소(cancelHqDispatch)·원격 즉시중단(revokeHqDispatch)·반복 예약 생성/취소(createHqDispatchSchedule·cancelHqDispatchSchedule) | APP_RATE_LIMIT_HQ_CONTROL_PER_MINUTE(기본 30) |
TICKET_ATTACHMENT | 10/분 | CS 티켓 첨부 업로드(4 채널) — accountId 단위. 스토리지 남용·대용량 반복 업로드 보호(BE PR #324) | — |
STORE_PROVISION | 20/분 | 매장 생성·점장 계정 발급(SPEC #184) — 매장 가입 2종(onboardIndependentStore·onboardAffiliatedStore)·본사 매장 등록(createHqStore)·본사 사후 발급(issueHqStoreManager). 이 경로들이 계정 설정(setup) 메일을 유발하게 되면서 부착. accountId 단위 | — |
BULK_PROVISION | 3/분 | 매장 CSV 일괄 등록(SPEC #184 — bulkCreateHqStores). 요청 1건이 최대 1000통의 메일을 유발하므로 단건 그룹과 분리한다. accountId 단위 | — |
env:
APP_RATE_LIMIT_HQ_CONTROL_PER_MINUTE로HQ_CONTROL그룹 분당 한도를 조정한다(미설정 시 기본 30). 운영 부하·악용 패턴에 맞춰 조정 가능.
코드 없는 전송 계층 실패 (413 · 429)
표준 envelope 를 만들지 못하는 계층(서블릿 본문 상한, Vercel 서버리스 4.5MB 본문 리밋, 게이트웨이
rate limit)이 만든 응답에는 error.code 가 없다. extractCode 는 undefined 를 돌려주므로
switch(code) 만으로 분기하면 전부 “알 수 없는 오류” 로 뭉개진다 — code 우선 + status 폴백이
원칙이다.
| status | 대표 상황 | FE 문구 |
|---|---|---|
| 413 | 업로드 본문이 서블릿/플랫폼 상한 초과 | ”파일이 너무 큽니다. 10MB 이하 파일만 올릴 수 있습니다.” |
| 429 | rate limit(코드가 실리면 RATE_LIMITED, 게이트웨이가 만들면 코드 없음) | “요청이 많습니다. 잠시 후 다시 시도해 주세요.” |
CS 티켓 첨부는 이제 4.5MB 벽에서 413 이 나지 않는다 — 4MB 초과분은 BFF 프록시를 지나지 않고 업로드 티켓 경로로 백엔드에 직접 올라간다(SPEC #177 확장). 413 은 방어적 폴백으로만 남는다. rate limit 한도는 경로마다 다르다 — multipart 경로는 계정 10회/분(
TICKET_ATTACHMENT그룹), 티켓 직접 업로드 경로는 IP 20회/분(permitAll 이라 principal 이 없다).
CS 티켓 첨부의 단일 소스는 @linkmusic/ui mapTicketAttachmentError(status, code) 이며 4 채널이
공유한다(위 UPLOAD_* 3 코드도 이 매퍼가 문구화한다).
Frontend 클라이언트 처리
// apps/admin/src/lib/error.ts — extractCode(body: unknown) 는 응답 payload(JSON)를 받는다
const code = extractCode(err.body);
switch (code) {
case "AUTH_INVALID_CREDENTIALS":
toast.error("이메일 또는 비밀번호가 일치하지 않습니다.");
break;
case "DUPLICATE_EMAIL":
setError("manager.email", { message: "이미 사용 중인 이메일" });
break;
case "BACKEND_ERROR":
case "BACKEND_UNREACHABLE":
toast.error("잠시 후 다시 시도해 주세요.");
break;
case "VALIDATION_FAILED":
// details[] 의 field 별 inline
err.body.error.details?.forEach(d => setError(d.field, { message: d.message }));
break;
default:
toast.error(err.body?.error?.message ?? "알 수 없는 오류");
}Roadmap
- Error code 추가 SPEC 별 명시
- i18n message catalog
- error tracking (Sentry)
References
- SPEC #003 §2-9
linkmusic-msa-space-was/.../api/error/GlobalExceptionHandler.ktlinkmusic-msa-space-was/.../api/error/ErrorResponse.ktlinkmusic-msa-space-was/.../domain/exception/