FeaturesHQ (본사)HQ Mode 플레이리스트 조회 (apps/space /admin/playlists)

HQ Mode — 본사 플레이리스트 조회 (apps/space /admin/playlists, read-only)

SPEC #057 도입 (read 슬라이스) · #058 기본 PL 배지 추가. 실 HQ_MANAGER 직접 로그인 표면(apps/space · space.linkmusic.io) — 운영사 임퍼소네이션(apps/admin)이 아니다.

이 페이지는 apps/space 의 실 HQ_MANAGER 본사 모드다. 운영사(OPERATOR)가 본사를 관리하는 apps/admin(onboarding·list·detail·impersonation)과는 별개다. 음악 2계층은 “운영사 편집 · 본사 조회”가 모델 불변식 — 본 슬라이스가 그 조회 쪽을 채운다(편집은 운영사 전용).

Overview

HQ_MANAGER 가 apps/space 본사 모드에서 자기 본사(hqId)의 플레이리스트 목록·상세를 조회한다 (read-only). 각 PL 의 라이브러리 수 + 적용 매장 수(자기 본사 매장 중 그 PL 을 활성으로 쓰는 수)를 함께 본다. 전부 hqId 스코프(본인 본사만 — 토큰 주체에서 도출, 요청 파라미터 없음, BE verifyHqScope. 타 본사 PL 접근 불가). 생성/편집/삭제/적용은 운영사(apps/admin) 전용 — 본사 모드는 mutating 진입점이 없다.

시안 출처: workspace parent dir — hq-playlist 전용 시안 부재로 본사 모드 기존 화면 (design/screens/hq-stores.jsx 테이블·검색·페이지네이션) 스타일과 일관(임의 디자인 금지). 검색·페이지네이션은 공용 ListToolbar/ListPagination(본사 /stores #051 관용구)으로 정합.

목록 (/admin/playlists)

apps/space/src/app/admin/playlists/page.tsx(server 셸) + hq-playlist-list-client.tsx(client). 본사 /stores(#051)와 동일 이유로 서버사이드 페이지네이션 + client-query(useListHqPlaylists) — q(이름)·page·size 를 client state 로 보유한다. 정렬은 서버 고정(created_at DESC, id DESC).

  • 툴바: 공용 ListToolbar — 검색(플레이리스트명, ≤100) + 적용/초기화. read-only 라 필터·생성 버튼 없음.
  • : 플레이리스트명(상세로 링크)·상태 배지(#060 read-only)·라이브러리 수(libraryCount)·적용 매장 수(appliedStoreCount)·수정일(updatedAt, KST).
  • 상태 배지(#060): 파생 status(EMPTY/UNUSED/ACTIVE/FALLBACK)를 공용 StatusPill 로 read-only 노출(사용 중/기본/미사용/비어있음 — 라벨 항상 병기). 라벨·톤은 본사 단일 소스 playlist-status-meta.ts + HqPlaylistStatusBadge(운영사와 라벨/톤 일치). status=FALLBACK 이면 상태 배지가 “기본” 을 표현하므로 별도 isDefault “기본” 배지(#058)는 숨기고, EMPTY 등 그 외 status 인 기본 PL 은 “기본” 배지를 함께 노출.
  • 빈 상태: 검색 0건(ListNoResults CTA) vs 진짜 빈 목록 구분.
  • 페이지네이션: 공용 ListPagination — 총 N개 + 이전/다음(경계 disabled).
  • read-only: 생성/편집/삭제/적용·기본 지정 진입점 없음(기본 PL 지정은 운영사 전용 — #058). 행 클릭 = 상세 진입(유일한 액션). 상태·기본 배지는 조회 전용.

상세 (/admin/playlists/[id] — server component)

[id]/page.tsx(server) + hq-playlist-detail-client.tsx(client). getHqPlaylist (GET /api/v1/hq/playlists/{id})를 server component 에서 refresh-aware (loadHqPlaylistRefreshAware)로 fetch 해 client 에 prop 으로 내린다(토큰 서버 전용).

  • 404 PLAYLIST_NOT_FOUND(타 본사 PL·미존재 — 존재 은닉) → Next notFound().
  • 401 + refresh 실패/login fail-closed.
  • 5xx·네트워크 일시 장애(data:null) → 셸 유지 + 본문 재시도 배너.
  • 헤더 배지: 제목 옆에 read-only 상태 배지(#060) StatusPill(EMPTY/UNUSED/ACTIVE/FALLBACK). status=FALLBACK 이면 상태 배지가 “기본” 을 표현하므로 별도 isDefault “기본” 배지(#058)는 숨기고, 그 외 status 인 기본 PL 은 “기본” 배지를 병치. 지정/해제는 운영사 전용.
  • 요약 카드: 라이브러리 수 · 적용 매장 수 · 마지막 수정 시각.
  • 담긴 라이브러리 목록: libraries: HqPlaylistLibraryItem[](server 가 position 순 정렬) — 순서·이름·타입(AI·TRUST StatusPill)·곡 수(musicCount).
  • read-only(라이브러리 표): 추가/제거/순서/이름수정/삭제·기본 지정 진입점 없음(라이브러리 편성은 운영사 전용). 시간표는 편집 가능(아래).
  • [라이브러리][시간표] 탭바(#171) — 상세 헤더 아래에 탭바를 두어 라이브러리 표(읽기 전용)와 시간표 편집 뷰를 전환한다(아래).

시간표 편집 (#171)

[시간표] 탭 → 본사 기본 시간표를 본사(HQ_MANAGER)가 직접 편집한다(운영사 admin PUT 과 같은 본사 기본 시간표를 수정 — 계약 동일). 시안 출처: workspace parent dir design_handoff_linkmusic_18/design/screens/ops-playlist-schedule.jsx(ops 모드).

  • 조회 useGetHqPlaylistSchedule(id)(GET /api/v1/hq/playlists/{id}/schedule) → PlaylistScheduleResponse → 그리드 draft. 404 PLAYLIST_NOT_FOUND(타 본사·미존재 은닉).
  • 저장 useSetHqPlaylistSchedule(id)(PUT /api/v1/hq/playlists/{id}/schedule, body SetPlaylistScheduleRequest) → 전체 교체(원자적 대체·부분 갱신 아님). 빈 배열 허용 = 시간표 해제(전 시간대 전체 셔플) — 운영사 admin PUT 과 동일(@minItems 0 · @maxItems 48). 성공 시 getGetHqPlaylistScheduleQueryKey invalidate + 성공 배너.
  • 공유 편집기 ScheduleGridEditor(surface=“ops”) — 브러시 팔레트(담긴 라이브러리) + 그리드 페인팅 + 저장 바([변경 취소][저장]). 팔레트 = 상세 libraries(+ stale 이름은 entries.libraryName 으로 보강 렌더만, 저장 시 팔레트 밖 libraryId 는 걸러 자연 제거 — SCHEDULE_LIBRARY_NOT_MEMBER 400 예방).
  • is_default PL 파급 경고: 이 PL 이 본사 기본이면 여기서 정한 시간표가 산하 매장의 기본 재생 스케줄이 된다(점장이 커스텀한 매장은 매장 override 우선 — 영향 없음). 운영사 탭과 동일 문구.
  • 저장 중 잠금(frontend.md #18): 저장 PUT 진행 중에는 탭 전환·뒤로가기 Link 를 잠근다(진행 중 언마운트로 결과 배너 유실·중복 요청 방지). 진행 신호는 자식 → 부모 lift.
  • 저장 에러 매핑(code 우선·status fallback): 409 SCHEDULE_TIME_OVERLAP·400 SCHEDULE_INVALID_BOUNDS/SCHEDULE_LIBRARY_NOT_MEMBER·404 PLAYLIST_NOT_FOUND·403·5xx·BACKEND_UNREACHABLE.
  • 매장별 조정은 점장 apps/space /store/schedule 소관(매장 override).

사이드바

HQSidebar 의 “플레이리스트”(/admin/playlists) 항목이 enabled(활성 라우팅) — #057 에서 disabled “준비 중” 에서 전환.

인가

/api/v1/hq/**hasRole("HQ_MANAGER") 1차 경계 + service verifyHqScope claim↔DB 재검증 (#049/#051 재사용). 미인증 401 · 비활성/role·소속 불일치 403 PRINCIPAL_SCOPE_MISMATCH. 목록 fetch 는 generated apiFetch 가 BFF catch-all /api/backend/... 경유(토큰 서버 전용).

Followups

  • 임퍼소네이션 readonly 토글(#057 §F1) — 운영사가 본사로 임퍼소네이트 시 편집 가능 토글.
  • 매장별 활성 PL 현황(#057 §F2) — 산하 매장 목록에 각 매장 활성 PL 컬럼.
  • 상태 파생 4종 ✅ 완료(#060)PlaylistStatus{EMPTY,UNUSED,ACTIVE,FALLBACK} read-only 상태 배지(목록·상세 헤더). MISMATCH(플랜 불일치)만 잔여(#060 F1 — 플랜 게이팅 후속).