FeaturesHQ (본사)HQ Mode 대시보드·산하 매장 (apps/space /admin)

HQ Mode — 본사 대시보드 + 산하 매장 목록 (apps/space /admin)

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

이 페이지는 apps/space 의 실 HQ_MANAGER 본사 모드다. features/hq/* 의 나머지 페이지(onboarding·list·detail·impersonation)는 운영사(OPERATOR)가 본사를 관리하는 apps/admin 화면이며 별개다.

Overview

HQ_MANAGER 가 apps/space 본사 모드(/admin)에서 본인 본사 대시보드(산하 매장 상태 요약)산하 매장 목록(검색·필터·페이지네이션) 을 본다. 전부 hqId 스코프(본인 본사만 — 토큰 주체에서 도출, 타 본사 접근 불가). #050 이 셸·로그인·topbar(getHqMe)까지 부팅했고, #051 이 /admin 대시보드 placeholder 를 실데이터로, /admin/stores 산하 매장 화면을 신규 추가한다.

시안 출처: workspace parent dir design_handoff_linkmusic/design/screens/hq-dashboard.jsx (StatCard 그리드)·hq-stores.jsx(테이블·필터·페이지네이션). 검색·필터·페이지네이션은 공용 ListToolbar/ListPagination(운영사 /stores #044 관용구)으로 정합.

대시보드 (/admin — server component)

apps/space/src/app/admin/page.tsx. getHqDashboard(GET /api/v1/hq/dashboard)를 server component 에서 refresh-aware(loadHqDashboardRefreshAware)로 호출한다. 401+refresh 실패 → /login fail-closed. 5xx·네트워크 일시 장애(data:null) → 셸은 유지하고 본문은 공용 ErrorState 재시도(RetryErrorState client 아일랜드=router.refresh(), hq-dashboard-error, SPEC #154).

카드데이터
산하 매장totalStores + 상태별 배지(활성 activeStores·비활성 inactiveStores·정지 suspendedStores). 클릭 → /admin/stores
폐점 매장closedStores (closedAt 설정, 전체 합계 포함)
오늘 송출 미도달 매장실데이터(SPEC #119 F3·D4 · #162 H11)getHqUndeliveredToday(HqUndeliveredTodayResponse.undeliveredStoreCount)를 60초 폴링(refetchInterval:60s·refetchIntervalInBackground:false). 오늘(KST) 본사 송출 중 미도달(status IN (PENDING, MISSED)) distinct 매장 수 — MISSED(grace 초과 자동 만료 · 점장 player 의 재생 실패·폐기 보고)도 “실제로 들리지 않은” 것이라 미도달로 센다(감사 #11). 점장이 직접 보낸 즉시방송·예약은 제외(is_hq_origin=true — 이 카드는 본사 송출의 도달률 지표다, 감사 #11-a). 목록 다이얼로그도 완전히 같은 조건이라 total == 카드 숫자가 정합한다. count>0 면 danger 강조(카드를 버튼화 → 클릭 시 미도달 매장 드릴다운 다이얼로그 UndeliveredTodayDialog 오픈: getHqUndeliveredTodayStores 로 목록 로드·행별 매장 상세 링크·로딩/에러 ErrorState 재시도/빈 상태·페이지네이션), 0 이면 “모두 도달” 비강조·비링크(false affordance 제거). UndeliveredTodayCard·UndeliveredTodayDialog client island(apps/space/src/app/admin/undelivered-today-card.tsx·undelivered-today-dialog.tsx, data-testid=hq-dash-card-undelivered·hq-undelivered-dialog). SPEC #162 이전에는 카드가 /admin/audit(delivery 필터 없는 송출 행위 로그)로 보내 “어느 매장인지” 볼 수 없던 dead-end(감사 H11) 였다. 배지/카드 시각은 atom-grounded(전용 시안 부재, design-debt 등재).
재생 상태실데이터(SPEC #172 FE-B)getHqPlaybackStatus(HqPlaybackStatusResponse)를 60초 폴링(refetchInterval:60s·refetchIntervalInBackground:false — 운영자가 화면을 볼 때만 의미 있는 지표). “지금 재생 중 N/전체 · 무음 M · offline K”. 미도달 카드(방송 도달률)와 별개인 음악 재생 축 — 미도달 옆에 나란히 둔다(두 축: 방송·음악). N=playingCount(점장 player 가 PLAYING 보고)·M=silentCount(재생 대상 없음/무음)·K=offlineCount(서버가 마지막 heartbeat staleness 로 파생 — 점장이 보고하지 않는 값). ⚠️ PAUSED 는 세 카운트 밖이라 N+M+K != total 가능(합계 아님 — “전체 중 재생 중 N” 으로만 문구화). 무음·offline>0 이면 warn 강조(보더). 매장이 1곳이라도 있으면(total>0) 카드를 버튼화해 클릭 시 PlaybackStatusDialog 드릴다운을 연다 — 전부 재생 중이어도 목록을 열 수 있다(사용자 요청). 다이얼로그는 카드 query 의 전체 매장 items별도 fetch 없이 props 로 받아 상태별 그룹(확인 필요=무음·offline 먼저, 그다음 재생중·일시정지 — hq-playback-group-{SILENT|OFFLINE|PLAYING|PAUSED})으로 렌더한다(매장명·상태·마지막 접속(KST)·현재곡·행별 매장 상세). 헤더는 전체·재생중·일시정지·무음·offline 카운트를 요약(재생중+일시정지+무음+offline==전체 자기정합). 카드 폴링 갱신 시 다이얼로그도 최신. 매장 0곳이면 드릴다운 대상이 없어 비버튼 div(false affordance 제거). PlaybackStatusCard·PlaybackStatusDialog client island(apps/space/src/app/admin/playback-status-card.tsx·playback-status-dialog.tsx, data-testid=hq-dash-card-playback·hq-playback-dialog). 적재는 점장 player 의 POST /api/v1/store/playback/report(→ 기기 보고면 store_device_playback_status, 아니면 store_playback_status). ⚠️ SPEC #178 — 집계가 기기 단위로 바뀌었다: 기기 행이 하나라도 있는 매장은 기기들로 접고(PLAYING > PAUSED > SILENT > OFFLINE 우선순위 = “하나라도 재생 중이면 그 매장은 재생 중”), 기기 행이 없는 매장만 종전 매장 행으로 폴백한다. 각 기기는 먼저 자기 lastHeartbeatAt staleness 로 OFFLINE 이 파생되므로 꺼진 PC 한 대가 매장 전체를 OFFLINE 으로 끌어내리지 않고 stale 한 PC 가 PLAYING 을 위조하지도 않는다. lastSeenAt = 기기들 중 가장 최근 신호 · 현재 곡 = 그 상태를 결정한 기기 중 첫 행(last_heartbeat_at DESC 정렬이라 결정적 · 곡 없는 기기가 앞서면 곡 있는 같은 상태 기기를 우선 · OFFLINE 이면 감춤). 회수된 기기의 유령 행이 섞이지 않게 store_device 조인 + deleted_at IS NULL 로 거른다. 종전엔 store_playback_statusstore_id PK 라 마지막에 보고한 기기가 앞 기기를 덮어써, 본사가 새로고침할 때마다 같은 매장이 PLAYING↔PAUSED↔SILENT 를 왕복하고 현재 곡도 튀었다(“저 매장 음악 안 틀고 있다” 오판 → 불필요한 CS). 카드 footer 에 [기간별 보기] 링크(SPEC #175, hq-dash-playback-report-link/admin/playback) — “지금”(카드)과 “일/주/월 누적 조망”(재생 리포트)의 두 축을 잇는다. 배지/카드 시각 atom-grounded(전용 시안 부재, design-debt 등재).
활성 CM송실데이터useListHqCommercials({ isActive: true }) 의 envelope total(활성 CM송 수)을 60초 폴링(refetchIntervalInBackground:false). 클릭 → /admin/commercials. 종전 “준비 중” placeholder 를 CM송 관리 실데이터로 교체. ActiveCommercialsCard client island(active-commercials-card.tsx, data-testid=hq-dash-card-commercials·count hq-dash-commercials-count).
미해결 매장 문의실데이터getHqStoreSupportUnreadSignal.openOrInProgressCount(산하 매장 CS 미해결 OPEN+IN_PROGRESS 수, 사이드바 “매장 문의” 배지와 같은 signal·#173)를 60초 폴링. 미해결>0 이면 warn pill(“처리 대기”), 0 이면 success(“모두 처리됨”). 클릭 → /admin/store-support(CS 는 0 이어도 목록 진입 유효 → 링크 유지). 종전 “대기 문의” 준비 중 placeholder 를 실데이터로 교체(대시보드에서 “매장 문의 있는데 준비중” stale 해소). OpenStoreSupportCard client island(open-store-support-card.tsx, data-testid=hq-dash-card-store-support·count hq-dash-store-support-count).

bootstrap 진척 체크리스트 (SPEC #120 §D1 — #118 CTA 확장): 카드 그리드 위에 본사 첫 진입 “무엇부터 시작하면 좋을까요?” 진척 체크리스트를 노출한다(hq-dashboard-bootstrap). #118 의 단순 빈상태 CTA 를 완료/미완료 진척형으로 확장(중복 표시 X):

  • ① 매장 등록 — 산하 매장 수(totalStores) > 0 이면 완료(Check 아이콘), 아니면 [매장 등록] (hq-dashboard-bootstrap-create/admin/stores/new) CTA.
  • ② 안내방송 만들기 — 본사 안내방송 수 > 0 이면 완료, 아니면 [안내방송 만들기] (hq-dashboard-bootstrap-announcement/admin/announcements) CTA.

두 단계 모두 완료면 자동 숨김(노이즈 방지). 새 상태/BE 0(D1): 매장 수는 server 대시보드 데이터로, 안내방송 수는 안내방송 목록과 동일한 generated query(useListHqTtsAnnouncements total)로 파생한다. server data 엔 매장 수만 있으므로 체크리스트는 client island(bootstrap-checklist.tsx)이며, 안내방송 query 가 로딩/에러면 ② 를 미완료(항상-CTA)로 보수 판정한다(폴백). #119 UndeliveredTodayCard·기존 카드와 충돌 없이 빈 영역에 배치. 체크 아이콘·버튼·카드는 atom-grounded(전용 시안 부재, design-debt 등재).

산하 매장 목록 (/admin/stores)

page.tsx(server 셸) + hq-store-list-client.tsx(client). 운영사 /stores(#044)와 동일 이유로 서버사이드 페이지네이션 + client-query(useListHqStores) — q(매장명·주소)·status·type·page·size 를 client state 로 보유한다. 정렬은 서버 고정(status 우선순위→name asc→id asc).

  • 툴바: 공용 ListToolbar — 검색(매장명·주소, ≤100)·유형(ListHqStoresType)·상태(ListHqStoresStatus점장 계정(hasManagerAccount, SPEC #184 D8) 필터 + 적용/초기화.
  • 헤더 액션(SPEC #106·#111): 우측에 [CSV 일괄 등록] 버튼(ghost, hq-store-list-bulk) → /admin/stores/bulk + [+ 매장 등록] 버튼(primary, hq-store-list-create) → /admin/stores/new. #084 F2(단일)·F3(CSV 일괄) 마감 — 본사가 산하 매장을 직접 등록한다(사이드바 신설 회피, 목록 컨텍스트 유지). 권한 가드는 layout(server)이 HQ_MANAGER-only 로 처리.
  • : 매장명(+폐점 배지 if closedAt)·상태(StatusPill)·유형·주소·담당자(managerName)·점장 계정(hasManagerAccount → 발급됨/미발급).
  • “계정 미발급” 필터(SPEC #184 D8): select 값은 ""(전체)·미발급(=false발급됨(=true) 이고 params 로 boolean 변환한다. 운영사 목록과 동일 술어(status <> WITHDRAWN)라 점장 계정을 회수한 매장이 다시 “미발급” 으로 잡힌다. 여기서 색출한 매장은 매장 상세의 [점장 발급](SPEC #184 D4)로 사후 발급한다 — 사후 발급 경로가 생겼기 때문에 이 필터가 의미를 갖는다.
  • 빈 상태: 필터 0건(ListNoResults CTA) vs 진짜 빈 목록 구분. 진짜 빈 목록(hq-store-list-empty)에는 bootstrap CTA(SPEC #118 §D3 / H1) — “무엇부터” 안내 + [매장 등록](hq-store-list-empty-create/admin/stores/new)·[안내방송 만들기](hq-store-list-empty-announcement/admin/announcements)를 본문에 노출한다(헤더 진입점을 빈 본문에서 한 번 더 명확히 — dead-end 완화).
  • 페이지네이션: 공용 ListPagination — 총 N개 + 이전/다음(경계 disabled).
  • 상세 진입점(SPEC #084): 매장명 셀 → /admin/stores/[id] (hq-store-link-{storeId}). HQ Mode 매장 상세 조회 페이지가 5 섹션(개요/점장 정보/활성 PL/운영 상태/메타)로 조회 + 매장 정보 인라인 편집(#105)을 제공. 개요는 담당자 연락 정보(managerName·managerEmail·phone, #116)를 노출하고 편집 폼은 그 현재값을 prefill 한다.

신규 매장 등록 (/admin/stores/new, SPEC #106)

page.tsx(server 셸) + hq-store-create-form.tsx(client). 본사 매장 편집(#105) 폼 idiom 미러 + CM송 등록(#093) 헤더/취소·등록 idiom 미러 — 단일 단계 폼으로 5 필드(name·address·managerName·managerEmail·managerPhone)를 입력하면 POST /api/v1/hq/stores(operationId createHqStore) 가 type 을 본사 유형으로 자동 결정해 201 + CreateHqStoreResponse{store, managerAccount?} envelope(SPEC #184 breaking — 이전 flat HqStoreDetailResponse)를 반환한다. 성공 시 ["/api/v1/hq/stores"] prefix 캐시 invalidate + 응답 store.id/admin/stores/{id} push(예전 data.id 접근을 그대로 두면 /admin/stores/undefined 로 깨진다).

점장 계정 통합 (SPEC #184 D1·D4)

managerEmail 은 더 이상 연락처가 아니라 점장 로그인 ID 다. 채우면 매장 생성과 같은 트랜잭션에서 STORE_MANAGER 계정이 만들어지고 계정 설정(setup) 메일이 그 주소로 발송된다. 임시 비밀번호는 서버 생성이며 응답에 노출하지 않는다(D6). 폼에는 이 사실을 명시하는 안내 블록(hq-store-create-manager-note)이 있고 라벨도 “점장 이메일 (로그인 ID)” 다. 비워두면 “점장 미정” 매장으로 등록되고(D2), 이후 매장 상세의 [점장 발급]으로 발급한다.

응답화면
managerAccount == null 또는 setupEmailSent !== false곧바로 /admin/stores/{store.id} push
managerAccount.setupEmailSent === falsepush 하지 않고 danger 화면(hq-store-create-setup-email-failed) — 계정은 있으나 임시 비밀번호 미노출로 진입 경로가 없다. 재발송은 OPERATOR-only 라 “운영사에 재발송 요청” 을 안내하고 [매장 상세 보기]/[매장 목록으로] 를 남긴다
  • 필수 표시: name 만 필수(Field required). 나머지 4 필드는 선택 — 빈 입력 → null payload(단순 nullable, #105 의 JsonNullable 분기 없음 — 신규 등록은 clear vs unchanged 구분 불필요).
  • 클라 검증(D4 backend OpenAPI 미러): name 1..50 비-blank · address ≤200 · managerName ≤50 · managerEmail @Email + ≤255 · managerPhone ≤30. 위반 시 Field error 노출 + [등록] disabled. 클라가 미리 reject 해 BE 호출 절약(frontend.md §8).
  • a11y: Field 가 라벨 + helper/error 슬롯 제공(role=alert error · helper id 자동 부여 ${htmlFor}-helper). 입력에 aria-invalid + aria-describedby 연관. Banner danger 는 role=alert (Banner 기본).
  • 취소: router.back() 우선, 히스토리 비어 있으면 /admin/stores push.
  • 에러 매핑: Banner tone="danger" (hq-store-create-error) — 400 HQ_STORE_INVALID_FIELD=“입력값이 올바르지 않습니다.” / 403 AUTH_HQ_SUSPENDED=“정지된 본사는 매장을 등록할 수 없습니다.” / 401·403 일반=“접근 권한이 없습니다.” / 429 RATE_LIMITED=“요청이 너무 잦습니다…”(SPEC #184 — STORE_PROVISION 20/분) / 5xx=“서버 오류가 발생했습니다.” / BACKEND_UNREACHABLE=“서버에 연결할 수 없습니다.” / 그 외=“매장 등록에 실패했습니다.” Banner 는 role="alert" — 제출 실패·“이 계정은 지금 로그인할 수 없습니다” 급 안내는 polite 로 두지 않는다.
  • 409 DUPLICATE_EMAIL(SPEC #184 D9): 상단 Banner 가 아니라 이메일 필드 에러로 되돌린다 — 매장 생성도 함께 롤백됐으므로 “매장은 등록됐다” 류 안내를 절대 띄우지 않고, 폼에 그대로 머물러 이메일만 고쳐 재제출하게 한다. 필드에 aria-invalid 부여.
  • audit 자동: BE 가 같은 트랜잭션으로 HqAuditAction.HQ_STORE_CREATED + HqAuditTargetType.STORE 1행 기록(detail {name, type, address, managerName} partial 스냅샷). 계정을 함께 발급하면 HQ_STORE_MANAGER_ISSUED 가 별도 row 로 추가된다(SPEC #184 D5). 본사 감사 뷰·운영자 본사 감사 뷰 모두 “매장 등록”·“매장 점장 계정 발급”(info) 으로 노출.

CSV 일괄 등록 (/admin/stores/bulk, SPEC #111, #084 F3 마감)

page.tsx(server 셸) + bulk-client.tsx(client). #106 단일 등록을 행 단위로 미러한다(영업시간은 후속). SPEC #184 — “담당자이메일” 컬럼이 점장 로그인 ID 로 승격됐다: 채워진 행은 매장 + 점장 계정이 함께 생성되고 setup 메일이 발송된다(임시 비밀번호는 서버 생성 · 응답 미노출 — D6). rate limit 은 단건과 분리된 BULK_PROVISION 3회/분. CSV 파일을 업로드하면 POST /api/v1/hq/stores/bulk(operationId bulkCreateHqStores, multipart file) 가 행별로 독립 처리해 200 + BulkCreateHqStoreResponse(부분 실패 결과 리포트)를 반환한다.

  • CSV 형식 안내: 페이지에 5컬럼 헤더 예시 노출(hq-store-bulk-csv-header) — 매장명,주소,담당자명,담당자이메일,담당자전화(UTF-8, 헤더 정확 일치). 샘플 다운로드는 후속(텍스트 안내로 MVP). SPEC #184 안내 블록(hq-store-bulk-manager-note) — “담당자이메일 = 점장 로그인 ID · 채운 행은 계정까지 발급 + 설정 메일 발송 · 비운 행은 ‘점장 미정’ · 중복이면 그 행만 실패”.
  • 파일 업로드: 점선 드롭존 시각(파일 선택 시 success 톤 전환·파일명/크기/행수 추정) + <input type="file" accept=".csv,text/csv">(hq-store-bulk-file, sr-only) + 업로드. 빈 파일은 BE 호출 전 클라가 가드(frontend.md §8). pending 시 업로드 단계 표시(파일 업로드→서버 처리). multipart 호출은 음원 업로드(#041) 패턴 미러 — generated useBulkCreateHqStores({ data: { file } }), FormData file 파트. (드래그앤드롭 실제 동작은 시각 힌트만 — 후속.)
  • 결과 리포트(200): 요약 카드(hq-store-bulk-report) — 성공/실패 비율 도넛 차트 + 중앙 총행수 + 큰 카운트(성공·실패), 톤 분기 카드 보더(allSuccess=success / 부분 실패=warn). + 실패 행만 노출(hq-store-bulk-failed-{rowNumber} — 행 번호·매장명·사유), 그룹핑 토글(행번호순 테이블 ↔ 사유별 <details> 묶음 — 표현만, 노출 필드 불변). 성공 행은 카운트로 충분. 실패 0이면 “전체 성공”(hq-store-bulk-no-failures). 성공분이 있으면 ["/api/v1/hq/stores"] prefix invalidate + 안내(hq-store-bulk-success-note) + 부분 실패 시 재업로드 루프 안내. sr-only “총 N행 처리 · 성공 N · 실패 M” 한 줄 요약(보조기술). [추가 업로드](폼 리셋) / [목록으로].
  • 후속 조치 배너(SPEC #184): 결과 리포트에 실패 행 테이블과 별개로 두 축을 드러낸다 — setupEmailFailedCount > 0 → danger hq-store-bulk-setup-email-failed(“설정 메일 발송 실패 N건 · 계정은 있으나 진입 경로 없음 → 운영사에 재발송 요청”) · successCount - managerAccountCreatedCount > 0 → warn hq-store-bulk-manager-skipped(“점장 미정 N건 · 목록의 [점장 계정: 미발급] 필터로 찾아 매장 상세에서 발급”). 요약 문구도 “그중 N개는 점장 계정까지 발급됐습니다” 로 밝힌다.
  • 부분 실패 구분(D2): 한 행 실패가 전체 롤백 X — 행 실패는 200 결과 리포트의 FAILED 행으로(주로 HQ_STORE_INVALID_FIELD, 이메일 중복은 DUPLICATE_EMAIL — FE 가 “이미 사용 중인 이메일 — 다른 이메일로 바꾸거나 담당자이메일을 비워 등록하세요” 로 조치까지 안내). 파일 자체 오류(헤더 불일치·빈 파일·파싱 불가)만 400 HQ_STORE_BULK_INVALID_FILE 로 분리.
  • 에러 매핑: Banner tone="danger" role="alert" (hq-store-bulk-error) — 400 HQ_STORE_BULK_INVALID_FILE=“CSV 형식 오류 — 헤더·파일을 확인해주세요.” / 403 AUTH_HQ_SUSPENDED=“정지된 본사는 매장을 등록할 수 없습니다.” / 401·403 일반=“접근 권한이 없습니다.” / 429 RATE_LIMITED(+status 폴백)=“일괄 등록 요청이 너무 잦습니다. CSV 일괄 등록은 분당 3회까지만 가능합니다 — 파일 문제가 아니니 그대로 두고 1분 뒤에 다시 시도해 주세요.” — CSV 전용 bucket(BULK_PROVISION 3회/분)이 단건(20회/분)보다 쉽게 걸리므로 분당 한도와 재시도 시점을 문구에 담아 파일 오인 재업로드를 막는다 / 5xx=“서버 오류가 발생했습니다.” / BACKEND_UNREACHABLE=“서버에 연결할 수 없습니다.” / 그 외=“일괄 등록에 실패했습니다.”
  • 행 실패 사유 문구: DUPLICATE_EMAIL 행은 BE 고정 문구 대신 FE 가 조치 안내로 치환한다 — “이미 사용 중인 이메일 — 이 행의 매장은 아직 등록되지 않았습니다. 다른 이메일로 바꾸거나 담당자이메일을 비워 다시 올려주세요.” (행 실패는 그 행의 매장 생성까지 롤백 — rules §22 대로 “아직 등록되지 않았습니다” 를 명시해 단건·마법사·본사 발급 3경로와 문구를 맞춘다.)
  • a11y: 결과 요약(도넛·카운트·sr-only 한 줄 요약)은 status(polite). 단 설정 메일 발송 실패 배너(hq-store-bulk-setup-email-failed — “그 점장들은 지금 로그인할 수 없습니다”)는 role="alert" 로 즉시 공지한다.
  • 시안: design_9 정식 시안 정합 완료(design_handoff_linkmusic_9/design/screens/hq-stores-bulk-csv.jsx) — style(space)(시각만 교체, 동작·API·testid·에러매핑·a11y 보존). CSV 헤더 컬럼 칩·드롭존·업로드 단계·결과 도넛·그룹핑 토글·재업로드 안내 흡수. 드래그앤드롭/실패행 CSV 다운로드는 시각 힌트만(후속). design-debt §2 등재.
  • 후속: 점장 계정 자동 발급(FR-13.6) ✅ SPEC #184 · 영업시간/정기휴무 컬럼·검증→미리보기 2단계·csvId idempotency·CP949 자동감지(한국 Excel)·드래그앤드롭·샘플 다운로드·성공 행 storeId 를 후속 진입점으로 잇기(감사 T3-3).

사이드바

HQSidebar 의 “대시보드”(/admin)·“재생 리포트”(/admin/playback, #175 — 대시보드 바로 아래·icon BarChart3)· “매장”(/admin/stores)·“플레이리스트”(/admin/playlists, #057) 항목이 enabled(활성 라우팅). 나머지 항목은 “준비 중”(disabled) 유지.

인가

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

Followups

  • 송출·정산 지표(대시보드 확장) — §F1 — 송출(미도달·재생 상태)·활성 CM송·미해결 매장 문의 카드 실데이터 연결 완료. 구독·결제(billing) 카드는 스코프 제외로 제거(무료 MVP·정산 도메인 미포함).
  • 본사 mutating: 매장 정보 편집(§F1) ✅ #105 마감 · 매장 등록(§F2) ✅ #106 마감 · CSV 일괄 등록(§F3) ✅ #111 마감 · 상태 전이 본사 위임(§F4) · 점장 계정 발급 본사 위임(§F5)SPEC #184 마감(등록 시점 동시 발급 + POST /api/v1/hq/stores/{storeId}/managers 사후 발급).
  • 플레이리스트 조회(read-only) 도착(#057)HQ Mode 플레이리스트 조회. 안내방송·라이브러리 등 나머지 Surface 기능은 후속.