HQ Mode — 본사 매장 상세 (apps/space /admin/stores/[id])
SPEC #084 도입(read 슬라이스) → SPEC #103 CM 사이클 빈도 override 편집 → SPEC #105 매장 정보 인라인 편집(#084 F1 마감) → SPEC #116 detail 에 managerName/managerEmail 노출(#084 F6 마감 — 개요 표시 + 편집 폼 prefill). 실 HQ_MANAGER 직접 로그인 표면(apps/space · space.linkmusic.io) — 운영사 임퍼소네이션(apps/admin)이 아니다.
이 페이지는
apps/space의 실 HQ_MANAGER 본사 모드다. 운영사(OPERATOR)가 매장을 관리하는apps/admin/stores/[id](SPEC #036 5탭 — 정지/복구/폐점·점장 발급/관리·활성 PL 적용/해제)와 별개다. 본사 모드는 매장 마스터 정보(이름·주소·매니저 연락처) 와 재생 동작 override (CM 사이클 빈도·더킹) 만 직접 편집 가능하다(SPEC #105·#103 + 더킹). 정지/복구/폐점·점장 발급/관리· 활성 PL 적용/해제는 여전히 운영자(OPERATOR) 영역(apps/admin).
Overview
HQ_MANAGER 가 apps/space 본사 모드에서 자기 본사 산하 매장 1건의 상세를 조회한다(read-only).
매장 기본 정보·운영 상태·점장 계정 정보·활성 플레이리스트·생성/수정 시각을 5 섹션에서 본다.
전부 hqId 스코프(본인 본사 산하 매장만 — 토큰 주체에서 도출, BE verifyHqScope. 타 본사 매장
ID 호출 → 404 STORE_NOT_FOUND 으로 존재 은닉, SPEC #084 §D2).
시안 출처: workspace parent dir — 본사 매장 상세 전용 시안 부재로 운영사 매장 상세 (
apps/admin/stores/[id]SPEC #036) 5 섹션 패턴 + 본사 PL 상세(/admin/playlists/[id]#057) 헤더 스타일 일관(본사 모드 관용구, 임의 디자인 금지).
진입점
본사 매장 목록(/admin/stores, SPEC #051) 행의 매장명 셀 을 클릭하면 본 상세 페이지로
네비게이션한다(hq-store-link-{storeId} Link). 운영사 매장 목록의 매장명 셀 → 상세(#036) 패턴과
동일(단, 본사는 OrgAvatar·plan 컬럼이 없는 read-only 목록 — SPEC #051 §FE).
5 섹션
apps/space/src/app/admin/stores/[id]/page.tsx(server 셸) + hq-store-detail-client.tsx(client).
본사 매장 목록(#051)이 client-query 패턴(서버사이드 페이지네이션·필터)이라 일관 유지 — id 도
path 파라미터라 server 사전 fetch 의 가치가 없다(보조 데이터 없음·mutating 진입점 없음). client
가 useGetHqStore(id)(catch-all BFF proxy 경유)로 직접 조회한다.
| 섹션 | 데이터 | 빈 상태 |
|---|---|---|
| 개요 | name·address·managerName·managerEmail·phone(담당자 전화)·유형 배지·상태 배지·폐점 시각(있을 때) | 주소·담당자·전화 null → — |
| 점장 정보 | storeManager.{email,name,status} | storeManager == null → “점장 계정 미발급” 점선 박스 |
| 활성 플레이리스트 | activePlaylist.{id,name} | activePlaylist == null → “활성 플레이리스트 없음 — 본사 기본 PL fallback 가능” 점선 박스(SPEC #058) |
| 운영 상태 | 상태 배지 재노출 + 정지/폐점 시 danger 안내 | — |
| 메타 정보 | 생성/수정 시각(KST formatKstDateTime) | — |
헤더는 본사 PL 상세 미러(뒤로가기 → /admin/stores, 제목 매장명 + 상태/유형/폐점 배지 칩).
상태 분기
- 로딩 — 스켈레톤 박스 3 줄(
hq-store-detail-loading). - 404
STORE_NOT_FOUND(타 본사 매장·미존재 모두 은닉) — Banner danger “매장을 찾을 수 없습니다.” (hq-store-detail-not-found). servernotFound()가 아니라 client 분기 — useGetHqStore 가 ApiError 로 보고하므로mapDetailError가 status===404 || code===STORE_NOT_FOUND흡수. - 403
PRINCIPAL_SCOPE_MISMATCH— Banner danger “이 페이지를 볼 권한이 없습니다.” - 5xx · 네트워크 — Banner danger “서버에 일시적인 문제…” 또는 “서버에 연결할 수 없습니다.”
인가
/api/v1/hq/stores/{id} → hasRole("HQ_MANAGER") 1차 경계 + service PrincipalScopeGuard
claim↔DB 재검증(SPEC #049). 타 본사 매장은 404 으로 존재 은닉(403 으로 노출 시 매장 ID 유효성
정보 누설). 미인증 401 · 비활성/role·소속 불일치 403 PRINCIPAL_SCOPE_MISMATCH. 클라이언트 fetch
는 generated apiFetch 가 BFF catch-all /api/backend/... 경유(토큰 서버 전용).
재생 동작 설정 — CM 사이클 / 더킹 override
5 섹션 아래에 매장별 재생 동작 override 섹션 2종이 있다. 둘 다 본사 default(/admin/settings)를
매장 단위로 덮어쓰는 같은 위계 — 라디오(본사 default 사용 / 이 매장만 다르게) + 입력 + [저장].
저장 성공 시 매장 상세 invalidate(effective 가 점장 player me 로 read-back).
| 섹션 | endpoint | payload | 의미 |
|---|---|---|---|
| CM 사이클 빈도 override (#103) | PATCH /api/v1/hq/stores/{id}/commercial-cycle (useUpdateStoreCommercialCycle) | { commercialCycleSongs: number | null } (null=override 제거) | N곡마다 본사 CM송 1회 — 매장 단위 빈도 |
| 더킹 override | PATCH /api/v1/hq/stores/{id}/ducking (useUpdateStoreDucking) | UpdateStoreDuckingRequest{ duckEnabled, duckVolumePercent, duckFadeMs } — 각 필드 null=override 제거 | 멘트 중 배경음악 감쇠(사용·볼륨%·fade ms) — 매장 단위 |
- 더킹 override 구조: 라디오 “본사 기본값 사용(상속)” / “이 매장만 다르게(override)”. override 모드면
사용 토글 + 볼륨%(0
100) + fade ms(05000) 노출. 현재 override / HQ default 참조는HqStoreDetailResponse의duck*(매장 override, null=상속) /hqDuck*(본사 default 참조용)에서. - ⚠️ 더킹 PATCH 는 전체 replace(부분 PATCH 아님): “상속” 모드 = 세 필드 모두 null(override 전체 제거), “override” 모드 = 세 필드 모두 값. 한 필드만 바꿔도 세 값을 함께 전송한다.
- 검증(frontend.md §8):
duckVolumePercent0100,5000. 범위 밖/변경 없음 → [저장] disabled.duckFadeMs0 - testid:
hq-store-ducking-section·-mode-default·-mode-override·-toggle·-volume-input·-fade-input·-submit·-success·-error·-effective.
매장 지역 편집 (SPEC #144)
CM 사이클·더킹 섹션과 같은 위계로 매장 지역(시/도) 섹션이 있다 — REGION 모드 송출의 그룹핑 축.
본사가 매장에 지역을 태그하면 송출/반복예약 다이얼로그에서 target=REGION 으로 같은 시/도 매장군에
일괄 송출할 수 있다(미지정 매장은 지역 송출에 포함 X).
| 섹션 | endpoint | payload | 의미 |
|---|---|---|---|
| 매장 지역 (#144) | PATCH /api/v1/hq/stores/{id}/region (useUpdateStoreRegion) | UpdateStoreRegionRequest{ region: Region | null } (null=미지정 clear) | 매장 시/도 태그 — REGION 송출 그룹핑 |
- 구조: 시/도 select(
Region17종 + 맨 위 “미지정” 옵션) + [저장].HqStoreDetailResponse.region으로 현재값 prefill, 변경 0 시 [저장] disabled(no-op 차단). “미지정” 선택 = null clear. - 한글 라벨은 FE 단일 소스
apps/space/src/lib/region.ts(REGION_LABELS·REGION_OPTIONS·regionLabel) — BERegion.displayName은 OpenAPI 에 enum name 만 노출되므로 FE 가 한글 맵을 보유. - 저장 성공 시 매장 상세 invalidate + success Banner(“저장되었습니다.”). audit
HQ_STORE_REGION_UPDATED. - testid:
hq-store-region-section·-select·-submit·-success·-error·-effective.
매장 정보 인라인 편집 (SPEC #105 — #084 F1 마감)
매장 정보(개요) 섹션 헤더 우측 [편집] 버튼 → 같은 페이지에서 read ↔ edit 모드 토글한다(D5
inline · 별도 /edit 라우트 추가 X — 시안 부재라 atom-grounded). edit 모드에서는 DescCard 가
<HqStoreEditForm> 인라인 폼으로 교체된다(apps/space/src/app/admin/stores/[id]/ hq-store-edit-form.tsx).
| 필드 | 입력 | 제약(BE OpenAPI 미러, D8) | clear 의미(D2) |
|---|---|---|---|
| 매장명 | text 필수 | 1..50 비-blank | null 불가(명시 null 시 BE 400) |
| 주소 | text | ≤200 | 빈 입력 → null clear |
| 담당자 이름 | text | ≤50 | 빈 입력 → null clear |
| 담당자 이메일 | @Email + ≤255 | 빈 입력 → null clear | |
| 담당자 전화번호 | tel | ≤30 free-form | 빈 입력 → null clear |
PATCH 의미(D2) — 변경된 필드만 payload 에 담는다(키 부재=미변경). 빈 입력은 명시 null 로
전송해 BE 가 컬럼을 비운다. orval 이 JsonNullable partial 의미를 OpenAPI 로 표현하지 못해
generated 타입은 5 필드 모두 required 로 떨어지는데, FE 는 Partial<UpdateHqStoreRequest> 를
generated client 에 cast 해 우회한다(runtime 으로만 정합 — apps/space/src/lib/backend.ts
BackendUpdateHqStoreRequest 주석 참조).
초기값 prefill(SPEC #116 — #084 F6 마감) — HqStoreDetailResponse 가 store 연락 컬럼
managerName/managerEmail 을 read 응답에 노출하므로(phone=store.manager_phone 은 기존),
편집 폼 5필드(name·address·managerName·managerEmail·managerPhone)가 detail 의 현재값을 그대로
prefill 한다(toFormState). null/미설정 → 빈 문자열. 변경 추적이 trim 비교라 사용자가 손대지
않은 필드는 미변경(키 부재) 으로 BE 가 기존 값 보존, 빈 입력으로 비우면 D2 의 null clear 로 전송.
이 store 연락 컬럼은 점장(STORE_MANAGER) 계정(storeManager)과 별개 — 혼동 금지.
검증 UX
- 입력 단계에서 길이/형식 검증 → 위반 시 helper(
role=alertField error) 노출 + [저장] disabled. - 변경 0 또는 검증 실패 시 [저장] disabled(no-op 호출 차단).
- backend 가 최종 권위 —
HQ_STORE_INVALID_FIELD400 도 에러 Banner 로 매핑.
에러 매핑 (hq-store-edit-form.tsx mapSaveError):
- 401/
AUTH_UNAUTHENTICATED→ “다시 로그인해주세요.” - 403/
PRINCIPAL_SCOPE_MISMATCH→ “이 매장을 편집할 권한이 없습니다.” - 404/
STORE_NOT_FOUND→ “매장을 찾을 수 없습니다.” (타 본사 매장 BE 가 404 로 은닉) - 400/
HQ_STORE_INVALID_FIELD→ “입력값을 확인해 주세요.” - 5xx → “서버에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.”
BACKEND_UNREACHABLE→ “서버에 연결할 수 없습니다.”
저장 성공 흐름 — 부모(HqStoreDetailClient)가 query invalidate
(getGetHqStoreQueryKey(id)) + read 모드 복귀 + 1 회성 success Banner(“매장 정보가 저장
되었습니다.”) 노출. read-back response(D7)가 동일 query 키로 캐싱되어 refetch 1회로 즉시
반영된다.
audit — 본사 audit 페이지(/admin/audit)에 HQ_STORE_UPDATED 액션 + STORE target type 으로
1행 노출(변경된 필드만 detail.changedFields + before/after partial 스냅샷). 변경 0 = audit row X.
사이드바
HQSidebar “매장”(/admin/stores) 항목 유지 — 상세 페이지는 그 하위로 들어와도 같은 메뉴
하이라이트(상세 전용 별도 메뉴 항목 없음).
Followups
- F1 매장 편집 — ✅ SPEC #105 마감.
- F2 매장 등록 — 본사 직접 매장 등록(현재는 운영사 onboarding
POST /api/v1/admin/hq/{hqId}/stores전용, SPEC #011).type정책 결정 후. - F3 매장 CSV 일괄 등록 — 대규모 본사 시나리오.
- F4 매장 상태 전이 본사 위임 — 정지/복구/폐점(현재 운영자 전용) 본사 위임 결정 후.
- F5 점장 계정 발급/관리 본사 위임 — SPEC #036 운영자 endpoint 의 본사 위임 버전.
- F6 detail response 에
managerName/managerEmail노출 — ✅ SPEC #116 마감(개요 카드 표시 + 편집 폼 prefill 정합). - F7 시안 도착 — 본사 매장 상세 전용 시안 도착 시 5 섹션 시각 재정합.
References
- SPEC
docs/specs/084-hq-store-detail.md(BE getHqStore + FE 5 섹션 + 진입점). - SPEC
docs/specs/105-hq-store-info-edit.md(#084 F1 마감 — 매장 정보 인라인 편집). - SPEC
docs/specs/116-hq-store-detail-manager-fields.md(#084 F6 마감 — detail 에managerName/managerEmail노출 + 편집 폼 prefill). - BE:
api/hq/HqStoreController.ktgetHqStore·updateHqStore·api/hq/HqStoreDtos.ktHqStoreDetailResponse·UpdateHqStoreRequest·application/hq/HqStoreQueryService.ktupdateStore(변경 필드 collect + audit detail) ·domain/enums/HqAuditAction.ktHQ_STORE_UPDATED·domain/enums/HqAuditTargetType.ktSTORE. - FE:
apps/space/src/app/admin/stores/[id]/page.tsx·hq-store-detail-client.tsx·hq-store-edit-form.tsx(#105 인라인 편집 폼) · 진입점apps/space/src/app/admin/stores/hq-store-list-client.tsx(매장명 Link). - 운영사 매장 상세 패턴 미러:
apps/admin/src/app/(protected)/stores/[id]/store-detail-client.tsx(SPEC #036).