FeaturesHQ (본사)HQ Mode CM송 관리 (apps/space /admin/commercials)

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건(ListNoResults CTA) 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자.
  • 음원 URLtype="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 섹션:

  1. 정보(DescCard) — 제목 · 재생 시간(mm:ss + 초) · 활성 상태 · 음원 URL(<a target="_blank">) · 생성/수정 시각(KST).
  2. 미리듣기<audio controls preload="metadata" src={audioUrl}> (본사 안내방송 #062 announcement-row-player 미니 패턴 미러). cache-buster ?v={updatedAt} 는 audioUrl 자체가 바뀌지 않는 본 SPEC 에선 불필요.
  3. 편집 form — 제목 input(1~200자) + 활성 토글(Switch Radix). [저장] → useUpdateHqCommercial. PATCH 가 title?: string|null·isActive?: boolean|null (null=미변경, BE D2)이라 변경 필드만 보내도 되지만 항상 두 필드 다 전송해 단순화(BE D6 검증 통과 + DB dirty-checking 으로 실제 UPDATE 절약). 저장 성공 시 상세·목록 캐시 모두 invalidate.
  4. 삭제 — 빨간 [삭제] 버튼 + 2-step 인라인 confirm strip (”…CM송을 삭제합니다. 이 작업은 되돌릴 수 없습니다.” + [취소]/[삭제 확정]). 시안 부재로 본사 안내방송 DeleteAnnouncementDialog (2-step)의 변형 inline 미러. 성공 시 캐시 invalidate + /admin/commercials push, 404 COMMERCIAL_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/... 경유(토큰 서버 전용).

MethodPathoperationId비고
GET/api/v1/hq/commercialslistHqCommercialsq?·isActive?·page·size. 정렬 created_at DESC, id ASC
POST/api/v1/hq/commercialscreateHqCommercialbody CreateHqCommercialRequest. 201 + HqCommercialDetailResponse
GET/api/v1/hq/commercials/{id}getHqCommercialDetail단건 — 404 COMMERCIAL_SONG_NOT_FOUND(타 본사/미존재 은닉)
PATCH/api/v1/hq/commercials/{id}updateHqCommercialbody UpdateHqCommercialRequest(title/isActive 각 null=미변경). 두 필드 모두 null = no-op 200
DELETE/api/v1/hq/commercials/{id}deleteHqCommercialsoft-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_songs V32 + 본사 UI).
  • F2 매장별 사이클 빈도 override ✅ SPEC #103 완료(store.commercial_cycle_songs V33 + 본사 매장 상세 편집).
  • F3 CM 라운드로빈 정책 ✅ SPEC #104 완료(store.last_commercial_song_id V34 매장 단위 순환 — 랜덤에서 결정적 라운드로빈으로 전환, 같은 CM 연속·1건 미노출 문제 해소).
  • F4 CM송 라이브러리 묶음 — 음원/플레이리스트 2계층의 별도 카테고리화(라이브러리 ↔ CM송 매핑).
  • F5 audit 누적HQ_COMMERCIAL_* 액션(HqAuditAction 확장)으로 본사 audit 에 합류.