Skip to content

Screen Blueprints — Auth ​

Status: Active Last reviewed: 2026-11-11

Current-state blueprints for the pre-auth screens the auth gate and the app shell draw (no route serves them: **Route:** none), read from the components. Conventions and the screen index live in screen-blueprint.md. History lives in git and rebuild-log.md — not here.


Email sign-in (이메일 로그인, L0-02) ​

  • Route: none — the page the auth gate renders when signed out (web: /login)
  • Screen: LoginScreen — packages/app/src/presentation/screens/auth/login-screen.tsx (body: login-form-body.tsx; state: use-login-form.ts, use-sign-in-attempt.ts; rules: login-form-model.ts; web: apps/web/app/login/page.tsx)
  • Shell: DetailShell · title t().authStart.title, no back
  • Design: unreviewed
  • Verified: 2026-11-11

Purpose. The one sign-in page for an account the operators provisioned (flat-v2-auth#em-ready, L0-02): email, password, 로그인. Until the start page exists (W5) it is the gate's first page, so the Header has no back. Web /login is this same screen; the web shell only keeps returnTo (through safeReturnTo) and replaces to it, or to /home, after sign-in.

Anatomy.

├─ [DetailShell] title=이메일로 로그인 (no back) · footer=[CtaBand]
├─ [Notice neutral] 로그인이 필요해요 (fromLink only)
├─ Gap $5 (24)
├─ [FieldColumn]
│  ├─ [Field] 이메일 (required) → [Input] email keyboard, autofill username
│  └─ [Field] 비밀번호 (required) → [Input] password, autofill password
├─ [Button tonal] Dev 계정으로 로그인 (not on the board; only when EXPO_PUBLIC_DEV_EMAIL and EXPO_PUBLIC_DEV_PASSWORD both exist)
└─ [CtaBand] primary 로그인 · helper (danger) = the general failure line

States. Ready. Pressed with empty fields (em-press): each empty field carries its own line and focus goes to the first; typing clears that field's line. Pending: the band shows 로그인하는 중... and is off. Credential mismatch (em-wrong): one line under the password (이메일 또는 비밀번호가 맞지 않아요), both inputs kept; editing the password clears it. Any other typed failure: the general line in the band's helper (getAuthErrorMessage). A failure that is not a DomainError propagates. Offline (si-offline): the strip shows and the band is disabled. From a link (si-link): the neutral Notice states sign-in is needed first; the caller passes fromLink (mobile: the pathname the gate keeps is not home; web: returnTo is present and not /home).

Actions. 로그인 → validate → useAuth().signInWithEmail(trimmed email, password) → the gate re-renders the stack at the kept path (mobile) or onSignedIn replaces to safeReturnTo(returnTo) ?? '/home' (web). Dev button → the same call with the env pair, skipping validation.

Gaps. The start page (lockup, tagline, provider buttons, 이용약관 / 개인정보 처리방침 links), provider sign-in (Kakao, Naver, Apple, Google), sign-up, password reset and the back to the start page wait for W5. The dev button is not on the board. The page has no 이용약관 door yet. Nothing ran on a device or in a browser; the web page has no test project.



Onboarding (온보딩, L0-04) ​

  • Route: none — the auth gate renders the wizard while the session needs onboarding, over the path the person arrived by (web: apps/web/components/route-guard.tsx renders the same screen)
  • Screen: OnboardingWizardScreen — packages/features/onboarding/src/onboarding-wizard-screen.tsx (steps: onboarding-step-basics.tsx, onboarding-step-under-14.tsx, onboarding-step-consent.tsx, onboarding-step-start.tsx; state: use-onboarding-flow.ts, use-basics-form.ts, use-consent-form.ts, use-onboarding-finish.ts; rules: onboarding-model.ts, onboarding-validation.ts, onboarding-consent.ts, onboarding-finish-payload.ts, onboarding-first-actions.ts)
  • Shell: WizardShell (one per step) · header × or back, progress 1/3 to 3/3 (1/2 to 2/2 when 3/3 is skipped), the step's title · the exit Dialog over it
  • Design: unreviewed
  • Verified: 2026-11-15

Purpose. The three steps between the first sign-in and the app (flat-v2-auth, section onboarding, L0-04): 1/3 who the person is (interim: typed, until 본인인증 exists), 2/3 the PIPA consents, 3/3 what to do first. The age check precedes consent (PIPA §22-2); nothing is stored before the finish.

Anatomy.

├─ [WizardShell step 1/3] title 기본 정보를 알려 주세요 · × (exit Dialog) · CtaBand 다음 (soft until both valid)
│  ├─ reason line (secondary) 나이대가 맞는 클럽과 경기를 보여 드리려고 물어봐요.
│  └─ [FieldColumn] [Field 출생 연도 *] → [Input number-pad, unit 년, max 4]  ·  [Field 이름 *] → [Input, limit 20]
├─ [WizardShell step 1/3, under-14 stop] × · no band · [EmptyState kind=permission] 만 14세부터 쓸 수 있어요 / 로그아웃
├─ [WizardShell step 2/3] title 약관에 동의해 주세요 · back · CtaBand 동의하고 계속 (soft while one required row is left)
│  └─ [ConsentGroup] 전체 동의 · terms, privacy, transfer (required) · location (optional) · marketing last; each row's 보기 door
├─ [WizardShell step 3/3] title 어떻게 시작할까요? · back · 건너뛰기 (header) · no band
│  ├─ [Notice error] 시작하지 못했어요 + 다시 시도 (a failed finish only)
│  └─ [Cell nav ink, 2 lines] 클럽에 가입할래요 · 매치로 칠래요 · 클럽을 만들래요
└─ [Dialog] 가입을 그만둘까요? — 계속하기 / 로그아웃

States.

  • Offline (F1c, the app's root strip says it): offline step 3 (첫 시작): the three Cells are write Cells the kit holds, 건너뛰기 is disabled by the page (the wizard footer is not gated); no strip of its own.

  • 1/3 (vi-typed): both fields empty, 다음 soft. Pressed (vi-typed-press): an empty year says 출생 연도를 입력해 주세요, a year outside 1900 to this year says the range line, an empty or out-of-range name says 이름을 입력해 주세요 or the 2 to 20 line; focus goes to the first; typing updates the lines live. A year under 14 (vi-u14): the stop, nothing stored, 로그아웃 the only way out (the header × asks first); 14 continues.

  • 2/3 (co-ideal, co-none, co-one, co-press, co-all): nothing pre-ticked; the primary is soft only while exactly one required row is left and validates on press while several are; a press marks each unticked required row with its line (서비스 이용약관에 동의해 주세요 and its two siblings), cleared as it is ticked; 전체 동의 ticks every shown row, or clears them when all are ticked. Back does not repeat the age check.

  • 3/3 (fs-ideal, fs-busy, fs-error, fs-offline): the tapped Cell spins, the others and 건너뛰기 are inert; a failed finish stays with the Notice (no toast); offline shows the strip and every Cell and 건너뛰기 are inert.

  • A sign-up that began from an invite or share link (the destination the gate resolved is not the plain home): 3/3 is not part of it, the header reads 1/2 and 2/2, and 동의하고 계속 finishes and goes there; a failed finish shows the band's danger helper.

  • Exit (vi-exit): the header × and the hardware back at 1/3 open the Dialog; 계속하기 keeps every typed value, 로그아웃 signs out. Elsewhere the header back and the hardware back go one step back.

  • A document opened from 보기 covers the wizard; the wizard stays mounted, keeps its state, and stands its hardware back down until the document closes.

Actions.

  • 다음 (1/3) → validate; invalid: mark and focus; valid: the under-14 stop or 2/3.
  • 보기 (2/3) → useOpenPublicDocument()(policy URL) (the terms and privacy pages, over the wizard; never a push).
  • 동의하고 계속 → the missing rows, or 3/3, or (link sign-up) the finish.
  • Cell → finish, then routes.discoverClubs (클럽에 가입할래요), routes.activityPickups (매치로 칠래요) or routes.createClub (클럽을 만들래요). 건너뛰기 → finish, then the post-onboarding destination (home by default).
  • The finish: revalidate every answer (an invalid one sends the person to its step); one useGrantConsent per ticked shown row, then useUpdateProfile (display name, birth year, the PIPA timestamps, marketing timestamp), both silent and one after the other, so the consents are on record before the profile marks the onboarding complete; then completeOnboarding(), appRouter.replace(destination). A rejected grant or profile write fails the finish the same way; 다시 시도 sends every write again (the grant returns the active grant, the profile update repeats the same values).
  • × / hardware back at 1/3 → the exit Dialog → 로그아웃 signOut().

Gaps. Step 1/3 is interim until 본인인증 exists (EXT-19): the verification frames (vi-intro to vi-agency), the duplicate-account and no-phone stops, and the 문의하기 door wait with the vendor; none is built or tested. The step 3/3 skip rule reads "the destination is not the plain home", so a link that resolves to home itself still shows 3/3. The offline strip's 다시 시도 only repeats a failed finish. Nothing ran on a device or in a browser.


Update required (업데이트가 필요해요, L0-05) ​

  • Route: none (the whole app is replaced by it)
  • Screen: UpdateRequiredScreen — packages/app/src/presentation/components/update-required-screen.tsx (rendered by AppVersionGate)
  • Shell: none (a full EmptyState, no header, no tab bar)
  • Design: unreviewed
  • Verified: 2026-11-11

Purpose. Replaces the app when this build is below the server's minimum version (flat-v2-auth#up-ideal, L0-05); it fails open, so offline and errors never show it.

Anatomy.

└─ [EmptyState kind=full] lockup · title 업데이트가 필요해요 · body · [action] 업데이트 (when a store link exists)

States. With a store link (up-ideal): the body 새 버전에서 계속 이용할 수 있어요 and the one action. Without one (up-nostore, until the stores list the app, EXT-8): no button, and the body continues with 스토어에서 앱을 업데이트해 주세요 (no app name; the name is open). English matches without a trailing period.

Actions. 업데이트 → openExternalUrl(storeUrl). There is no way past the gate.

Gaps. The board's action word is 업데이트하기; the house style refuses 하기 outside its idioms, so the app keeps 업데이트. The gate cannot run on web or Expo Go (the runtime version is null there, so it fails open). Nothing ran on a device.


웹 공유 페이지 (L0-07) ​

  • Route: none — web only, no app route (apps/web: /s/[sessionId], /m/[matchId], /p/[userId], /lb/[clubId])
  • Screen: WebSharePage — apps/web/components/web-share-page.tsx (server half apps/web/lib/share-page.tsx under apps/web/app/{s,m,p,lb}/*/page.tsx; frame web-share-frame.tsx, lead web-share-lead.tsx, Cells web-share-rows.tsx, door web-door-link.tsx, dispatch web-share-view.ts)
  • Shell: none (a bare 480 px phone column on web; no header, no tab bar, no AppNav on a public path)
  • Design: unreviewed
  • Verified: 2026-11-11

Purpose. A share link opened without the app (flat-v2-auth#web, L0-07): what, when, where, level and seats, never who. One action, 앱에서 열기.

Anatomy.

├─ [server page] read `get_public_share_preview` once per request → metadata (title, description from `shareCardMeta`, robots noindex) · not found / error segments
└─ [WebSharePage] (client island) → [WebShareFrame]
   ├─ lockup slot (앱 이름 placeholder until the name is filed, EXT-17)
   ├─ (24) the link's card as the hero (`/{kind}/{id}/opengraph-image`, alt = `shareCardLines(preview).alt`)
   ├─ (24) lead: the title as the h1 + one meta line (club, district) — or, for /lb, the club's MediaRow (mono initial, district · level band · members; no h1)
   ├─ (24) [CellGroup] the sessions models' `web.rows` (drawn after mount; the kit's loading Cells until then)
   └─ (32) [WebDoorLink] an anchor to `twomore://{kind}/{id}` wrapping the inert primary lg Button 앱에서 열기

States. Found; a cancelled or finished session (일시 + 상태 Tag only); a match under review (검토 중, numerals kept) or not counted (기록 제외, no numerals); /p without a score before five sessions (no 신뢰 점수 row); not found or private (not-found.tsx: the kind's title, 삭제됐거나 주소가 바뀌었어요, no card, the door still shown); a failed read (error.tsx, never "not found"). Not indexed; rendered on request. A signed-in visitor sees the same page.

Actions. 앱에서 열기 → the app's scheme (a plain link, working before hydration).

Gaps. No store link (EXT-8) and no verified universal link (EXT-13): the door is the scheme alone. /join-club for a signed-out visitor is owed (W12; the page-links rows are owed). /lb has no h1 (the club row leads, as drawn). The web shell draws no page content on the server (SafeAreaProvider renders its children only once the browser has measured, for every web route): the server's part is the metadata, the link card and the status, and a missing target answers 200 with noindex, not 404. The island's props are serialized into the document, so the server hands it previewForBrowserPage(preview) (no stored title, free-text place, coordinates, photo or raw tier). The page was fetched and rendered in a desktop browser against a local database only; it did not run on a phone browser or in KakaoTalk's in-app browser.

Markdown remains the source of truth. Run yarn docs:check before handoff.