DomainEnums

Enums (도메인 enum 카탈로그)

Kotlin enum (backend) + TypeScript const enum / union (frontend packages/api-client/src/generated/). 모두 @Enumerated(EnumType.STRING) 저장 — 가독성 + migration 안전.

HqType

값의미
FRANCHISE프랜차이즈 본부. 산하에 DIRECT/FRANCHISE 매장. plan, billingAnchorDay NOT NULL.
INDEPENDENT가상 본사. 시스템 전체 1개 (partial unique). 개인 매장 (INDEPENDENT type) 산하. plan = NULL, billing 모두 NULL.

HqStatus

현재 도입 완료 — BE v0.7.0 (SPEC #013) Flyway V7. 4 종 enum. generated 스키마 HqAdminListItemStatus. 정지/복구 전이는 SPEC #018 (BE v0.11.0) 도입 완료 — suspend/reactivate endpoint + hq-list 액션. 자동 전환·사유 저장은 후속.

값의미비고
ACTIVE정상 운영임퍼소네이션 가능 (FRANCHISE 한정) · 가상 본사는 이 값 고정 · 정지 대상
ONBOARDING초기 등록 후 활성화 전신규 본사 default · 정지 대상
UNPAID결제 미납UI 경고 (배경 색 변경) · 정지 대상
SUSPENDED협약 위반 / 운영사 정지임퍼소네이션 confirm 비활성 · 관리자 세션 무효화 + 로그인 차단 · 복구(reactivate)로만 ACTIVE 복귀

전이: ACTIVE \| ONBOARDING \| UNPAID → SUSPENDED (suspend) · SUSPENDED → ACTIVE (reactivate). INDEPENDENT 가상 본사는 전이 불가. 상세는 Status Lifecycle.

StoreType

값의미결제 단위
DIRECT직영 매장HQ
FRANCHISE가맹 매장HQ
INDEPENDENT개인 매장Store

StoreStatus

값의미자동 전환
ACTIVE정상default
SUSPENDED정지 (운영사 액션, #037)수동 — ACTIVE|INACTIVE → SUSPENDED
INACTIVE90일 미사용스케줄러 (FR-11.10, 후속)

SPEC #019 매장 목록(/stores) 의 StoreAdminListItemStatus(generated) 와 정합: ACTIVE · INACTIVE · SUSPENDED. StoreAdminListItemType 도 INDEPENDENT · DIRECT · FRANCHISE 로 위 StoreType 과 일치. SPEC #037 정지/복구 전이 도입 — suspend(ACTIVE\|INACTIVE→SUSPENDED)·reactivate(SUSPENDED→ACTIVE). 상세는 Status Lifecycle 참고.

PlanType

값의미라이브러리
AI자체 음원 onlyAI 라이브러리
TRUSTAI + 신탁음원AI + TRUST 라이브러리

Role

값의미hqIdstoreId
OPERATOR운영사NULLNULL
HQ_MANAGER본사 관리자NOT NULLNULL
STORE_MANAGER점장NULLNOT NULL

도메인 invariant — entity 생성자 require(...).

  • HQ_MANAGER 는 본사 온보딩(POST /api/v1/admin/hq, #010)에서 발급된다.
  • STORE_MANAGER 는 매장 생성과 분리해 POST /api/v1/admin/stores/{storeId}/managers(#021)로 사후 발급한다 — storeId tenancy, hqId=null, passwordMustChange=true. 매장당 복수 발급 가능(하드 블록 없음). 운영사 매장 목록(/stores)이 StoreAdminListItem.hasManagerAccount 로 발급 여부를 표시한다.

AccountStatus

값의미
ACTIVE정상
SUSPENDED로그인 차단 (정지 — 즉시 세션 무효화)
WITHDRAWN회수/탈퇴 (soft-delete, terminal — 이메일 재사용 불가)

STORE_MANAGER 계정은 SPEC #029 로 전이 액션이 도입됐다(ACTIVE ↔ SUSPENDED, ACTIVE/SUSPENDED → WITHDRAWN). generated 노출 enum StoreManagerListItemStatus = 같은 3값. 상세 전이·효과는 Status Lifecycle 참고.

HqAffiliatedStoreType (현재 미도입, 후속)

값 (예정)의미
DIRECT_ONLY직영만
FRANCHISE_ONLY가맹만
MIXED직영 + 가맹 혼합

본사의 매장 구성 형태. 현재는 type 별 store row count 로 유추.

ImpersonationTokenStatus (현재 entity 컬럼 없음, 상태는 used_at 으로 유추)

의미
활성 (60초 이내, used_at = NULL)
만료 (expires_at < now())
사용됨 (used_at != NULL)

TicketStatus (#027 — CS 티켓)

값의미tone(FE)
OPEN열림 (생성 직후)info
IN_PROGRESS처리 중warn
RESOLVED해결됨success
CLOSED닫힘muted

전이 규칙은 status-lifecycle 참고 (OPEN → IN_PROGRESS → RESOLVED → CLOSED

  • 재오픈 RESOLVED/CLOSED → IN_PROGRESS). 잘못된 전이는 backend 409 TICKET_INVALID_STATUS_TRANSITION.

TicketPriority (#027 — CS 티켓)

값의미색점 tone(FE, 임시)
URGENT긴급danger
HIGH높음warn
NORMAL보통info
LOW낮음muted

v1 — 우선순위는 생성 시점에만 설정(변경 endpoint 없음). 색점은 전용 시안 부재로 기존 StatusPill tone 만 재사용한 임시 레이아웃.

TicketSlaState (#182 Part A — CS 첫 응답 SLA)

값의미라벨·tone(FE)
ON_TRACK미응답 + 기한 이내기한 내 · muted (목록 배지 없음)
OVERDUE미응답 + 비종착 + 기한 경과기한 초과 · danger
MET첫 응답 완료 또는 종착(RESOLVED·CLOSED)응답 완료 · success (목록 배지 없음)

BE 서버 시각 기준 파생값이다 — FE 는 firstResponseDueAt 을 단말 시계와 비교해 재판정하지 않는다(A-D4). 재판정하면 목록 필터(overdueOnly — 서버의 now)와 화면 배지(브라우저의 now)가 서로 다른 시각을 본다. 기한은 우선순위별 상수(URGENT 2h · HIGH 8h · NORMAL 24h · LOW 72h, 달력 시간)이며 우선순위 변경 시 재계산된다. 운영사 채널 전용(본사·점장 view DTO 에 필드 없음 — A-D7). generated 는 응답별로 분리 emit(TicketListItemSlaState·TicketDetailSlaState — 값 동일). 목록 배지는 OVERDUE 에서만 그린다(신호 희석 방지 — A-D5).

CommentKind (#027 — CS 티켓 코멘트)

값의미
REPLY고객 응답 (외부 노출 톤)
INTERNAL내부 메모 (내부 통제용 톤)

LegalDocumentStatus (#033 — 약관·개인정보 게시본 상태)

값의미FE StatusPill 톤
SCHEDULED예약 — effectiveAt 가 미래(아직 발효 전)info
ACTIVE게시 시점 유효 (즉시 게시·발효됨)success
SUPERSEDED대체됨 (이후 버전에 의해)muted

generated 노출 enum LegalDocumentResponseStatus·LegalDocumentHistoryItemStatus = 같은 3값. stale 주의: 스케줄러가 없어 status 는 게시 시점 스냅샷이다. 예약본이 발효 시각을 지나도 SCHEDULED 로 남을 수 있으므로, “현재 유효본” 판정은 effectiveAt (<= now 중 최신)으로 한다. /active 응답은 status 를 항상 ACTIVE 로 보정. 상세는 Status Lifecycle.

OperatorAuditAction (#026 — 운영자 액션 감사 로그 액션 종류)

운영자 핵심 변경 액션의 append-only 감사 로그 액션 enum. generated 노출 enum OperatorAuditItemAction(/audit/actions). 신규 액션은 VARCHAR 영속이라 마이그레이션 무관.

값의미FE StatusPill 톤도입
HQ_SUSPENDED / HQ_REACTIVATED본사 정지 / 복구danger / success#018·#024
HQ_UPDATED본사명 편집 (운영사 전용)info#123
STORE_SUSPENDED / STORE_REACTIVATED매장 정지 / 복구danger / success#037
STORE_CLOSED매장 폐점 (비가역 terminal)danger#039
STORE_MANAGER_ISSUED점장 발급info#021
STORE_MANAGER_PASSWORD_RESET점장 비밀번호 재설정muted#029
STORE_MANAGER_SUSPENDED / STORE_MANAGER_REACTIVATED / STORE_MANAGER_REVOKED점장 정지 / 복구 / 회수warn / success / danger#029
STORE_MANAGER_EMAIL_CORRECTED / STORE_MANAGER_EMAIL_CORRECTION_REQUESTED점장 이메일 정정(즉시 교체) / 정정 요청(새 주소 인증)info / mutedV74
OPERATOR_ISSUED … OPERATOR_REVOKED (5종)운영자 초대·재설정·정지·복구·회수info·muted·warn·success·danger#032
TERMS_PUBLISHED / PRIVACY_POLICY_PUBLISHED약관 / 개인정보 게시playing#033
TERMS_SCHEDULE_CANCELED / PRIVACY_POLICY_SCHEDULE_CANCELED약관 / 개인정보 예약 취소warn#038
MUSIC_UPLOADED음원 업로드info#041
MUSIC_FILE_REPLACED음원 파일 교체warn#041
MUSIC_DELETED음원 소프트삭제 (deleted_at 채움, blob 유지)danger#042
TICKET_STATUS_CHANGED티켓 상태 변경info#047
TICKET_ASSIGNED티켓 담당자 배정 / 해제 (단일 액션, detail 분기)info#047
TICKET_PRIORITY_CHANGED티켓 우선순위 변경warn#047
TICKET_CATEGORY_CHANGED티켓 분류 변경info#167
MUSIC_METADATA_UPDATED음원 메타데이터 편집(title·artist·isrc·신탁단체)info#183
HQ_ONBOARDED본사 온보딩playing—
MANUAL_INTERVENTION수기 DB 조치 — 제품 경로가 없어 사람이 DB 를 직접 고친 뒤 기록(targetType=영향 도메인, detail=무엇을 왜). DB 행을 이 값으로 정정하는 작업은 이 enum 이 배포된 뒤에만 한다(역매핑 실패로 목록·CSV 가 터진다)danger2026-10-01

대상 타입(OperatorAuditItemTargetType): HQ · STORE · OPERATOR · TERMS · PRIVACY_POLICY · MUSIC(#041) · TICKET(#047). 매장 정지/복구/폐점의 targetType = STORE, targetId = storeId, targetLabel = 매장명, detail = 사유(suspend/close). 음원 업로드/교체의 targetType = MUSIC, targetId = 음원 PK, targetLabel = 곡 제목 스냅샷, detail = 원본 파일명·크기(#041). 음원 소프트삭제(MUSIC_DELETED)의 targetType = MUSIC(재사용), targetId = 음원 PK, targetLabel = 곡 제목 스냅샷(#042). 티켓 상태/우선순위/담당자 변경의 targetType = TICKET, targetId = 티켓 PK, targetLabel = 티켓 title 스냅샷. detail 은 상태/우선순위 이전→이후(예 OPEN→IN_PROGRESS·HIGH→URGENT), 담당자 배정: {email}(이메일) / 해제(#047).

MusicSource (#059 — 음원 타입)

음원의 출처 타입. 업로드 시 지정·불변. 라이브러리 타입(libraryType)과 동일 도메인으로, 음원을 라이브러리에 담을 때 musicSource == libraryType 이어야 한다(불일치 시 400 LIBRARY_TYPE_MISMATCH). generated 노출 enum MusicListItemMusicSource / MusicResponseMusicSource / UploadMusicMusicSource / ListMusicMusicSource. FE 는 MUSIC_SOURCE_META 단일 소스로 라벨/톤을 매핑하며 라이브러리 타입 배지와 동일 표현(색만으로 구분하지 않고 라벨 병기).

값의미FE StatusPill 톤
AIAI 생성 음원info
TRUST신탁 음원playing

플랜(PlanType)의 AI/TRUST 는 본사 구독 등급, 음원의 musicSource 는 개별 음원의 출처 타입으로 의미 레이어가 다르다(라벨만 공유). 라이브러리·플랜·음원의 AI/TRUST 표현은 톤/라벨을 일관 유지한다.

MusicTagOptionType (#132 — 음원 태그 옵션 타입)

음원 분류 태그 옵션(music_tag_option)의 타입. 생성 후 불변. 같은 타입 내 value 는 unique (중복 시 409 MUSIC_TAG_OPTION_DUPLICATE). generated 노출 enum MusicTagOptionResponseType / CreateMusicTagOptionRequestType / ListMusicTagOptionsType(모두 동일 2값). FE /settings/music-options 는 타입별로 2 섹션(장르·무드)을 분리해 렌더한다.

값의미
GENRE장르 (예: 재즈·힙합·클래식)
MOOD무드 (예: 차분한·신나는·잔잔한)

라이브러리·플레이리스트 분류에 쓰일 태그 옵션의 마스터 데이터. 운영사가 CRUD 로 관리하며 삭제는 soft delete(active=false). 음원 업로드 폼이 이 옵션을 소비(드롭다운)하는 연동은 후속.

PlaylistStatus (#060 — 플레이리스트 파생 상태)

플레이리스트의 파생(계산) 상태. DB 컬럼·마이그레이션 없는 응답 계산값으로, BE 단일 함수 derivePlaylistStatus(libraryCount, appliedStoreCount, isDefault)(운영사·본사 서비스 공유)가 집계로 판정한다. 한 PL 은 정확히 1개 상태이며 배타적 우선순위로 결정한다. generated 노출 enum PlaylistResponseStatus / PlaylistListItemStatus / HqPlaylistListItemStatus / HqPlaylistDetailResponseStatus(모두 동일 4값). FE 는 표시만 하며(서버 파생값 신뢰) playlist-status-meta.ts 단일 소스로 라벨/톤을 매핑한다(운영사·본사 일치·라벨 항상 병기).

우선순위값판정 조건FE 라벨FE StatusPill 톤
1EMPTYlibraryCount == 0 (라이브러리 미보유, 최우선)비어있음warn
(2)(MISMATCH)플랜 불일치 — PL 상태 배지는 아직 미구현(후속). #122 는 점장 큐 게이팅만 도입——
3FALLBACKisDefault == true (본사 기본 PL·점장 큐 fallback 대상)기본info
4ACTIVEappliedStoreCount >= 1 (적용 매장 있음)사용 중success
5UNUSED그 외 (libraryCount>0 && !isDefault && appliedStoreCount==0)미사용muted

상태 모델은 5종 설계(EMPTY/MISMATCH/FALLBACK/ACTIVE/UNUSED)이나 PL 상태 배지는 비게이트 4종만 구현한다. SPEC #122(#060 F1)는 plan↔type 게이팅의 첫 구현이나, 운영사/본사 PL 화면의 MISMATCH 배지가 아니라 점장 재생 큐에서 plan 위반 음원을 서버가 제외하는 게이팅만 도입한다(effective plan=store.plan ?? hq.plan vs library.libraryType). 게이팅은 계층적(#169 — 정확일치가 아님): TRUST plan 은 TRUST + AI 라이브러리 모두 허용(TRUST ⊇ AI), AI plan 은 AI 라이브러리만, plan null 은 무제약(전곡). 전부 위반→EMPTY_PLAYLIST(Store Player §“플랜 위반 음원 큐 제외”). PL 상태 배지로서의 MISMATCH(운영사/본사 편집 화면 경고)는 여전히 후속 — 도입 시 우선순위에서 EMPTY 다음·FALLBACK 앞에 들어갈 예정. FALLBACK status 는 기존 isDefault “기본” 배지와 라벨이 겹치므로 FE 는 status 배지로 통합하고 별도 기본 배지를 숨긴다(status≠FALLBACK 인 기본 PL 은 병치 — 상세는 Playlist · HQ Mode 플레이리스트).

TtsVoice (#061 — TTS 안내방송 voice)

본사 TTS 안내방송 합성에 쓰는 voice. 5종. BE TtsVoice enum 이 actor_id·model_version·표시명을 내부 보유하고, API 에는 enum name + 한글 표시명만 노출(actor_id 비노출). generated 노출 enum CreateTtsAnnouncementRequestVoice / TtsAnnouncementListItemVoice / TtsAnnouncementDetailResponseVoice (모두 동일 5값). 목록·상세는 BE voiceDisplayName(한글)을 단일 소스로 표시한다. 생성 폼만 합성 전이라 BE 표시명을 받을 수 없으므로 FE tts-voice-meta.ts 가 value→한글 라벨을 보유(라벨 어긋나도 합성은 enum value 로 진행 — 안전).

값FE 생성 폼 라벨(예시)
SHEAN시안 (차분한 여성)
WOOSUNG우성 (안정된 남성)
CYRUS사이러스 (밝은 남성)
AERAN애란 (따뜻한 여성)
SEUNGA승아 (또렷한 여성)

✅ voice별 tone preset 선택 — #063 도착(아래 TtsTonePreset). FE 생성 폼 라벨은 BE TtsVoice.displayName 과 정합을 유지한다(BE 가 응답 voiceDisplayName 의 단일 소스).

TtsTonePreset (#063 — TTS 안내방송 톤 프리셋)

본사 TTS 안내방송 합성의 감정 톤(Typecast emotion). 6종. voice별 지원 집합이 다르며 모든 voice 가 NORMAL(기본) 을 지원한다. BE TtsTonePreset enum 이 Typecast emotion 파라미터·표시명을 내부 보유하고, API 에는 enum name + 한글 표시명(*DisplayName)만 노출. generated 노출 enum CreateTtsAnnouncementRequestTonePreset(@nullable) / UpdateTtsAnnouncementRequestTonePreset(@nullable) / TtsAnnouncementListItemTonePreset / TtsAnnouncementDetailResponseTonePreset / TtsTonePresetOptionPreset (모두 동일 6값). voice별 허용 매핑은 GET /api/v1/hq/tts-voices(TtsVoiceListResponse)가 단일 소스.

값FE 폴백 라벨(예시)
NORMAL기본
HAPPY밝게
TONEUP톤 업
TONEDOWN톤 다운
ANGRY강하게
SAD차분하게

톤 표시명은 BE tonePresetDisplayName(voices 응답·목록·상세)이 단일 소스다. FE 는 voices 응답이 아직/실패일 때의 폴백 라벨만 tts-voice-meta.ts 에 보유(NORMAL 만 노출). 생성·수정 폼은 선택 voice 의 허용 톤만 옵션으로 보이고, voice 미지원 톤은 UI 가 차단(혹시 도달 시 BE 400 TTS_INVALID_TONE_PRESET).

DispatchRequestTarget (송출 슬라이스 — 안내방송 송출 대상)

본사 안내방송 송출(POST /api/v1/hq/announcements/{id}/dispatch) + 반복 예약 (POST /api/v1/hq/dispatch-schedules)의 대상 구분. 3종 (SPEC #144 REGION 추가). generated DispatchRequestTarget(DispatchRequest.target) / CreateDispatchScheduleRequestTarget / DispatchScheduleResponseTarget.

값의미
ALL산하 매장 전체. storeIds·region 무시. (매장 0건이면 dispatchedCount=0)
STORES지정 매장. storeIds 필수(≤1000, 모두 본인 본사 산하). 비어있거나 타 본사 → 400 DISPATCH_INVALID_TARGET
REGION(SPEC #144) 지정 지역(Region 시/도). region 필수(누락 시 400 DISPATCH_REGION_REQUIRED). service 가 hqId AND region=:region 매장으로 fan-out(해당 region 매장 0건이면 dispatchedCount=0)

ALL/STORES/REGION 분기는 즉시 송출·반복 예약 양쪽에서 동일. FE 는 송출/예약 다이얼로그 target 라디오에 “지역” 옵션 + region select(시/도) 를 노출하고, target=REGION 인데 미선택이면 제출 disabled(BE 400 선검증). 상세는 HQ TTS 안내방송 송출.

Region (SPEC #144 — 매장 지역 시/도)

매장(Store.region, nullable)의 시/도 enum. REGION 모드 송출의 그룹핑 축. 17종. 본사가 매장 상세에서 태그하면 target=REGION 으로 같은 시/도 매장군에 일괄 송출한다. BE Region enum 이 displayName(한글)을 내부 보유하고, OpenAPI 에는 enum name 만 노출 → FE 는 한글 라벨 맵을 apps/space/src/lib/region.ts 에 단일 소스로 보유(generated 의 per-field region enum 17값을 value 소스로 재사용). generated 노출 enum: UpdateStoreRegionRequestRegion / HqStoreDetailResponseRegion / DispatchRequestRegion / CreateDispatchScheduleRequestRegion / DispatchScheduleResponseRegion(모두 동일 17값, nullable).

enum name라벨enum name라벨
SEOUL서울GANGWON강원
BUSAN부산CHUNGBUK충북
DAEGU대구CHUNGNAM충남
INCHEON인천JEONBUK전북
GWANGJU광주JEONNAM전남
DAEJEON대전GYEONGBUK경북
ULSAN울산GYEONGNAM경남
SEJONG세종JEJU제주
GYEONGGI경기

Store.region 이 null(미지정)이면 지역 송출에 포함되지 않는다. 편집은 본사 매장 상세 매장 지역 편집 참고. 자동 주소 파싱 채움은 범위 밖(수동 선택).

DispatchStatus (송출 슬라이스 — 안내방송 송출 상태)

송출 row(announcement_dispatch.status)의 생명주기. 5종 (SPEC #077 CANCELED 추가 + SPEC #078 SCHEDULED 추가 + SPEC #143 MISSED 추가). 전이는 단방향 — SCHEDULED → PENDING → PLAYED(정상 경로) · SCHEDULED → CANCELED(예약 직접 취소) · PENDING → CANCELED(즉시/예약-전이된 PENDING 취소) · PENDING → MISSED(들리지 않은 채 종결 — 사유 3종, 아래 DispatchMissedReason). PLAYED·CANCELED·MISSED 는 종착 상태(이후 전이 없음 — 시간 되감기 X · 중복 취소 가드).

SCHEDULED ──┬──> PENDING ──┬──> PLAYED   (ack outcome=PLAYED — 실제 재생 완료)
            │              ├──> MISSED   (EXPIRED · PLAYBACK_FAILED · SKIPPED)
            └──> CANCELED <─┘
값의미
SCHEDULED(SPEC #078) 예약 송출 row 의 초기 상태. 본사가 미래 scheduledAt 으로 송출 요청 시 적재됨. 백그라운드 디스패처(HqDispatchScheduler, @Scheduled(cron='0 * * * * *'))가 도래 시 원자 UPDATE(WHERE status='SCHEDULED' AND scheduled_at <= :now) 로 PENDING 전이. 점장 player 는 WHERE status='PENDING' 만 폴링하므로 SCHEDULED 는 자연 제외
PENDING송출 직후 기본값(즉시 송출) 또는 디스패처가 SCHEDULED 에서 전이한 상태. 점장 player 의 pending 폴링 대상(listStorePendingAnnouncements)
PLAYED점장이 재생 완료(ack outcome=PLAYED, 본문 생략 시 기본)를 보고. BE 는 원자 조건부 UPDATE(WHERE status='PENDING') — 첫 ack 만 204, 중복 ack·이미 종착·타 매장·미존재는 모두 404 DISPATCH_NOT_FOUND(상태·존재 은닉). 효과는 멱등(상태 불변)이나 응답은 404. 실제로 재생된 송출만 여기 들어온다 — 실패·폐기는 MISSED 로 갈린다(감사 #11)
CANCELED본사가 PENDING 또는 SCHEDULED(SPEC #078) dispatch 를 취소(cancelHqDispatch, SPEC #077·#078). 원자 조건부 UPDATE(WHERE id AND hq_id AND (status='PENDING' OR status='SCHEDULED')) — 1행 영향 시 204, 0행(이미 PLAYED·CANCELED·미존재·타 본사)은 모두 404 DISPATCH_NOT_FOUND(상태·존재 은닉). 점장 pending 조회는 WHERE status='PENDING' 만 매치하므로 CANCELED 자동 제외(별도 cleanup 0). 같은 트랜잭션에 본사 audit 1건(HQ_ANNOUNCEMENT_DISPATCH_CANCELED) 기록. 원격 즉시중단(revoke)도 CANCELED 로 전이(revokeHqDispatch, SPEC #077 확장 — PENDING-only + revoked_at set + audit HQ_DISPATCH_REVOKED, 점장 player 가 revokedDispatchIds 신호로 재생 중 멘트 즉시 중단)
MISSED(SPEC #143 · 감사 #11 확장) “매장에서 들리지 않은 채 종결” 을 뜻하는 종착 상태. 진입 경로가 3가지이고 missedReason 으로 구분한다 — ① EXPIRED: PENDING 이 도래+grace(10분) 을 넘겨도 ack 가 없어 디스패처가 원자 UPDATE(WHERE status='PENDING' AND created_at <= :now - grace)로 자동 만료(점장이 늦게 ack 하면 race 로 PLAYED 가 이기거나 404). ② PLAYBACK_FAILED: 점장 player 가 ack outcome=FAILED 로 재생 실패를 보고했고 그 누적 횟수가 임계치 3회에 도달했을 때만 전이한다(3회 미만이면 PENDING 유지 → 재전달). ③ SKIPPED: 점장 player 가 재생을 시도조차 않고 폐기(시작 시 backlog drain, ack outcome=SKIPPED) — 즉시 전이. 점장 pending 조회는 WHERE status='PENDING' 만 매치하므로 MISSED 자동 제외 — 종결 후엔 player 에 더 노출되지 않는다. 통계에선 송출 상태 분포에 집계되나 도달률 분모(PENDING+PLAYED)에선 제외. 별도 audit 없음(시스템/클라이언트 보고 기반, 로그로만)

DispatchMissedReason (미재생(MISSED) 사유 — 감사 #11)

announcement_dispatch.missed_reason. MISSED 가 아닌 송출은 항상 null. 상태를 더 쪼개지 않고 사유 컬럼으로 분리한 이유: 모든 조회·집계의 status IN (...) 을 동기화해야 해 오집계 위험이 커지기 때문이다 (같은 “들리지 않음” 버킷을 유지하고 사유로만 구분).

값의미
EXPIRED도래+grace(10분) 초과 시스템 자동 만료. 매장 오프라인·player 미기동 등이 전형
PLAYBACK_FAILED점장 player 의 재생 실패 보고가 3회 누적됐을 때(임계치 미만이면 아직 PENDING 이라 이 값이 아니다). 음원 URL 만료·CDN 5xx·디코드 실패·오버레이 hang(워치독 강제 종료)이 원인
SKIPPED점장 player 가 재생을 시도조차 않고 폐기(늦은 진입 시 backlog drain). 긴급은 drain 대상에서 제외되므로 이 사유로 남지 않는다

DispatchAckOutcome (점장 ack 재생 결과 — 감사 #11)

POST /api/v1/store/announcements/{dispatchId}/ack 의 선택 본문 AckAnnouncementRequest outcome. 생략 시 PLAYED(하위 호환). generated 타입은 endpoint scope AckAnnouncementRequestOutcome.

값서버 처리
PLAYEDPENDING→PLAYED · played_at 기록
FAILEDplayback_failure_count +1 · last_failed_at 갱신. 3회 미만이면 PENDING 유지 → 다음 pending 폴링에 재전달(서버 주도 재시도) · 3회째에 PENDING→MISSED(PLAYBACK_FAILED)
SKIPPED즉시 PENDING→MISSED(SKIPPED). 재시도 대상 아님(재생을 시도조차 안 한 폐기라 의미가 다르다)

응답은 종착이든 재전달 대기든 동일한 204 — FE 는 결과 보고만 하고 상태를 판단하지 않는다. 재시도 정책은 BE 소유라 FE 는 자체 재시도 카운터를 두지 않고 실패할 때마다 그대로 FAILED 를 보고한다. 무한 재전달은 grace(10분) 자동 만료가 상한. 종착 이후 재전송은 outcome 과 무관하게 404(첫 결론 불변).

상세는 AnnouncementDispatch 테이블 · Store Player 동시재생 · 본사 송출 취소.

DispatchCalendarEventKind / DispatchCalendarEventStatus (송출 캘린더)

송출 캘린더(GET /api/v1/hq|store/dispatch-calendar, DispatchCalendarEvent)의 이벤트 분류 enum. 본사·점장 공용.

DispatchCalendarEventKind — is_emergency·is_hq_origin 에서 파생.

값의미
EMERGENCY긴급 안내(최우선). FE 캘린더는 status 무관 danger 톤으로 강조
HQ_ANNOUNCEMENT본사 안내방송 송출
STORE_BROADCAST점장 매장 방송 송출

DispatchCalendarEventStatus — DispatchStatus 와 동일 5종(SCHEDULED → PENDING → {PLAYED, CANCELED, MISSED} 단방향). 캘린더 조회는 CANCELED(본사 취소)만 제외하고 MISSED(들리지 않은 채 종결)는 포함한다 — 안 나간 방송을 캘린더에서 지우면 본사가 결손을 못 본다. FE 캘린더 톤: SCHEDULED/PENDING=info, PLAYED=success, CANCELED·MISSED=muted(취소선).

상세는 DispatchCalendarResponse DTO · 본사 캘린더 · 점장 캘린더.

DispatchScheduleFrequency (반복 송출 예약 — 빈도, #139 확장)

본사 반복 송출 예약(dispatch_schedule.frequency)의 빈도. 5종 (#139 — 시각형 HOURLY·EVEN_HOURS· ODD_HOURS 추가, 기존 DAILY·WEEKLY 보존). generated CreateDispatchScheduleRequestFrequency / DispatchScheduleResponseFrequency(동일 값). FE radiogroup·라벨(dispatch-schedule-meta.ts FREQUENCY_LABELS)·타임테이블 전개(expandOperatingHours)가 이 enum 단일 소스를 재사용한다.

값의미연관 필드
DAILY매일 1회slotTime(HH:MM). byWeekday·startHour·endHour 무시
WEEKLY지정 요일마다 1회byWeekday(요일 CSV, 필수) + slotTime(HH:MM)
HOURLY운영시간 매 정시마다startHour·endHour(0-23, start≤end, 필수) + slotTime 의 분(MM)만 매시 :MM 오프셋(시 무시)
EVEN_HOURS운영시간 내 짝수 hour마다위와 동일
ODD_HOURS운영시간 내 홀수 hour마다위와 동일

시각형(HOURLY/EVEN_HOURS/ODD_HOURS)은 startHour·endHour(운영시간)가 필수이며 slotTime 의 시(HH)는 무시된다(분 MM만 매시 오프셋). 빈도↔요일·빈도↔운영시간 불일치·범위 위반은 400 DISPATCH_SCHEDULE_INVALID. 백그라운드 디스패처가 시각형이면 하루 N건(운영시간 hour마다)으로 전개한다. 상세는 반복 송출 예약.

TtsAnnouncementSource (즉시방송 슬라이스 — 안내방송 출처)

tts_announcement.source(V24)의 출처 구분. 2종. 같은 테이블을 본사 안내방송과 점장 즉시방송이 공유하되 source 로 격리한다.

값의미
HQ본사(HQ_MANAGER)가 만든 안내방송. 기존 행 backfill 기본값. 본사 화면(목록·상세·수정·삭제·dispatch)이 다루는 유일한 source
STORE_BROADCAST점장(STORE_MANAGER)이 즉시방송 미리듣기(previewStoreBroadcast)로 만든 draft. created_by_store_id 채워짐. send 로 본인 매장에만 dispatch

보안 불변식 — 본사-facing 쿼리는 모두 source='HQ' 로 격리해 STORE_BROADCAST row 가 본사 화면·계약에 노출되지 않는다. 점장 즉시방송 흐름은 점장 즉시방송 · TtsAnnouncement 테이블.

HqAuditAction (#067 도입 · #068 확장 — 본사 감사 액션)

hq_audit_log.action(V27)의 본사(HQ_MANAGER) 액션 enum. generated FE 도메인은 HqAuditItemAction·ListHqAuditDispatchesAction(value 동일) — 총 19종(#181 영업시간 기준). 아래 표는 안내방송 라이프사이클 핵심 + 원격중단·지역·CM송·점장 발급 액션이며, ticket·store·반복 예약 액션은 HQ Mode 감사 참조.

값의미
HQ_ANNOUNCEMENT_DISPATCHED본사 안내방송 송출. HqAnnouncementDispatchService.dispatch 트랜잭션 내 1행 기록 — actor·시각·target=ALL|STORES·count·storeIds(앞 10개) 스냅샷
HQ_ANNOUNCEMENT_CREATED본사 안내방송 생성. HqTtsAnnouncementService.create 트랜잭션 내 1행 — title·voice·tonePreset·durationSeconds(null = “null” 명시) 스냅샷
HQ_ANNOUNCEMENT_UPDATED본사 안내방송 수정. HqTtsAnnouncementService.update — title·voice·tonePreset·resynthesized(text/voice/tonePreset 변경 시 true) 스냅샷. no-op(모든 필드 동일) 시 audit 미기록(early return)
HQ_ANNOUNCEMENT_DELETED본사 안내방송 soft-delete. HqTtsAnnouncementQueryService.delete — title 스냅샷(삭제된 안내방송 식별)
HQ_ANNOUNCEMENT_DISPATCH_CANCELED본사 송출 취소(SPEC #077). cancelHqDispatch 트랜잭션 내 1행 — PENDING/SCHEDULED dispatch CANCELED 전이
HQ_DISPATCH_REVOKED본사 송출 원격 즉시중단(SPEC #077 확장). revokeHqDispatch 트랜잭션 내 1행 — PENDING dispatch CANCELED 전이 + revoked_at set. cancel(재생 전 무효화)과 별도 audit 액션으로 운영 의도 구분(revoke=재생 중/직전 best-effort 중단, 점장 player 가 revokedDispatchIds 신호로 즉시 멈춤)
HQ_STORE_REGION_UPDATED본사 매장 지역(시/도) 변경(SPEC #144). updateStoreRegion 트랜잭션 내 1행 — targetType=STORE, before/after region 스냅샷. FE audit 뷰 라벨 “매장 지역 변경”(info 톤)
HQ_COMMERCIAL_CREATED본사 CM송 등록(SPEC #174 — 파일 업로드). createHqCommercial 트랜잭션 내 1행 — targetType=COMMERCIAL, target_label=제목 스냅샷. FE audit 뷰 라벨 “CM송 등록”(info 톤)
HQ_COMMERCIAL_FILE_REPLACED본사 CM송 오디오 파일 교체(SPEC #174). replaceHqCommercialFile(PUT /{id}/file) 트랜잭션 내 1행 — targetType=COMMERCIAL. FE audit 뷰 라벨 “CM송 파일 교체”(info 톤)
HQ_STORE_MANAGER_ISSUED본사가 자기 산하 매장에 점장(STORE_MANAGER) 계정을 발급(SPEC #184 D4/D5 — 새로 연 권한). 매장 등록 시 동시 발급(createHqStore)과 사후 발급(issueHqStoreManager) 둘 다 기록하며, 동시 발급 시 HQ_STORE_CREATED 와 별도 row 다(조직 생성 ≠ 계정 발급). targetType=STORE. FE audit 뷰 라벨 “매장 점장 계정 발급”(info 톤) — 운영사(/audit/hq)·본사(/admin/audit) 양쪽 Record<…Action,…> 전수 매핑이라 enum 추가가 컴파일로 누락을 잡는다
HQ_STORE_BUSINESS_HOURS_UPDATED본사가 산하 매장의 영업시간을 편집(SPEC #181). 재생 준수율 판정의 기준이 된다. FE 톤 info

generated 도메인(HqAuditItemAction·ListHqAuditDispatchesAction)에는 추가 액션(ticket·store·반복 예약 등)도 포함된다 — 위는 안내방송 송출 라이프사이클 핵심 액션 + #144 지역 변경 + #174 CM송 등록/교체. Followups: 점장 즉시방송 audit 은 별도 enum/테이블(F3 — store_audit_log). 상세는 HqAuditLog · HQ Mode 감사.

HqAuditActorRole (#067 — 본사 감사 행위자 역할)

hq_audit_log.actor_role(V27). 2종. impersonate 컨텍스트 구분 — impersonated_by_operator_id 컬럼과 정합성을 CHECK 제약으로 강제한다((actor_role='OPERATOR_IMPERSONATING') = (impersonated_by_operator_id IS NOT NULL)). generated FE 도메인은 HqAuditItemActorRole(value 동일).

값의미
HQ_MANAGER본사 매니저 직접 송출(임퍼소네이션 아님). impersonated_by_* 컬럼 null
OPERATOR_IMPERSONATING운영자(principal.impersonatedBy != null)가 본사로 위장해 송출. impersonated_by_operator_id/email 동시 기록 — 원본 운영자와 위장 본사 매니저 둘 다 추적

impersonate 미러 — principal.accountId = HQ_MANAGER 계정(actor_account_id), principal.impersonatedBy = 원본 운영자(impersonated_by_operator_id). 둘 다 lookup 실패 시 IllegalStateException → 액션 롤백. 상세는 HqAuditLog.

HqAuditTargetType (#067 — 본사 감사 대상 유형)

hq_audit_log.target_type(V27)의 대상 유형 enum. generated FE 도메인은 HqAuditItemTargetType (value 동일) — TTS_ANNOUNCEMENT(#067)·SUPPORT_TICKET(#101)·STORE(#105)·COMMERCIAL(#174) 4종.

값의미
TTS_ANNOUNCEMENT본사 TTS 안내방송(tts_announcement.id). target_id=안내방송 id, target_label=제목 스냅샷
COMMERCIAL본사 CM송(commercial_song.id, SPEC #174). target_id=CM송 id, target_label=제목 스냅샷. 등록·파일 교체 액션의 대상
STORE본사 산하 매장(store.id, SPEC #105). target_id=매장 id, target_label=매장명 스냅샷. 매장 편집·등록·지역 변경·점장 계정 발급(#184) 액션의 대상

Followups: 후속 액션 확장에 따라 대상 유형도 추가 가능(예: announcement create/update/delete 도 같은 유형). 상세는 HqAuditLog · HQ Mode 감사.

StoreAuditAction (#114 — 매장 감사 액션)

store_audit_log.action(V36)의 점장(STORE_MANAGER) 액션 enum. 현재 11종(HqAudit 처럼 점진 확장 — 새 점장 액션은 VARCHAR(48) 영속이라 마이그레이션 무관). HqAuditAction· OperatorAuditAction 과 의미가 분리된 별도 enum (actor·테넌트 스코프·UI 권한 모델이 다름). v1 은 기록만이라 generated FE 도메인 노출은 조회 view 후속에서.

값의미
STORE_TICKET_CREATED점장 CS 티켓 생성 (#112 audit tail 마감). StoreTicketService.createStoreTicket — target_id=ticket.id·target_label=ticket.title·detail=title=...·category=NAME·bodyLength=N 스냅샷
STORE_TICKET_COMMENT_ADDED점장 CS 티켓 댓글 추가 (#112). StoreTicketService.addStoreTicketComment — target_id=ticket.id(댓글 id 아님 — audit 는 ticket scope)·detail=bodyLength=N
STORE_TICKET_CLOSED점장 CS 티켓 확인 종료 RESOLVED→CLOSED (#121). StoreTicketService.closeStoreTicket — target_type=SUPPORT_TICKET·target_id=ticket.id·target_label=ticket.title 스냅샷. store_id 격리 원자 UPDATE affected=1 확정 시에만 기록(409/404 분기 후)
STORE_PROFILE_UPDATED점장 본인 매니저 계정 name 편집 (#087 F4 마감). StoreMeService.updateMe — target_type=STORE_MANAGER·detail=changedFields=name·name:before->after. 변경 0(멱등 no-op) 시 audit 미기록
STORE_PASSWORD_CHANGED점장 본인 비밀번호 변경 (#100 F2 마감). 공용 AuthService.changePassword role=STORE_MANAGER 분기 — target_type=STORE_MANAGER·detail 없음(보안 — 비밀번호·길이 미기록)·임퍼소네이션 차단되어 actor 항상 본인
STORE_DISPATCH_CANCELED점장 본인 매장 예약 송출 취소 (#092 F2 마감). StoreBroadcastService — SCHEDULED dispatch 를 CANCELED 로 단방향 전이·target_type=DISPATCH·detail 없음(원자 UPDATE affected=1 확정 시에만 기록)
STORE_ACTIVE_PLAYLIST_CHANGED점장 본인 매장 활성 PL 선택·해제 (#129). 활성 PL 지정(playlistId) 또는 본사 기본 fallback(null) 로 실제 변경 시·target_type=PLAYLIST. 기존 운영사/본사 활성 PL 지정 로직 재사용
STORE_DISPATCH_BROADCAST_NOW점장 예약 송출 즉시 방송 (#142). StoreBroadcastService — SCHEDULED dispatch 의 안내방송으로 새 IMMEDIATE dispatch 1건 생성(원본 SCHEDULED 무변경)·target_type=DISPATCH·target_id=새 IMMEDIATE dispatch.id·detail = 원본 SCHEDULED dispatch.id 스냅샷(sourceScheduledDispatchId) — 즉시 방송이 어느 예약본에서 비롯됐는지 역추적용
STORE_DEVICE_RENAMED점장 기기 이름 변경 (#178). StoreDeviceService.rename — target_type=STORE_DEVICE·target_id=store_device.id·target_label=변경 후 이름·detail=before->after. 실제로 이름이 바뀐 경우에만 기록(같은 이름 재저장은 no-op, STORE_PROFILE_UPDATED 멱등 관례 미러)
STORE_DEVICE_REVOKED점장 기기 회수 (#178, soft-delete). StoreDeviceService.revoke — target_type=STORE_DEVICE·target_id=store_device.id·target_label 없음(사전 조회 없는 원자적 조건부 UPDATE 라 race window 0 을 유지하려고 이름을 lookup 하지 않는다 — STORE_DISPATCH_CANCELED 관례 미러)·detail=deviceId=…. 회수는 그 PC 의 기기별 활성 PL·재생 상태를 끊으므로 “왜 저 PC 만 음악이 바뀌었나” 추적에 필요
STORE_DEVICE_PLAYLIST_CHANGED점장 기기별 활성 PL 변경 (#178). StoreDeviceService.setDevicePlaylist — target_type=STORE_DEVICE(변경 대상은 기기)·target_id=store_device.id·target_label=기기 이름 스냅샷·detail=playlist=<id|none>-><id|none>. 변경 0(같은 PL 재지정)이면 미기록(멱등 — STORE_ACTIVE_PLAYLIST_CHANGED 관례 미러)

⚠️ 기기 등록(registerStoreDevice)은 audit 하지 않는다 — player 마운트마다 일어나는 멱등 동작이라 남기면 감사 로그가 그것만으로 찬다(#178).

Followups: 점장 액션 도착 시 enum 확장. v1 은 기록만 — 조회 view(운영자/본사) 는 후속(D5). 상세는 StoreAuditLog · Store 감사.

StoreAuditActorRole (#114 — 매장 감사 행위자 역할)

store_audit_log.actor_role(V36). 2종. HqAuditActorRole 미러 — impersonate 컨텍스트를 구분하고 impersonated_by_operator_id 컬럼과 정합성을 CHECK 제약으로 강제한다 ((actor_role='OPERATOR_IMPERSONATING') = (impersonated_by_operator_id IS NOT NULL)).

값의미
STORE_MANAGER점장 본인이 직접 액션(임퍼소네이션 아님). impersonated_by_* 컬럼 null
OPERATOR_IMPERSONATING운영자(principal.impersonatedBy != null)가 점장 모드로 위장해 액션. impersonated_by_operator_id/email 동시 기록 — 원본 운영자와 위장 점장 둘 다 추적

impersonate 미러 — principal.accountId = 점장 계정(actor_account_id), principal.impersonatedBy = 원본 운영자(impersonated_by_operator_id). 둘 다 lookup 실패 시 IllegalStateException → 액션 롤백. 상세는 StoreAuditLog.

StoreAuditTargetType (#114 — 매장 감사 대상 유형)

store_audit_log.target_type(V36)의 대상 유형 enum. 현재 5종(ASCII 사전순). 후속 액션 확장에 따라 추가 가능.

값의미
DISPATCH예약 송출(announcement_dispatch.id). STORE_DISPATCH_CANCELED·STORE_DISPATCH_BROADCAST_NOW(#142) 의 대상
PLAYLIST플레이리스트(playlist.id). STORE_ACTIVE_PLAYLIST_CHANGED(#129) 의 대상
STORE_DEVICE매장 기기(store_device.id·target_label=기기 이름 스냅샷, #178). STORE_DEVICE_RENAMED·STORE_DEVICE_REVOKED·STORE_DEVICE_PLAYLIST_CHANGED 의 대상
STORE_MANAGER점장 본인 매니저 계정(self, 점장 계정 id). STORE_PROFILE_UPDATED·STORE_PASSWORD_CHANGED 의 대상
SUPPORT_TICKETCS 티켓(ticket.id·target_label=title 스냅샷). STORE_TICKET_CREATED·STORE_TICKET_COMMENT_ADDED 의 대상

상세는 StoreAuditLog · Store 감사.

OpenAPI 동기화

backend enum 변경 시 pnpm sync-api 로 packages/api-client/src/generated/ 의 TypeScript union / const enum 자동 갱신. 수기 정의 금지 (frontend [[feedback-15]] 정합).

References

  • SPEC #002 §2-3 (HqType · StoreType · PlanType · StoreStatus)
  • SPEC #003 §2-1 (Role · AccountStatus)
  • SPEC #005 (ImpersonationToken)
  • linkmusic-msa-space-was/src/main/kotlin/.../domain/enum/
  • linkmusic-frontend-space/packages/api-client/src/generated/schemas/