API 카탈로그Error Codes

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_EXPIREDrefresh token 만료
AUTH_REFRESH_REVOKEDrefresh token 회수됨 (rotation race)
AUTH_TOKEN_INVALIDJWT 형식 / 서명 불일치
AUTH_TOKEN_EXPIREDaccess 만료 (BFF refresh 트리거)

Impersonation (status 401 / 403)

Code의미
IMPERSONATE_INVALID_TARGETINDEPENDENT / SUSPENDED 본사 대상
IMPERSONATE_TOKEN_EXPIREDexchange token 60s 초과
IMPERSONATE_TOKEN_USEDexchange token 재사용
IMPERSONATE_FORBIDDENOPERATOR 외 호출

Validation (status 400)

Code의미
VALIDATION_FAILEDrequest 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_VALUEenum 값 잘못
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 130초 범위 밖(recordStoreBroadcast). FE 매핑 “녹음이 올바르지 않아요. (빈 파일·10MB 초과·길이 130초 범위 밖)” + 클라 사전 검증으로 미리 거름
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" 고정이라 방어적 매핑(“송출 대상이 올바르지 않습니다.”)

Conflict (status 409)

Code의미
DUPLICATE_EMAIL같은 이메일 OperatorAccount 존재 (409)
HQ_INVALID_STATUS_TRANSITIONHqStatus 전이 불가 — 허용 외 출발 상태(이미 같은 상태 등). suspend/reactivate (#018)
STORE_INVALID_STATUS_TRANSITIONStoreStatus 전이 불가 — 허용 외 출발 상태(같은 상태·역행). 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 매핑 “본사 방송이 이미 예약된 시각입니다. 다른 시각을 선택해 주세요.”
TICKET_INVALID_STATUS_TRANSITIONTicketStatus 전이 불가 — 상태머신 외 전이 시도. PATCH /tickets/{id}/status (#027). FE 클라 가드로 1차 차단
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_RACEpartial 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의미
FORBIDDENrole / scope 부족
ACCESS_DENIEDSpring Security 의 AccessDeniedException
INDEPENDENT_HQ_SUSPENSION_FORBIDDENINDEPENDENT 가상 본사 정지/복구 시도 — 전이 불가 (#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_REQUIREDpasswordMustChange=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 강제 → 무영향

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 연동 상태 확인 안내)
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_FOUNDhqId 존재 X
STORE_NOT_FOUNDstoreId 존재 X
TERMS_NOT_PUBLISHED현재 유효본 TermsDocument 없음 (effective_at <= now 없음)
PRIVACY_NOT_PUBLISHED현재 유효본 PrivacyPolicy 없음
LEGAL_DOC_NOT_FOUNDby-id 약관/개인정보 게시본 미존재 (GET·DELETE /admin/{terms,privacy-policy}/{id}, #033·#038). 예약 취소 시 FE 매핑 “이미 삭제된 예약본입니다”
ACCOUNT_NOT_FOUNDaccountId 존재 X
TICKET_NOT_FOUNDticketId 존재 X (#027) — FE /tickets/{id} 에서 notFound(). 본사 #086(hq_id 불일치·미존재 은닉, 상세·댓글·상태/우선순위 변경) · 점장 #112(store_id 불일치·미존재 은닉, /store/support/{id} 상세·댓글) 도 동일 코드로 통일(타 본사/타 매장 티켓 id 존재 흘림 차단). 점장 FE 는 “문의를 찾을 수 없습니다.” inline Banner
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 를 “이미 삭제됨” 으로 흡수, 송출·이력은 “삭제되었거나 존재하지 않는 안내방송입니다.”
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
SYNTHESIS10/분TTS 합성(안내방송 생성·재합성·점장 미리듣기·smoke-test) — Typecast 외부 비용·쿼터 보호
DISPATCH30/분안내방송 송출(dispatchHqTtsAnnouncement) — fan-out row 생성 부하 보호
HQ_CONTROL30/분본사 제어 액션 — 송출 취소(cancelHqDispatch)·원격 즉시중단(revokeHqDispatch)·반복 예약 생성/취소(createHqDispatchSchedule·cancelHqDispatchSchedule)APP_RATE_LIMIT_HQ_CONTROL_PER_MINUTE(기본 30)

env: APP_RATE_LIMIT_HQ_CONTROL_PER_MINUTEHQ_CONTROL 그룹 분당 한도를 조정한다(미설정 시 기본 30). 운영 부하·악용 패턴에 맞춰 조정 가능.

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.kt
  • linkmusic-msa-space-was/.../api/error/ErrorResponse.kt
  • linkmusic-msa-space-was/.../domain/exception/