HQ Mode — 본사 계정 설정 (apps/space /admin/settings)
SPEC #085 도입(본사 본인 매니저 이름 편집 + 계정 설정 페이지 신규). 본사(HQ_MANAGER)가 본인 매니저
계정의 이름을 편집할 수 있는 경로 + /admin/settings 라우트 신설. 사이드바 “설정” 항목 enable.
SPEC #095 (CM송 사이클 빈도 설정 섹션 추가) — 본사가 점장 player CM 사이클 빈도(N곡마다 1회)를 직접
조정. 더킹 default 설정 섹션 추가 — 본사가 멘트 중 배경음악 감쇠(사용·볼륨%·fade ms) 산하 매장
기본값을 직접 조정(PATCH /api/v1/hq/me/ducking, 전체 replace).
시안 출처: 본 페이지 전용 시안 부재 — atom-grounded 임시 레이아웃(
/admin/stores/[id](#084) DescCard idiom +/admin/announcements(#061) form 다이얼로그 idiom 미러). 시안 도착 시 정합 교체.
Overview
HQ_MANAGER 가 /admin/settings 에 진입하면 본인 본사 요약(read-only) 과 본인 매니저 이름 편집
form 을 본다. 베타 핵심 — 본사명·요금제·상태 등 굵직한 정보를 본사 매니저가 직접 확인하고, 본인
이름은 본인이 수정한다.
- 본인 정보(read-only): 본사명 · 본사 유형 · 요금제 · 본사 상태 · 사업자등록번호 · 산하 매장 수 · 생성 시각.
- 본인 매니저 이름 편집: input + [저장] (1~50자). 성공 시 success Banner + me 캐시 invalidate.
- CM송 사이클 빈도 설정: number input + [저장] (1~100). N곡마다 1회 본사 CM송 자동 삽입 재생 (점장 player). 성공 시 success Banner + me 캐시 invalidate.
- 더킹 default 설정: 사용 토글 + 볼륨%(0
100) + fade ms(05000) + [저장]. 멘트(안내방송·CM) 중 배경음악을 정지하지 않고 볼륨만 감쇠해 동시 재생하는 산하 매장 기본값(off=멘트 중 음악 정지). 별도 endpointPATCH /api/v1/hq/me/ducking— 부분 PATCH 아닌 3필드 전체 replace. 성공 시 me invalidate. - F1 본사명 편집(운영자 영역) · F2 비밀번호 변경(
/onboarding/change-password별도 슬라이스) · F3 이메일 변경(보안 결정 후) · F4 다중 매니저 관리(운영자 영역) ·F5 매장별 CM 사이클 빈도 override(#094 F2)✅ SPEC #103 도착 ·F6 라운드로빈 정책(#094 F3)✅ SPEC #104 도착.
페이지 (/admin/settings)
apps/space/src/app/admin/settings/page.tsx(server 셸 — body 만 렌더) + hq-settings-client.tsx(client).
/admin layout(server) 이 role 가드(HQ_MANAGER) + HQShell 셸을 이미 제공하므로 본 page 는 본문만.
1) 본인 정보 섹션 (read-only)
useGetHqMe() 응답(HqMeResponse)을 DescCard 7행으로 노출:
| 라벨 | 값 | 비고 |
|---|---|---|
| 본사명 | me.name | string |
| 본사 유형 | me.type | FRANCHISE→“프랜차이즈” · INDEPENDENT→“독립 매장” |
| 요금제 | me.plan | AI · TRUST (신탁) · null→“미설정” |
| 본사 상태 | me.status | StatusPill — ACTIVE=success · ONBOARDING=info · UNPAID=warn · SUSPENDED=danger(dotPulse) |
| 사업자등록번호 | me.businessNumber | null→— |
| 산하 매장 수 | me.storeCount | tabular-nums + “개” |
| 생성 시각 | me.createdAt | formatKstDateTime (KST 24시간제) |
CM 사이클 빈도(
me.commercialCycleSongs)는 read-only 섹션에 노출하지 않고 아래 편집 섹션에서 직접 input 으로 표시한다(현재값 = 편집 시작값).
2) 본인 매니저 이름 편집 섹션
form input + [저장] 버튼. backend PATCH /api/v1/hq/me(operationId updateHqMe) 를
useUpdateHqMe() mutation 으로 호출.
- payload:
UpdateHqMeRequest { name }— non-null 매번 보냄(빈 입력은 클라이언트에서 disabled). - 검증 (backend OpenAPI 제약과 일치, frontend.md §8):
name@minLength 1 @maxLength 50.- 빈 입력 → [저장] disabled + 보조 “이름을 입력해 주세요.”
- 51자 초과 → [저장] disabled + 보조 “이름은 50자 이하로 입력해 주세요.” +
aria-invalid=true - 카운터
N / 50(tabular-nums).
- 성공: Banner success “저장되었습니다.” (
hq-settings-success) + [닫기] +getGetHqMeQueryKey()invalidate + input 비움. - 실패: Banner danger (
hq-settings-error) —mapSaveError(status, code):- 401
AUTH_UNAUTHENTICATED·NO_SESSION→ “다시 로그인해주세요.” (mutator 가/login으로 풀 리로드). - 403
PRINCIPAL_SCOPE_MISMATCH·AUTH_FORBIDDEN→ “이 페이지를 사용할 권한이 없습니다.” - 5xx → “서버에 일시적인 문제가 있습니다. 잠시 후 다시 시도해주세요.”
BACKEND_UNREACHABLE→ “서버에 연결할 수 없습니다.”- 그 외 4xx → “이름을 저장할 수 없습니다. 입력을 확인해 주세요.”
- 401
- 로딩: 저장 중 [저장] → “저장 중…” + disabled + input disabled.
HqMeResponse.name은 본사명이고UpdateHqMeRequest.name은 본인 매니저 본인 이름이라 의미가 다르다. 본 슬라이스의 me 응답에는 본인 매니저 name 이 노출되지 않아 편집 input 초기값은 빈 문자열이고, 저장 후에도 me 응답에는 반영되지 않는다(invalidate 는 미래 호환 목적). 본인 매니저 name read 는 F 후속(/auth/me확장 또는 별도 endpoint).
3) CM송 사이클 빈도 편집 섹션 (SPEC #095 §D8)
number input + [저장] 버튼. backend PATCH /api/v1/hq/me 를 같은 useUpdateHqMe() mutation 으로
호출 — 이번에는 commercialCycleSongs 만 페이로드에 포함.
- payload:
UpdateHqMeRequest { commercialCycleSongs }— non-null 정수 매번 보냄. - 검증 (backend OpenAPI 제약과 일치, frontend.md §8):
commercialCycleSongs@minimum 1 @maximum 100.- 비-정수 / 범위 밖(
< 1또는> 100) → [저장] disabled + 보조 “1~100 사이의 정수를 입력해 주세요.”aria-invalid=true.
- 현재값과 동일 → [저장] disabled(dirty 아님).
- 정상 값 → 보조 “N곡마다 본사 CM송 1회 자동 삽입 재생됩니다.”
- 비-정수 / 범위 밖(
- 초기값:
me.commercialCycleSongs. 0(레거시 — V32 default 5 이전 본사)이면 fallback 5. - 성공: Banner success “저장되었습니다.”(
hq-settings-cycle-success) + [닫기] +getGetHqMeQueryKey()invalidate. me 응답에commercialCycleSongs가 포함돼 있어 새 값이 자동 read-back → input 초기값과 dirty 비교 기준점도 갱신. - 실패: Banner danger(
hq-settings-cycle-error) —mapCycleSaveError(status, code). 매핑은 이름 편집과 동일 코드체계, 기본 메시지만 “CM 사이클 빈도를 저장할 수 없습니다. 입력을 확인해 주세요.”
점장 player 연동: BE 가
StoreMeResponse.hqCommercialCycleSongs로도 같은 값을 노출하므로 점장store-player-client의useGetStoreMe()응답이 본 값을 그대로 사용한다(SPEC #095 §D9). 본사가 빈도를 변경하면 점장 me query 갱신 시점부터 새 빈도가 적용되며, 변경 시점의 누적 카운터는 유지된다(다음 임계치 도달 판정에서 새 값 비교).
4) 더킹 default 편집 섹션
사용 토글 + 볼륨%(number) + fade ms(number) + [저장]. 멘트(안내방송·CM) 재생 동안 배경음악을 정지하지
않고 볼륨만 감쇠해 동시 재생하는 동작의 산하 매장 기본값을 정한다(off 면 멘트 중 음악 정지 = 종전
동작). CM 사이클 빈도(섹션 3)와 같은 본사 default + 매장 override 위계 — 매장별 override 는 매장
상세에서(features/hq/store-detail).
- endpoint:
PATCH /api/v1/hq/me/ducking(useUpdateHqDucking).useUpdateHqMe와 별도 endpoint. - payload:
UpdateHqDuckingRequest { duckEnabled, duckVolumePercent, duckFadeMs }— ⚠️ 부분 PATCH 가 아니라 3필드 전체 set(전체 replace). 본사 default 는 항상 세 값이 존재하므로 사용 토글만 바꿔도 세 값을 모두 전송한다. - 검증 (backend OpenAPI 제약과 일치, frontend.md §8):
duckVolumePercent@minimum 0 @maximum 100,duckFadeMs@minimum 0 @maximum 5000. 범위 밖/현재값과 동일 → [저장] disabled + 보조 텍스트. - 초기값:
me.duckEnabled/me.duckVolumePercent/me.duckFadeMs(본사 default 는 항상 값 존재 — 레거시 fallback 표기 불필요, BE 가 신규 default true/20/400 보장). - 성공: Banner success(
hq-settings-ducking-success) +getGetHqMeQueryKey()invalidate(duck*read-back). - 실패: Banner danger(
hq-settings-ducking-error) —mapDuckSaveError(status, code)(이름·CM 매핑과 동일 코드체계, 기본 메시지만 “더킹 설정을 저장할 수 없습니다. 입력을 확인해 주세요.”).
점장 player 연동: BE 가
StoreMeResponse.duckEnabled/duckVolumePercent/duckFadeMs(effective — 매장 override ?? HQ default)로 노출한다. 점장 player 가 effective 값을 단일 소비.
사이드바
HQSidebar 의 “설정” 항목(/admin/settings) 이 #085 에서 enable. icon Settings(lucide).
위치: 감사 아래 — handoff HQ_NAV 끝 순서 보존.
인가
/api/v1/hq/me GET/PATCH → hasRole("HQ_MANAGER") 1차 경계 + service verifyHqScope claim↔DB
재검증(accountId 토큰 주체 도출, 요청 파라미터 없음). 미인증 401 · role/소속 불일치 403
PRINCIPAL_SCOPE_MISMATCH. PATCH 는 본인 본사 매니저 본인 row 만 변경(타 매니저·타 본사 무관).
모든 호출은 generated apiFetch 가 BFF catch-all /api/backend/... 경유(토큰 서버 전용).
States & Edge Cases
- me 로딩: 스켈레톤 박스 2개(
hq-settings-loading). - me 5xx · 네트워크 실패: Banner danger(
hq-settings-load-error) — 편집 form 미노출(편집 컨텍스트 없음). - me 403/401: 위 매핑(권한 없음·재로그인).
- 저장 성공 후 추가 입력: input 변경 시 성공 Banner 자동 dismiss(잔상 회피).
- 저장 실패 후 추가 입력: input 변경 시 error Banner 도 dismiss.
Decisions
- 시안 부재 → atom-grounded: 매장 상세(#084) DescCard 와 안내방송 폼(#061) 패턴을 그대로 미러 했다. 새 컴포넌트 추출 0(DescCard·SectionHeader·SkeletonSection 은 매장 상세에서 가져온 시각 패턴 inline). 시안 도착 시 본 페이지·매장 상세·플레이리스트 상세를 함께 정합 교체할 가능성.
- 본인 매니저 name read 부재 → 편집 input 초기값 빈 문자열. me 응답의
name을 채우면 “본사명을 본인 매니저 이름인 것처럼 보여주는” 혼동이 생긴다(HqMeResponse.name=본사명≠UpdateHqMeRequest. name=매니저 이름). F 후속에서 본인 매니저 name read 도착 시 seed. - 본인 매니저 이메일·역할 도 본 슬라이스에서는 미노출. me 응답에 없어 별도 endpoint(
/auth/me) 연동이 필요하나, change-password 관문(SPEC #003·#022)과 충돌하지 않는 후속 슬라이스로 분리.
Roadmap (F)
- F1 본사명 편집 — 운영자 권한 영역(매니저는 본인 매니저 계정만).
F2 매니저 비밀번호 변경 (재로그인 없이)✅ SPEC #099 도착 —/admin/settings비밀번호 변경 섹션 추가. 강제 변경 폼(/onboarding/change-password)과 동일한 BFF(POST /api/auth/change-password) 호출 + reseal 로 같은 세션 유지(redirect 없음). 폼 3 필드(현재·새·새 확인). 클라 검증: 모두 입력 · 8~100자 · 새 != 현재 · 새 == 확인. 실패 매핑: AUTH_INVALID_CREDENTIALS / PASSWORD_REUSED / VALIDATION_ERROR / 5xx · BACKEND_UNREACHABLE / 기타.- F3 이메일 변경 — 보안 정책 결정 필요(검증 메일·재로그인 강제 등).
- F4 본사 audit 누적 (#067) —
HQ_MANAGER_PROFILE_UPDATED·HQ_COMMERCIAL_CYCLE_UPDATED액션 확장. F5 매장별 CM 사이클 빈도 override✅ SPEC #103 도착 — V33store.commercial_cycle_songs INT NULL컬럼 +PATCH /api/v1/hq/stores/{id}/commercial-cycle(null=override 제거, 1..100=매장 override).StoreMeResponse3 필드(hqCommercialCycleSongs / storeCommercialCycleSongs / commercialCycleSongs effective). 점장 player 는 effective 값 단일 소비. 본사/admin/stores/[id]에 CM 사이클 섹션 신규(라디오 default/override + number input 1~100 + [저장]).F6 라운드로빈 정책(#094 F3)✅ SPEC #104 도착 — V34store.last_commercial_song_id UUID NULL FK ON DELETE SET NULL컬럼 +StoreCommercialService.getNext가id > :last다음 후보(0건이면 wrap-around 로 첫 CM)를 native 로 가져온 뒤 같은 트랜잭션 dirty-checking 으로lastCommercialSongId = nextId갱신. 매장 단위 결정적 라운드로빈으로 같은 CM 연속·1건 미노출 분포 문제 해소(랜덤 → 결정적). FK ON DELETE SET NULL 로 referenced CM hard-delete 시 자동 clear → 다음 호출 wrap-around 회복.
References
- SPEC #085 — 본사 계정 설정(BE PATCH
/api/v1/hq/me+ FE/admin/settings). - SPEC #095 — 본사 CM송 사이클 빈도 설정(
commercialCycleSongs1~100, V32 default 5). - SPEC #094 — 점장 player CM송 사이클 삽입 재생(빈도 동적값 소비 측).
- SPEC #049 —
getHqMe(본인 본사 요약 read). - SPEC #084 — 매장 상세 DescCard idiom(시안 부재 atom-grounded 패턴 미러).
- SPEC #061 — 안내방송 form 다이얼로그 idiom(input·검증·Banner 패턴 미러).
- SPEC #003·#022 — 비밀번호 강제 변경 관문(F2 미러 대상).
- code:
apps/space/src/app/admin/settings/page.tsx(server shell)apps/space/src/app/admin/settings/hq-settings-client.tsx(client + form)apps/space/src/components/hq-shell/hq-sidebar.tsx(사이드바 enable)