Store Onboarding · Env Check — /store/onboarding (점장 환경 점검)
시안 design_handoff_linkmusic_7/design/screens/onboarding.jsx (골격) + …/onboarding-supplement.jsx (보강 상태: fail·수동확인·스피커 3단계·인터넷 폴백). FE-only · BE 변경 0.
Overview
점장(STORE_MANAGER)이 apps/space /store/onboarding 에서 매장 PC가 음악을 제대로 재생할
수 있는지 4항목을 점검하는 온보딩 화면. 시안 Stepper 4단계(비밀번호 변경=done · 환경 점검=
current · 간단 소개=todo · 음악 시작=todo) 중 “환경 점검” 단계다. 4항목 모두 통과해야 다음으로
진행할 수 있다.
선행 단계 — 비밀번호 변경 → 약관 동의 강제: Stepper “비밀번호 변경=done” 은 실제 구현된 관문이다. 신규 임시 비번 계정(
me.passwordMustChange=true)은/storelayout 가드가/onboarding/change-password(Change Password)로 강제 redirect 하므로, 환경 점검 진입 자체가 비밀번호 변경 완료 후다. 비번 변경 통과 후 약관/개인정보 미동의 (me.termsRequiresAgreement || privacyRequiresAgreement)면 layout 이 다시/onboarding/reagree(Legal Model)로 보낸다(SPEC #156 — 첫 진입·개정 재동의 공용 화면). 즉 순서는 비번 변경 → 약관 동의 → 환경 점검이다.
진입 게이트 — 온보딩 미완료 시 자동 이동(SPEC #156 B-1):
/store진입 시 얇은 client 게이트(app/store/store-entry-gate.tsx)가 매장 id 확정 후 완료 플래그 (readProgress(storageKey(storeId)).done)를 확인해, 미완료면/store/onboarding으로 redirect 한다. 임시 비번 직접 공유 경로(setup 메일 미경유)가 온보딩을 건너뛰던 갭을 닫는다. 완료 후 (또는 매장 id 미확정 장애 시)에는 player 를 렌더한다. 완료 플래그는 마지막 start 화면이 기록한다. BE 백드(onboardingCompletedAt·이력)는 후속 SPEC(B-2).fail-open 카운터(2026-07 감사 #10-c): 이 게이트의 판정 근거는 localStorage 다. 그 저장소가 쓰기 불가(시크릿 모드·저장소 차단·쿼터 초과)면 온보딩을 끝내도
done이 남지 않아/store↔/store/onboarding이 오류 표시 없이 무한 반복됐다. 이제 세션당 리다이렉트를 1회로 제한하고 2회째 요구가 오면(= 루프 징후) 게이트를 통과시키며 warn 배너(store-entry-gate-bypass)를 노출한다. 카운터는 sessionStorage(lm_space_onboarding_redirects:<storeId>)에 두되, 그 저장소마저 막힌 환경을 위해 모듈 변수로 이중화한다(sessionStorage 접근이 한 번이라도 실패하면 그때부터 모듈 변수를 쓴다). 정상 흐름은 영향이 없다 — 온보딩을 마치면done=true라 애초에 두 번째 리다이렉트가 없다. 한 마운트에서 같은 매장에 대한 판정은 1회만 수행한다(decidedForRef—router참조가 바뀌어 effect 가 재실행돼도 리다이렉트 중복·카운터 부풀림 없음).지속성 마커(리뷰 PR #429): 통과 여부를 저장소 상태로 한 번 더 가른다. 카운터만 보고 통과시키면 저장소가 멀쩡한 기기에서도 온보딩을 건너뛰고 “이 기기에 저장할 수 없어…” 라는 틀린 원인을 안내하는 경로가 있었다 — (a) 온보딩 중도 이탈 후 주소·북마크로
/store직접 열기(카운터가 이미 1), (b) opener 에서 연 새 탭은 브라우저가 sessionStorage 를 복제해 첫 진입이 곧바로 2회째로 계산.단 측정 대상은 “
setItem이 예외를 던지는가” 가 아니라 “쓴 값이 다음 진입까지 남는가” 다(리뷰 2차). 전자는 후자의 부분집합이라, 쓰고 즉시 지우는 1바이트 프로브는 네비게이션마다 저장소를 비우는 환경 (키오스크·POS 프라이버시 모드·저장소 자동삭제 확장·엄격 파티셔닝)이나 쿼터 임계·다른 탭의 삭제를 전혀 재지 못한다 — 그런 기기는 프로브를 통과하는데done이 안 남아 배너조차 없이 영구 루프가 된다. 그래서 리다이렉트 직전에 마커(lm_space_storage_probe)를 남기고, 다음 진입에서 (i) 그 마커가 살아 있는지 (ii) 지금 다시 쓸 수 있는지를 함께 본다. 둘 다면 이 기기는done도 남길 수 있다 = 원인은 “온보딩 미완료” 이므로 그대로 다시 온보딩으로 보내고(마치면done이 저장돼 끝나므로 루프가 아니다), 하나라도 아니면 통과 + 배너다((i)만 보면 쿼터 초과를, (ii)만 보면 비-지속 기기를 놓친다). 마커는 bypass·정상 완료 시 삭제한다(잔여 키 0). 즉 배너 문구(“저장할 수 없어”)는 항상 실제 측정과 일치한다.하드 실링(리뷰 2차): 마커 판정과 무관한 상한(
HARD_CEILING = 3)도 함께 둔다. 미지의 실패 모드에서도/store↔/store/onboarding왕복이 유한하게 끝나야 하기 때문이다(무인 매장은 자가복구가 불가능).start-client의 [그래도 음악 시작하기] 탈출구가/store에서 카운터를 다시 만나 한 바퀴 더 도는 부수 문제도 이 상한이 흡수한다. 카운터 읽기는 sessionStorage 와 모듈 변수의 최댓값을 쓴다 — 예외 없이 쓰기를 버리는 저장소에서 카운터가 영원히 0 이면 상한 자체가 도달 불가능해지기 때문이다.배너 레이아웃(리뷰 PR #429): bypass 배너를
children의 형제로 나열하면 player 루트 (h-screen=100vh) 위에 배너 높이가 더해져 문서가 뷰포트를 넘고 하단 트랜스포트가 스크롤 뒤로 밀린다(터치 POS 치명). 게이트는flex h-screen flex-col로 감싸고 player 를min-h-0 flex-1에 넣으며, 자식의h-screen은.lm-entry-gate-body > *(apps/spaceglobals.css— Tailwind 유틸 레이어 밖 규칙이라 우선)가 남은 공간(100%)으로 덮는다.
운영사측 매장 등록(
/stores/new, SPEC #011)의 “Store Onboarding” 과는 별개 화면이다. 이쪽은 점장이 본인 매장 PC에서 수행하는 재생 환경 점검이다.
범위: 환경 점검 4항목 + 진행 저장 + “다음으로”→/store/onboarding/intro(간단 소개). intro·start
스텝은 SPEC #110 으로 구현됨(/store/onboarding/intro·/store/onboarding/start). 환경 점검 통과
시 “다음으로” 는 곧장 /store 가 아니라 intro 로 체이닝한다(비번변경 → 환경점검 → 간단소개 → 음악시작 풀 플로우). 완료 플래그(done)는 환경 점검이 아니라 마지막 start 화면에서 저장한다.
구성 (server 셸 + client)
app/store/onboarding/page.tsx— server 셸(force-dynamic). 본문(OnboardingClient)만 렌더. role 가드는app/store/layout.tsx(STORE_MANAGER, SPEC #050 §F1)가 담당(추가 가드 없음).app/store/onboarding/onboarding-client.tsx— client. 브라우저 측정·Web Audio·진행 저장 보유.app/store/onboarding/onboarding-shared.tsx— 공유 atom(SPEC #110): Stepper(4단계 done/current/ todo 파생)·storageKey·진행 저장(readProgress/mergeProgress)·슬림BrandHeader·PrimaryButton. env-check·intro·start 가 함께 쓴다(atom-grounded 임시 — 시안 부재).useGetStoreMe(GET /api/v1/store/me) — 헤더 매장명(name). 5xx/네트워크 시 “내 매장” 폴백.
환경 점검 4항목
시안의 하드코딩 목업값(Chrome 124.0 · 245Mbps 등)을 실제 브라우저 측정으로 대체했다. 브라우저에서 OS 절전 상태·실제 스피커 출력은 측정 불가이므로 해당 항목은 안내 + 수동 확인으로 처리.
| # | 항목 | 방식 | 판정(시안 보강 상태) |
|---|---|---|---|
| 1 | 브라우저 종류 | 자동 | navigator.userAgent 로 Chromium(Chrome Chrome/ · Edge Edg/) 감지. Chrome/Edge → pass + 버전. 그 외(Safari·Firefox·Opera 등) → fail — danger 톤 보더·아이콘 + “확인 필요” 배지, value 에 실제 브라우저명(예 “Safari 17”), “Chrome 또는 Edge에서 열어주세요”. fail 이면 4/4 불가라 진행 차단 + 푸터 danger 안내. |
| 2 | 절전 모드 | 수동 확인 | OS 절전모드는 브라우저가 측정 불가. value “직접 확인 필요” → 안내(“PC가 자동으로 꺼지지 않게…”) + [확인했어요] 버튼으로 pass(“확인했어요 ✓”). 매장 id 확정 전엔 버튼 라벨 “매장 정보 불러오는 중…” + disabled. |
| 3 | 스피커 연결 | 반자동·3단계 | idle([소리 테스트]) → playing(“재생 중…” · 아이콘 glow 박동, 버튼 disabled) → confirm([다시]/[소리가 들렸어요]) → pass(“소리 들렸음 ✓”). Web Audio API 로 440Hz 사인파 1초 재생, AudioContext 는 사용자 제스처 안에서 생성·resume(~1.1s 후 confirm 전이). [다시] 는 idle 복귀. AudioContext 미지원(jsdom 등) 시 graceful fallback — 신호음 없이도 단계 전이·확인 버튼 노출. |
| 4 | 인터넷 연결 | 자동(best-effort) | navigator.connection 있으면 downlink(Mbps) 표시. downlink < 10 → pass 하되 warn 톤 + “주의” 배지 “연결이 느려요 — 음악이 가끔 끊길 수 있어요”. navigator.connection 미지원이면 value “측정 미지원” · “이 브라우저는 속도 측정을 지원하지 않아요 · 연결 양호로 가정합니다” 로 pass. |
권장 다운링크 임계값은 10Mbps(RECOMMENDED_DOWNLINK).
행 톤(data-tone)은 fail(danger·“확인 필요” 배지) > warn(주의 배지) > pass(success) > neutral
순으로 분기하며, fail/warn 등 동적 color-mix 색은 inline style 로 적용(Tailwind arbitrary 회피).
“재생 중” glow 는 .lm-onboarding-glow(globals.css · prefers-reduced-motion 시 정지).
진행 동작
- 통과 게이트: 4항목 모두 pass 여야 푸터 “다음으로” 활성. 미달 시 disabled + “먼저 모든
항목 통과”. 통과 카운트(
passed/4)를 푸터에 표시. - 진행 저장(localStorage): 매장별 키
lm_space_onboarding:<storeId>(미확정 시:unknown)에 수동 항목(power·speaker) 통과를 저장. 자동 항목(browser·net)은 마운트 시 재측정하므로 저장하지 않는다. 새로고침 시 수동 항목을 복원해 재개한다. 완료 플래그(done)는 환경 점검이 아니라 마지막 start 화면에서 저장한다(SPEC #110 — 같은 키done). 저장 실패 노출(2026-07 감사 #10-c):writeProgress/mergeProgress는 성공 여부(boolean) 를 반환하고(종전catch {}로 조용히 삼킴), 호출부가 실패를 확인해 danger 배너 (onboarding-storage-error— “이 기기에 설정을 저장할 수 없습니다…”)로 알린다. 세션 내 진행 자체는 state 로 계속 가능하다. - 다음으로:
router.push("/store/onboarding/intro")— 간단 소개로 체이닝(SPEC #110). 환경 점검은done을 저장하지 않는다. - 이전 / 로그아웃(2026-07 감사 #10-a·b): 푸터 좌측 보조 버튼은 완료 여부로 갈린다.
done=true(이미 완료하고 다시 들어온 경우) → [이전](onboarding-back) =/store로 이동.done=false→ [로그아웃](onboarding-logout) =POST /api/auth/logout→/login. 미완료 상태의 [이전]은 진입 게이트가/store를 즉시 여기로 되돌려 “눌러도 아무 일이 없는” 핑퐁이 되므로 노출하지 않는다.
- 미지원 브라우저 안내(2026-07 감사 #10-b): 비-Chromium 은 browser 항목이 영구 fail 이라 [다음으로]
가 절대 활성화되지 않는다. 종전엔 [이전]마저 핑퐁이라 탈출구가 없었다. 이제 전용 danger 배너
(
onboarding-browser-blocked— 원인 + 현재 주소 + “Chrome 또는 Edge로 다시 열어주세요”)를 띄우고 위 [로그아웃]으로 앱을 벗어날 수 있다. “그래도 계속” 우회는 두지 않는다 — 미지원 브라우저의 player 는 자동재생·오디오 파이프라인이 보장되지 않아 무인 매장 무음 리스크가 더 크다.
간단 소개·음악 시작 (SPEC #110 · intro/start)
환경 점검 다음 두 스텝. design_handoff_linkmusic_10 시안 정합 완료
(design/screens/store-onboarding-intro-start.jsx). 동작·라우팅·완료플래그·testid·접근성은 100%
보존하고 시각만 교체(style(space)). 슬라이드 문구는 시안 INTRO_SLIDES(PRD Page 9) 그대로.
onboarding-shared.tsx 의 Stepper·헤더·버튼(PrimaryButton — iconRight·size="lg" 확장)·진행 저장을 재사용.
- 간단 소개(
/store/onboarding/intro,intro-client.tsx) — 4슬라이드 캐러셀(FR-9.3): 점 인디케이터 + [이전](ChevronLeft·첫 슬라이드 disabled)/[다음](primary 그라데이션·ChevronRight) + 헤더 [건너뛰기](저강조 보더 텍스트 버튼·항상 노출). 4슬라이드(🎵 자동 재생 / 🎙 즉시 방송 / 🚨 긴급 체크 / 🔄 재시작), emoji 6480px·제목 2630px·본문 18px(40~50대 가독성). 슬라이드 3(긴급) = warn 톤 차등: warn 보더·배경 + “긴급 상황 한정” StatusPill(warn) + danger 경고 박스(AlertTriangle). 마지막 슬라이드 [시작 준비 완료] 또는 [건너뛰기] → start. - 음악 시작(
/store/onboarding/start,start-client.tsx) — 준비 완료 확인(FR-9.4): 큰 success 원(120px·glow 박동) + 🎉 PartyPopper + “준비가 끝났어요!”(30~36px) + 체크리스트 3줄 (원형 success 체크 배지). [음악 시작하기](64px 큰 탭·Music아이콘) → 완료 플래그(done:true) 저장 +router.push("/store")(player 홈 자동재생 진입). 매장 id 미확정 시 버튼 disabled(:unknown키 desync 방지). Stepper 4단계 current=start. 저장 실패 분기(2026-07 감사 #10-c):mergeProgress가 false 를 반환하면 이동하지 않는다 — 그대로/store로 보내면 진입 게이트가done=false를 보고 되돌려 무한 리다이렉트가 되기 때문이다. danger 배너(onboarding-start-storage-error) + [그래도 음악 시작하기](onboarding-start-force) 를 노출하고, 명시적으로 눌렀을 때만 이동한다(게이트의 2회째 fail-open 이 루프를 끊는다). - BE 미연동:
PATCH /api/store/me/onboarding-complete·환경 체크 이력(FR-9.5)은 BE 미구현 → 완료는 localStoragedone으로만 추적(후속 SPEC 으로 분리).
시안 대비 차이 (목업값 대체 · 브라우저 한계)
- 시안의 고정 “Chrome 124”/“Safari 17” → 실제 UA 파싱 버전·브라우저명. fail 행도 실제 비-Chromium 브라우저명을 best-effort 추출(Safari·Firefox·Opera, 그 외 generic).
- 시안의 고정 “245 Mbps · 핑 18ms” →
navigator.connection.downlink/effectiveType실측. 핑(18ms)은 브라우저에서 측정 불가라 표시 생략(effectiveType 로 대체). 미지원 시 “측정 미지원 · 양호 가정” 폴백. - 절전 모드는 시안 supplement 와 동일하게 수동 확인 버튼(브라우저가 OS 절전 측정 불가). 시안의 시연 토글(SupToggle·netMode select)은 데모 전용이라 옮기지 않고 실제 측정으로 구동.
- 스피커 3단계·인터넷 warn/폴백·브라우저 fail 의 시각(톤·배지·glow)은 supplement 시안 정합.
- 데모용 래퍼(Artboard·BrandLockup·ThemeToggle)는 옮기지 않고
store-player-client의 슬림 헤더(TopBar) 스타일로 치환. 아이콘은lucide-react(Chrome·Power·Volume2·Wifi·Check· CheckCircle2·ChevronRight·RefreshCw·AlertTriangle).
인가·세션
/store/* 는 app/store/layout.tsx(server)가 STORE_MANAGER role 가드를 담당(세션 없음→/login,
HQ_MANAGER→/admin, 그 외→fail-closed /login) + 첫 진입 게이트(passwordMustChange →
change-password, 그다음 terms/privacyRequiresAgreement → reagree; SPEC #156). 본 화면은 추가
가드 불필요. /store 홈의 온보딩 완료 게이트(store-entry-gate)는 온보딩 화면 자체엔 적용되지
않는다(무한 루프 방지 — 게이트는 /store 진입에만).
PRD FR-9.2 대비 드리프트(명시)
PRD FR-9.2 / FR-9.2-1 / FR-9.2-2 는 서버 측 device-check 를 요구한다 —
POST /api/store/maintenance/device-check(30-TRD §7-7-5)로 RAM·브라우저 종류·해상도·인터넷 속도를 자동 측정하고 기준 미달 시 빨간 배지 + 즉시 차단(브라우저)·경고(RAM·해상도), 온보딩 이력 기록(FR-9.5). 현 구현은 BE device-check 부재로 이를 클라이언트 best-effort 측정·경고로 축소했다.
| FR | PRD 요구 | 구현 현황 |
|---|---|---|
| FR-9.2-1 자동 측정(RAM·해상도) | device-check 응답 배지 | 미측정(RAM·해상도 항목 없음 — 브라우저 한계) |
| FR-9.2-1 인터넷 속도 | 서버 측정값 | navigator.connection.downlink best-effort(미지원 시 양호 가정 pass · 느림 시 warn 배지) |
| FR-9.2-2 브라우저 자동 차단 | 진행 불가 + Chrome 링크 안내 | fail 톤(danger 보더·아이콘) + “확인 필요” 배지 + 4/4 불가로 게이트 + 푸터 danger 안내 + 전용 미지원 안내 배너(현재 주소 표기) + [로그아웃] 탈출구(감사 #10-b). 단 강제 차단은 클라 측 게이트 수준(서버 enforcement 부재) |
| FR-9.5 환경 체크 이력 | 서버 기록 | 미기록 — localStorage 진행 저장만 |
서버 device-check 도메인 도착 시 자동 차단·RAM/해상도·이력 기록을 후속 SPEC 에서 보강한다. 전 화면 대응은 Store Surface Matrix 참조.
미구현 / 후속
Stepper intro(간단 소개)·start(음악 시작) 스텝 화면— 구현됨(SPEC #110) + design_handoff_linkmusic_10 시안 정합 완료(위 “간단 소개·음악 시작” 섹션).- 절전·스피커의 자동 측정(브라우저 한계로 불가 — OS 연동 시 가능).
- 서버 device-check(
POST /api/store/maintenance/device-check) — RAM·해상도·자동 차단·이력 기록(위 드리프트 표). - 온보딩 완료 BE 연동(
PATCH /api/store/me/onboarding-complete·환경 체크 이력 FR-9.5) — 현재 localStoragedone만(후속 SPEC #156 B-2)./store진입 온보딩 게이트(SPEC #156 B-1)는 이 localStoragedone을 소비한다.