Store Mode — 점장 본인 매니저 프로필 (apps/space /store/profile)
SPEC #087 도입(점장 본인 매니저 이름 편집 + 프로필 페이지 신규). 점장(STORE_MANAGER)이 본인 매니저
계정의 이름을 편집할 수 있는 경로 + /store/profile 라우트 신설. 점장 player 헤더 우측 [프로필]
진입점 추가.
시안 출처: design_14 핸드오프(Phase 8 §8G-④
store-profile.jsx)로 시안 정합 완료. 골격은 본사 #085/admin/settingsDescCard idiom 을 이어받되 정식 시각으로 교체했다 — read-only 8행 + “조회 전용” 배지, 매장 상태 배지(ACTIVE success·INACTIVE warn·SUSPENDED danger·dot pulse), 매니저 이름 편집·비번 변경·고객지원 진입 카드. 서브페이지 헤더는 신규 공용 atomStoreSubHeader(/support·/profile·/playlist·/scheduled공용). 시각만 교체 — 동작·계약 보존.
Overview
STORE_MANAGER 가 /store/profile 에 진입하면 본인 매장 요약(read-only) 과 본인 매니저 이름 편집
form 을 본다. 본사 #085 와 동일 패턴 — 매장명·상태·요금제 등 굵직한 정보를 점장이 직접 확인하고,
본인 이름은 본인이 수정한다.
- 본인 정보(read-only): 매장명 · 매장 유형 · 매장 상태 · 소속 본사 · 요금제 · 매장 주소 · 청구 기준일 · 생성 시각.
- 본인 매니저 이름 편집: input + [저장] (1~50자). 성공 시 success Banner + me 캐시 invalidate.
- F1 매장 정보 편집(정책 결정 필요 — 운영사/본사만 vs 점장도) · F2 비밀번호 변경
(
/onboarding/change-password별도 슬라이스) · F3 이메일 변경(보안 결정 후) · F4 audit 누적.
페이지 (/store/profile)
apps/space/src/app/store/profile/page.tsx(server 셸 — body 만 렌더) + store-profile-client.tsx(client).
/store layout(server) 이 role 가드(STORE_MANAGER) + passwordMustChange 관문을 이미 제공하므로
본 page 는 본문만.
1) 본인 정보 섹션 (read-only)
useGetStoreMe() 응답(StoreMeResponse)을 DescCard 8행으로 노출:
| 라벨 | 값 | 비고 |
|---|---|---|
| 매장명 | me.name | string |
| 매장 유형 | me.type | DIRECT→“직영” · FRANCHISE→“프랜차이즈” · INDEPENDENT→“독립 매장” |
| 매장 상태 | me.status | StatusPill — ACTIVE=success · INACTIVE=warn · SUSPENDED=danger(dotPulse) |
| 소속 본사 | me.hqName | string (Hq join) |
| 요금제 | me.plan | AI · TRUST (신탁) · null→“미설정” |
| 매장 주소 | me.address | null→— |
| 청구 기준일 | me.billingAnchorDay | null→— · 1~31 → “매월 N일” |
| 생성 시각 | me.createdAt | formatKstDateTime (KST 24시간제) |
2) 본인 매니저 이름 편집 섹션
form input + [저장] 버튼. backend PATCH /api/v1/store/me(operationId updateStoreMe) 를
useUpdateStoreMe() mutation 으로 호출.
- payload:
UpdateStoreMeRequest { 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 “저장되었습니다.” (
store-profile-success) + [닫기] +getGetStoreMeQueryKey()invalidate + input 비움. - 실패: Banner danger (
store-profile-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.
StoreMeResponse.name은 매장명이고UpdateStoreMeRequest.name은 본인 매니저 본인 이름이라 의미가 다르다(본사 #085 와 동일 패턴). 본 슬라이스의 me 응답에는 본인 매니저 name 이 노출되지 않아 편집 input 초기값은 빈 문자열이고, 저장 후에도 me 응답에는 반영되지 않는다(invalidate 는 미래 호환 목적). 본인 매니저 name read 는 F 후속(/auth/me확장 또는 별도 endpoint).
진입점
점장 모드는 본사 모드와 달리 사이드바가 없는 단일 화면 셸(/store Classic player)이라, 헤더 우측
[재시작] 옆에 [프로필] 링크를 둔다(player 헤더는 design_14 §8G-① 정합 완료).
apps/space/src/app/store/store-player-client.tsxTopBar 우측:<Link href="/store/profile">(lucideUser아이콘 + “프로필” 라벨, [재시작] 과 동일한 rounded-full border 스타일).- 프로필 페이지 헤더 좌측 [← 재생 화면으로 돌아가기] 링크로 복귀(
/store). - 고객지원 진입(SPEC #112): 프로필 본문에 “고객지원” 섹션 추가 — [운영사에 문의하기] 카드
(
store-profile-support-link, lucideLifeBuoy) →/store/support. player 헤더의 [고객지원] 링크(store-player-support-link)와 함께 두 곳에서 discoverable. 상세는 Store Customer Support.
인가
/api/v1/store/me GET/PATCH → hasRole("STORE_MANAGER") 1차 경계 + service verifyStoreScope claim↔DB
재검증(accountId 토큰 주체 도출, 요청 파라미터 없음). 미인증 401 · role/소속 불일치 403
PRINCIPAL_SCOPE_MISMATCH. PATCH 는 본인 매장 본인 매니저 row 만 변경(타 매니저·타 매장 무관).
모든 호출은 generated apiFetch 가 BFF catch-all /api/backend/... 경유(토큰 서버 전용).
States & Edge Cases
- me 로딩: 스켈레톤 박스 2개(
store-profile-loading). - me 5xx · 네트워크 실패: Banner danger(
store-profile-load-error) — 편집 form 미노출(편집 컨텍스트 없음). - me 403/401: 위 매핑(권한 없음·재로그인).
- 저장 성공 후 추가 입력: input 변경 시 성공 Banner 자동 dismiss(잔상 회피).
- 저장 실패 후 추가 입력: input 변경 시 error Banner 도 dismiss.
Decisions
- 본사 #085 정확 미러: 점장의 me/PATCH 시그니처가 본사와 동형(
StoreMeResponse.name=매장명≠UpdateStoreMeRequest.name=매니저 이름)이라 본사 client·테스트를 그대로 미러했다. 새 컴포넌트 추출 0(DescCard·SectionHeader·SkeletonSection 은 본사 #085 의 시각 패턴 inline). - atom-grounded 골격 → design_14 정합: 도입 시 본사 #085(또한 #084 매장 상세) DescCard 와 form 패턴을 미러한 atom-grounded 임시였으나, design_14 §8G-④ 핸드오프로 정식 시각을 흡수했다(시각만 교체).
- 본인 매니저 name read 부재 → 편집 input 초기값 빈 문자열. me 응답의
name을 채우면 “매장명을 매니저 이름인 것처럼 보여주는” 혼동이 생긴다. F 후속에서 본인 매니저 name read 도착 시 seed. - 본인 매니저 이메일·역할 도 본 슬라이스에서는 미노출. me 응답에 없어 별도 endpoint(
/auth/me) 연동이 필요하나, change-password 관문(SPEC #003·#022)과 충돌하지 않는 후속 슬라이스로 분리. - 진입점 위치 — 시안에 메뉴/사이드바 없음(home-classic.jsx TopBar 우측은 [재시작] 만). 점장 셸이 단일 화면이라 헤더 우측 [재시작] 옆에 [프로필] 링크를 둔다(player 헤더는 design_14 §8G-① 정합 완료).
Roadmap (F)
- F1 매장 정보 편집 — 매장명·주소·연락처 등. 정책 결정 필요(운영사/본사만 vs 점장도).
F2 매니저 비밀번호 변경 (재로그인 없이)✅ SPEC #100 도착 —/store/profile비밀번호 변경 섹션 추가. 본사 #099 정확 미러 — 같은 BFF(POST /api/auth/change-password) 호출 + reseal 로 같은 세션 유지. 폼 3 필드(현재·새·새 확인) + 검증 모두입력/8~100자/새≠현재/새==확인 → [변경] disabled + helper. 실패 매핑: AUTH_INVALID_CREDENTIALS / PASSWORD_REUSED / VALIDATION_ERROR / 5xx · BACKEND_UNREACHABLE / 기타.- F3 이메일 변경 — 보안 정책 결정 필요(검증 메일·재로그인 강제 등).
F4 점장 audit 누적✅ SPEC #114 도착 —store_audit_log(V36) 백본 + hook. 프로필 이름 편집은STORE_PROFILE_UPDATED(StoreMeService.updateMe같은 트랜잭션·targetSTORE_MANAGER·detailchangedFields=name·name:before->after·실제 변경 0 멱등 no-op 이면 audit 미기록), 비밀번호 변경은STORE_PASSWORD_CHANGED(공용AuthService.changePasswordrole=STORE_MANAGER 분기·detail 없음 보안·임퍼소네이션 차단되어 actor 항상 본인). audit INSERT 실패 시 본 작업도 함께 롤백. v1 은 기록만(조회 view 후속). 상세는 Store 감사.
References
- SPEC #087 — 점장 프로필 편집(BE PATCH
/api/v1/store/me+ FE/store/profile). - SPEC #049 —
getStoreMe(본인 매장 요약 read). - SPEC #085 — 본사 계정 설정(
/admin/settings) — 정확 미러 대상. - SPEC #064 — 점장 Classic player(헤더 진입점 위치 부모).
- SPEC #003·#022 — 비밀번호 강제 변경 관문(F2 미러 대상).
- SPEC #114 — 점장 액션 audit 백본(F4 마감 — 프로필 편집
STORE_PROFILE_UPDATED·비밀번호 변경STORE_PASSWORD_CHANGED). Store 감사. - code:
apps/space/src/app/store/profile/page.tsx(server shell)apps/space/src/app/store/profile/store-profile-client.tsx(client + form)apps/space/src/app/store/store-player-client.tsx(헤더 우측 [프로필] 링크)