FeaturesStore (매장)Store Profile (/store/profile)

Store Mode — 점장 본인 매니저 프로필 (apps/space /store/profile)

SPEC #087 도입(점장 본인 매니저 이름 편집 + 프로필 페이지 신규). 점장(STORE_MANAGER)이 본인 매니저 계정의 이름을 편집할 수 있는 경로 + /store/profile 라우트 신설. 점장 player 헤더 우측 [프로필] 진입점 추가.

시안 출처: design_14 핸드오프(Phase 8 §8G-④ store-profile.jsx)로 시안 정합 완료. 골격은 본사 #085 /admin/settings DescCard idiom 을 이어받되 정식 시각으로 교체했다 — read-only 8행 + “조회 전용” 배지, 매장 상태 배지(ACTIVE success·INACTIVE warn·SUSPENDED danger·dot pulse), 매니저 이름 편집·비번 변경·고객지원 진입 카드. 서브페이지 헤더는 신규 공용 atom StoreSubHeader (/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.namestring
매장 유형me.typeDIRECT→“직영” · FRANCHISE→“프랜차이즈” · INDEPENDENT→“독립 매장”
매장 상태me.statusStatusPill — ACTIVE=success · INACTIVE=warn · SUSPENDED=danger(dotPulse)
소속 본사me.hqNamestring (Hq join)
요금제me.planAI · TRUST (신탁) · null→“미설정”
매장 주소me.addressnull→
청구 기준일me.billingAnchorDaynull→ · 1~31 → “매월 N일”
생성 시각me.createdAtformatKstDateTime (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 → “이름을 저장할 수 없습니다. 입력을 확인해 주세요.”
  • 로딩: 저장 중 [저장] → “저장 중…” + 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.tsx TopBar 우측: <Link href="/store/profile"> (lucide User 아이콘 + “프로필” 라벨, [재시작] 과 동일한 rounded-full border 스타일).
  • 프로필 페이지 헤더 좌측 [← 재생 화면으로 돌아가기] 링크로 복귀(/store).
  • 고객지원 진입(SPEC #112): 프로필 본문에 “고객지원” 섹션 추가 — [운영사에 문의하기] 카드 (store-profile-support-link, lucide LifeBuoy) → /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 같은 트랜잭션·target STORE_MANAGER·detail changedFields=name·name:before->after·실제 변경 0 멱등 no-op 이면 audit 미기록), 비밀번호 변경은 STORE_PASSWORD_CHANGED(공용 AuthService.changePassword role=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 (헤더 우측 [프로필] 링크)