HQ Mode — 본사 CM송 관리 (apps/space /admin/commercials)
SPEC #093 도입. 실 HQ_MANAGER 직접 로그인 표면(apps/space · space.linkmusic.io) — 운영사 임퍼소네이션
(apps/admin)이 아니다.
이 페이지는
apps/space의 실 HQ_MANAGER 본사 모드다. 본사가 광고/공지 음원(CM송)을 등록· 관리하고, 점장 player 의 음악 큐 사이에 주기적으로 삽입 재생할 수 있는 백본을 만든다. 점장 player 의 CM송 사이클 삽입은 F1 후속.
Overview
본사(HQ_MANAGER) 가 apps/space 본사 모드에서 광고/공지 음원(CM송)을 직접 생성·조회·편집·삭제
한다. 가시 범위는 본인 본사의 활성 CM송만(다른 본사 CM송 비노출, BE D3). 본사 안내방송(#061)이
“TTS 합성으로 음원을 만들어 매장에 송출” 한다면 CM송은 “사전 업로드된 광고/공지 음원을 점장 player
큐 사이에 사이클 삽입” 하는 다른 도메인이다(F1 후속에서 재생기 연동).
시안 출처: workspace parent dir — hq-commercial 전용 시안 부재로 본사 모드 기존 화면 (
design/screens/hq-stores.jsx테이블·검색·페이지네이션, hq-library 목록 #080) 스타일과 일관 (임의 디자인 금지). 검색·페이지네이션은 공용ListToolbar/ListPagination(본사/stores#051 관용구), 폼 입력은Field+Input(본사 안내방송 #061 idiom) 미러. 상세는 본사 매장 상세(#084)DescCard+ 본사 안내방송(#061)<audio controls>미니 플레이어 idiom 미러.
목록 (/admin/commercials)
apps/space/src/app/admin/commercials/page.tsx(server 셸) + hq-commercial-list-client.tsx(client).
본사 라이브러리(#080)와 동일 이유로 서버사이드 페이지네이션 + client-query
(useListHqCommercials) — q(제목)·isActive(활성/비활성/전체)·page·size 를 client state 로
보유한다. 정렬은 서버 고정(created_at DESC, id ASC, BE D5).
- 헤더: 제목 “CM송” + [새 CM송 등록] →
/admin/commercials/new. - 툴바: 공용
ListToolbar— 검색(제목, ≤100) + 활성 필터(활성/비활성/전체) + 적용/초기화. - 행: 제목(셀 클릭 →
/admin/commercials/{id}) · 재생 시간(mm:ss포맷) · 활성 배지 (StatusPill— 활성=success/비활성=muted) · 등록 시각(KST) · 수정 시각(KST). - 빈 상태: 검색/필터 0건(
ListNoResultsCTA) vs 진짜 빈 목록(“CM송이 없습니다.”). - 페이지네이션: 공용
ListPagination— 총 N개 + 이전/다음(경계 disabled).
등록 (/admin/commercials/new)
apps/space/src/app/admin/commercials/new/page.tsx(server 셸) + hq-commercial-new-client.tsx(client).
useCreateHqCommercial mutation. 폼 3 필드(BE D2/D6 OpenAPI 제약 미러):
- 제목 — text input, 1~200자.
- 음원 URL —
type="url"input, ≤2048자. 음원 업로드 UI 는 후속(F) — 본 슬라이스는 공개 접근 가능한 URL(예: Azure blob)을 사용자가 직접 붙여넣는다(SPEC §D9 가드). - 재생 시간 (초) —
type="number"input, 1~3600 정수. mm:ss 보조 표시(약 1:30 (90초)).
빈 필드·범위 위반은 클라이언트에서 disabled 로 차단해 불필요한 BE 호출을 절약한다(frontend.md §8).
성공 시(201 HqCommercialDetailResponse) id 로 /admin/commercials/{id} push — 사용자가 등록 직후
미리듣기·편집·삭제로 즉시 이동. 실패는 code→메시지 매핑(403 권한 / 400 입력값 / 5xx 서버 / BACKEND_UNREACHABLE).
상세 (/admin/commercials/[id])
apps/space/src/app/admin/commercials/[id]/page.tsx(server 셸) + hq-commercial-detail-client.tsx
(client). useGetHqCommercialDetail(id) + useUpdateHqCommercial() + useDeleteHqCommercial() 3 훅.
4 섹션:
- 정보(DescCard) — 제목 · 재생 시간(
mm:ss + 초) · 활성 상태 · 음원 URL(<a target="_blank">) · 생성/수정 시각(KST). - 미리듣기 —
<audio controls preload="metadata" src={audioUrl}>(본사 안내방송 #062announcement-row-player미니 패턴 미러). cache-buster?v={updatedAt}는 audioUrl 자체가 바뀌지 않는 본 SPEC 에선 불필요. - 편집 form — 제목 input(1~200자) + 활성 토글(
SwitchRadix). [저장] →useUpdateHqCommercial. PATCH 가title?: string|null·isActive?: boolean|null(null=미변경, BE D2)이라 변경 필드만 보내도 되지만 항상 두 필드 다 전송해 단순화(BE D6 검증 통과 + DB dirty-checking 으로 실제 UPDATE 절약). 저장 성공 시 상세·목록 캐시 모두 invalidate. - 삭제 — 빨간 [삭제] 버튼 + 2-step 인라인 confirm strip (”…CM송을 삭제합니다. 이 작업은
되돌릴 수 없습니다.” + [취소]/[삭제 확정]). 시안 부재로 본사 안내방송
DeleteAnnouncementDialog(2-step)의 변형 inline 미러. 성공 시 캐시 invalidate +/admin/commercialspush, 404COMMERCIAL_SONG_NOT_FOUND(이미 삭제됨) 도 동일 처리.
상태:
- 로딩: 3-block 스켈레톤(본사 매장 상세 #084 미러).
- 404
COMMERCIAL_SONG_NOT_FOUND(타 본사·미존재 모두 은닉): Banner danger “CM송을 찾을 수 없습니다.” - 4xx/5xx/네트워크: Banner danger 일반 메시지.
사이드바
HQSidebar 의 “CM송” 항목이 enabled(SPEC #093). placeholder 경로 /admin/cm 가 본 슬라이스에서
/admin/commercials 로 정렬됐다(라이브러리 #080 의 /admin/library → /admin/libraries 정렬과 동형).
icon Volume2 (lucide) 유지.
인가 / endpoint
5 endpoint 전부 HQ_MANAGER-only — /api/v1/hq/** prefix 매처 → hasRole("HQ_MANAGER") 1차 경계 +
service verifyHqScope claim↔DB 재검증. 미인증 401 · 비활성/role·소속 불일치 403
PRINCIPAL_SCOPE_MISMATCH. 모든 호출은 BFF catch-all /api/backend/... 경유(토큰 서버 전용).
| Method | Path | operationId | 비고 |
|---|---|---|---|
| GET | /api/v1/hq/commercials | listHqCommercials | q?·isActive?·page·size. 정렬 created_at DESC, id ASC |
| POST | /api/v1/hq/commercials | createHqCommercial | body CreateHqCommercialRequest. 201 + HqCommercialDetailResponse |
| GET | /api/v1/hq/commercials/{id} | getHqCommercialDetail | 단건 — 404 COMMERCIAL_SONG_NOT_FOUND(타 본사/미존재 은닉) |
| PATCH | /api/v1/hq/commercials/{id} | updateHqCommercial | body UpdateHqCommercialRequest(title/isActive 각 null=미변경). 두 필드 모두 null = no-op 200 |
| DELETE | /api/v1/hq/commercials/{id} | deleteHqCommercial | soft-delete 204 |
자세한 schema 는 DTOs · Hq Commercial DTOs · endpoint 카탈로그는 Endpoints · HQ Mode 참조.
점장 player 자동 재생 (SPEC #094 · #095 · #103 · #104)
본사가 등록한 활성 CM송은 점장 player(/store)의 음악 큐 사이에 effective 빈도 N곡마다 1회
(본사 default + 매장 override) 자동 삽입 재생된다. 신규 endpoint
getStoreNextCommercial(GET /api/v1/store/commercial-song/next, STORE_MANAGER-only) — 본인 매장
본사의 활성 CM 중 매장 단위 라운드로빈 다음 1건(SPEC #104, V34 store.last_commercial_song_id
기반 — id > last 다음 row 또는 wrap-around 로 첫 row, 다음 호출용 last 갱신) 또는 204(CM 미등록).
점장 player 가 음악 곡 ended 카운터가 effective 빈도에 도달하면 imperative 호출 → 200 시 CM 삽입
재생 + ended 후 카운터 리셋 + 다음 음악, 204/실패 시 silent + 다음 음악. 인터럽트 우선순위는
긴급 > 일반 안내방송 > CM(CM 재생 중 안내방송 도착 시 CM 폐기 후 안내방송 우선). 자세한
재생 흐름은 Store Player Home — CM송 사이클
참조.
Followups
F1 본사 사이클 빈도 설정✅ SPEC #095 완료(hq.commercial_cycle_songsV32 + 본사 UI).F2 매장별 사이클 빈도 override✅ SPEC #103 완료(store.commercial_cycle_songsV33 + 본사 매장 상세 편집).F3 CM 라운드로빈 정책✅ SPEC #104 완료(store.last_commercial_song_idV34 매장 단위 순환 — 랜덤에서 결정적 라운드로빈으로 전환, 같은 CM 연속·1건 미노출 문제 해소).- F4 CM송 라이브러리 묶음 — 음원/플레이리스트 2계층의 별도 카테고리화(라이브러리 ↔ CM송 매핑).
- F5 audit 누적 —
HQ_COMMERCIAL_*액션(HqAuditAction확장)으로 본사 audit 에 합류.