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) → 셸은 유지하고 본문은 재시도 안내.
| 카드 | 데이터 |
|---|---|
| 산하 매장 | totalStores + 상태별 배지(활성 activeStores·비활성 inactiveStores·정지 suspendedStores). 클릭 → /admin/stores |
| 폐점 매장 | closedStores (closedAt 설정, 전체 합계 포함) |
| 오늘 송출 미도달 매장 | 실데이터(SPEC #119 F3·D4) — getHqUndeliveredToday(HqUndeliveredTodayResponse.undeliveredStoreCount)를 60초 폴링(refetchInterval:60s·refetchIntervalInBackground:false). 오늘(KST) 송출 중 미도달(PENDING) distinct 매장 수. count>0 면 danger 강조(클릭 → /admin/audit 송출 이력), 0 이면 “모두 도달” 비강조. UndeliveredTodayCard client island(apps/space/src/app/admin/undelivered-today-card.tsx, data-testid=hq-dash-card-undelivered). 배지/카드 시각은 atom-grounded(전용 시안 부재, design-debt 등재). |
| 활성 CM송·구독·결제·대기 문의 | ”준비 중” placeholder — billing 도메인 미도착(SPEC #051 §F1) |
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) 필터 + 적용/초기화. - 헤더 액션(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→ 발급됨/미발급). - 빈 상태: 필터 0건(
ListNoResultsCTA) 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 + HqStoreDetailResponse 를 반환한다. 성공 시 ["/api/v1/hq/stores"] prefix 캐시 invalidate + 응답 id 로 /admin/stores/{id} push.
- 필수 표시: name 만 필수(
Field required). 나머지 4 필드는 선택 — 빈 입력 → null payload(단순 nullable, #105 의 JsonNullable 분기 없음 — 신규 등록은 clear vs unchanged 구분 불필요). - 클라 검증(D4 backend OpenAPI 미러):
name1..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/storespush. - 에러 매핑:
Banner tone="danger"(hq-store-create-error) — 400HQ_STORE_INVALID_FIELD=“입력값이 올바르지 않습니다.” / 403AUTH_HQ_SUSPENDED=“정지된 본사는 매장을 등록할 수 없습니다.” / 401·403 일반=“접근 권한이 없습니다.” / 5xx=“서버 오류가 발생했습니다.” /BACKEND_UNREACHABLE=“서버에 연결할 수 없습니다.” / 그 외=“매장 등록에 실패했습니다.” - audit 자동: BE 가 같은 트랜잭션으로
HqAuditAction.HQ_STORE_CREATED+HqAuditTargetType.STORE1행 기록(detail{name, type, address, managerName}partial 스냅샷). 본사 감사 뷰·운영자 본사 감사 뷰 모두 “매장 등록”(info) 으로 노출.
CSV 일괄 등록 (/admin/stores/bulk, SPEC #111, #084 F3 마감)
page.tsx(server 셸) + bulk-client.tsx(client). #106 단일 등록을 행 단위로 미러한 MVP — 매장 마스터 5필드만(점장 계정 발급·영업시간은 후속). CSV 파일을 업로드하면 POST /api/v1/hq/stores/bulk(operationId bulkCreateHqStores, multipart file) 가 행별로 독립 처리해 200 + BulkCreateHqStoreResponse(부분 실패 결과 리포트)를 반환한다.
- CSV 형식 안내: 페이지에 5컬럼 헤더 예시 노출(
hq-store-bulk-csv-header) —매장명,주소,담당자명,담당자이메일,담당자전화(UTF-8, 헤더 정확 일치). 샘플 다운로드는 후속(텍스트 안내로 MVP). - 파일 업로드: 점선 드롭존 시각(파일 선택 시 success 톤 전환·파일명/크기/행수 추정) +
<input type="file" accept=".csv,text/csv">(hq-store-bulk-file, sr-only) + 업로드. 빈 파일은 BE 호출 전 클라가 가드(frontend.md §8). pending 시 업로드 단계 표시(파일 업로드→서버 처리). multipart 호출은 음원 업로드(#041) 패턴 미러 — generateduseBulkCreateHqStores({ data: { file } }), FormDatafile파트. (드래그앤드롭 실제 동작은 시각 힌트만 — 후속.) - 결과 리포트(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” 한 줄 요약(보조기술). [추가 업로드](폼 리셋) / [목록으로]. - 부분 실패 구분(D2): 한 행 실패가 전체 롤백 X — 행 실패는 200 결과 리포트의
FAILED행으로(주로HQ_STORE_INVALID_FIELD). 파일 자체 오류(헤더 불일치·빈 파일·파싱 불가)만 400HQ_STORE_BULK_INVALID_FILE로 분리. - 에러 매핑:
Banner tone="danger"(hq-store-bulk-error) — 400HQ_STORE_BULK_INVALID_FILE=“CSV 형식 오류 — 헤더·파일을 확인해주세요.” / 403AUTH_HQ_SUSPENDED=“정지된 본사는 매장을 등록할 수 없습니다.” / 401·403 일반=“접근 권한이 없습니다.” / 5xx=“서버 오류가 발생했습니다.” /BACKEND_UNREACHABLE=“서버에 연결할 수 없습니다.” / 그 외=“일괄 등록에 실패했습니다.” - 시안: 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)·영업시간/정기휴무 컬럼·검증→미리보기 2단계·csvId idempotency·CP949 자동감지(한국 Excel)·드래그앤드롭·샘플 다운로드.
사이드바
HQSidebar 의 “대시보드”(/admin)·“매장”(/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
- 송출·정산 지표(방송·billing 도메인 도착 후 대시보드 확장) — §F1.
- 본사 mutating:
매장 정보 편집(§F1)✅ #105 마감 ·매장 등록(§F2)✅ #106 마감 ·CSV 일괄 등록(§F3)✅ #111 마감(MVP 5필드) · 상태 전이 본사 위임(§F4) · 점장 계정 발급 본사 위임(§F5). - 플레이리스트 조회(read-only) 도착(#057) → HQ Mode 플레이리스트 조회. 안내방송·라이브러리 등 나머지 Surface 기능은 후속.