FeaturesHQ (본사)HQ Mode 계정 설정 (apps/space /admin/settings)

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 설정: 사용 토글 + 볼륨%(0100) + fade ms(05000) + [저장]. 멘트(안내방송·CM) 중 배경음악을 정지하지 않고 볼륨만 감쇠해 동시 재생하는 산하 매장 기본값(off=멘트 중 음악 정지). 별도 endpoint PATCH /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.namestring
본사 유형me.typeFRANCHISE→“프랜차이즈” · INDEPENDENT→“독립 매장”
요금제me.planAI · TRUST (신탁) · null→“미설정”
본사 상태me.statusStatusPill — ACTIVE=success · ONBOARDING=info · UNPAID=warn · SUSPENDED=danger(dotPulse)
사업자등록번호me.businessNumbernull→
산하 매장 수me.storeCounttabular-nums + “개”
생성 시각me.createdAtformatKstDateTime (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 → “이름을 저장할 수 없습니다. 입력을 확인해 주세요.”
  • 로딩: 저장 중 [저장] → “저장 중…” + 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-clientuseGetStoreMe() 응답이 본 값을 그대로 사용한다(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 도착 — V33 store.commercial_cycle_songs INT NULL 컬럼 + PATCH /api/v1/hq/stores/{id}/commercial-cycle(null=override 제거, 1..100=매장 override). StoreMeResponse 3 필드(hqCommercialCycleSongs / storeCommercialCycleSongs / commercialCycleSongs effective). 점장 player 는 effective 값 단일 소비. 본사 /admin/stores/[id] 에 CM 사이클 섹션 신규(라디오 default/override + number input 1~100 + [저장]).
  • F6 라운드로빈 정책(#094 F3) ✅ SPEC #104 도착 — V34 store.last_commercial_song_id UUID NULL FK ON DELETE SET NULL 컬럼 + StoreCommercialService.getNextid > :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송 사이클 빈도 설정(commercialCycleSongs 1~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)