FeaturesHQ (본사)HQ Mode 매장 상세 조회 (apps/space /admin/stores/[id])

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). server notFound() 가 아니라 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).

섹션endpointpayload의미
CM 사이클 빈도 override (#103)PATCH /api/v1/hq/stores/{id}/commercial-cycle (useUpdateStoreCommercialCycle){ commercialCycleSongs: number | null } (null=override 제거)N곡마다 본사 CM송 1회 — 매장 단위 빈도
더킹 overridePATCH /api/v1/hq/stores/{id}/ducking (useUpdateStoreDucking)UpdateStoreDuckingRequest{ duckEnabled, duckVolumePercent, duckFadeMs } — 각 필드 null=override 제거멘트 중 배경음악 감쇠(사용·볼륨%·fade ms) — 매장 단위
  • 더킹 override 구조: 라디오 “본사 기본값 사용(상속)” / “이 매장만 다르게(override)”. override 모드면 사용 토글 + 볼륨%(0100) + fade ms(05000) 노출. 현재 override / HQ default 참조는 HqStoreDetailResponseduck*(매장 override, null=상속) / hqDuck*(본사 default 참조용)에서.
  • ⚠️ 더킹 PATCH 는 전체 replace(부분 PATCH 아님): “상속” 모드 = 세 필드 모두 null(override 전체 제거), “override” 모드 = 세 필드 모두 값. 한 필드만 바꿔도 세 값을 함께 전송한다.
  • 검증(frontend.md §8): duckVolumePercent 0100, duckFadeMs 05000. 범위 밖/변경 없음 → [저장] disabled.
  • 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).

섹션endpointpayload의미
매장 지역 (#144)PATCH /api/v1/hq/stores/{id}/region (useUpdateStoreRegion)UpdateStoreRegionRequest{ region: Region | null } (null=미지정 clear)매장 시/도 태그 — REGION 송출 그룹핑
  • 구조: 시/도 select(Region 17종 + 맨 위 “미지정” 옵션) + [저장]. HqStoreDetailResponse.region 으로 현재값 prefill, 변경 0 시 [저장] disabled(no-op 차단). “미지정” 선택 = null clear.
  • 한글 라벨은 FE 단일 소스 apps/space/src/lib/region.ts(REGION_LABELS·REGION_OPTIONS·regionLabel) — BE Region.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 비-blanknull 불가(명시 null 시 BE 400)
주소text≤200빈 입력 → null clear
담당자 이름text≤50빈 입력 → null clear
담당자 이메일email@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=alert Field error) 노출 + [저장] disabled.
  • 변경 0 또는 검증 실패 시 [저장] disabled(no-op 호출 차단).
  • backend 가 최종 권위 — HQ_STORE_INVALID_FIELD 400 도 에러 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.kt getHqStore·updateHqStore · api/hq/HqStoreDtos.kt HqStoreDetailResponse·UpdateHqStoreRequest · application/hq/HqStoreQueryService.kt updateStore(변경 필드 collect + audit detail) · domain/enums/HqAuditAction.kt HQ_STORE_UPDATED · domain/enums/HqAuditTargetType.kt STORE.
  • 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).