FeaturesStore (매장)Store Onboarding (/stores/new)

Store Onboarding — /stores/new (2-step)

SPEC #011 정합 · SPEC #147 약관 동의 시점 이전 · SPEC #184 점장 계정 통합. v0.4.0 도입.

SPEC #147: 약관 동의 step 이 발급 마법사에서 제거됐다(3-step → 2-step). 약관 동의는 점장 본인이 계정 설정(첫 로그인) 시 진행한다. 발급 request 에서 consent 가 빠졌고(LegalConsentPart 삭제), 발급 page 의 활성 약관 server fetch·미게시 차단 Banner 도 제거됐다.

SPEC #184: 이메일을 한 번만 받는다. 예전에는 등록 폼의 managerEmailStore 의 연락처 컬럼일 뿐이고 로그인 ID 는 발급 다이얼로그에서 새로 타이핑했다 — 두 값이 갈려도 프리필·대조·경고가 0건이었다(감사 A-0 ①). 이제 등록 폼의 “점장 계정” 섹션에 입력한 이메일이 곧 로그인 ID 이고 매장 생성과 같은 트랜잭션에서 STORE_MANAGER 계정이 만들어진다(D1). 임시 비밀번호는 서버가 만들고 화면에 표시하지 않으므로(D6) 계정 설정 메일이 유일한 진입 경로다. 계약 시점과 점장 채용 시점이 다를 수 있어 [점장 미정으로 등록] 건너뛰기를 명시 토글로 제공한다(D2). 또 기본 소속 프리셋(INDEPENDENT)이 제거됐다(D7 — 감사 T5-1).

Overview

운영사가 매장을 등록 — 두 갈래:

  • INDEPENDENT — 개인 매장. 가상 본사 산하 (hqId = IndependentHqIds.SINGLETON). POST /api/v1/admin/stores/independent (SPEC #011 v0.6.0).
  • DIRECT / FRANCHISE — 가맹 본사 산하 매장. POST /api/v1/admin/hq/{hqId}/stores (SPEC #011 v0.6.0~v0.6.1). FE /stores/new 의 affiliation 라디오로 분기.

두 endpoint 모두 backend 도입 완료. plan·billingAnchorDay 는 HQ 소속 매장의 경우 HQ 에서 자동 상속.

Spec

Step 구조

Step내용
1. 기본 정보소속(명시 선택 — 프리셋 없음, D7) · 매장명 · 주소 · 점장 계정 섹션(점장 이메일 = 로그인 ID · 이름 · 전화 · [점장 미정으로 등록] 토글) · plan(AI/TRUST) · billingAnchorDay
2. 확인요약(점장 계정 발급 여부·로그인 ID 단정) + “약관 동의는 점장 본인이 계정 설정 시 진행” 안내 + [등록]
(등록 완료)매장 등록 완료 화면 — 계정 발급/메일 발송 결과 안내 + [설정 이메일 다시 보내기] + [매장 상세 보기](응답 storeId) / [목록으로]

소속 프리셋 제거 (D7)

예전 initialStoreOnboardingState.affiliation"INDEPENDENT" 였다. 소속을 안 건드리면 본사 산하 매장이 가상 본사로 영구 고정되고 되돌릴 endpoint 가 없다(감사 T5-1). 독립 매장은 PL 배정 수단이 제품에 0(OP-F1)이라 가장 쉽게 도달하는 영구 무음 경로였다. 이제 초기값은 ""(미선택)이고, 선택 전에는 분기 섹션(요금 설정 / 본사 선택)이 렌더되지 않으며 [다음]도 비활성이다.

점장 계정 섹션 (D1/D2)

상태동작
이메일 입력 (기본 경로)등록과 동시에 STORE_MANAGER 계정 발급 + 계정 설정 메일 발송. 그 이메일이 로그인 ID
[점장 미정으로 등록] 체크이메일 입력란이 비워지고 잠긴다(stale 값이 계정을 만드는 사고 차단) · payload 에서 managerEmail 이 빠진다 · 매장만 등록
이메일·토글 둘 다 없음[다음] 비활성 — 결정을 강제한다

이름·전화는 여전히 선택이며 매장 연락처로 저장된다(전화는 로그인에 쓰이지 않는다). 임시 비밀번호 입력란은 없다(D6 — 서버 생성 · 미노출).

State shape

type StoreOnboardingState = {
  step: 0 | 1;
  affiliation: "" | "INDEPENDENT" | "HQ";  // SPEC #184 D7 — "" = 미선택(프리셋 제거)
  skipManager: boolean;                    // SPEC #184 D2 — "점장 미정으로 등록" 토글
  store: {
    name: string;
    address?: string;
    managerName?: string;
    managerEmail?: string;
    managerPhone?: string;
    plan: "AI" | "TRUST";          // INDEPENDENT 분기 전용 (HQ 소속은 HQ 에서 상속)
    billingAnchorDay: number;      // INDEPENDENT 분기 전용
  };
  hqAffiliation: {                 // affiliation === "HQ" 일 때만 사용
    hqId: string;
    storeType: "DIRECT" | "FRANCHISE" | "";
  };
  // SPEC #147 — consent state 제거. 동의는 점장 계정 설정(setup) 단계로 이전.
};

SPEC #184 — managerEmail 은 더 이상 optional 이 아니다. skipManager === false(기본)면 필수이고 그 값이 점장 로그인 ID 다. skipManager === true 일 때만 미입력이 허용되며 payload 에서도 제외된다. managerName·managerPhone 은 여전히 선택(매장 연락처).

Endpoint

마지막 step “등록” 시 affiliation 에 따라 단일 POST 로 분기 (SPEC #147 — request 에서 consent 제거):

INDEPENDENTPOST /api/v1/admin/stores/independent (StoreOnboardingRequest):

  • request: { store: {...plan, billingAnchorDay} }
  • 단일 transaction: Store INSERT (type=INDEPENDENT, hqId=SINGLETON) + managerEmail 이 있으면 OperatorAccount(STORE_MANAGER) INSERT + ACCOUNT_SETUP 토큰 발급(SPEC #184 D1). 메일 발송만 커밋 후(afterCommit)
  • response 200: { storeId, managerAccountId?, managerAccountCreated, setupEmailSent? }

HQ 소속POST /api/v1/admin/hq/{hqId}/stores (HqAffiliatedStoreOnboardingRequest):

  • request: { store: {...storeType} } — plan·billingAnchorDay 없음 (HQ 상속)
  • response 200: 위와 동일 shape

등록 완료 화면 (SPEC #184)

예전에는 성공 시 router.replace("/") 로 대시보드에 튕기며 응답 storeId 를 버렸다(감사 OP-M16). 이제 완료 화면으로 전환해 네 경우를 갈라 안내하고 [매장 상세 보기]로 잇는다:

응답화면
managerAccountCreated=true · setupEmailSent=truesuccess — “계정 발급 + 입력한 이메일 주소로 안내 메일 발송. 그 이메일이 로그인 ID”
managerAccountCreated=true · setupEmailSent=falsedanger(role="alert") — 계정은 있는데 진입 경로가 없다(임시 비밀번호 미노출). [설정 이메일 다시 보내기] 강조
managerAccountCreated=falsewarn — “점장 미정으로 등록됨. 매장 목록의 [점장 계정: 미발급] 필터로 찾아 발급하세요”
2xx 인데 본문 파싱 실패warn role="alert"(store-onboarding-result-unknown) — “매장은 등록됐지만 점장 계정 발급 여부를 확인하지 못했습니다. 매장 상세의 [점장 계정] 을 먼저 확인하세요”. managerAccountCreated ?? false 로 폴백해 “점장 미정” 으로 단정하면 실제로는 발급된 계정 위에 두 번째 계정을 발급하게 된다(매장당 복수 점장 허용)

재발송은 POST /api/v1/admin/accounts/{accountId}/resend-setup(OPERATOR-only, useResendAccountSetup). [설정 이메일 다시 보내기] 버튼은 managerAccountId 가 있을 때만 렌더되므로, danger 배너 문구도 같은 조건으로 갈라 없는 버튼을 안내하지 않는다(없으면 “매장 상세의 [점장 계정] 에서 재발송”).

약관 consent INSERT(StoreConsent·StorePrivacyConsent)는 발급이 아니라 점장 본인의 계정 설정 (setup/complete) 단계에서 수행된다(SPEC #147).

이탈 경로 (SPEC #164 C4)

마법사 footer 왼쪽에 [목록으로](onboarding-cancel) 이탈 액션이 있어 진행 중에도 매장 목록(/stores)으로 빠져나갈 수 있다. 입력이 있는 단계(isStoreOnboardingDirty — affiliation·hqAffiliation·store 가 초기값에서 하나라도 달라짐)에서는 본사 마법사와 동일한 OnboardingLeaveConfirmDialog“작성 중인 내용이 사라집니다” 확인을 먼저 받고, [나가기] 시 router.push("/stores") 한다. step0 빈 입력에서는 확인 없이 곧장 이동한다.

States & Edge Cases

상태처리
필수 필드 미입력다음 step disabled
[목록으로] · 입력 있음 (SPEC #164 C4)확인 다이얼로그 → [나가기] 시 /stores 이동
[목록으로] · step0 빈 입력확인 없이 곧장 /stores 이동
소속 미선택 (SPEC #184 D7)[다음] 비활성 + “소속을 먼저 선택해주세요” 힌트. 분기 섹션 미렌더
점장 이메일 미입력 + [점장 미정] 미체크 (D1)[다음] 비활성
본사 미선택/없음 (HQ 분기)HQ_NOT_FOUND(404)·INVALID_HQ(400) → step1 inline
이메일 중복 409 DUPLICATE_EMAIL (D9)매장도 생성되지 않았다. step0 으로 되돌려 이메일 필드 에러 + Banner “매장은 등록되지 않았으니 다른 이메일로 다시 등록해주세요”. “매장은 만들어졌다” 류 안내 금지
409 이후 비-409 에러(5xx 등)로 재제출 실패이메일 인라인 경고를 유지한다(409 계열 응답에서만 갱신). 지워버리면 매장명만 고쳐 재제출한 사용자에게 “이메일은 이제 괜찮다” 로 읽히지만 값은 여전히 중복이다
소속 미선택 상태로 submit 도달(내부 불변식 위반)네트워크 실패와 구분해 step0 으로 되돌리고 “매장 소속이 선택되지 않았습니다…” 안내(회선을 의심하며 재시도만 반복하지 않게)
429 RATE_LIMITED (STORE_PROVISION 20/분)“요청이 너무 잦습니다. 잠시 후 다시 시도해주세요.”
setupEmailSent === false완료 화면 danger + [설정 이메일 다시 보내기]
5xx”잠시 후 다시 시도”
새로고침state 소실

활성 약관 부재·약관 version 검증은 발급이 아니라 점장 계정 설정(setup) 단계에서 다룬다(SPEC #147).

Constraints

  • INDEPENDENT 분기: 가상 본사 (SINGLETON) 가 자동 hqId — UI 가 본사 선택 안 함
  • HQ 분기: hqId + storeType(DIRECT/FRANCHISE) 선택, plan·billingAnchorDay 는 HQ 상속 (UI 미입력)
  • 소속은 등록 후 변경 불가 — 사후 이동 endpoint 가 없다(SPEC #184 §1-1 명시적 비포함)
  • managerEmail = 점장 로그인 ID(SPEC #184 D1). 기존 매장의 managerEmail 은 백필하지 않았다(D3) — 운영 중 매장에서는 여전히 연락처일 수 있으므로 “정합” 마이그레이션을 만들면 멀쩡한 계정의 로그인 ID 가 바뀐다(§6 ⚠️)

Roadmap

  • 운영자용 점장 이메일 정정 endpoint (감사 AC-F2 잔여 — SPEC #184 §1-1 비포함)
  • 사후 소속 이동 endpoint (감사 T5-1 잔여)

References

  • SPEC #011 · #147 · #184
  • linkmusic-frontend-space/apps/admin/src/app/(protected)/stores/new/
  • 감사: docs/audit/persona-flow-audit-2026-08.md 부록 A(A-0 ①)·부록 B(B-3 T1-3 · B-5 T5-1)