Skip to content

Flat v2 — information architecture and core task flows (2026-10-09) ​

Status: Draft · Date: 2026-10-09 · Phase C0 of flat-grammar-v2-migration-plan.md (the "Information architecture & navigation" and "Task flows" rows of What the plan was missing). Inputs: the grammar (§5, six screens), the component library (the only vocabulary this document uses to describe a screen), the usability audit (U-01 to U-37), the information-flow audit (§3 to §7), the open-issues plan, the screen blueprints, the screen audit and the terminology guide. Everything stated about the app was read from code on 2026-10-09; citations are file:line for code and document plus section for docs. Korean appears only as in-app labels.

This document is the input to the Phase C page-layout boards. It draws no pixels. It fixes five things: where every screen lives and how a user gets there (§1), the IA questions that are still open with a recommendation each (§2), the core jobs as screen sequences on v2 components with tap counts (§3), which template board every route belongs to and which states it must show (§4), and what only the owner can decide (§5).

Counting rules. A tap is one deliberate touch on a control, including a tap that dismisses a confirm dialog. Typing, scrolling, an OS permission prompt and a trip to another app are listed separately and never counted as taps. Today means the code on 2026-10-09, not the usability audit's task table of 2026-09-16 (see the first finding below).

0. Findings that change the plan ​

  1. The route inventory is 94 files, not 94 screens. apps/mobile/app/** holds 7 layouts, 11 alias or redirect files, 4 public share receivers and 72 screens. By template: hub 4 · list 28 · detail 22 · form/wizard 12 · settings 6 (§1.3, §4). Web mirrors all of them and adds /login, /status, /preview and three /settings/* duplicates of /legal/* (§1.7).
  2. No route has a modal or sheet presentation. Grep for presentation:, animation: and gestureEnabled over apps/mobile and packages finds only use-leave-guard.ts:50 (the iOS swipe lock on a dirty form). Every non-tab screen is a push onto the root Stack (apps/mobile/app/_layout.tsx:222) and therefore hides the tab bar; sheets, dialogs and menus are overlays inside a screen (51 files render ModalPanel, 63 call confirm.show, 16 reference HeaderActionsMenu). The comments in routes.ts:4-8, :52, :59 and :147 that say detail screens keep the tab bar (and that the match board calls a useHideTabBar hook, which does not exist) are stale.
  3. The usability audit's task table is out of date and several "gaps" no longer exist. Shipped since 2026-09-16 and verified in code: the payment account line (U-02), confirms on dues and session-fee actions (U-04), a labelled 관리 entry (U-05), a labelled 초대 코드 entry (U-09), a join-first empty state (U-10), bulk mark-paid and 납부 취소 (U-15, U-04b), a create-club quick path (U-32), 형식 seeded from the club (U-33), a score recap toast (U-34). What is still missing is named per flow in §3. The "dues for 20 members, about 45 taps" row in the migration plan is therefore wrong today (§3 flow 8).
  4. The v2 boards leave four holes in the map. (a) Dropping the 홈 scope row orphans /records/week, /records/month and /records/day (their only entries are the 홈 recap card and the Record Detail calendar). (b) The 기록 and 프로필 boards drop the only entries to /records/history, /records/analysis/rivals, /profile/practice and /messages. (c) The Header spec (icons only) and the Menu spec (at most 5 rows) cannot hold the 관리 text action or the session ⋯ menu, which holds up to nine rows for a host. (d) Detail pages with a cover photo or map band (club hub, session, venue) lose it silently: the inventory re-specs MediaHero to a text-only hero ("no band") and PlaceShell to a collapsing header, and the library has no media row, while the owner rulings COVER-ALWAYS (2026-09-26) and MAP-BAR-INK (2026-09-28) put a photo or map there.
  5. Back is unsafe on cold-start deep links. The app sets no router anchor (unstable_settings appears nowhere under apps/mobile/app) and appRouter.back() has no fallback; only the notification centre checks canGoBack() (notification-center-screen.tsx:242). A universal link or twomore:// URL opened on a killed app starts a one-entry stack, because the router builds its initial state from the URL and nothing anchors (tabs) beneath it; a push tap on a killed app is replayed after the tabs have mounted (use-notification-handler.ts:88-98), so it has history. Unverified on device.
  6. Two push routes are dead ends or detours. Every payment-category push with a sessionId goes to /sessions/[id]/payments (signal-routes.ts:83-86), but that screen is walled to canManageDues or the host (session-payments-screen.tsx:300-301), while session_payment_hold_created and session_payment_confirmed are emitted to the payer (00385_alert_model_wiring.sql:62-66, :207-211). A new join request push takes three hops to the queue (§1.4).
  7. Share links lose the person at the moment of intent. A signed-in non-member who opens /s/[sessionId] gets a read-only fact list with no action (apps/mobile/app/s/[sessionId].tsx:42-56); the web landing offers only 앱에서 열기 (apps/web/components/open-in-app-button.tsx), no install path; the signed-out web join page renders and then ignores the tap on 가입하기 (join-club-screen.tsx:101-102), and the onboarding destination for /join-club drops the code parameter (onboarding-intent.ts:78-84, no test covers it).
  8. Onboarding collects 플레이 스타일 and nothing reads it. Only onboarding-* and auth.store.ts reference playStyle. The step can become the first-action chooser that routes the new user (flow 1).
  9. Score entry exists in three grammars. Inline numeric fields on the match board (match-card.tsx:98-105), a ±1 stepper sheet on the live scorecard (score-edit-sheet.tsx:82-107, ten taps to enter 6 to 4) and a full-set correction sheet for completed matches.
  10. Dead and duplicate paths exist and should be cleaned before boards are drawn from them: 8 route builders with no route file, one in-app-orphan screen (/courts), 11 aliases, 4 URL shapes for one session (§1.6).

1. Navigation map ​

1.1 The model, as built ​

  • Gate before routes. AuthGate (apps/mobile/app/_layout.tsx:181-228) renders a skeleton while loading, LoginScreen when unauthenticated, OnboardingWizardScreen when needs_onboarding (its post-onboarding destination comes from resolveOnboardingDestination, :185-188, :205-207), and the root Stack only when ready. AppVersionGate wraps all of it (:336-338). Login and onboarding are therefore not routes.
  • One Stack, one tab group. The root Stack has no per-screen options (:222). (tabs) is one child of it: Tabs initialRouteName="(home)" with five tabs, 홈 · 클럽 · 경기 · 기록 · 프로필 ((tabs)/_layout.tsx:115-154), each tab hosting its own header-less Stack ((tabs)/(home)/_layout.tsx:15-20, the other four :4-9). Those stacks contain only each tab's index.tsx and one redirect ((clubs)/discover.tsx). Every other route file sits at the root, so opening it pushes over the tabs and the tab bar disappears. The tab bar's only badge is a danger dot on 홈 while a session is live today (HomeTabIcon, :168-197).
  • Presentation values. T tab root (inside (tabs)), P push over the tabs (tab bar hidden), R redirect or replace (renders nothing). There is no modal route. Sheets, confirm dialogs, the anchored header menu and the venue search takeover live inside screens (catalogue in §4.7).
  • Router helper. appRouter (packages/app/src/navigation/app-router.ts) is the one importer of expo-router. It drops a second push to the same href within 1000 ms and any second push or replace within 300 ms (:50-64); back() is not debounced and has no fallback (:75-77); canGoBack() exists (:78) but only notification-center-screen.tsx:242 calls it. useLeaveGuard locks the iOS edge swipe while a form is dirty (use-leave-guard.ts:50); guarded screens: create-club, create-session, board create, match rules, edit profile, venue contribution and owner-claim editors, session fact editing, onboarding.
  • Back target. back() pops to the opener. A screen entered by replace returns to whatever preceded the flow: join-club to the club hub (join-club-screen.tsx:64), create-club to the hub (use-club-submit.ts:228), create-session to the session or host dashboard (use-submit-session.ts:246,257), board post to the post (post-create-screen.tsx:204), contribution and claim submit to their receipt (contribution-editor.tsx:54, application-editor.tsx:93), DM new group to the thread (dm-create-group-screen.tsx:124), pickup preview to the detail once the viewer is in (public-pickup-preview-screen.tsx:428-443).
  • A cold-start link has no history. The root layout declares no anchor (no unstable_settings under apps/mobile/app), so a universal link or twomore:// URL opened on a killed app starts a stack whose only entry is the linked screen, and back() queues a GO_BACK with nothing to pop (node_modules/expo-router/build/global-state/router.js:89-95). A push tap on a killed app is different: it is replayed after the first paint, on top of the mounted tabs (use-notification-handler.ts:88-98), so Back reaches 홈. Unverified on device; see IA-9.
  • Entry types in use. Tab bar; row tap (routes.clubSession has 25 call sites, routes.club 19, routes.profileDetail 11); header action; band CTA or FAB-like button; push (getSignalRoute, §1.4); custom scheme twomore:// (apps/mobile/app.json:8); universal links for /s/*, /m/*, /p/*, /lb/*, /join-club* (app.json:21,44-76, apps/web/lib/app-link-association.ts:3); share links (§1.5); invite links (/join-club?code=).

1.2 Map at a glance ​

AuthGate (not a route)
 ├─ unauthenticated   LoginScreen (email and password only; copy: accounts come from ops)
 ├─ needs_onboarding  value props (3 cards) → 6 steps → replace(destination)
 └─ ready  →  root Stack
     ├─ (tabs): the tab bar exists only here
     │    홈      bell → 알림 · session row → 일정 상세 · week/month recap card → 기간 기록 → 일일 요약
     │    클럽     row → 클럽 홈 (a non-member sees its public view, review 41) · 초대 코드 → 가입 · "+" → 클럽 만들기
     │    경기     row → 일정 상세 / 번개 미리보기 · 번개 만들기 → 일정 만들기 · 테니스장 → 장소 상세
     │    기록     footer rows → 리더보드 · 경기 기록 · 상대 전적
     │    프로필   message icon → 메시지 · gear and menu row → 설정 · 친구 · 업적 · 연습 기록 · 최근 경기 → 경기 기록
     └─ pushed over the tabs (tab bar hidden)
          클럽 홈 (5 panes, review 41) → 관리 → {멤버 관리 → 가입 심사, 회비, 일정 관리, 출석 현황, 신고, 테니스장 관리, 도전 과제, 성장 분석, 클럽 설정}
          일정 상세 → {참석자, 출석 확인, 호스트 관리, 경기 규칙, 참가비, 스코어카드, 대진표, 토너먼트}
          person row anywhere → 공개 프로필 → 메시지

1.3 Route table (mobile, 87 route files that are not layouts) ​

Template codes: H hub · L list · D detail · F form or wizard · S settings · A auth or public receiver · R redirect. "Back" is appRouter.back() to the opener unless stated; the opener is therefore any of the entries listed. Citations in "Reached from" are the tap sites (file:line).

Tab roots (5)

PathScreen (as titled)TplReached fromBack · Pres
/(tabs)/(home)홈HTab bar; alias /home; default first destination after onboarding (onboarding-intent.ts:26-27); the missing-id fallback of seven route files (clubs/[clubId], directory-venues/[id], profile/[userId], sessions/[sessionId], venues/[id], and literally in match-board.tsx and tournament-board.tsx)— · T
/(tabs)/(clubs)클럽 (내 클럽 / 탐색)LTab bar; aliases /clubs, /clubs/discover; ?tab=discover from the 홈 discovery card (home-club-discovery-card.tsx:23); leave or archive a club ends here (club-detail-screen.tsx:229, club-settings-screen.tsx:241)— · T
/(tabs)/(sessions)경기 (예정 / 번개 찾기 / 테니스장)LTab bar; alias /activity; ?tab=pickup_finder from the 홈 empty state (home-today-view.tsx:454), the 홈 nearby card (home-nearby-pickups-card.tsx:105), the 기록 empty state (records-screen.tsx:359)— · T
/(tabs)/(records)기록HTab bar; aliases /records, /ranking; push elo_changed, tier_promoted, tier_demoted (signal-routes.ts:63-65)— · T
/(tabs)/(profile)프로필HTab bar; alias /profile; push for progression, rating, trust, system and a payment without a session (signal-routes.ts:76-90)— · T

Aliases and redirects (11) — no UI, one hop (two for the first row), all kept for legacy URLs or for builders that still emit them

PathGoes toNotes
/home, /clubs, /activity, /records, /profilethe five tab roots, query kept (TabAliasRedirect, app-router.ts:104-107)routes.home, routes.clubs, routes.activity, routes.records, routes.profile emit these; web nav uses them (app-destinations.ts:23-27)
/activity/pickups(sessions)?tab=pickup_finderroutes.activityPickups builds /activity?tab=pickup_finder instead; the file serves legacy links and the web sitemap
/(tabs)/(clubs)/discover(clubs)?tab=discoversecond hop of /clubs/discover
/clubs/discover/(tabs)/(clubs)/discoverroutes.discoverClubs builds /clubs?tab=discover, so this file is legacy only
/leaderboard, /ranking/records/leaderboard, /recordsroutes.ranking and routes.matchHistory duplicate other builders and have no caller
/clubs/[clubId]/sessions/[sessionId]/sessions/[sessionId]routes.clubSession ignores its club argument and returns /sessions/{id} (routes.ts:114)

Public share receivers (4) — see §1.5 for the links that reach them

PathScreenTplBehaviour
/s/[sessionId]일정 초대AMember: replace to /sessions/{id}. A signed-in non-member with a public preview: read-only 시간 and 장소 facts, no action (apps/mobile/app/s/[sessionId].tsx:42-56). Unknown id: not-found state.
/m/[matchId]경기 결과AViewer with session access: replace to /sessions/{id} (not to the match). Otherwise a public result preview (apps/mobile/app/m/[matchId].tsx:84-98).
/p/[userId]프로필 공유AResolves the profile, then replace to /profile/{id} (apps/mobile/app/p/[userId].tsx:36).
/lb/[clubId]리더보드 공유AResolves the club, then replace to /clubs/{id}?tab=records (apps/mobile/app/lb/[clubId].tsx:37); a non-member is then bounced a second time to the club preview (club-detail-screen.tsx:178).

Home (1)

PathScreenTplReached fromBack · Pres
/home/notifications알림L홈 bell (notification-bell.tsx:60); web nav 알림 (app-nav.tsx:287). Not a push target: pushes open the entity the signal references (§1.4).pop, else replace(home) when history is empty (notification-center-screen.tsx:242-247) · P (on web it sits inside the tab group and keeps the bar)

Clubs (19)

PathScreen (as titled)TplReached fromBack · Pres
/clubs/[clubId]club name; panes 홈 · 채팅 · 순위 · 멤버 · 회비 (review 41: the 일정 pane folds into 홈's Feed; a non-member sees one public pane)HClub card for a member (club-card.tsx:233); join success (join-club-screen.tsx:64); push club, interclub and social without a post (signal-routes.ts:48-58); /lb/* (?tab=records); create-club successpop; a non-member is replaced to the preview (club-detail-screen.tsx:178) · P
/clubs/[clubId]/preview클럽 미리보기 (retires, review 41: a non-member opens /clubs/[clubId] itself)DClub card for a non-member (club-card.tsx:233); bounce from the hub (club-detail-screen.tsx:178)pop; a member is replaced to the hub · P
/join-club클럽 가입하기 (or "{club} 가입하기")F클럽 header 초대 코드 (club-join-header-action.tsx:13); 내 클럽 empty state (club-list-screen.tsx:395); an invite-only preview CTA (public-club-profile-screen.tsx:224); invite link /join-club?code=; onboarding inviteesuccess: replace to the hub · P
/create-club클럽 만들기F클럽 header "+" for a member with clubs, or the secondary band for a newcomer (club-list-screen.tsx:484,500); onboarding club_adminguard; success: replace to the hub · P
/clubs/[clubId]/admin관리SHub header 관리 text action, officials only (club-detail-screen.tsx:519-523); legacy ?tab=admin bounce (:190); push club_join_request_received (three hops, §1.4)pop; a non-admin is replaced to the hub (club-admin-screen.tsx:54) · P
/clubs/[clubId]/members멤버 관리L관리 row (admin-tab.tsx:80); hub header member avatars (club-place-header.tsx:88); public profile (public-club-profile-screen.tsx:401); admin attention kind 가입 (club-admin-attention.ts:243)pop · P
/clubs/[clubId]/partners복식 파트너 추천L멤버 pane row only (members-pane.tsx:232)pop · P
/clubs/[clubId]/join-review/[userId]applicant name (가입 심사)D멤버 관리 queue row (members-list-header.tsx:170)pop · P
/clubs/[clubId]/dues회비L관리 row (admin-tab.tsx:99); the hub's 회비 pane embeds the same body; push dues category (signal-routes.ts:34-36); admin attention kind 미납 (club-admin-attention.ts:248)pop · P
/clubs/[clubId]/sessionsclub name with subtitle 일정L관리 row 일정 관리 (admin-tab.tsx:108); admin attention kinds 송금 확인 and 게스트 (club-admin-attention.ts:246). The hub's 일정 pane has no link to it.pop · P
/clubs/[clubId]/attendance출석 현황L관리 row (admin-tab.tsx:90); snapshot cell 출석 주의pop · P
/clubs/[clubId]/reports신고 관리LSnapshot cell 신고 and admin attention kind (club-admin-attention.ts:250) only; no 관리 row, so resolved reports cannot be browsed when the count is zeropop · P
/clubs/[clubId]/venues테니스장 관리L관리 row (admin-tab.tsx:117); club settings info row (settings/info-section.tsx:219)pop · P
/clubs/[clubId]/settings클럽 설정S관리 row (admin-tab.tsx:165); archived-club row on 클럽 (archived-club-row.tsx:15); create-club receipt (club-setup-receipt.tsx:149, replace)pop · P
/clubs/[clubId]/challenges도전 과제L관리 row (admin-tab.tsx:126)pop · P
/clubs/[clubId]/analytics성장 분석D관리 row, only with the growth-analytics entitlement (admin-tab.tsx:153)pop · P
/clubs/[clubId]/album앨범LAlbum banner on the hub (club-place-header.tsx:79); 사진 업로드 feed row (auto-media-row.tsx:57)pop · P
/clubs/[clubId]/board/[postId]게시글DFeed row (shared/post-row.tsx:371); push social with a post (signal-routes.ts:48-50); post-create success (replace)pop · P
/clubs/[clubId]/board/create글쓰기FHub 홈-pane band (club-detail-screen.tsx:543); mission card steps 1 and 3 (use-home-tab-data.tsx:366)guard; success: replace to the post · P

Sessions (11)

PathScreen (as titled)TplReached fromBack · Pres
/sessions/[sessionId]일정 상세 (hero is the title)DAny session row or card (25 routes.clubSession call sites: 홈, 경기, club panes, match history match-history-row.tsx:94, auto match row auto-match-row.tsx:29); create success (replace); push session, rsvp (signal-routes.ts:43-46); /s/* and /m/* for members; legacy club-scoped URLpop · P
/sessions/[sessionId]/preview번개 미리보기 (hero is the title)D번개 찾기 card (activity-screen.tsx:353); 홈 nearby card (home-nearby-pickups-card.tsx:51)pop; member, host or approved: replace to the detail · P
/sessions/[sessionId]/participants참석자 (viewer) or 참석자 관리 (manager)LRoster "외 N명" door (participation-section.tsx:121); host menu 참석자 관리 (host-menu-actions.ts:52)pop · P
/sessions/[sessionId]/attendance출석 확인LHost menu only (host-menu-actions.ts:77-80)pop · P
/sessions/[sessionId]/host호스트 관리LPending-applicants banner (session-alerts-section.tsx:70); host menu (host-menu-actions.ts:66); pickup create success (use-submit-session.ts:246, replace)pop · P
/sessions/[sessionId]/match-rules경기 규칙FHost menu (host-menu-actions.ts:57); a fact row in the edit state (use-session-fact-editing.ts:177); create-tournament modalguard · P
/sessions/[sessionId]/payments참가비 (결제 현황 or 비용 정산)LThe host's CTA 정산 on a completed paid session (primary-action.tsx:395); refund banner (refund-banners.tsx:64); match-board unsettled notice (unsettled-pool-notice.tsx:39); push payment category (signal-routes.ts:83-86). No entry while the session is open or locked.pop · P
/sessions/[sessionId]/scorecard경기 스코어카드DLive-session CTA for a confirmed participant (primary-action.tsx:381); 전체 대진표 보기 (session-matchups-card.tsx:213); 홈 disputed-match banner (home-disputed-alert-banner.tsx:89); push matchup without a club id (signal-routes.ts:40)pop · P
/create-sessionper-step titles (e.g. 언제 만나나요?)FHub 일정-pane band (club-detail-screen.tsx:555); 일정 관리 (club-sessions-screen.tsx:327); 번개 만들기 band, 번개 찾기 only (activity-screen.tsx:535); a venue's "host here" (use-directory-venue-detail.ts:437); club settings 운영 row (operations-section.tsx:72); web nav create, which always starts a pickup (app-nav.tsx:209); onboarding pickup_hostguard; success: replace to the detail or host dashboard · P
/match-board?sessionId=session titleDHost CTA while live (primary-action.tsx:365); club settings 운영 row (operations-section.tsx:93); push matchup with club and session (signal-routes.ts:39)pop; missing id: replace to 홈 (match-board.tsx:14-16) · P
/tournament-board?tournamentId=토너먼트DSession detail 이어보기 chip (session-detail-screen.tsx:449); create-tournament modal (create-tournament-modal.tsx:260)pop; missing id: replace to 홈 · P

Records (5)

PathScreenTplReached fromBack · Pres
/records/[period]기간 기록 (week or month)DOnly the 홈 week and month recap card (recap-summary-card.tsx:201)pop · P
/records/day?date=일일 요약DOnly a day cell on 기간 기록 (record-detail-screen.tsx:192)pop; an invalid date or no user calls router.back() before mount (records/day.tsx) · P
/records/history경기 기록 (전적)L기록 footer row (records-screen.tsx:411); 프로필 최근 경기 전체 보기, own profile with matches (recent-matches-section.tsx:66)pop · P
/records/leaderboard리더보드L기록 footer row (records-screen.tsx:417); alias /leaderboardpop · P
/records/analysis/rivals상대 전적DOnly the 기록 footer row (records-screen.tsx:405)pop · P

Profile, social and messages (9)

PathScreenTplReached fromBack · Pres
/profile/[userId]player nameDAny person row, avatar, podium or identity chip (routes.profileDetail 11 sites, routes.userProfile 3); /p/* redirectpop · P
/profile/friends친구 목록L프로필 friends summary (friends-summary-card.tsx:102,127)pop · P
/profile/friends/add친구 찾기LFriends header "+"; summary card (friends-summary-card.tsx:116); 기록 empty state (records-screen.tsx:298); DM pickers' empty statespop · P
/profile/achievements업적L프로필 achievements preview (achievements-preview-section.tsx:127)pop · P
/profile/practice연습 기록 (removed 2026-10-31, owner review 52: the practice log left the product; 테니스 여정, library § 1.23, took its Cell)L프로필 menu row only (profile-menu-card.tsx:48; the file's own comment records that it has no other entry)pop · P
/messages메시지L프로필 header message icon with unread dot (profile-screen.tsx:236); push chat without a thread (signal-routes.ts:93); web navpop · P
/messages/[threadId]counterpart or group nameDInbox row; club 채팅 pane rows (dm-thread-row.tsx:131); public profile DM button (public-profile-screen.tsx:129); pickup host card (pickup-host-card.tsx:79); session menu group chat and 문의하기 (share-and-chat-actions.ts:58,78); push chatpop · P
/messages/new-group단톡방 만들기FInbox header icon (dm-inbox-screen.tsx:187); 멤버 관리 header (club-members-screen.tsx:242); session menu (share-and-chat-actions.ts:62)success: replace to the thread · P
/messages/add-members멤버 초대 (?threadId=)FThread participants modal (participants-modal.tsx:162)pop · P

Settings and legal (13)

PathScreenTplReached fromBack · Pres
/settings설정S프로필 header gear (profile-screen.tsx:255) and menu row (profile-menu-card.tsx:53); web navpop · P
/settings/edit-profile프로필 수정FSettings 계정 row; 기록 empty states (records-screen.tsx:310); leaderboard region empty stateguard · P
/settings/change-password비밀번호 변경FSettings 계정 row (settings/account-section.tsx:19)pop · P
/settings/accounts연결된 계정 (new)SSettings 계정 row 연결된 계정 (new; owner review 4, D6)pop · P
/settings/signal-preferences알림 설정SSettings 알림 row (settings/notifications-section.tsx:47); 알림 header gear (notification-center-screen.tsx:517)pop · P
/settings/privacy프라이버시SSettings 프라이버시 row (settings/privacy-section.tsx:18)pop · P
/settings/consent동의 관리SSettings 프라이버시 row (settings/privacy-section.tsx:19)pop · P
/settings/consent/history동의 이력L동의 관리 전체 내역 보기 (settings-consent-management-screen.tsx:276)pop · P
/settings/blocked차단 목록LSettings 프라이버시 row (settings/privacy-section.tsx:20)pop · P
/settings/delete-account회원 탈퇴FSettings danger row (settings/destructive-section.tsx:16); required-consent caption (settings-consent-management-screen.tsx:139)pop · P
/legal/notices공지사항DSettings 지원 rows (settings/support-section.tsx:19-31)pop · P
/legal/contact문의하기DSettings 지원 row (support-section.tsx:23)pop · P
/legal/terms이용약관DSettings 지원 row (support-section.tsx:27); onboarding consent 보기 (twomore://legal/terms, policy-versions.ts:24)pop · P
/legal/privacy개인정보 처리방침DSettings 지원 row (support-section.tsx:31); onboarding consent 보기 (twomore://legal/privacy, policy-versions.ts:25)pop · P

Venues (9)

PathScreenTplReached fromBack · Pres
/courts테니스장LNone in the app (routes.courts has no caller). The same body is mounted as the 경기 tab's 테니스장 pane; web reaches it by URL onlypop · P
/venues/[id]venue name (club-scoped)D테니스장 관리 rows (club-venues-screen.tsx:238); session hero map band (session-hero-section.tsx:92)pop · P
/directory-venues/[id]venue name (directory)D테니스장 pane cards and every VenueFinder row (venue-finder.tsx:294,416); create-session and create-club venue steps (create-session-step2.tsx:176, create-club-step2.tsx:343); contribution and claim receipts; push venue_correction_* (signal-routes.ts:27-32)pop · P
/venue-contributions장소 정보 제안L테니스장 header 제안 내역 (courts-directory-screen.tsx:50); 테니스장 관리 header (club-venues-screen.tsx:349); VenueFinder end-of-list row (venue-finder-contribution-row.tsx:49); receipt linkspop · P
/venue-contributions/new없는 장소 추가 (and kind-specific titles)F테니스장 band CTA (courts-directory-screen.tsx:58); 테니스장 관리 (club-venues-screen.tsx:321); venue detail menu 수정 and 신고하기; VenueFinder rowguard; submit: replace to the receipt · P
/venue-contributions/[id]제안 접수 내역DInbox rows; push system with contributionId (signal-routes.ts:24-26)pop · P
/venue-owner-claims소유권 신청 내역LContributions inbox link (venue-contributions/list-screen.tsx:71); claim editor (application-editor.tsx:157)pop · P
/venue-owner-claims/new소유권 신청FDirectory venue menu 소유권 확인 (directory-venue-detail-screen.tsx:291); claims list footerguard; submit: replace to the claim · P
/venue-owner-claims/[id]장소 소유권DClaims list rows; push system_announcement with ownerClaimId (signal-routes.ts:16-21)pop · P

Totals: 5 + 11 + 4 + 1 + 19 + 11 + 5 + 9 + 13 + 9 = 87, plus the 7 _layout.tsx files = 94.

1.4 Push notification targets ​

One function decides every push tap and every row tap in the notification centre: getSignalRoute (packages/app/src/presentation/utils/signal-routes.ts:11-97). The tap handler refreshes the signal list and the referenced entity's queries, then appRouter.pushes the result; a payload with no route is logged and nothing happens (use-notification-handler.ts:72-98, 216-233). Cold-start taps are replayed after the first paint (:88-98). Order below is the order the function tests, so the first matching row wins.

Signal (category, type) and required contextLands onVerdict
system_announcement with ownerClaimId/venue-owner-claims/[id]Good; authorization is re-checked on open (signal-routes.ts:16-21).
system with contributionId/venue-contributions/[id]Good.
venue_correction_approved or _rejected with venueId/directory-venues/[id]Good.
dues (assigned, due soon, overdue, severe, paid, cleared, waived, refund) with clubId/clubs/[id]/duesGood for both audiences: the screen has a roster view and a member view.
matchup (published, started, completed, score pending or disputed, …) with club and session/match-board?sessionId=; without a club, /sessions/[id]/scorecardEvery recipient lands on the host's controller. Whether a participant sees a read-only board is unverified; the scorecard is the participant surface.
session and rsvp (created, updated, cancelled, starting soon, deadline, waitlist promoted, reminders, guest open)/sessions/[id]; without a club, the scorecardGood.
social (post new, reply, mention, reaction) with a post/clubs/[id]/board/[postId]; without a post, /clubs/[id]Good.
club_join_request_received with clubId/clubs/[id]?tab=admin, which the hub bounces to /clubs/[id]/adminThree hops to the queue: hub → 관리 → 멤버 관리 → request row. The notification centre's own admin row goes straight to 멤버 관리 (club-admin-attention.ts:243). Route the push the same way.
club and interclub (application decided, member joined or left, role, archive, …)/clubs/[id]Good.
elo_changed, tier_promoted, tier_demoted/recordsGood.
match_outcome_unratedthe scorecard; else the clubGood.
progression, rating, achievement_unlocked/profileachievement_unlocked could open /profile/achievements; low priority.
payment with sessionId (hold created, hold expiring, hold expired, confirmed, submitted, refund notice)/sessions/[id]/payments; without a session, /profileDead end for members. Hold created and confirmed are emitted to the payer (00385_alert_model_wiring.sql:62-66 and :207-211), hold expired and refund notice likewise (00444_information_flow_wave2.sql:242-246, 00307_session_refund_acknowledgment.sql:113-118), yet the screen is walled to canManageDues or the host (session-payments-screen.tsx:300-301). Route payer-directed types to /sessions/[id] where the payment banner lives.
trust, other system/profileTrust types gained emitters in information-flow wave 2 (00444_information_flow_wave2.sql). The profile is a coarse target; TrustTierInfoSheet is the precise explainer.
chat with threadId/messages/[threadId]; without one, /messagesGood.
Anything else (no usable context)nothingThe tap acknowledges and does nothing (use-notification-handler.ts:225-234).

twomore://{path} opens any route through the router's linking. Only five path prefixes are universal links (app.json:44-76, app-link-association.ts:3): /s/, /m/, /p/, /lb/, /join-club. Everything else is reachable from outside only through the custom scheme or a push.

LinkBuilt byInstalled app, signed inWeb (not installed or signed out)Gap
https://twomore.app/join-club?code=buildClubInviteShareCard (share-cards.ts:209-221); the message body also prints the club name and the 초대 코드Join screen with the code prefilled; one tap on 가입하기 (join-club-screen.tsx:59-69, 94). The screen does not show which club the code is for unless the link carries clubId./join-club is a public path, so it renders for a signed-out visitor (public-paths.ts); 가입하기 then returns silently because userId is null (join-club-screen.tsx:101-102): no login prompt, and no sign-up exists (U-01).Preview the club before joining (client hook over the existing lookup_club_by_invite_code, migration 00146). Signed-out web visitors get a silent no-op. After onboarding the destination drops ?code= (onboarding-intent.ts:78-84).
https://twomore.app/s/{sessionId}share-cards.ts:149Member: detail. Non-member: facts only, no action (§1.3).Server-rendered card with title, club, time and one link, 앱에서 열기 (apps/web/app/s/[sessionId]/page.tsx, open-in-app-button.tsx).No apply path for a non-member, no install path for a person without the app. /sessions/[id]/preview already holds the apply state machine.
https://twomore.app/m/{matchId}share-cards.ts:327Session detail for viewers with access; else a public result preview.Server-rendered result headline and 앱에서 열기.Lands on the session, not on the match.
https://twomore.app/p/{userId}share-cards.ts:387, 462/profile/{id}.Server-rendered profile line and 앱에서 열기.—
https://twomore.app/lb/{clubId}share-cards.ts:539/clubs/{id}?tab=records; a non-member is bounced to the club preview.Server-rendered leaderboard line and 앱에서 열기.Double redirect for non-members.

1.6 Orphans and duplicate paths ​

No entry (or entries only from content the v2 boards drop).

ItemEvidenceConsequence
/courts (mobile and web)routes.courts has no caller; the 경기 tab's 테니스장 pane mounts the same screen bodyA route nobody can tap to. Delete the route or give it a real entry (IA-2).
Route builders without a route fileanalysisFormat, analysisPartners, analysisClubs, analysisActivity (routes.ts:53-57), tournament (:115), subscription (:297), challenges (:303), signalConsent (:172); profileSetup (:36) survives only as a blocked redirect path (onboarding-intent.ts:34)Eight builders that would 404 and that nothing references. Remove them.
/records/week, /records/month, /records/dayOne entry each: the 홈 recap card (recap-summary-card.tsx:201) and the 기간 기록 calendar (record-detail-screen.tsx:192)Orphaned the moment the 홈 scope row is dropped (IA-1).
/records/analysis/rivals, /records/history, /records/leaderboard기록 footer rows (records-screen.tsx:405-417); history also from 프로필 최근 경기The v2 기록 board has no footer rows (IA-8).
/profile/practiceSingle entry, the 프로필 menu row (profile-menu-card.tsx:48); its comment says there is no otherThe v2 프로필 board has no menu (IA-8).
/messagesSingle entry, the 프로필 header message icon (profile-screen.tsx:236) plus pushesThe v2 프로필 header is settings only (IA-5).
/clubs/[clubId]/reports browseReached from a snapshot cell that renders only when the count is non-zeroResolved reports cannot be browsed.
Session money while open or lockedroutes.sessionPayments callers are the completed-session CTA, refund banner, match-board notice and the push; the 관리 snapshot cell 송금 확인 goes to 일정 관리 (admin-snapshot-cells.ts:35)A 총무 who missed the push has no path to 입금 확인 until the session completes. Needs a persistent entry (flow 3, host side).
Web /preview, /status/preview iframes the old UI kit and is a public path (apps/web/app/preview/page.tsx, public-paths.ts); /status is gated by isStatusPageEnabledDev surfaces. /preview is public and linked from nowhere; remove it or put it behind the gate /status uses.

Several paths to one content.

ContentPathsRuling proposed
A session/sessions/{id}; /s/{id}, /m/{id} (members), /clubs/{c}/sessions/{id} (all replace to the first); routes.clubSession ignores its club argumentOne URL. Delete the club-scoped shim after one release.
Tab rootsfive aliases plus two discover and one pickups redirect (§1.3)Keep TabAliasRedirect for legacy links; stop emitting aliases from builders where a tab-root path exists.
Club 멤버, 일정, 회비Hub panes and routed screens /clubs/{id}/members, /sessions, /dues (the panes embed the same bodies: members-pane.tsx header, dues-pane.tsx, sessions-pane.tsx)Panes are for members, routed screens are for management (IA-7).
The club leaderboard/records/leaderboard (club scope), /leaderboard, /clubs/{id}?tab=records, /lb/{id}Same basis since the club rating pool was retired (migration 00670, club-rating-unification-plan-2026-09-24.md). Keep, and name one canonical in the boards.
The venue directory/courts, 경기 › 테니스장 paneIA-2.
A person/profile/{id} for oneself and others; routes.profileDetail and routes.userProfile build the same pathMerge the builders.
"My sessions"홈 sections and 경기 › 예정 (different grouping, same data class)Keep both; draw the difference: 홈 is next-up, 경기 is the full list.
Pending obligations알림 할 일 band and the 관리 attention snapshot (same obligations, two surfaces, two dismiss states)Keep; the snapshot is the club-scoped view of the same counts.

1.7 Web deltas (follow-up, not Phase C) ​

Web mirrors every mobile route file. It adds / (redirect to 홈 or login), /login, /status, /preview, /activity/sessions (redirect), /settings/announcements, /settings/contact and /settings/terms (duplicates of /legal/notices, /legal/contact, /legal/terms; the audit's U-37 records this), and server-rendered share pages for /s, /m, /p, /lb. /home/notifications sits inside the (tabs) group on web and keeps the bar, unlike mobile. At 600 px and above settings and messages become list-detail layouts (apps/web/app/settings/layout.tsx, messages/layout.tsx) and the tab bar becomes a rail (app-nav.tsx).

2. Open IA decisions ​

Each decision gives the evidence, the options and one recommendation. "Peers" are cited only where the repo holds the frame or the measurement (the benchmark notes under research/visual-benchmark-2026-10-08/); where it does not, the reasoning rests on the grammar and the usability findings, and says so.

IA-1. The 홈 scope row (일 / 주 / 월) ​

Evidence.

  • Today 홈 is three panes behind a 일 / 주 / 월 pill row, each pane its own list with a prev/next/scrubber panel; 일 shows live and today's sessions, 주 and 월 show a recap card plus day-grouped upcoming sessions (screen-blueprints/home.md, Home anatomy).
  • The v2 board has Header [bell] → Block (the next day that has sessions) → 내일 일정 → 이번 주 → FAB 일정, and no row (flat-v2.jsx:1311-1391; grammar §5 홈). The migration plan lists "홈 scope row dropped" as carried for the owner.
  • The row cost three findings in one audit: an empty recap drawing a fake donut (B5), a title that restated the pills (C8), and a 일 pane that answered the wrong question (H1, fixed 2026-09-23 by a union query) (screen-audit-2026-09-23.md §3).
  • What a drop removes: the recap card and, with it, the only door to /records/week, /records/month and /records/day (§1.6); week-by-week browsing on 홈. Upcoming sessions beyond the week stay on 경기 › 내 경기.
  • No usage numbers for the pills are in the repo, and the benchmark set holds no peer frame for a time-scope switch on a landing tab.

Options. (A) Drop the row; 홈 is "next up" only. Move the period recap to 기록 as a fourth facet, 기간 (이번 주 · 이번 달 · 전체), in the existing scope Sheet (MatchHistoryScopeSheet already carries 종목, 성격 and 구분, which are exactly the parameters /records/[period] takes) opening the existing 기간 기록. (B) Keep it as v2 Tabs (오늘 · 이번 주 · 이번 달): about 68 pt of chrome above the Block, and a Block plus a recap are not one list. (C) As A, plus one "이번 주 기록" Strip on 홈 when the week has matches.

Recommendation: A. 홈 answers one question (what is next), the grammar allows one Block and a flat section list, and a recap is history, which 기록 already owns (ELO, win rate, matches). A band would be needed to separate history from upcoming on 홈 (grammar §4b), which spends the screen's one band on a minor card. Ask the owner for the pill usage before building; if 주 or 월 is the majority view, take C. Build note: /records/[period] and /records/day keep their routes and gain a 기록 entry; nothing else changes.

IA-2. The 경기 tab's explore entry point (테니스장) ​

Evidence. Today four tabs: 내 경기, 번개 찾기, 저장됨, 테니스장 (activity-screen.tsx:84, 468-476); the fourth label was shortened from 테니스장 찾기 so four labels fit one strip (comment at :472-474, audit G1). The v2 board draws three (flat-v2.jsx:1400-1475). The directory is a ratified screen with its own filters, nav chips and a 없는 장소 추가 band (screen-blueprints/sessions.md, Court Directory) and /courts has no in-app caller (§1.6). Other doors to venue pages (session map band, VenueFinder rows, 테니스장 관리) are not browse-all. Library C5 sets Tabs as content-width labels, gap 24, scrolling past four (library §0 C5, §3 Tabs). Four labels measure about 300 pt of the 350 rail at 100% text by estimate (the board must measure it); at larger text they scroll.

Options. (a) Keep the fourth tab. (b) Fold venue browse into the 경기 header search takeover (results grouped 번개, 테니스장). (c) Move the directory to 클럽 or 프로필. (d) A last Cell on the 번개 찾기 list.

Recommendation: (a), labelled 테니스장. All four tabs answer "where and when can I play"; the surface is unchanged and ratified; the route /courts is then deleted because the tab is the entry. The header search acts on the active tab. Fallback if the owner wants three tabs: (b), accepting that province browsing without a query is lost.

IA-3. Session-detail section order ​

Evidence. Three orders exist. Ratified law (SD-IA, 2026-09-17, canon/sessions.md §Detail-screen overview IA; decision log): hero → stat strip → live bar → refund banners → notice → payment gate → participants → matches → info → host callout → host tools → danger zone, rejecting "info before participants" because the roster is the social proof that drives RSVP. Shipped after the owner's 2026-09-28 complaint that the roster sat below the fold: alerts → notice → facts (lead layer open, detail layer behind a chevron) → 참석 → 경기, host tools in the ⋯ menu (screen-blueprints/sessions.md, Session Detail). The v2 board: hero with three facts (일시, 장소, 참가비) → 참가자 roster → 정보 cells (종목, 대상, 호스트, 참가비) → CTA band (flat-v2.jsx:1767-1860). The board has no place for the payment banner and hold clock, refund banners, the live bar, the 공지 notice, 경기, cancellation or rain warnings, and the 23-branch CTA (primary-action.tsx). The information-flow audit calls out "commit precedes disclosure": the cancellation terms sit below an always-tappable RSVP (§4). Peers: the one doubles-shaped member row in the benchmark set is Playtomic's labelled slot row, name and level chip with an outlined "+ Available" circle for an empty slot (lane-b-category.md §2, member row, playtomic_3), the same roster-with-empty-slots form the v2 roster uses; the benchmark's session cards keep CTAs out of list rows and in a footer band or FAB (lane-b-category.md §2, scheduled session).

Options. (A) The order shipped after 2026-09-28: facts first, roster after. (B) The board's order, with the missing blocks appended. (C) The original SD-IA order: money and notices first, info last. Recommendation: B with a Notice stack on top and a CTA helper line, i.e. keep roster-before-info (SD-IA and the board agree) and make the order explicit for the Phase C board:

  1. Notice stack under the header, one Notice at a time by priority: refund due, payment hold with clock, live, cancelled, pending applicants (host). Obligations are never below the fold (session-alerts-section.tsx:1-10, "Option A, 2026-09-22").
  2. Hero: eyebrow club, title venue, three facts 일시 · 장소 · 참가비. Once the viewer's spot is settled the third fact becomes the viewer's state (FEE-SETTLED).
  3. 참가자 roster, 4-up, dashed empty slots, "외 N명" door, 호스트 Tag.
  4. 경기, only when a draw exists (B3).
  5. 정보 Cells: 종목, 대상, 호스트, 신청 마감, 취소 규정, 날씨; an ExpandToggle past six rows.
  6. CTA band: one coloured action by state precedence, with a one-line helper above the button carrying the consequence (fee and cancellation terms, or the cooldown, tier or deadline reason). The helper already exists in code for the cooldown and tier gates (primary-action.tsx:100-112, :231-270); the library has no row for it, so Phase C needs "CTA band helper line, 13/18 secondary, one line".

IA-4. Officials' entry (U-05) and the ⋯ menus ​

Evidence. U-05 shipped as a header text action 관리 for officials, with ⋯ holding 공유 and 나가기 (club-detail-screen.tsx:519-523, :242-257), and U-09 gave the 클럽 tab a labelled 초대 코드 (club-join-header-action.tsx). The v2 Header allows "≤ 2 action icons" and no text action; the Menu allows at most five rows (library §3 Header, §8 Menu). The v2 클럽 board draws 초대 코드 as a link icon again (flat-v2.jsx:1519). On session detail the host tools were moved into the ⋯ menu on 2026-09-28 (ONE-BOTTOM-CTA): 참석자 관리, 경기 규칙, 호스트 관리, 출석 확인, 경기 종료, 일정 취소, 일정 삭제 (host-menu-actions.ts:49-122) plus 공유 and 단톡방 (문의하기 shows to everyone but the host), up to nine rows for a host (share-and-chat-actions.ts:50-80). The audit had held the visible, labelled host-tools section up as the template for U-05 and listed hidden ⋯ items as a defect (H4).

Options. (a) An icon-only gear: fails U-05 and H5. (b) Everything in the ⋯ Menu: more than five rows, fails U-05. (c) A text action plus a grouped Sheet. (d) Restore the visible host-tools section on the page: conflicts with ONE-BOTTOM-CTA and the flat page.

Recommendation: (c).

  • Add to the Header spec one ghost text action (15/22·600 ink, 44 target, pad 0 12): at most two trailing items, at most one of them text.
  • Club hub: [관리] [⋯] for officials; ⋯ is 공유 and 나가기. 클럽 tab: [초대 코드] ["+"].
  • Session detail, host or officials: [관리] [⋯]. 관리 opens a sheet of grouped Cells (not the Menu): 운영 (참석자 관리, 경기 규칙, 출석 확인 with a count tag, 호스트 관리 for pickups, 참가비 관리 for paid sessions), then a destructive group (경기 종료, 일정 취소, 일정 삭제). Everyone else: [bookmark] [⋯] with 공유, 단톡방, 문의하기. The editing pencil replaces the bookmark for a host of an open session, as today.
  • The new 참가비 관리 row is the persistent door to 입금 확인 that is missing today (§1.6, flow 3).

IA-5. Header actions per tab: where 알림 and 메시지 live ​

Evidence. Today the bell is on 홈 only (home-feed-screen.tsx:191); 프로필 carries a message icon with an unread dot and the settings gear (profile-screen.tsx:229-255); 경기 and 기록 headers are empty; 클럽 has 초대 코드 and "+" (club-list-screen.tsx:482-492). The board gives 홈 [bell], 경기 [search, bell], 클럽 [link, plus], 기록 [bell], 프로필 [settings] (flat-v2.jsx:1321, 1411, 1519, 1569, 1650): a bell on three tabs, no entry to /messages, and the invite code back to an icon. The club 채팅 pane already aggregates every DM thread (chat-pane.tsx header), but only for members of a club.

Options. (i) Board as drawn plus [message, settings] on 프로필. (ii) One inbox pair on 홈: [message, bell]; 경기 [search]; 기록 none; 클럽 [초대 코드, +]; 프로필 [settings]. (iii) Bell and message on every tab root.

Recommendation: (ii). Both are global inboxes with unread state; a single home gives each one count to learn, the Header allows two icons, and the tab bar cannot carry unread (C9 reserves its one dot for live). Cost: a user on 경기 or 기록 is one tab from 알림, and push covers the gap. On web at 600 px and above the rail carries both (WebNavCovered).

IA-6. What the FAB creates ​

Evidence. Library C8 puts one FAB on 홈 (일정) and 경기 (번개), none on 클럽. Today 홈 has no create entry (no createSession in packages/features/home), 경기 has a 번개 만들기 band on the 번개 찾기 pane only (activity-screen.tsx:528-537), the club hub has a contextual band 글쓰기 (홈 pane, members) or 일정 만들기 (일정 pane, canManageSchedule) (club-detail-screen.tsx:536-559), and /create-session with no club id means "host a pickup" (route-params.ts:57). Peers: 당근's brand orange appears only on the 글쓰기 FAB pill and the pay icons (lane-a-korean-consumer.md §1); Band, TennisFriends and Somoim keep creation in a FAB or a footer band (lane-b-category.md §2).

Recommendation.

  • 홈 FAB label by role, one component: an official of exactly one club → "+ 일정" straight into that club's wizard; of several → a sheet "어느 클럽의 일정인가요?" (Cells, last Cell "번개, 클럽 없이"); a member who cannot schedule → "+ 번개".
  • 경기 FAB "+ 번개" on the 번개 찾기 pane only, as the band is today; a FAB on 예정 would promise what that list does not do.
  • Club hub: a contextual FAB, 홈 pane "+ 글쓰기", 일정 pane "+ 일정" for officials, other panes none.
  • 클럽 tab: no FAB; 클럽 만들기 stays the header "+" and a newcomer's secondary row.

IA-7. Club hub panes versus the routed management screens ​

Evidence. The 멤버, 일정 and 회비 panes embed the same bodies as /clubs/{id}/members, /sessions, /dues (members-pane.tsx, sessions-pane.tsx, dues-pane.tsx headers). The members pane leaves out the 가입 심사 queue and 추천 게스트; the sessions pane has only 예정 and 완료 and no link to 일정 관리; dues pushes and admin attention route to the routed screens (signal-routes.ts:34-36, club-admin-attention.ts:243-250).

Options. (a) One surface per content: delete the routed screens and give the panes inline admin controls (retargets every push, attention row and 관리 row). (b) Keep both with explicit roles. (c) Delete the panes and link out (loses the one-scroll hub). Recommendation: (b). Keep both and state the roles: panes are member-facing reading, routed screens are officials' tools. Every pane ends, for officials only, with one Cell row to its tool ("멤버 관리 ›", "일정 관리 ›", "회비 관리 ›"). Pushes and 관리 rows keep landing on the routed screens. Phase C draws one hub board (six panes) and one management-list board that reuses the same rows.

IA-8. Entries the v2 boards drop ​

Dropped entryWhere it wasProposed home on the board
경기 기록, 상대 전적, 리더보드 footer rows기록 (records-screen.tsx:405-417)A "내 기록" group of three nav Cells after 클럽 랭킹; the 클럽 랭킹 section head keeps 전체 보기 to the leaderboard
개인 기록, 활동 히트맵, 종목별 승률, 최고의 파트너, ELO 추이 card, and the 종목 · 성격 · 구분 filter기록 body (screen-blueprints/records.md, Records anatomy)Flat sections under the ELO chart in that order, each a Section head with Strip, Cells or PersonRows; the filter stays one Choice that opens the existing scope Sheet. The board draws only the Strip, the chart and 클럽 랭킹, so Phase C must draw the rest
연습 기록, 공유 menu rows프로필 menu (profile-menu-card.tsx:41-52)A two-Cell group at the end of 프로필; 로그아웃 already exists under 설정 › 계정 관리 (settings-screen.tsx:14-17), so it leaves 프로필
Club discovery and nearby-pickup recommendations홈 footer (HomeRecommendationsSection)The 홈 first-run state (§4.1): Cells 클럽 찾기, 초대 코드로 가입, 번개 찾기 under the empty Block
메시지프로필 header iconIA-5
기간 기록, 일일 요약홈 recap cardIA-1

IA-9. Back model and tab-bar visibility ​

Evidence. Pushed screens hide the tab bar (§0.2), the opposite of the stale comments. Back is router.back() with no fallback on a one-entry stack (§1.1). Web keeps the bar on 알림 only.

Options. (a) Per-screen guards, the status quo (one screen has one). (b) One fallback inside appRouter.back. (c) An anchor so (tabs) always sits beneath a deep-linked screen (changes the stack for every link; needs a device test). Recommendation: (b). (1) Ratify "pushed screens hide the tab bar" and correct routes.ts and the canon. (2) Give appRouter.back() a single fallback: when !canGoBack(), replace to the parent tab root; that is one change in the one file that imports expo-router (app-router.ts:75-77) and deletes the notification centre's special case (notification-center-screen.tsx:242-247). (3) Confirm the cold-start behaviour on a device first. (4) Web: move 알림 out of the tab group or accept the bar on both platforms.

IA-10. Media bands on detail pages ​

Evidence. The club hub is a photo-first place surface (PlaceShell, CollapsingTopBar, album banner; COVER-ALWAYS, 2026-09-26); session detail has a map band under white header glyphs (MAP-BAR-INK, 2026-09-28); venue detail and the club preview use a collapsing header over a photo (screen-blueprints/clubs.md, venues.md). The library has no row for a media band: §11 keeps "the detail hero image" as the one Card use, the L1 ruling describes a text hero whose title fades into the header, and the inventory dispositions re-spec MediaHero to a text-only hero ("v2 hero is text-only … no band") and PlaceShell to a collapsing header (component-inventory.md, rows MediaHero and PlaceShell / CollapsingTopBar). The v2 session board has no map band (flat-v2.jsx:1767-1810). So the band is removed by omission, against two owner rulings.

Options. (a) Drop media bands everywhere and keep the text hero, as the board draws. (b) One MediaBand row in the library. (c) Leave the bands as one-offs outside the library, which is what the code does today. Recommendation: (b). Add one MediaBand row to the library: 2:1 across the full width (195 pt at 390), top scrim under white header glyphs, collapsing into the solid Header with the L1 title fade; used by the club hub and preview, venue and directory-venue detail, and session detail when a venue map exists. Text Hero stays for pages without media (records, profile, receipts). Owner question: does session detail keep its map band, given the 2026-09-28 complaint that the roster sat below the fold?

IA-11. Push and share targets ​

Options. For the payer pushes: route them to the session detail (recommended), or make /sessions/{id}/payments show a plain member their own row. For the other items there is no better option than the fix.

Recommendation. Fix list, all small and none needs a backend change: route payer-directed payment pushes to /sessions/{id} (§1.4); route club_join_request_received straight to 멤버 관리; make /s/{id} for a non-member replace to /sessions/{id}/preview (the apply state machine) or the club preview; keep ?code= through onboarding and show a sign-in prompt instead of a silent no-op on the signed-out web join page (§1.5); add an install path to the web landing pages once the app is in the stores (owner, §5).

IA-12. Information-flow audit waves 3 and 4 in v2 ​

Item (audit §4, §5, §7)State in code on 2026-10-09Where it lands in v2
Dues at the club decision momentA fact row on the preview (public-club-profile-screen.tsx:453), not at the CTACTA band helper "월 회비 30,000원" on the club page's join band (a non-member, review 41) and on the join screen
Cancellation and refund terms next to RSVPA lead fact only when a penalty appliesCTA band helper line and a 취소 규정 Cell (IA-3)
Distance on cardsNot shown; distanceKm is a model slot (session-info-model/types.ts:147, 319) populated only in the venue finderMediaRow L2 segment ("1.2km") on 번개 찾기 and 테니스장 rows when location is granted; needs the model slot populated
Friends goingBuilt (session-info-model/builder.ts, shown as a strip chip, decision FRIENDS-GOING)L2 segment "친구 2명"; the one-trailing-element rule keeps it off the right edge
Head-to-head from a matchupNot linked; analysisRivals has one caller (records-screen.tsx:405)A Cell "상대 전적 ›" in the match profiles sheet and the scorecard match row
Host identity on cardsNot on the row; on the detail roster as a 호스트 TagKeep on the roster; on rows only if the owner wants it in L2 (unverified need)
Club onboardingThe mission card for regular members only (mission-header.tsx:49-52); owners get nothingOwner: Notice "멤버를 초대해 보세요" with 초대 링크 보내기. Member: a Cell checklist. No card
Waitlist positionBuilt as #N on the participants row and sort (participant-row.tsx:143-146, participation-section.tsx:94)Tag "예비 2번" in the hero fact and the CTA helper
Activity recency for dormant clubsNot builtClubRow L2 segment "최근 일정 3일 전"; needs the last completed session date on the list payload
Unresolved no-show countBuilt as a count in the 출석 확인 menu row's label (host-menu-actions.ts:77-80); no signal or badge elsewhereCount tag on the 출석 확인 Cell in the 관리 sheet

3. Core task flows, today and proposed ​

Counts are taps as defined at the top. "Today" was read from code on 2026-10-09 and corrects the usability audit's task table where the code has moved on (§0.3). Korean strings in the Proposed tables are illustrative labels, to be settled against the terminology guide when the boards are drawn. "Proposed" uses only library components; where a flow needs something the library or the backend does not have, it is listed under Needs, backend and native items separately from client work.

FlowToday (verified)Target
1 Sign up and land on 홈no sign-up; 12 taps and 4 entries once ops has provisioned the account5 taps (+ OAuth hop)
2 Join the first clublink 1 · pasted code 3 · browse 4 or 5same counts, the club shown before commit
3 RSVP and pay (member)3 in-app taps, a hidden long-press to copy, a bank-app trip, a 10-minute clock4 taps, nothing hidden
3 Confirm 5 payers (총무)10 taps, and no door while the session is open3 taps: door, CTA, Dialog
4 Create a session with club defaultsabout 17 from a tab root (12 with a 정기 일정 chip)3 prefilled, 9 from blank
5 Create a club9 screens and about 17 taps; quick path 4 screens and 10 to 12 tapsquick path 7 to 8 taps
6 Generate matches and start play65
7 Record a scorehost 3 taps and 2 digits; a player 12 on ±1 steppersa player 4 from the scorecard (3 in the sheet), the host reviews an appeal in 2
8 Dues, 20 members, 17 paid34 per row, or 11 with bulk and 3 undos6 (3 if all paid)
9 The 총무's weekly loop (5 payers)24 to 29 with a weekly post, 12 with a monthly seriesabout 7 a week

Measured on the flows board (2026-10-11, /wireframes/flat-v2-flows, recounted on the page boards' own phones after owner review 1):

FlowIA targetMeasuredWhy it differs
1 Sign up56 taps, 0 entries on our screens (Kakao 1, 본인인증하기 1, 계속 1, 약관 동의 2, 첫 시작 1), plus the agency's own screens (owner review 4, D6); 7 taps, 4 entries (interim email)the verified identity precedes our consent, so the age is known first (needs counsel on the order); the name and birth date arrive from the 본인인증, not from typing
2 Join a club1 · 3 · 4–5link 1 · code 3 (and the paste) · browse 4, 5 with approval—
3 RSVP and paymember 4, host 3member 4; host 3 through the pending-work Notice on the open session—
4 Create a session3 prefilled, 9 blank3 prefilled; 9 taps and 1 entry blank (10 taps with a recent search instead of typing)the time band keeps its default; the venue pick is staged before it commits
5 Create a club7–8 quick6 taps, 1 entry from the 클럽 tabthe quick path is on by default; the region and the logo arrive settled; 시간 has a default
6 Matches55the session's 대진 짜기 opens the 대진표 with the method Sheet up
7 Record a score4six paths (owner review 5, honor system): a player enters in 4 from the scorecard, win or loss (점수 입력, then keypad first: 6, 4, 점수 저장; no presets; settled at once, nobody confirms); the host enters in 4 (대신 입력, 6, 4, 점수 저장); a second writer’s sheet turns into the appeal sheet and 검토 요청 is 1 more tap (2 from the Match state block’s door 점수 검토 요청 ›); the host or an official reviews an appeal in 2 (the 검토 Task Cell, then 그대로 유지 or 이 점수로 바꾸기; 3 with several); a held match is released by entering the earlier score in 4 and the host starts the ready ones in 2; a retirement is recorded in 10 (6 without the score’s 4 digits: 경기 중단, the reason and team on one screen, the score so far, 기권 저장) and is settled at oncereview 5 replaced the confirm, dispute, contest and escalation paths with one appeal; 12 states became 7, 34 transitions 14, 19 races 5, 21 pushes 5
8 Dues, 20 members6 (3 if all paid)5 against a bank statement, from the 홈 할 일 door; 3 when every payer reported in the appthe page opens in Select mode with 확인 대기 ticked; 미납 rows start unticked (ruling 12)
9 Weekly loop~75 (the deferred remind would add 4)ruling 13 defers the remind

Flow 1. Sign up and land on 홈 (U-01) ​

Today.

  • Signed out, mobile: LoginScreen offers email and password and a 로그인 button. Under it: "Kakao/Google 로그인은 곧 추가돼요" and "계정이 없다면 운영팀에 문의해 주세요" (login-screen.tsx:160-163, config/i18n/ko/common.ts:556-557). useAuth exposes signInWithEmail only; the header comment promising sign-up is stale (login-screen.tsx:2). There is no in-app account creation.
  • A provisioned user then meets onboarding: value-props (three cards, skip in the header, 시작하기) → welcome (시작하기) → 출생연도 (the under-14 gate fires at the next step) → 동의 (five rows at signup, three required: 이용약관, 개인정보 처리방침, 국외 이전; each its own checkbox; no master row; consent-matrix.ts:45-100, onboarding-step-consent.tsx) → 이름 (2 to 20) → 플레이 스타일 (three choices stored in auth.store.ts and read by nothing) → 완료 (시작하기) → replace(destination), 홈 by default (onboarding-intent.ts:26-27, onboarding-wizard-screen.tsx:284).
  • 12 taps (로그인 1, 시작하기 1, 시작하기 1, 다음 1, three checkboxes and 다음 4, 다음 1, choice and 다음 2, 시작하기 1) and 4 entries (email, password, birth year, name).
  • The consent step's 보기 links push twomore://legal/terms while AuthGate renders the wizard instead of the Stack (onboarding-step-consent.tsx, policy-versions.ts:24-25, _layout.tsx:205-207, 217-226). Whether the page shows is unverified.

Problems. U-01 (blocker, gated on the identity-verification and OAuth program, open-issues-plan-2026-09-24.md §6, EXT-11); a consent step with no 전체 동의 master row (the Korean signup convention; no frame in the benchmark set, confirm in Phase C research); a step that collects an unused value; the invite code lost through onboarding.

Proposed (new-user path).

#ScreenComponentsColoured actionStates to draw
A1시작Logo, one line of brand copy, Button primary lg 카카오로 시작하기, Button ghost 이메일로 로그인, a 13/18 secondary line with 이용약관 and 개인정보 처리방침 as ghost links that open a Sheetthe primary Button (Kakao's brand rules may require its yellow, an exception to S2; owner, §5)busy (spinner in the button), provider cancelled (helper in danger), offline (Offline strip, Button disabled), returning user (skips to 홈)
A2본인인증Wizard header (×), title 본인인증을 해 주세요, a reading line (why: a real person on every court, payments later), a note (주민등록번호는 받지 않아요), four equal provider buttons (PASS, 토스, 네이버, 카카오; each goes straight to that provider) and the quiet link 다른 방법으로 인증 (the vendor's own window); then the provider's app (not ours); no result screen of our ownthe provider buttonsbusy, cancelled (helper danger), failed (EmptyState, 다시 시도), under 14 (a stop screen, 로그아웃 only; today's gate copy), 14 to 18 (allowed; the result notes that card payment opens at 19), no phone of one's own (a family line is refused; use a certificate app), foreigner (the flag is taken from the phone or PASS result only), 이미 가입한 계정이 있어요 (the same CI through a second provider: names the first provider, one button to sign in with it and connect), exit Dialog
A3약관 동의Header (back), title 24/32·700, Cell rows with a trailing checkbox: 전체 동의 (600), 이용약관, 개인정보 처리방침, 국외 이전 (필수), 위치정보, 마케팅 (선택); 필수 rows have a ghost 보기 that opens the document in a Sheet; CTA band 동의하고 계속CTA band, disabled until the three required rows are ticked, the helper line names what is missingnone ticked, only required ticked, all ticked (the master mirrors), 보기 Sheet loading
A4첫 시작Title "어떻게 시작할까요?", three nav Cells: 클럽에 가입할래요 · 번개로 칠래요 · 클럽을 만들래요; the header’s 건너뛰기 (R3, 2026-10-20: a departure lives in the header)none; the Cells are the actions—

Destinations from A4: 클럽에 가입 opens 클럽 › 내 클럽 (its empty state leads with 탐색 and 초대 코드, flow 2); 번개 opens 경기 › 번개 찾기; 클럽을 만들래요 opens flow 5; 나중에 opens the 홈 first-run (§4.1). A4 replaces 플레이 스타일 and uses what it collects to route.

Target. 5 taps on our screens (A1 1, A2 1: the provider, A3 2: 전체 동의 and 동의하고 계속, A4 1) and no entries, plus the OAuth hop and the agency's own screens; from 12. The order is provider login, 본인인증, 약관, 첫 시작 (owner review 4, D6; supersedes the birth-year and name steps, which the verified name and birth date now fill).

Redrawn 2026-10-28 (the owner: "as ruled before"). The order above stands and the first pass's one screen of 약관 and 본인인증 is undone: A2 offers the four providers, so 본인인증하기 and the result's 계속 are gone; the provider's app is not ours and not counted (decision log, 2026-10-28).

Sign-up rulings (owner review 4, D6).

  • 본인인증 once, at sign-up. It follows the owner's 2026-06-02 directive (verified identity, with nationality from the identity provider) and their review-4 instruction to sign in non-anonymously. The 본인확인기관's own screen collects its consent and tells us the result.
  • The order keeps "age is known before consent" (PIPA §22-2): the verified birth date drives the under-14 stop, and our 약관 comes after it. Needs counsel: whether our consent may follow the verification, and what happens to the verified data of an under-14.
  • Providers give only the stable id, a nickname and the email where offered. No Kakao business review is needed for sign-up. Naver goes through Supabase custom OAuth (custom:naver); a one-day spike confirms the userinfo mapping and whether it takes part in automatic email linking.
  • Identity route (owner review 5, lane J). PortOne v2 identity verification on the KG이니시스 통합인증 channel: one vendor screen offers 네이버, PASS, 토스, 금융인증서, 카카오, 신한, KB모바일 and 삼성패스; we never draw our own picker. Danal phone verification is the fallback (only channelKey changes). Kakao stays hidden in the list until its CI paperwork is done. It returns name, birth date, gender, phone, CI and the foreigner flag; no DI. A 주민등록번호 is never received or typed (the Danal and KCP API-only mode is ruled out). Test before launch: the CI from the phone path and from two 통합인증 methods must be identical.
  • Cost. sign-ups x attempts per success x unit price (assume 1.2 attempts per success); no list price is published, so quotes are pending from 이니시스 and Danal, including tiers and what counts as a billable attempt. At 50, 100 and 200 KRW per check: 1k sign-ups 60k, 120k, 240k; 10k 0.6M, 1.2M, 2.4M; 100k 6M, 12M, 24M KRW (illustrative only).
  • Juniors (staged). Under 14 stops (as shipped; a child on a parent's line verifies as the parent, so the check is no age gate). 14 to 18 is allowed with the card rail blocked below 19 (payAge=19, in sessionPaymentGateState) and the transfer rail kept; minAge=14 and payAge=19 live in one config. Later: a guardian-managed junior profile under a verified parent, then club-registered juniors with no login of their own (each an axis in config, not a new flow).
  • Needs counsel (lane J §5.5). CI storage (basis, retention, hash and key, consent wording); whether the aggregator route needs the agency's CI/DI service approval; the exact 시행령 §17-2 under-14 methods and fine exposure; whether 14 to 18 needs a guardian's consent, the Civil Act §5 risk on transfer dues and PG rules for minors; youth-protection duties and adult-to-minor contact; data minimisation of name, birth date, gender, phone and CI.
  • One person, one account, keyed by the 본인인증 CI. A second provider whose 본인인증 returns the same CI gets 이미 가입한 계정이 있어요: sign in with the first provider and connect. Never a silent merge. Needs counsel: storing the CI (hashed or encrypted, with a unique constraint), its consent and retention.
  • 설정 › 계정 rows: 프로필 수정, 비밀번호 변경, 연결된 계정 (its own page: one Cell per provider of the platform, connected or not, with 연결 or 해제; the last connected login's 해제 is disabled and its helper says how to enable it; 해제 asks a Dialog), 본인인증 (완료, name and birth date masked), 공개 프로필 보기 › (the public profile, as owner review 4's chevron law, D1, moves it out of the identity Block).
  • The money ladder. Payer: through the PG (Toss Payments). Payee (a club 총무 receiving fees): through the PG's payee registration plus an account-holder name check. The PG (its 지급대행 or escrow) holds and pays out; TwoMore never holds funds. Needs counsel: the 지급대행 versus escrow choice and the fee flow. Only the account screens are drawn in this round.

Needs.

  • External: the OAuth provider and the identity-verification vendor (PortOne with KG이니시스 통합인증, Danal as the fallback; quotes pending; the vendor's result fills the name and birth date; the providers give only the id, nickname and email). Interim without them: a client signUp in use-auth.ts plus email confirmation (Supabase auth; the profiles row creation on sign-up is unverified).
  • Client only: master consent row; under-14 stop folded into A2 (from the verified birth date); A4 in place of 플레이 스타일; keep ?code= through onboarding (onboarding-intent.ts:78-84, with a test); 보기 as a Sheet; the Kakao button exception.
  • Backend: none for consents (grants stay in useGrantConsent).

Flow 2. Join the first club ​

Today (three entries).

  • Invite link, app installed and signed in: the link is https://twomore.app/join-club?code=… (share-cards.ts:209-221); JoinClubScreen opens with the code prefilled and does not say which club it is, because the link carries no clubId (join-club-screen.tsx:59-69) → 가입하기 → replace to the hub. 1 tap.
  • Pasted code: 클럽 (1) → 초대 코드 header action (1) → paste, 가입하기 (1). 3 taps and a paste.
  • Browsing: 클럽 (1) → 탐색 (1) → a club row (1) → the preview's footer CTA by join policy (join-cta-band.tsx): open 가입하기 (1) = 4; approval 가입 신청하기 opens the apply panel (1) then 신청 제출 (1) = 5, then a disabled "신청 검토 중" with "총무가 승인하면 알려드릴게요" (U-23); invite_only routes to the code screen.
  • Landing: the hub 홈 pane. A regular member sees the 시작 미션 card; owners and admins do not (mission-header.tsx:49-52, audit H4), so a new 총무 lands on a feed with nothing telling them to invite anyone.

Problems. The code-only link hides the club before commitment; dues are a fact row, not at the CTA (IA-12); the signed-out web join page does nothing on tap (§1.5); onboarding drops the code; the first-run does not fit the role.

Proposed.

#ScreenComponentsColoured actionStates to draw
B1클럽 가입하기Header (back); club Thumb 56 r12, name 24/32·700, MetaSegments (district, 멤버 N, NTRP band); Cells 월 회비 and 가입 방식; Notice info when approval is needed; Input 초대 코드 only when no code arrived with the link; CTA band 가입하기 or 가입 신청하기CTA bandresolving (Skeleton thumb and two lines), unknown code (Input helper danger), already a member (Notice neutral and ghost 클럽 보기), full or archived (Notice warning, CTA disabled), signed out on web (CTA reads 로그인하고 가입하기), offline
B2클럽 홈, first visitHub (§4.1). Member: a 시작하기 Cell checklist (1/3) in place of the mission card. Owner: Notice "멤버를 초대해 보세요" with an action 초대 링크 보내기FAB 글쓰기a pending application shows on 클럽 as a MediaRow with Tag 신청 중 (a new state: today the status exists only per club on the preview)

Target. Invite link 1, pasted code 3, browse 4 or 5: the counts hold; what changes is that the person sees the club, its fee and its policy before the one tap, never meets a silent control, and lands on a first-run that fits their role.

Needs. Client only: a useClubByInviteCode hook over the existing port method clubs.findByInviteCode (club.repository.port.ts:28) and RPC lookup_club_by_invite_code (migration 00146); a role-aware first-run; a bulk query of the viewer's own pending join requests for the 클럽 list (only the per-club useMyJoinRequest exists, use-my-join-request.ts:15). Backend: none expected, since the viewer already reads their own request per club. Check: the RPC returns the club row; confirm it includes the dues amount for a non-member.

Flow 3. RSVP and pay a session fee ​

Today, member.

  • 홈 or 경기 row (1) → 일정 상세 → 참석 확정하기 (1, primary-action.tsx:274-292). If notification permission is undetermined a push soft-prompt sheet follows (once, 1 to 2 taps, :280-286).
  • For a fee session the payment banner appears at the top (payment-gate-banner.tsx:54-121): the amount, a countdown, and the club's account as selectable text with a hint. On native the copy is a long-press: CopyableValue copies only on web (copyable-value.tsx:70-71) and no package.json lists expo-clipboard. The hold window is sessions.payment_hold_minutes, default 10 (migration 00301:39).
  • The member leaves for a bank app, returns, and taps 입금 완료 (1, :87), which shows "입금 확인 중" (:123). The host confirms; a push follows.
  • 3 in-app taps (plus 1 to 2 once), a hidden gesture and a bank-app trip inside 10 minutes. Meanwhile session_payment_hold_created pushes to the member and its tap lands on a permission wall (§1.4).

Today, 총무 or host. Push session_payment_submitted → 참가비 list → per payment 입금 확인 (1) and a confirm Dialog (1) (use-payment-actions.ts; confirms exist since U-04). No bulk: 5 payers = 10 taps. If the push was missed there is no door until the session completes (§1.6).

Problems. U-11 open (one-tap copy needs a native module and the next EAS build; the honest-hint interim shipped); the payer-push dead end; the 10-minute clock with nothing visible but a banner; no persistent host door; no bulk confirm. U-02 is fixed.

Proposed, member.

#ScreenComponentsColoured actionStates to draw
C1일정 상세, open, paidIA-3 order. CTA band primary 참석하기 with the helper "참가비 10,000원 · 시작 2시간 전까지 무료 취소"CTA bandthe CTA-state set in §4.3
C2입금 안내 Sheet (opens once after RSVP, closable)Sheet title 입금 안내; Notice warning with the live clock ("10분 안에 입금해 주세요"); Cells 금액 (700), 은행, 계좌번호 with a copy IconButton, 예금주; Sheet CTA primary 입금 완료; ghost 나중에the Sheet CTAclock expired (Notice danger, CTA 다시 참석하기), no account on file (Notice neutral "총무에게 문의해 주세요" and ghost 단톡방), copy Toast, offline
C3일정 상세 after submitNotice neutral "입금 확인 중이에요. 총무가 확인하면 알려드려요."; the hero fee fact becomes Tag 입금 확인 중; CTA band becomes ghost 참석 취소 with the refund rule in the helpernoneconfirmed (Tag success 입금 완료 and a Toast), hold expired (Notice danger, CTA 다시 참석하기), refund owed (Notice with 환불 확인)

In-app taps: row 1 + 참석하기 1 + 복사 1 + 입금 완료 1 = 4, zero hidden gestures (today 3 plus a long-press, so the same number of interactions, now visible). The push soft-prompt moves after 입금 완료 so it never sits between the member and a 10-minute clock. Payer-directed pushes open C1/C3, not the 참가비 list.

Proposed, 총무 or host.

#ScreenComponentsColoured actionStates to draw
C4일정 상세, host or officialsNotice info "입금 확인 3건" with action 확인하기, shown only when something is pending; 관리 Sheet row 참가비 관리 (IA-4)Notice action (ghost)nothing pending, so no Notice
C5참가비 (list)Header; Strip 확인 대기 · 미입금 · 완료; Section heads with PersonRows (amount, Tag); CTA band "3명 입금 확인 · 합계 30,000원" present whenever 확인 대기 rows exist, defaulting to all of them, with a ghost 선택 to refine; the CTA opens a Dialog with the count and amountCTA bandthe host's own payment row (payer cannot confirm, migration 00536), refund rows, waived rows, refined to nothing (CTA disabled), error with retry

Target. Member 4; host 3 (the door, the CTA, the Dialog) for any number of pending payers, 2 once on the list.

Needs.

  • Client: the Sheet, the Notice and 관리 row, the selection refinement, the routing fix.
  • Backend: bulk confirm for session fees, a new RPC modelled on mark_dues_paid_bulk(uuid[]) (migration 00648) with the payer exclusion of 00536.
  • Native build: expo-clipboard for the one-tap copy (U-11). Optional, to research before design: a bank-app transfer deep link (needs LSApplicationQueriesSchemes in app.json, so also a build); it would save the manual retyping of a 12-digit account.

Flow 4. Create a session with club defaults ​

Today.

  • Reach: 클럽 (1) → the club (1) → 일정 pane (1) → 일정 만들기 band (1) = 4. There is no create entry on 홈. 경기 has 번개 만들기 (a pickup, no club).
  • The wizard has 7 screens (STEP_COUNT = 7, create-session-draft.ts:219): 언제 (date, start, end; 정기 일정 and 지난 일정 prefill chips) → 어디서 (venue through the finder takeover) → 종목 · 구성 · 대상 (형식 seeded from club.preferredFormat, create-session-step3.tsx:150-157) → 인원 (모집 인원, 한 경기 시간) → 옵션 (참가비, 신청 마감, 반복 매주 or 매월; skippable; create-session-step4.tsx:447-466) → 경기 규칙 (이대로 진행) → 검토 (일정 만들기). The audit counts about 13 taps; a 정기 일정 chip seeds time, venue and capacity (use-regular-meet-prefill.ts) and brings it to about 8.
  • Total about 17, or 12 with a chip. Success replaces to the detail; failure prints err.message in a dialog (use-submit-session.ts:264-270, the U-03 class).

Problems. Seven screens for a weekly repeat; repeat buried on the fifth; 경기 규칙 is a confirmation screen; the wizard's design is marked unreviewed in its blueprint; raw error text. U-19 and U-33 are fixed.

Proposed: default first, review and edit.

#ScreenComponentsColoured actionStates to draw
D1어떤 일정인가요? (Sheet from the FAB)One Cell per active 정기 일정 ("토 09:00 올림픽공원"), then 지난 일정 복사 and 새로 만들기. For several clubs, a club picker Sheet comes first (IA-6)the picked Cellno 정기 일정 (only the last two Cells), one club, several clubs
D2일정 검토 (the record page)Header 일정 만들기; Cells 일시, 장소, 종목, 대상, 인원, 참가비, 신청 마감, 반복, 경기 규칙, each with a pencil that opens a bottom Sheet editor (the EDIT-SHEET grammar of the in-place session edit); a neutral Tag for provenance ("정기 일정에서"); CTA band 일정 만들기CTA banda required Cell empty (helper danger and the CTA disabled with a hint), recurrence summary ("매주 4회"), error (inline, plain language, retry), offline
D3새로 만들기The same page starting empty with only 일시 and 장소 required; tapping a required Cell opens its Sheet. A first-time host can keep the existing step-by-step order inside the same componentsCTA bandfirst-ever session, venue picked through the takeover

Target. 3 taps from a hub (FAB, pick a 정기 일정 or past session, 일정 만들기); 9 from blank (FAB 1, 새로 만들기 1, 일시: date, start, end and 적용 4, 장소: open and pick 2, 일정 만들기 1); one more tap if the viewer officiates several clubs (the club picker). From about 17 and 12.

Needs. Client only: entry Sheet; review-first page (the record components exist, session-record/); reuse usePastSessionPrefill and useRegularMeetPrefill; plain-language error. Backend: none. Owner: whether the seven-step wizard stays as the first-timer path (§5).

Flow 5. Create a club ​

Today. Entry: 클럽 header "+" for a member, or the secondary band for a newcomer (club-list-screen.tsx:484, 500). Full wizard: 9 screens (STEP_COUNT = 9, create-club-helpers.ts:44): 이름 · 로고 → 지역 · 코트 → 일정 → 스타일 → 인원 → 회비 (optional) → 소개 (optional) → 멤버십 · 게스트 → 확인, about 17 taps with seven required decisions. Quick path (U-32): a 빠르게 만들기 toggle on the first screen shortens the sequence to [0, 1, 2, last], four screens (create-club/quick-start.ts:24); by the step gates (step-validity.ts:16-44) that is 이름 and 로고, optional 지역, 요일 and 시간, then 확인: 10 to 12 taps by that count, plus the name. Creation is keyed and recoverable: a receipt screen resumes incomplete setup (club-setup-receipt.tsx).

Problems. The quick path hides behind a toggle; it still asks for a logo choice and a region; nothing tells the new 총무 to invite members (the mission card is hidden from owners, mission-header.tsx:49-52).

Proposed. Keep the quick path and the wizard shell (canon: the toggle is the deliberate act). Pre-answer what the app already knows so quick needs only 이름, 요일, 시간: region from the profile, logo generated and settled. After creation the hub opens with the owner Notice "멤버를 초대해 보세요" and its action 초대 링크 보내기 (one tap to the share sheet). Screens: WizardShell step (progress line, title 24/32, Inputs and Choices at the field grammar, one CTA band) × 4; the receipt as an EmptyState variant with primary 다시 시도.

Target. Quick path 7 taps (toggle, 다음, 다음, 요일, 시간, 다음, 만들기), at most 8; full path unchanged at about 17.

Needs. Client only, to verify against the draft model: a seeded region and a settled generated logo. Backend: none. Owner: fork screen versus toggle (§5).

Flow 6. Generate matches and start play ​

Today. Host opens the session (1) → 경기 방식 선택 in the 참석 section (1) → FormatChooserSheet, a pure picker (1) → AddRoundSheet options and "{방식}으로 대진 짜기" (1) → the draw shows → 경기 시작 (1, disabled until a draw exists, with the reason on the CTA: canStartMatches, U-16b) → confirm Dialog (1) = 6. Participants who have not settled their fee are left out of the draw behind a warning (unsettled-pool-notice.tsx), so a fee session may need flow 3's host step first.

Problems. Two sheets in sequence for one choice; the CTA changes meaning with state; the unsettled-players dependency is a notice, not a path.

Proposed (match board, live variant of Detail).

#ScreenComponentsColoured actionStates to draw
G1대진표, no drawHeader; Notice warning "입금 확인 전 3명은 대진에서 빠져요" with action 확인하기 (to 참가비); EmptyState-style block with the explanation "경기 상대와 코트를 자동으로 배정해요"; CTA band primary 대진 짜기CTA bandunsettled players, fewer players than courts, zero courts
G2대진 SheetRadio Cells for the method, preselected to the club's last or default method, each with its one-line description; options behind an ExpandToggle; Sheet CTA primary "{방식}으로 대진 짜기"the Sheet CTAking-of-court minimums, tournament methods (hand off to the tournament modal)
G3대진표, drawRound Tabs, match rows; CTA band primary 경기 시작; 대진 다시 짜기 in the header menuCTA bandstart Dialog; disabled with reason
G4liveLive Tag, score entry (flow 7); when every match is done a Notice with 경기 종료 as the CTACTA bandall done, host-ended

Target. 5 taps (open 1, 대진 짜기 1, Sheet CTA 1, 경기 시작 1, Dialog 1); the two sheets merge. Needs. Client only; verify that a last or default method can be read for the club (GenerationPolicy, canon match-generation.md).

Flow 7. Record a score ​

Today (three grammars).

  • Match board: inline numeric fields on the match card, then save: team 1 field (1) + digit, team 2 field (1) + digit, save (1) = 3 taps and 2 digits; multi-set matches use MultiSetScoreEntry (match-card.tsx:98-105, 148-184). A recap toast follows, scoreSavedToast(s1, s2) (:184, :257).
  • Live scorecard (what a participant sees): tap the card (1) → ScoreEditSheet, two stepper rows that change by 1 per tap and are not editable (score-edit-sheet.tsx:82-107); 6 to 4 is 10 taps, then 점수 저장 (1) = 12. Finishing a match is a consensus: ScoreCallSheet collects every participant's 동의.
  • Completed matches: 점수 수정 opens a full-set correction applied only after every participant agrees.

Problems. One act, three grammars; the stepper cost; the consensus step is unexplained in the flow. U-34's recap shipped; whether the toast carries an undo is unconfirmed.

Proposed: one ScoreEntry from library parts.

#ScreenComponentsColoured actionStates to draw
H1Match row in edit (host, or a participant on the scorecard)ResultRow shell; two numeric Inputs side by side (44 high, number pad, focus on the first); a Choice row of common results for the match rules (6-0 to 6-4, 7-5, 7-6) that fills both fields in one tap; tiebreak Inputs only when the rules trigger one; Stepper with editable kept for point formatsrow CTA primary 점수 저장typing, filled by Choice, tiebreak shown, over the maximum (helper danger), saving, saved Toast with a 취소 action (ink toast, ball text), offline (queued)
H2경기 종료 consensusSheet listing PersonRows with Tag 동의 or 대기; CTA 동의Sheet CTAdisputed (Tag danger), you already agreed, you are the last to agree

Target. 3 taps for everyone (open, Choice, 저장), or 3 taps and 2 digits by typing; participants 12 → 3. Needs. Client only; undo is a client re-set of the previous values before consensus (confirm the mutation allows it).

Superseded: owner reviews 2 to 4 shaped earlier versions of this flow (retroactive recording with consent, the state machine, keypad entry, matches in sequence, stop outcomes); their rulings docs keep that history. The current flow is below.

Replaced by owner review 5 (2026-10-12). Scoring runs on the honor system: any participant, the host or an official enters the score and it is settled and rated after the 8 s undo; the first write wins on score_revision, and a second writer's sheet turns into the appeal sheet. Any player may ask for a review (점수 검토 요청, an optional proposed score, no reason picker) until seven days after the session, from the door on the scorecard's Match state block; an appeal never holds the next match. The host or an official who is not playing decides (그대로 유지, 이 점수로 바꾸기 or 점수 고치기, or 기록 제외) from the 할 일 Task Cell 검토 N경기; an appeal left seven days stands as entered. The sheets go from four plus a request row to two (ScoreEntry in three modes, the Host review sheet); the confirm, dispute, contest and escalation paths are gone. A retirement counts as a win for the side that stayed and is rated only once either side has won 4 games, at the house half weight. The flows board draws six paths of Flow 7 (a player enters, the host enters, a second writer appeals, the host reviews an appeal, a held match is released, a retirement is recorded); library §10 holds the machine and lane G the evidence. Owner review 27: a session is recorded or casual (기록 안 함: no scores, nothing rated); in a recorded session the last score ends the session: time never closes it, no timer voids a match, and, from owner review 28, a player is held in the session's live page from check-in until their last match is settled, so no score is owed outside it (review 27, library §10 principle 7 and A14).

Revised by owner review 6 (2026-10-12). Stopping a match is a proper process, never inline text: ScoreEntry's footer is the button pair, a tonal 경기 중단 and the primary 점수 저장. 경기 중단 opens one screen for what happened (기권, 부전승, 일시 중단, 취소됨; the host also 실격) with the team revealed below a reason that needs it, then the score so far, then a commit named for the outcome (기권 저장); no extra modal, an 8 s undo follows. A retirement is 10 taps with the four digits, 6 without. The rain outcome is 일시 중단. The tiebreak cell sits under the set that needs it.

Flow 8. Collect a month of dues from 20 members (U-04, U-15) ​

Today (verified; this replaces the audit's "40 taps, no bulk, no confirm").

  • Reach: 클럽 (1) → the club (1) → 회비 pane (1); the pane is the admin roster for officials (dues-pane.tsx). Via 관리 it is 4.
  • First visit of a month: the list is empty, a band says 이번 달 회비 생성 (1) and a confirm Dialog (1) creates a row per member (handleGenerateDues, use-club-dues-actions.ts:307).
  • Members may attest 입금 완료 on their own view; those rows go to a 확인 대기 section and each is confirmed one by one (handleConfirmPending, :131).
  • Settling: per row a 납부 확인 pill (1) and a confirm Dialog (1) (handleMarkPaid, :164) = 2 each, 34 taps for 17 payers. Or 전체 완료 처리 (1) with a Dialog (1) (handleMarkAllPaid, :264), which marks every unpaid or partial row except the viewer's own (use-club-dues-data.ts:213-217; button at club-dues-screen.tsx:339-352), then 납부 취소 for each who had not paid: row tap (1), 납부 취소 (1), Dialog (1) = 3 each (handleUnmarkPaid, :227) → 11 taps with 3 exceptions.
  • mark_dues_paid_bulk(uuid[]) accepts any id list but updates only unpaid and partial rows (migration 00648:50-70), so 확인 대기 rows are outside it. The viewer's own row cannot be confirmed by the viewer (migration 00536).

Problems. A 총무 reconciling against a bank statement must "mark all, then undo the exceptions", which writes wrong states before correcting them; 확인 대기 has no bulk path; U-11 (copy) for the members' side.

Proposed: reconcile by selection.

#ScreenComponentsColoured actionStates to draw
I1회비 (officials)Header with a month Choice (menu) and ghost 선택; Strip 완납 · 미납 · 확인 대기; Section heads with PersonRows (amount, Tag). 선택 puts a checkbox on 확인 대기 and 미납 rows, all ticked, with 전체 선택 as a Section head action; sticky CTA band "17명 납부 확인 · 340,000원"CTA bandmonth with no rows (EmptyState whose primary Button is 이번 달 회비 만들기, because that is the screen's action), the viewer's own row (shown, unticked, caption "내 회비는 다른 운영진이 확인해요"), partial, waived, error with retry
I2Dialog"17명, 340,000원을 납부 확인할까요?" tonal 취소 and primary 확인; danger styling only for 납부 취소Dialog CTApending, partial failure with a list of who failed

Target. 6 taps with 3 exceptions (선택 1, untick 3, CTA 1, Dialog 1); 3 if everyone paid; 2 for the monthly generation. From 11 and 34.

Needs. Client: select mode. Backend: unpaid and partial rows use the existing mark_dues_paid_bulk; confirming 확인 대기 rows in bulk is a gap (a bulk form of confirm_dues_payment(p_payment_id), or sequential client calls); the payer exclusion already holds server-side. Native build: one-tap copy of the club account on the members' side (U-11).

Flow 9. The 총무's weekly loop ​

Today.

  1. Post. Flow 4, or once a month if the wizard's 반복 (매주 × 4, 8 or 12) was used (create-session-draft.ts:159-160, use-submit-session.ts:168-174).
  2. Chase RSVPs. The host's pinned CTA on an open session is 초대하기, which opens the share sheet (primary-action.tsx:80-95; 1 tap and a target app); 글쓰기 on the club board; automatic pushes: an hourly deadline-soon tick for members who have not answered, day-before and two-hour reminders that now include the host (00443_information_flow_wave1.sql, 00464_cron_local_hour_retiming.sql). There is no manual "remind those who have not answered" (no 미응답 or nudge in features or i18n).
  3. Settle. Nothing on the open session points to 입금 확인 (§1.6); after it completes the 정산 band (1) opens the list; monthly dues are flow 8.
  4. Attention. 알림 할 일 band and the 관리 snapshot, per club; the 홈 admin strip was retired 2026-07-31 (information-flow audit §4).

Proposed.

#ScreenComponentsColoured actionStates to draw
J1홈, officialsA 할 일 section between the Block and 오늘 일정: up to three Cells with a Count tag (입금 확인 3건, 가입 신청 1건, 미납 4명) that open the exact screen; absent when empty. Superseded by owner review 26: 할 일 is for anyone with work waiting (members too), capped at three with 전체 보기 to 알림.FABzero items (section absent), 99+, several clubs (the Cell names the club)
J2일정 상세, openHost CTA band 초대하기; 관리 Sheet gains 응답 안 한 멤버에게 알리기 (Dialog with the count, then a Toast), once a day per sessionCTA bandeveryone answered (row hidden), already nudged today (disabled with the time), nobody to nudge
J3참가비 and 회비Flows 3 and 8CTA band—

Target. About 7 taps a week for a club with a monthly series: chase 4 (open the session, 관리, 알리기, Dialog) and settle 3 (홈 할 일 Cell, CTA, Dialog); dues are 6 once a month. From 24 to 29 with a weekly post and 12 with a series, plus the hunt for a door that does not exist.

Needs. Client: 홈 할 일 from the existing attention source (useMyAdminClubAttention), the 관리 Sheet. Backend (new): a manual remind action (a server function scoped to the session's host or officials, a signal type reusing the dead rsvp_pending slot, a daily rate limit) and the session-fee bulk confirm of flow 3. Owner: whether to build the manual remind at all (§5).

4. Template groups for Phase C ​

Every route belongs to one board. Counts: hub 4 · list 28 · detail 22 · form/wizard 12 · settings 6 = 72 screens, plus 4 public share receivers and the two non-route entry flows (login, onboarding) on the auth board, and the overlay board, which has no routes. The 11 aliases and 7 layouts draw nothing.

4.0 What every board must show ​

The migration plan requires each page to appear in seven states (migration plan, Every page in every state). In v2 components:

StateHow it is drawn
IdealTypical populated page for the primary role.
Empty or first-runEmptyState (disc, title, at most two lines, one button). Three distinct kinds: first-run (nothing yet), filtered-empty (the filter hid everything: the button resets it, never the first-run action), permission wall (a lock glyph and who can see this). The button is tonal when the screen has a FAB or CTA band, primary only when the empty state is the screen's one action.
LoadingSkeleton at the v2 pitches (media row, person row, Cell, Block 144, Strip 80, Chart 160, Hero), one skeleton per screen so nothing reveals in cascade; pull-to-refresh spinner only for a user pull.
Error or offlineEmptyState error (다시 시도) or offline when nothing is cached (a cold offline start is never drawn as "empty"); the Offline strip pinned under the header when stale data is on screen; a failed section shows an in-section error, not a page error.
OverflowLong names wrap to the name line limit (people never cut; a place may crop); long Korean venue names; many rows (virtualised, with section caps and 전체 보기); counts capped at 99+; more tabs than fit scroll; 20-character club names.
Englishko and en ship. Tags stay one or two words; sentence case; plural forms; durations, money and ranges through the shared formatters (terminology guide, English rules).
200% textNo fixed heights on rows; Tabs scroll; Strip columns stack; display numerals capped; Choice wraps left-aligned; CTA label wraps to two lines; tab-bar labels capped (library addendum B6).

Role variants are a second axis on most boards: member, officials (총무, 운영진), host, guest or non-member, blocked, archived, signed out.

Library gaps the pages expose (to decide in Phase B follow-up, before the boards are drawn): (1) Header text action; (2) a grouped-Cell Sheet for more than five actions; (3) CTA band helper line; (4) MediaBand (cover photo, map); (5) feed post row for the club 홈 pane (author, excerpt, photos, reactions, comment count); (6) message bubble and composer for the DM thread; (7) a calendar (the 기간 기록 activity calendar and the 홈 picker; the library has only the wheel picker); (8) leaderboard podium (inventory §4.2); (9) ScoreEntry and the live scoreboard (inventory §4.2); (10) select mode (checkbox rows with a sticky CTA, flows 3 and 8); (11) a countdown Notice for the payment hold.

4.1 Hub board (4 routes) ​

Anatomy. Header (title 20/28, at most two trailing items) → Tabs (기록 only; the club hub's six panes) → Block (홈 144, 기록 160, 프로필 144) → Section heads with MediaRow, PersonRow, Strip, Chart or Roster → at most one Band → FAB (홈; the club hub's contextual FAB) → Tab bar (not on the club hub, which is pushed).

PageVariants and edge cases to draw
홈 /(tabs)/(home)First-run: zero clubs and zero sessions: Block empty ("일정 없음", Block empty rule) and under it three nav Cells 클럽 찾기, 초대 코드로 가입, 번개 찾기 replacing today's recommendation cards; the FAB stays the one coloured action. Live now: a Live Tag row first, the tab-bar dot, the Block's state while a session runs. Block eyebrow: today, tomorrow, a day within the week, nothing for 7 or more days. Notices: none since owner review 26 (the disputed-match Notice went with the dispute path; an appeal is the 할 일 Cell 점수 검토); a payment due is the row's Tag 입금 필요 and a 할 일 Cell. 할 일: anyone with work waiting (review 26; was officials only, flow 9). Data: same-day sessions across clubs, a cancelled session, offline with cached data (Offline strip), 99+ on the bell. Roles: FAB label by role (IA-6).
기록 /(tabs)/(records)Exactly one empty kind at a time: no friends, no region (action 프로필 편집), friends with no matches, filtered-empty (reset), scope-empty (names the scope and offers 전국), zero matches (selectRecordsEmptyState, screen-blueprints/records.md). ELO Block while unrated (측정 중, no progress bar) and at the top tier. Chart with 0 or 1 point (Chart empty rule). 승률 with zero decided games. Sections below the board's three (IA-8). The scope Sheet with the 기간 facet (IA-1). Tabs 지역 needs a profile region.
프로필 /(tabs)/(profile)Day one (EMPTY-ONCE): Strip, manner tags and achievements render nothing; 친구 keeps its prompt as the page's one prompt. Tier hidden for the lowest tier (no level Tag; 신뢰 등급 Sheet reachable). Identity Block with a 2-line name and a photo versus initials. DM unread count on the 홈 header after IA-5. Sections 최근 경기 only for own profile with matches.
클럽 홈 /clubs/[clubId]MediaBand with the club photo, the club's default cover when it has none (COVER-ALWAYS), album banner. Non-member: never drawn (replaced to the preview). Archived: read-only Notice. Six panes, each with its own loading, empty and role variants: 홈 feed (posts, 경기 결과 and 사진 업로드 auto rows, unread count Tag on the pane's tab, starter checklist when empty), 일정 (예정 and 완료 chips; officials' FAB), 채팅 (the club room is provisioned on first open; blocked users filtered), 기록 (club ranking as the global rating filtered to members; 전체 and 조별), 멤버 (role groups, 조 legend only with two or more groups, 파트너 추천 row, officials' Cell to 멤버 관리), 회비 (member view versus officials' roster; the viewer's own row). Chrome by role: 관리 text action for officials, FAB per pane. Six Tabs scroll past four.

4.2 List board (28 routes) ​

Anatomy. Header (optionally Search takeover, Tabs or a Choice row) → Day header or Section head → MediaRow or PersonRow, no card and no line between rows → end-of-list row → FAB or CTA band → EmptyState. Sub-groups so the board can be drawn in strips:

  • Tab-root lists (2): 클럽, 경기.
  • Inboxes (2): 알림, 메시지.
  • Club management (9): 멤버 관리, 회비, 일정 관리, 출석 현황, 신고 관리, 테니스장 관리, 도전 과제, 앨범, 복식 파트너 추천.
  • Session sub-lists (4): 참석자, 참가비, 출석 확인, 호스트 관리.
  • Personal lists (8): 친구 목록, 친구 찾기, 업적, 연습 기록, 경기 기록, 리더보드, 차단 목록, 동의 이력.
  • Directory and receipts (3): 테니스장, 장소 정보 제안, 소유권 신청 내역.
PageVariants and edge cases to draw
클럽Newcomer (lead with 탐색 and 초대 코드; 클럽 만들기 as a secondary row) versus member; archived memberships in their own section with the 30-day recovery deadline or expired copy, and a list that holds only archived ones; 탐색 with search, filters and infinite scroll; a pending application as Tag 신청 중 (new; needs the viewer's pending requests on the list); unread Count tag; guest clubs.
경기Four Tabs; genuinely empty versus filtered-to-empty (reset) per tab; 저장됨 has no filter-aware empty; the GPS-defaulted region hint and its dismissal; LIVE Tag, 마감 임박, 예비 as the one trailing element; day headers with today dot; FAB 번개 on 번개 찾기 only; end-of-list row 찾는 곳이 없나요?; friends going and distance in L2.
알림Bands 할 일, 새 소식, 이전 알림; filter chips that wrap to a second row (never scroll); admin kinds grouped when two or more clubs hold one (expandable); 모두 읽음; dismiss; the bell count; empty text by filter; cold offline; signals hidden by the user's preferences.
메시지주요 and 요청 Tabs; 요청 count badge (cap 99); inline 수락 and 거절; blocked 1:1 threads filtered but groups not; group versus 1:1 avatar; long previews; load-more.
멤버 관리Role groups, the 멤버 group collapsed, 조 legend only with two or more groups, 가입 심사 queue header, kebab Sheet (역할 변경, 조 지정, 내보내기), the viewer's own row, names that wrap to two lines (people never cut).
회비Flow 8: month Choice, empty month, sections, select mode, own-row caption, member view (own rows only, 입금 완료, 확인 대기, "총무에게 문의").
일정 관리Three panes 모집 중, 진행 중, 완료 with day headers; infinite 완료; officials' FAB; non-managers get no CTA.
출석 현황, 신고 관리Permission wall; empty; flagged members (three or more strikes) drill-down Sheet; reports: open versus resolved rows; reports have no browse entry when the count is zero (§1.6).
테니스장 관리Add only through the finder (a duplicate pick is a no-op with a Toast); court-count validation; delete through the row's own confirm; permission wall.
도전 과제, 앨범, 파트너Challenges grouped by status with the accept Sheet only for pending away-side ones; album: empty and tiles without a lightbox; partner candidates collapsed by default, several open at once.
참석자Viewer versus manager (확정 and 예비, plus 취소 for managers); a bucket with zero rows draws no section; waitlist order #N; admit and remove only for those who may; a row soft-gated by 조.
참가비Flow 3 host side: permission wall (members), pending, paid, waived, refund-owed rows; the host's own payment; fee session versus legacy or free session with an editable amount and a generate button; closeout CTA disabled until all settled.
출석 확인, 호스트 관리Exception-first attendance (only 불참 is marked; undo by tapping again; closeout disabled until resolved); host dashboard: no applicants, one, two or more (전체 승인), creator-only wall.
친구 목록, 친구 찾기Incoming, outgoing, accepted groups; empty; search idle, searching, no result, per-row state (친구, 요청 보냄, 친구 신청).
업적, 연습 기록Only earnable achievements are listed (about 54 of 74 types stay hidden until they can be earned); locked ones with progress; practice with an Add Sheet, running total, empty.
경기 기록, 리더보드History: never-played versus filtered-empty, month grouping, five sorts; leaderboard: three scopes, podium, the viewer's rank row when outside the top 50, no primary club, no region (action).
차단 목록, 동의 이력Names resolved in bulk with an id fallback; 차단 해제 confirm; consent history up to 200 rows with a purpose fallback.
테니스장Browse (grouped by province) versus search (flat, paged); save and unsave; the two contribution doors (제안 내역, 없는 장소 추가); one inclusive result set, so one empty cause.
제안 내역, 소유권 신청 내역Scope toggle 내 제안 and 받은 제안 (mine and inbox); signed out; empty copy by scope; invalid route for claims.

4.3 Detail board (22 routes) ​

Anatomy. Header (back; hero title fades in on scroll; at most one text action and one icon) → MediaBand or Hero → Notice (at most one) → Section heads with Roster, Cells, Strips, PersonRows → one CTA band or none. Sub-groups: session (일정 상세, 번개 미리보기, 스코어카드, 대진표, 토너먼트), club (게시글, 가입 심사, 성장 분석), place and receipt (venue, directory venue, contribution, claim), person and records (공개 프로필, 기간 기록, 일일 요약, 상대 전적), thread (DM), document (the four legal pages).

PageVariants and edge cases to draw
일정 상세The CTA state strip (draw each): open × host 초대하기; open × member free 참석하기; open × member fee 참석하기 with the fee helper; confirmed 참석 취소; waitlisted 예비 취소; guest 참가 신청 or 바로 참가하기; cooldown (disabled with the helper); tier-blocked (disabled with the helper); locked × host 경기 시작; locked × member 예비 등록; live × host 대진표; live × participant 라이브 스코어보드; completed × host paid 정산; completed × participant 매너 태그; refund pending (overrides all); cancelled (Notice only). Notice stack one at a time (IA-3). Roster full with dashed empty slots, over 8 with "외 N명". Edit state: bare page, 취소 left and 완료 right, bottom Sheet editors, locked rows with a reason, a leave guard. Venue without a map, venue closed, weather rain-only. Offline.
번개 미리보기Closed (cancelled or completed: disabled "마감된 일정이에요"), signed out (로그인하고 신청하기), rejected, pending (amber Notice and 신청 취소), none or cancelled (apply verb by auto versus manual approval), then the member bounce.
스코어카드, 대진표, 토너먼트Live variant: Live Tag, elapsed clock, tabs (draws, live, rounds, standings or ladder for score-driven formats), per-match rules override for officials, an entered score with the appeal door 점수 검토 요청 ›, under review (검토 중), 기록 제외, ELO preview; board: no draw, draw ready, live, all done, host-ended; unsettled-player Notice; tournament with no rounds (recovery action), winner pick, 다음 라운드 confirm; spectator offline paper-backup state.
클럽 미리보기 (retired, review 41: these join states now belong to the club page's non-member view)Join policy drives the CTA (open, approval, invite_only, pending, rejected with re-apply); archived Notice; the member bounce; album pager; dues fact (IA-12); not found for a non-discoverable club.
게시글Not found or deleted; signed out (no composer); comments loading, error, empty; reply mode; photos by signed URL (a denied grant draws nothing); the author's edit and delete.
가입 심사Evidence fields each with their own empty string; non-admin wall; request not found or already decided; both CTAs disabled while deciding.
성장 분석Entitlement gate; permission gate; conversion cells guarded off draw a plain value; empty.
장소 상세 (two)Closed venue (muted body and a banner), per-kind section order (full court, practice, screen, shop), pending review present or absent, sources, booking alert toggle, three-or-more action icons only (a two-item row never draws), 길찾기 and 지도 mapped to the right handlers, missing id.
제안 접수 내역, 장소 소유권Not signed in, loading, error, not found; review actions only for authorised viewers; decision history; withdraw, resubmit, relinquish, revoke (destructive Dialog); transfer-consent-only view; invalid route.
공개 프로필Friendship band in five states (none, outgoing, incoming, accepted, blocked hides the band); masked fields drop their rows; format-preference fact only when the ELO gap exceeds 50; the share and ⋯ (신고, 차단) actions; own profile reuses the page.
기간 기록, 일일 요약, 상대 전적Empty period versus empty after filters; opponents section hidden at zero; calendar cell states; the day page's invalid-date guard; rivals: 0 to 4 callouts and the full list; draws excluded from win rate.
DM 스레드Request gate (recipient 수락 and 거절, initiator waiting, declined) versus composer; 1:1 versus group header; blocking error versus a non-blocking banner when messages are already on screen; image messages; message long-press Sheet (reaction, delete within 60 seconds, report); empty thread with the counterpart header; a thread that opens before messages load.
약관, 개인정보, 공지, 문의Long static text (the 200% text risk is the whole test); privacy's data-export action (idle, pending, error, success); announcements deliberately empty; contact rows render only for configured channels (email today).

4.4 Form and wizard board (12 routes, plus onboarding) ​

Anatomy. WizardShell (progress line, back and skip as real buttons in the header, title 24/32, fields at 32 seams, one CTA band; a pair only on the confirm step) for create-club and create-session; the single-page forms use Header, Fields (label, control, helper) and one CTA band.

States every form shows: untouched (no errors yet), validation (inline helper in danger under the field, never a Toast), disabled CTA with the hint line saying why, submitting (spinner in the button), server error (inline, plain language), offline (CTA disabled with the reason), keyboard open (the CTA band rides above the keyboard, the focused field scrolls into view), dirty-leave Dialog 나갈까요? for the guarded forms, English and 200% text (labels wrap, Choices wrap left-aligned).

PageVariants and edge cases to draw
클럽 만들기Fresh versus resumed draft (the "이어서 작성 중이에요" Toast with 처음부터); quick path (4 segments, answers marked 기본값 until edited) versus full (9); edit-from-review (the CTA reads 검토로 돌아가기); the issued-creation receipt and its recovery states (committed, incomplete follow-ups, retry, end with a warning); court-less club; names that wrap.
일정 만들기Flow 4: fresh, resumed, prefill provenance (정기 일정, 지난 일정), 반복 summary, free versus fee session, edit-from-review detour, field-edit scope open (skip and back gated), submitting, error, destructive-exit Dialog on the first step.
클럽 가입하기Flow 2 B1.
글쓰기Category clamp by role (admin-only categories fall back to 자유; 운영진방 forces an official audience), up to 4 photos with an uploading overlay and removal disabled until done, submit disabled while uploading or empty, leave guard, auto-delete of unposted uploads.
경기 규칙Permission wall (deep link only), reset to defaults, validation text from a failed parse, dirty guard on swipe, Android back and tab switch.
장소 정보 제안, 소유권 신청Five kinds with kind-specific titles; replace and cannot-replace; missing target; the date typed as text today (audit H5); venue picker through the finder; blocked or ineligible; submit then replace to the receipt.
단톡방 만들기, 멤버 초대Seeded candidates (session or club, all pre-selected once) versus the friends list; zero candidates; title plus at least one member; the disabled CTA; replace to the thread.
프로필 수정Birth year write-once (locked once set, absent from the payload), under-14 error, name 2 to 20, 구력 range, avatar upload overlay and permission-denied Dialog, not-dirty versus dirty save.
비밀번호 변경, 회원 탈퇴Errors only after blur; server current-password error inline; type-to-confirm phrase with a placeholder that does not look pre-filled (audit H5), then a second Dialog; deleting state.

4.5 Settings board (6 routes) ​

Anatomy. Header → grouped Cells (nav with chevron, toggle with Switch, value with chevron, destructive in danger) with the cell hairline inside a group and a 32 seam between groups.

PageVariants and edge cases to draw
설정Groups 계정 (프로필 수정, 비밀번호 변경, 연결된 계정 with its count, 본인인증 완료 with the name and birth date masked, 공개 프로필 보기 ›), 알림, 프라이버시, 앱, 지원, 계정 관리 (로그아웃, 회원 탈퇴); optimistic toggles with rollback; the theme choice; the OTA check (idle, checking, result Dialog); developer mode hidden versus revealed; cold offline.
프라이버시Each of six fields at one of four audience levels (Choice per row), disabled until the profile is ready and online, the preview Sheet recomputed per audience.
알림 설정Per-category push Switch (in-app is always on), one quiet-hours window with start and end pickers, payments exempt; loading, error, optimistic mirror.
동의 관리Per purpose: required and granted (caption and a delete-account link), optional and granted (철회 with a Dialog), optional and not granted (활성화).
클럽 설정Sections shown by permission; image uploads with per-kind pending; gallery limits and permission Dialogs; 정원 Stepper; transfer ownership (candidate Sheet, confirm naming the recipient, blocked while pending); owner-only danger zone; an archived club shows the recovery receipt (owner within 30 days sees restore).
관리The attention strip draws only non-zero cells (one line when none); 운영 and 설정 groups by permission; 성장 분석 only with the entitlement; non-admin bounce; cold offline.

4.6 Auth, onboarding and public receivers (4 routes, 2 non-route flows, web pages) ​

SurfaceStates and edge cases to draw
Login and sign-up (flow 1)Busy, provider cancelled, wrong credentials (plain language, never the raw error), offline, a signed-out deep link that must return after login (returnTo on web, the destination kept on mobile).
Value props and onboarding (flow 1)Skip versus 시작하기, the 3-card swipe, under-14, required consent missing (the helper line), resume after a kill, sign-out from the close button with its Dialog.
App version gateThe force-update screen shown before login (AppVersionGate); it fails open.
/s, /m, /p, /lb receiversLoading, not found, public preview for a signed-in non-member (needs the CTA of IA-11), offline cold start, the redirect for members (draw nothing). Web server-rendered pages: card, one link 앱에서 열기, the missing install path.
/join-club public landing (web)Signed out (needs a sign-in prompt), code invalid, club full.

4.7 Overlay catalogue (no routes) ​

Every overlay is drawn on its trigger screen, not in isolation. Counts are files, not distinct overlays: ModalPanel 51, confirm.show 63, HeaderActionsMenu 16.

v2 componentOverlays in code todayTo draw and decide
Sheet: filters and pickersMatchHistoryScopeSheet, MatchListControlsSheet, the 경기 filter panel, ChipPickerPanel, DropdownChip and RangeChip panels, the native date and time pickers (6 files), DaumPostcodeModalOne filter Sheet grammar (Choice groups, a reset, a count on the trigger); the native pickers inside a Sheet with 완료
Sheet: edit and settingsThe session edit Sheets (EditFieldModal, EditControlModal: date, time, venue, courts, tier, composition, max players, fee, notice), club settings (GroupEditor, RecurrenceEditor, TierRangeEditorModal, DefaultCoverPicker, RegroupPreview, gallery), the privacy previewThe shared pair 취소 and 적용 at full width, no X, no grabber
Sheet: explainersTierInfoSheet, ProfileTierInfoSheet, TrustTierInfoSheet, MemberStatsModalLong content with a sticky close; reachable from every place the tier shows (canon trust)
Sheet: session and playFormatChooserSheet and AddRoundSheet (merge, flow 6), LiveRoundEditorSheet, SwapSheet, CreateTournamentModal, MatchRulesEditorSheet, ScoreEditSheet, ScoreCallSheet, ScoreCorrectionSheet, MatchProfilesSheet, MannerTagsSheet, TierPromotionSheetThe ScoreEntry composition (flow 7); the consensus list; a keyboard open under a Sheet
Sheet: safety and promptsReportModal, VenueCorrectionModal and its report sheet, PushPermissionSoftPromptSheetReason Choices, free text, sent state
Sheet: member and club actionsMember role, 조 and 내보내기 modals, GuestActionModal, GuestPickerModal, ChallengeAcceptModal, TransferOwnershipModal, dues note, waive and 납부 취소 panel, DuesAttestModal, post edit and delete, ParticipantsModalDestructive rows last in danger; naming the person in the confirmation
Sheet: new in v2입금 안내 (flow 3), 어떤 일정인가요? (flow 4), 대진 Sheet (flow 6), the 관리 grouped-Cell Sheet (IA-4), club picker (IA-6), 약관 보기 (flow 1)Each new Sheet is drawn once with its states
DialoguseConfirm and ConfirmSheet: leave a form, destructive actions, money confirmations (dues, session fees, bulk), start a session, the OTA result, the join mismatchWidth 350, tonal cancel and primary confirm, danger only for destructive; the backdrop does not dismiss
MenuHeaderActionsMenu (anchored popover on a raw Modal; canon still says centred ModalPanel, audit I1)At most five rows; anything longer is a Sheet (IA-4)
TakeoverVenueSearchTakeover for every venue pick, the proposed Search takeover on list headersFull-screen with a cancel; results; the end-of-list contribution row
Toast, Coachmark, CelebrationToastHost (one at a time, above the tab bar and FAB), the spotlight tour (the 홈 coachmark names a "FAB" that is a discovery card today), CelebrationOverlayToast with an action; the coachmark's new target is the real FAB

4.8 Route counts by template ​

TemplateRoutesPages
Hub4홈, 기록, 프로필, 클럽 홈
List28클럽, 경기, 알림, 메시지, 멤버 관리, 회비, 일정 관리, 출석 현황, 신고 관리, 테니스장 관리, 도전 과제, 앨범, 복식 파트너 추천, 참석자, 참가비, 출석 확인, 호스트 관리, 친구 목록, 친구 찾기, 업적, 연습 기록, 경기 기록, 리더보드, 차단 목록, 동의 이력, 테니스장, 장소 정보 제안, 소유권 신청 내역
Detail21일정 상세, 번개 미리보기, 스코어카드, 대진표, 토너먼트, 게시글, 가입 심사, 성장 분석, 테니스장 (club), 테니스장 (directory), 제안 접수 내역, 장소 소유권, 공개 프로필, 기간 기록, 일일 요약, 선수별 전적, DM 스레드, 공지사항, 문의하기, 이용약관, 개인정보 처리방침
Form / wizard12클럽 만들기, 일정 만들기, 클럽 가입하기, 글쓰기, 경기 규칙, 장소 정보 제안 (new), 소유권 신청 (new), 단톡방 만들기, 멤버 초대, 프로필 수정, 비밀번호 변경, 회원 탈퇴
Settings6설정, 프라이버시, 알림 설정, 동의 관리, 클럽 설정, 관리
Screens72
Public receiver4/s, /m, /p, /lb
Alias11no UI
Layout7no UI
Route files94

5. What C0 needs from the owner ​

Each row is a decision the code and the docs cannot settle. The recommendation is this document's; the evidence is in the section named.

#DecisionRecommendationEvidence
1Drop the 홈 일 / 주 / 월 row and move the period recap into 기록's scope Sheet?Yes. Check pill usage first if you have it; if 주 or 월 dominates, keep one 이번 주 기록 Strip on 홈.IA-1
2Keep 테니스장 as the fourth 경기 tab?Yes; delete the orphan /courts route.IA-2
3Session-detail order and a CTA band helper line carrying fee and cancellation termsNotice stack, hero with 3 facts, roster, 경기, 정보 Cells, CTA band with the helper. Add "CTA band helper line" to the library.IA-3
4A ghost text action in the Header, and a grouped-Cell 관리 Sheet for hosts and officialsYes to both; the Menu stays at five rows. Keep 초대 코드 as text on 클럽.IA-4
5Where do 알림 and 메시지 live?One pair on 홈 (message, bell); 경기 gets search only; 프로필 gets settings only.IA-5
6What does the 홈 FAB create?By role: 일정 for an official of one club, a club picker for several, 번개 for everyone else. 경기 FAB on 번개 찾기 only; the club hub's FAB follows its pane.IA-6
7A MediaBand row in the library; does session detail keep its map band?Add the row for club hub, preview and venues. For session detail, keep the band when a venue map exists and measure the roster's position on the board; drop it if the roster falls below the fold.IA-10
8Sign-up route and the Kakao buttonDecide the Phase 8 route (OAuth, identity verification, or an interim email sign-up). Treat the Kakao button as a brand-mandated exception to S2. Replace 플레이 스타일 with the first-action chooser; add 전체 동의.Flow 1
9Default-first session creationYes: entry Sheet, review page with Sheet editors, the step order kept as the first-timer path inside the same components. This changes an unreviewed wizard, so it is yours to ratify.Flow 4
10Create club: toggle or fork screenKeep the toggle (the canon calls it the deliberate act) and pre-answer region and logo so quick needs only name, day and time.Flow 5
11Build bulk confirm for session fees, and schedule the native build for one-tap copyYes to the RPC (modelled on mark_dues_paid_bulk). Put expo-clipboard in the next EAS build (U-11) and research a bank-app transfer deep link in the same pass, since both touch native config.Flow 3
12Dues reconcile: what is ticked by default, and who confirms the 총무's own duesSelect mode with every 확인 대기 and 미납 row ticked. The payer never self-confirms (migration 00536), so another official confirms; show the row with a caption rather than hiding it.Flow 8
13Re-introduce an officials' 할 일 section on 홈, and build a manual "remind those who have not answered"Yes to the section (the strip was retired 2026-07-31 and closed-app officials rely on single pushes). The remind needs a server function, a signal type and a rate limit; build it only if you hear that chasing is the 총무's main chore.Flow 9
14One ScoreEntry for host and participantsYes; number fields plus common-result Choices; retire the ±1 stepper sheet for games.Flow 7
15Back and chrome rulesRatify "pushed screens hide the tab bar"; put the single back() fallback in appRouter; fix the push routes of IA-11.IA-9, IA-11
16Mechanical clean-up before boards are drawnDelete 8 dead route builders, the club-scoped session redirect, /courts (with decision 2), and keep /preview and /status out of the sitemap.§1.6
17Share-link landingsGive non-members an apply path on /s/{id} and a sign-in prompt on the web join page now; add store links to the web landing pages when the apps are listed.IA-11
18Dropped 기록 modulesKeep them as flat sections under the ELO chart in the order of IA-8; the filter stays one Choice.IA-8

Checks to run on a device before the boards are drawn (none is a decision, all change a board): cold-start back on a push tap and on a universal link; the consent step's 보기 links during onboarding; whether a participant lands on a read-only match board from a matchup push; the member push session_payment_hold_created landing; what lookup_club_by_invite_code returns to a non-member (dues amount); the first-run of 홈 for a user with no clubs. Research before drawing: the 전체 동의 convention in Korean sign-up flows (no frame in the benchmark set) and bank-app transfer deep links.

Suggested board order for Phase C. Hub (it fixes the Block, Strip, section rhythm and the first-run), List (the most pages), Detail starting with 일정 상세 and its CTA state strip, Form and wizard, Settings, Auth and onboarding (needs decision 8), Overlay (drawn last, on the trigger screens already done).

Docs to update when these are ratified (not done here): canon/components.md (FAB, Menu, Header, media band, tab-bar visibility), canon/sessions.md (the detail order), screen-blueprints/* per page as each ships, terminology-guide.md (입금 안내, 할 일 if used), usability-audit-2026-09-16.md §3 (the stale task table), and the migration plan's task-flow row (the 45-tap dues figure).

6. Rulings (2026-10-09) ​

Owner: "Go with best practices and your recommendation" and "work on all remaining gaps". Rulings below adopt §5's recommendations except where noted; each is logged in the decision log for veto.

#RulingNote
1Drop the 홈 일/주/월 row; the period recap becomes a 기간 facet of 기록's scope Sheet, which also becomes the entry to `/records/weekmonth
2테니스장 stays the fourth 경기 tab; /courts is deleted
3Session detail: Notice stack → hero (3 facts) → roster → 경기 → 정보 Cells → CTA band with a helper line (fee, cancellation)library §14 adds the helper line
4Header text action (관리, 초대 코드, 완료) + an ActionSheet of grouped Cells for more than five actions; the Menu stays at five rowslibrary §14
5One [메시지, 알림] icon pair on 홈 only; 경기 carries search; 프로필 carries settings
6홈 FAB by role: + 일정 (official of one club), + 일정 with a club picker (several clubs), + 번개 (everyone else); 경기 FAB on 번개 찾기 only; the club hub's FAB follows its panethe label always names the action
7MediaBand joins the library (club hub, preview, venues: photo band with COVER-ALWAYS; session detail: map band when a venue map exists, dropped if the roster falls below the fold on the board)library §14; fold test refined 2026-10-10 (library §19 D3): the roster's head and first row sit above the CTA band, so the map band stays whenever a venue map exists
8Owner / external: the sign-up route (OAuth, 본인인증 or an interim email sign-up) stays EXT-11; owner review 4 (D6) draws it: 본인인증 once at sign-up, before 약관, one account per person by the CI (needs counsel on the order). Ruled now: the Kakao login button keeps Kakao's own colours (a brand-mandated exception to S2); the unread 플레이 스타일 onboarding step becomes the first-action chooser
9Default-first session creation: an entry Sheet with the club defaults prefilled → a review page with Sheet editors; the step-by-step order stays as the first-timer path in the same componentslogged for veto: changes the wizard
10Create club keeps the quick/full toggle; region and logo are pre-answered so quick needs name, day, time
11Bulk confirm for session fees (RPC modelled on mark_dues_paid_bulk); expo-clipboard and a bank-app transfer deep-link study go into the next native build
12Dues reconcile in select mode: only 확인 대기 rows are ticked by default; 미납 rows start unticked (the 총무 ticks those verified against the bank statement); the payer never self-confirmsadjusted from §5: defaulting 미납 to ticked invites false "paid" records — a money action defaults to the evidenced set
13An officials' 할 일 section returns to 홈; the manual "remind non-responders" function waits for evidence that chasing is a problem
14One ScoreEntry for host and participants (number fields + common-result Choices); the ±1 stepper sheet retireslibrary §14
15Pushed screens hide the tab bar (ratified); appRouter.back() gets a fallback to the tab root when there is no history; the IA-11 push routes are fixedfixed now as bugs, not redesign
16Mechanical clean-up now: 8 dead route builders, the club-scoped session redirect, /courts; /preview and /status stay out of the sitemap
17Non-members get an apply path on /s/{id}; the signed-out web join page prompts sign-in instead of ignoring the tap; store links when the apps are listed/s/ apply path is drawn on the detail board; the web sign-in prompt is a bug fix now
18The dropped 기록 modules return as flat sections under the ELO chart in IA-8's order; the filter stays one Choice

Bugs fixed now (independent of the redesign): payer-directed payment pushes route to the payer's own view; the join-request push lands in one hop; back() fallback on cold starts; onboarding keeps ?code=; the signed-out web join prompts sign-in; the dead route builders and /courts are removed.

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