Skip to content

Owner review 50 — research (2026-10-30) ​

Status: Reference · Date: 2026-10-30 · Three briefs for the round-50 record, each by a research agent; the lead read them and drew the decisions into the record. A is outside research with its sources (graded: [V] read in full, [S] a search excerpt only, [R] the agent's recommendation); B and C are audits of the app against the boards with file:line evidence on both sides. Where a source could not be read in full, the brief says so.

A. Outside: tab sets, the operator layer, colour, forms, profiles ​

Researched 2026-10-30. No repo files touched, no agents spawned.

Evidence grades used below:

  • [V] = I opened the page myself (WebFetch or the browser pane) and read it.
  • [S] = only a search-result excerpt; the page itself was blocked (403) or JS-only. Treat as lower confidence.
  • [R] = my own recommendation / synthesis, not a claim from a source.

Naming rule honoured: the four Grand Slam names are never written in prose. Where a URL contains a site's domain I kept the domain only. Two sources I read had the London tournament's name in the URL path, so I left them out entirely (an IBM newsroom release and a Smile UX case study); neither held anything load-bearing.

One state change to report: on uefa.com I clicked "Reject all" on the cookie banner (privacy-preserving option). On atptour.com I left the banner untouched. Nothing else was submitted anywhere.


1. Live scoreboard tab sets by competition format ​

1.1 What real products do (observed) ​

ATP Tour web, tournament page [V, live in the browser pane on 2026-10-30, Basel ATP 500 in progress] URL pattern: https://www.atptour.com/en/scores/current/basel/328/live-scores (also /daily-schedule, /draws)

  • Header: a dark crimson/magenta-leaning pill reading LIVE next to the tournament name, shown only while the event is live (visual inspection of the screenshot; I did not extract a hex). Tournament dates line under it.
  • Tab row (underline tabs, horizontally scrollable on a 375px viewport, last tab clipped): Live | Results | Schedule | Draws | Seeds. Active tab text computed colour rgb(14,104,148), a teal-blue, not red. The Live tab is always present; with nothing in play its body says "There Are Currently No Live Scores".
  • Schedule tab = order of play by court. A "Day 7 / Day 6" picker (day chips), a Local Time Zone toggle, print and refresh icons. Then a court heading ("Center Court") and an ordered queue under it: Starts At 12:00 / QF / ..., Not Before 14:00, Followed By, Not Before 18:00, Followed By. Each row has an H2H button. This is exactly the "ongoing + queued per court" model the owner describes.
  • Draws tab = bracket. Event chips: Singles | Doubles | Qual Singles | Qual Doubles. Round chips with Previous/Next arrows: R32 | R16 | QF | SF | F. Below, the page body is sectioned by round heading (Round of 32, Round of 16, Quarterfinals, Semifinals, Final), each a vertical list of two-player cards with set scores and H2H / Stats buttons. Undecided slots render as TBA.
  • Scores landing page (https://www.atptour.com/en/scores/current): each tournament is a card with its own 3-way segmented control All Scores | Schedule | Draw. Live match cards show Round of 16. Center Court plus an elapsed clock (02:21:01), the winner marked with a check mark (not a colour), an umpire line, H2H and Stats buttons, and a one-line event feed ("Game Set and Match ...").

WTA scores page [V, text only] https://www.wtatennis.com/scores : a filter row Live | Finished | Upcoming with an All option and Reset; lists load client-side.

UEFA EURO tournament site, groups view [V, browser pane] https://www.uefa.com/uefaeuro/history/seasons/2024/groups/

  • Top tabs (pill tabs): Overview | Matches | Groups | Stats | Teams; the active one is a white pill.
  • Inside Groups: underline sub-tabs Tournament phase | Qualifying.
  • One white rounded card per group. The card's first line is the heading "Group A"; then a 4-row table (Played, Won, Drawn, Lost, For, Against, Goal difference, Points in bold, Form guide). The qualifying rows get a thin blue bar on the left edge and a footer legend dot "Qualified". A "Group details" disclosure at the foot of each card expands that group's matches. All six cards are visually identical: groups are separated by card + heading + space, not by a hue per group.
  • Form guide: small circles, W green, D grey, L magenta/pink. (Note the loss colour: it is a red-family hue.)

Other football surfaces [V]

Grand Slam sites/apps [V but thin]: the London site's page is titled "Order of Play" under /scores/schedule (its /en_GB/scores/schedule page; empty off-season). Its app listing names Live Scores & Stats, Order of Play & Schedule, Live Court View (https://apps.apple.com/ee/app/id319284643). The Paris site's draws page has an event selector with 21 draws (main, qualifying, junior, wheelchair, quad), a Global view / Detailed view toggle and a "Full PDF draw" link (https://www.rolandgarros.com/en-us/results/SD/?round=5). The New York app listing has "Scores", "Official Schedule of Play", "draws" (https://apps.apple.com/us/app/id327455869). None of these pages exposed their mobile tab bar to a fetch, so I make no claim about their app tab names. A user review of the London app complains that Draws is "repetitive" with Scores and Order of Play (https://apps.apple.com/pt/app/id319284643?l=pt-pt) [S]: a warning against tabs whose content overlaps.

Tournament data models [V]

Padel/tennis club rotation apps [V, App Store copy]

  • Padel Americano Tournament: Home switches Tournaments | Match; "compact round views and clear round panels"; completed rounds hide the score controls; court-aware scheduling with sit-out handling; +/- score entry; leaderboard updates instantly; read-only live view by QR or share code (https://apps.apple.com/us/app/id6759302756).
  • Americano Padel Manager: rounds auto-generated, each round has its own visual interface, organiser can "delegate score entry to trusted players", live scores on another screen via QR, leaderboard with a per-round evolution chart (https://apps.apple.com/app/id6747935961).
  • Americano Padel (Perkodar): shareable web page updates in real time (e.g. on a venue TV); players enter scores from their phones by code/QR; multi-court is a premium feature (https://apps.apple.com/us/app/americano-padel/id1536633475).
  • Playtomic Manager: organiser enters each match result, standings recalculate in real time; players receive matchups, court assignments and final standings through tournament chat, with a live in-app view "coming" (https://helpmanager.playtomic.com/hc/en-gb/articles/44129657203985; page returns 403 to fetch) [S].

Korean apps found [V, App Store copy]

  • 쉬운 대진표 (tennis/badminton doubles): single/repechage/Swiss brackets; "라이브 스코어보드" shows "진행 중인 모든 코트의 현황을 한 화면에서"; "코트별 경기 배정과 시간표"; "선수가 직접 점수를 입력하면 모든 참가자 화면에 즉시 반영"; QR or 6-digit code entry without an account (https://apps.apple.com/kr/app/id6760689542).
  • 베이스라인: "본선 대진표" cards show players, clubs, courts, match times, status and scores; searching a player jumps the bracket to the relevant round; a "나의 오늘" block on Home gathers your own items; release note says tournaments / courts / matches were split into tabs (https://apps.apple.com/us/app/베이스라인-테니스-피클볼-대회-코트예약/id6741500563).
  • 테니스 대진표 생성 및 게임 관리: bracket generation (doubles 7-16 with seeding), schedule screen showing "match order and status", ranking sort (https://apps.apple.com/kr/app/테니스-대진표-생성-및-게임-관리/id6450150419).
  • 땡큐볼 테니스 (club app): 클럽 일정, 오픈 매치, 리그 랭킹 (https://apps.apple.com/app/id6774366619). No court-queue or live-board screen documented.
  • Nobody I found ships all of: per-court live board + group cards + round-chip bracket for amateur clubs. 쉬운 대진표 is closest on the live-court board.

1.2 Patterns that hold across the evidence ​

  1. One tab = one question. ATP: "what is on now" (Live), "what is it going to be and where" (Schedule), "who plays whom" (Draws), "what happened" (Results). UEFA: "tables" (Groups) vs "fixtures" (Matches). Overlap is penalised by users (the Draws-vs-Scores review above).
  2. A Live tab that is always present and shows an explicit empty state (ATP) is preferred over a tab that appears and disappears (no layout shift). A header LIVE pill on the event carries the "something is happening" signal everywhere else.
  3. Court is the grouping key for "now/next"; round or day is the key for "schedule"; group is the key for tables; round is the key for brackets. ATP uses court headings only inside Schedule; the owner's "live = overview per court" matches the Americano/쉬운 대진표 "all courts on one screen" board.
  4. Queue vocabulary is standardised: Starts at / Not before / Followed by (ATP, Sportradar). For a club with no clock, "Followed by" alone (a sequence number per court) is enough.
  5. Group separation = card + heading + whitespace + legend, never a hue per group (UEFA). Qualification is marked by a left edge bar and a legend, plus a disclosure for the group's matches.
  6. Bracket on mobile = event chips + round chips + a vertical list for the selected round (ATP). Full-canvas brackets are a desktop/secondary affordance (the Paris site offers "Global view / Detailed view" and a PDF).
  7. Tab count: ATP 5, UEFA 5, FotMob 7, Sofascore 3-5, WTA 3 filters. Toss's own Tab guidance: fixed tabs "at most 4"; from 4 items use the fluid (scrolling) mode (https://tossmini-docs.toss.im/tds-mobile/components/tab/). Apple: segmented controls "no more than about five segments on iPhone", and for "completely separate sections" use a tab bar instead (https://developer.apple.com/tutorials/data/design/human-interface-guidelines/segmented-controls.json).

1.3 Proposed tab sets per session type [R, derived from the above] ​

Session typeTabs (max 4)What each holds
Club rotation doubles (random/KDK pairing, many short rounds)지금 / 라운드 / 순위지금: one card per court = match in play (score) + "다음" queue line(s) + a "쉬는 사람" strip (sit-outs, as the Americano apps do). 라운드: round chips R1..Rn (current round auto-selected, finished rounds read-only like the Padel app that hides score controls once a round completes), court-by-court match rows. 순위: live leaderboard; tie-break rule shown once (Playtomic lets the organiser choose points vs wins). No bracket, no groups, no results tab: results are the finished rounds.
Round-robin league (one group, played over days/weeks)일정 / 결과 / 순위일정: day or round chips, upcoming matches as rows; a live match is a row with the LIVE pill pinned to the top. 결과: finished matches, newest first. 순위: single table. No 지금 tab because matches are not synchronous across courts.
Group stage + knockout tournament지금 / 일정 / 순위 / 대진표지금: per-court board (as above). 일정: one card per group (heading "A조", its matches inside, finished ones collapsed under a "완료" disclosure), with the round/day chip row on top. 순위: one table card per group, qualifying rows marked by a left bar + legend (UEFA pattern). 대진표: event chips if more than one draw, round chips (예선/16강/8강/4강/결승 as needed), vertical list per round, TBA placeholders until groups finish. Default tab follows phase: groups running -> 일정 or 지금; knockout running -> 대진표. Keep 대진표 present but with a placeholder before it exists (parallels ATP's always-present Live).
Single pickup matchno tabsOne scoreboard card: teams, sets/games, status pill, court, elapsed time. History is the 기록 tab elsewhere in the app. Anything with tabs here would be one tab long.

Why not a separate Results tab for tournaments: ATP and UEFA both have one, but their Results/Matches tabs are the only place finished matches live and they have far more matches than a club event. For 4-12 teams, finished matches fold into 일정 ("완료" disclosure) and into the bracket cards (winner check mark, as ATP does), keeping the tab count at or under 4.


2. Operator overlay: layering controls over the public view ​

2.1 What the tools actually do ​

ProductHow organiser controls relate to the public viewSource
TournatedSeparate manager surface: "Advanced Order of Play" (drag a match to a court/time, conflict checks for overlap/insufficient rest, a modal to edit details/enter scores/delete). Participants "mainly use the grid and timeline views". Same schedule data, different screens.https://tournated.gitbook.io/tournated/tournated-portals/tournated-manager/creating-and-managing-a-tournament/match-and-result-management/order-of-play/views [V]
PlaytomicSeparate Playtomic Manager for the organiser; players get matchups, courts and standings through chat.https://helpmanager.playtomic.com/hc/en-gb/articles/44129657203985 [S]
Challonge (via API wrapper)Per-match state machine on the match itself: pending / open / complete, plus organiser actions mark_as_underway() / unmark_as_underway() and report_winner(winner, "1-3,3-0,3-2"). A Team Liquid post says marking in-progress highlights the match on the public bracket and admins can enter a running score without picking a winner [S].https://achallonge.readthedocs.io/en/stable/api.html [V]; https://tl.net/blogs/549050-challonge-breakdown-by-cranky-ducklings [S]
BattlefyTask queue: players report, a confirmation countdown runs, the organiser can cancel the countdown on any match; a complaint opens a "Match Issue" the organiser resolves by reporting the correct score on the Match Page.https://help.battlefy.com/en/articles/9397088-using-score-confirmation-as-an-organizer [V]
Americano appsRole split inside one app: organiser creates/generates/enters scores or delegates score entry to trusted players; invited players read; spectators follow a read-only live view by QR; completed rounds hide score controls. 쉬운 대진표 goes further: players enter scores themselves and "운영자가 개입하지 않아도 경기가 진행".https://apps.apple.com/app/id6747935961 ; https://apps.apple.com/us/app/id6759302756 ; https://apps.apple.com/kr/app/id6760689542 [V]
Facebook Groups (membership requests, as an admin-queue analogue)A dedicated Member requests screen under Manage, per-row Approve / Decline, filters at the top, Approve all / Decline all, decline with feedback via the overflow menu.https://www.facebook.com/help/455972334752473 ; https://www.facebook.com/help/android-app/214260548594688 [V]
WordPress (analogue)The admin toolbar renders on top of the same front-end page for logged-in users; hide it with show_admin_bar(false). An actual always-on overlay on an identical page.https://developer.wordpress.org/reference/functions/show_admin_bar/ [V] (thin)
Toss TDSTab.Item redBean puts a red dot on a tab "for important notices or new updates": a precedent for a task badge on a tab.https://tossmini-docs.toss.im/tds-mobile/components/tab/ [V]

Honest gap: I found no primary source documenting a global "edit mode" toggle in a sports/tournament tool that restyles the public view. The evidence supports (a) a separate operator screen, (b) actions living on the match object itself, (c) an inbox/queue for exceptions.

2.2 Which keeps the view identical and the tasks separate [R] ​

Combine three things the evidence supports; do not use a mode toggle.

  1. Same components for everyone; role adds affordances, never layout. The scoreboard card (teams, score, status pill, court, clock) is rendered identically; an operator sees the same card plus one state-dependent primary action on it, derived from the Challonge-style state ladder: ready -> 시작 (mark underway) -> 점수 입력 (report) -> 확정. Finished rounds drop the control (Padel app). One action per card per state keeps the card from becoming a toolbar.
  2. Exceptions go to a queue, not onto the board. A small operator-only inbox with a count badge (Toss red dot on a tab; Facebook Member requests; Battlefy Match Issues) for: scores awaiting confirmation, disputes, no-shows, conflicts. Each queue row has its own Approve/Fix control, so the board stays clean.
  3. Structural edits live on a separate operator screen, never inside the public tabs: pairing changes, court assignment drag/drop, regenerate round, add/remove player (Tournated's Advanced Order of Play, Playtomic Manager, Padel apps' "schedule rebuilds itself" on player change). This is where bulk operations and conflict checks belong.
  4. Delegation is a first-class task: "delegate score entry to a trusted player" (Padel Manager) and players self-reporting with organiser override (쉬운 대진표, Battlefy) mean the operator's job is exception handling, not data entry.

Resulting split of operator tasks: advance a match (on the card), clear the inbox (queue), restructure the event (operator screen). The public tabs are untouched in all three.


3. A colour grammar for status ​

3.1 How the systems define it ​

  • Atlassian (https://atlassian.design/foundations/color) [V]: roles Neutral, Brand, Information ("informative UI, or UI showing that something is in progress"), Success ("a favorable outcome"), Warning ("caution that helps prevent a mistake"), Danger ("serious errors or dangerous actions"), Discovery ("something new"), Accent ("decorative, no fixed meaning; swapping one accent for another should leave the experience unchanged"). Rules: "Don't use an accent color where the color already carries semantic meaning"; choose a role by intent, not preference; pair selected states with a non-colour cue. Token names are role-first (color.background.danger.bold.hovered). Subtle semantic tokens for pills/lozenges exist and are non-interactive (no hover/pressed).
  • GitHub Primer (https://primer.style/product/getting-started/foundations/color-usage/) [V]: roles accent, success, attention ("warning states, active processes such as queued PRs and tests in progress"), danger, open, closed, done (lifecycle roles), sponsors. Note the two choices: in-progress sits with attention (amber) not blue, and lifecycle states have their own role names instead of borrowing success/danger. "Each semantic color is tied to a role with a specific meaning."
  • Carbon (https://carbondesignsystem.com/patterns/status-indicator-pattern/) [V]: red = danger/error, orange = serious warning, yellow = regular warning, green = normal/success, blue = passive notifications / workflow progress, grey = drafts or not started, purple = outliers/undefined. Never colour alone: pair with shape/symbol/text; at least 3:1 contrast; "for consolidated statuses use the highest-attention colour".
  • Apple HIG (https://developer.apple.com/tutorials/data/design/human-interface-guidelines/color.json) [V]: "Avoid using the same color to mean different things. Use color consistently ... especially when you use it to help communicate information like status or interactivity." "Avoid redefining the semantic meanings of dynamic system colors." Don't rely solely on colour; add labels or glyph shapes. Colours are "semantically defined by purpose, rather than appearance". Warns red means danger in some cultures and something positive in others.
  • Material 3 (https://raw.githubusercontent.com/material-components/material-components-android/master/docs/theming/Color.md) [V]: the scheme ships exactly one status family, Error (error, on-error, error-container, on-error-container). Success/warning are not roles; you add custom colours, each exporting accent / onAccent / accentContainer / onAccentContainer plus an optional harmonize flag (https://codelabs.developers.google.com/harmonize-color-android-views) [V]. The Android helper harmonises the Error roles by default.
  • Toss TDS [V]: no published hue-meaning guidance. The colour page is raw scales only (grey, blue, red, orange, yellow, green, teal, purple, 50-900, plus greyOpacity and four backgrounds) (https://tossmini-docs.toss.im/tds-mobile/foundation/colors/). Badge takes color: blue | teal | green | red | yellow | elephant x variant: fill | weak (https://tossmini-docs.toss.im/tds-mobile/components/badge/). Button/StepperRow right button takes color: primary | danger | light | dark (https://tossmini-docs.toss.im/tds-mobile/components/stepper/): so a danger role exists at the button level. AgreementV4.Necessity: mandatory renders blue, optional dark grey (https://tossmini-docs.toss.im/tds-mobile/components/Agreement/v4/). So Toss's visible grammar is: blue = primary/required, red = danger and notice-dot, grey = secondary. It is not documented, so do not cite Toss as authority for "live".
  • Sports apps [V]: ATP's LIVE pill is crimson/magenta-leaning; UEFA's form-guide loss is magenta/pink; FotMob's Live chip uses a neutral grey dot on a dark theme; ATP does not colour losses red (winner gets a check mark). None of these products shows an error state next to the live pill in the same view, which is why they can get away with a red-family live colour.

3.2 Should "live" share the error red? ​

The systems do not put live/in-progress on the error hue: Atlassian and Carbon put it on blue (Information / progress), Primer on amber (attention). Sports apps do use a red-family live colour (ATP), but only because the hue has no competing error meaning in the same view. For TwoMore, where an overdue payment or failed save can appear beside a live match, [R]:

  • Give "live" its own role token (e.g. status.live), not an alias of danger. Primer's open/closed/done show this works: separate names can share a hue today and be split later without touching call sites.
  • Keep it off the severity axis (info/success/warning/danger). Live is a lifecycle state like Primer's open/done.
  • Make the hue distinct from the one danger uses (a magenta-leaning crimson like ATP's reads as "different red", but test that it survives colour-blind simulation), and always add a non-colour cue: the word LIVE/진행 중 and a pulsing dot (Carbon/Apple both require a second channel).
  • Do not use the brand/primary blue for live (Toss blue means "primary action").
  • Loss/"lost" should not be red at all (ATP uses a check mark on the winner), or it adds a third meaning to red.

3.3 Avoiding one hue = two meanings; enforcing it ​

  • Name tokens by role, never by hue (Atlassian naming, Apple "defined by purpose"). Palette scales are an internal layer components cannot import.
  • Keep an accent set that is decorative-only and exclude every hue already assigned a role, so court/group identity colours (if ever used) cannot collide with status. UEFA avoids the issue by not colouring groups at all.
  • One component owns the mapping from domain status to role (the repo already routes status through XxxStatusBadge wrappers); a table status -> role -> hue with a unit test that no two roles share a hue unless listed as a deliberate alias (Primer-style).
  • Lint: Atlassian ships ensure-design-token-usage (options color: true, spacing: true), no-deprecated-design-token-usage, no-unsafe-design-token-usage in both ESLint and Stylelint (https://atlassian.design/components/stylelint-design-system/configuration-and-setup [V]); Primer ships primer/colors stylelint rule enforcing colour variables (https://github.com/primer/stylelint-config [V], rule detail thin). Equivalent for TwoMore [R]: ban raw palette tokens ($red5 etc.) outside the token package, allow only role tokens in features, and add a contrast test (Carbon: >= 3:1 for indicators).
  • Consolidation rule: when several statuses roll up into one badge, show the highest-attention one (Carbon).

4. Polished settings-style forms: concrete mechanics ​

4.1 Grouped list rows (label left, value right) ​

  • Toss TDS ListRow (https://tossmini-docs.toss.im/tds-mobile/components/ListRow/list-row-overview/ and .../list-row-components/) [V]: three slots, left / contents / right. Left: AssetIcon, AssetImage, AssetLottie, AssetText ("오늘"-style short text). Contents: ListRow.Texts with 1-row (A-C), 2-row (A-F), 3-row (A-F). Right: right-aligned Right1Row / Right2Row types for "date, time, price, status", plus IconButton, Switch, Badge, and withArrow. Dividers: border="indented" (default) or none to make rows read as one connected block. Padding: vertical small 8 / medium 12 (default) / large 16 / xlarge 24 px; horizontal small 20 / medium 24 px. disabled with two styles. The docs do not show a literal "label left / value right" sample; it is the 1RowTypeA + Right1RowTypeA combination (inference).
  • Apple lists (https://developer.apple.com/tutorials/data/design/human-interface-guidelines/lists-and-tables.json) [V]: grouped style "uses headers, footers, and additional space to separate groups"; iOS Settings is "a hierarchy of lists"; use a disclosure indicator to drill in; option rows highlight briefly then show a checkmark; keep item text succinct.
  • Material settings pattern (https://m1.material.io/patterns/settings.html) [V]: use dividers between groups, "avoid using dividers between each individual setting"; section titles optional but specific (no "Other"/"Miscellaneous"); 15+ settings go to a subscreen; switch/checkbox rows may carry a single-line description; for non-switch rows secondary text "should only show the current status"; avoid generic verbs ("Set", "Change", "Manage") in labels.

4.2 Inline stepper in a row ​

4.3 Switch vs checkbox vs segmented vs chips ​

4.4 Label above an input vs in a row ​

4.5 Section heads and disclosure of advanced options ​

4.6 People picker with selected chips ​

4.7 Sticky bottom CTA ​

4.7b Putting it together for the five plain forms [R] ​

  • Match rules: group heads ("경기 방식", "점수", "고급 설정"); steppers (sets, games, tie-break points) as NumericSpinner rows; yes/no rules as switch rows; 2-4 mutually exclusive options as a SegmentedControl in the row below its label; chips only for multi-select; one disclosure row for rare rules; sticky Single CTA.
  • Add venue: sequential fields with sustain labels above (name, address) + choice rows (indoor/outdoor, surface) as drill-in rows with a check-marked list; court count as NumericSpinner.
  • Create group chat: chip strip + search + checklist; CTA carries the count.
  • Edit profile: avatar row at top; text fields with labels above; preference rows below.
  • Bank account: bank picker as a drill-in row; account number with a number Keypad and fixedAboveKeyboard CTA; hasError + help for validation.

5. Why profile pages lead with an identity block; how application review pages show the applicant ​

5.1 Why identity first ​

  • Strava official help (https://support.strava.com/hc/en-us/articles/216917697-Your-Strava-Profile-Page) [V]: under the name, others see subscriber status, location and primary club (privacy-dependent); below that a calendar widget, trophies, photos; on someone else's profile the sidebar shows their essential stats plus a comparison with you. Identity + affiliation first, then a summary of activity, then relationship framing.
  • LinkedIn help (https://www.linkedin.com/help/billing/answer/38579) [V]: the Introduction section groups Name, photo, background photo, Headline, Current position, Education, Location, Contact info; some are auto-filled from lower sections. One block carries the canonical "who".
  • Daangn (https://www.daangn.com/kr/trust/ and FAQ https://www.daangn.com/wv/faqs/135) [V]: 매너온도 starts at 36.5, tops out at 99, and is "당근 내의 다양한 활동을 종합하여"; the page tells buyers to check it before trading and, if it is low, to check 가입일, 인증횟수, 재거래희망률, 후기 on the profile. So the profile leads with one compressed trust number and offers the supporting evidence beneath.
  • Trust research (BlaBlaCar, 18,289 members in 11 countries): 88% highly trust a member with a full digital profile versus 58% for colleagues and 42% for neighbours; bios, contact details and links to other profiles are cited as the trust-building elements (https://www.wbs.ac.uk/news/why-people-trust-strangers-using-uber-or-airbnb) [V]. A secondary write-up ranks photo < verified contact < positive ratings (https://legacy.ouishare.net/magazine/blablacar-community-members-trust-each-other-more-than-their-neighboors) [S].
  • Toss: I found no primary documentation of Toss's profile layout.
  • Synthesis [R]: a profile answers "who is this to me, and can I trust them?" before anything else. The identity card (photo, name, affiliation, tier/rating, tenure) is the one place that question is answered; the rest of the page is evidence for it. For a club app the card should hold: face, display name, club role, trust tier, join date, and one relational line (mutual clubs / played together), mirroring Strava's "comparison to you".

5.2 Membership-application review ​

  • Facebook Groups [V]: Manage > Member requests; each row shows the requester's name with Approve / Decline beside it; filters at the top (examples: invited, gender, location); Approve all / Decline all; decline-with-feedback via the overflow; up to 3 join questions whose answers are "a reference point for approval"; Admin Assist can auto-approve/decline on criteria (e.g. answered the questions, decline brand-new accounts). The help pages do not say what profile details a request row shows (https://www.facebook.com/help/455972334752473 ; https://www.facebook.com/help/android-app/214260548594688).
  • Meetup: organisers can review the join-question answers from the group profile page; MemberUp shows answers in request details before approval [S] (https://help.meetup.com/hc/en-us/articles/360002878091 ; https://help.memberup.com/en/articles/12707501-how-to-accept-membership-requests).
  • Common shape: applicant identity first, the applicant's own answers second, the decision controls attached to the same row/card, bulk actions and filters at list level, optional reason on decline.
  • Recommendation [R]: render the applicant with the same profile identity card (so tier, tenure and shared clubs are visible), their 신청 메시지/answers below it, and a Toss-style BottomCTA.Double (secondary "거절" + primary "승인") attached to that screen; a list screen with one-tap rows opening that detail; a count badge on the entry (see section 2).

Gaps and caveats ​

  • Grand Slam sites/apps are JS-heavy and off-season; I could not verify their mobile tab bars. ATP and UEFA were verified live in the browser pane. ATP's LIVE pill hex was not extracted.
  • Playtomic, Meetup help pages returned 403; their facts are from search excerpts.
  • Material 3's own role pages (m3.material.io) are client-rendered and could not be fetched; M3 claims rest on the Android Material Components doc and the Google codelab.
  • Toss publishes no hue-meaning rules and no list/settings design principles; everything Toss-specific above is component API documentation, not a stated philosophy.
  • tournamentsoftware.com pages were blocked by a cookie wall; I used Sportradar, Universal Tennis and Tournated documentation for the same concepts.
  • Recommendation items marked [R] are mine and are not claims any source makes.

B. Match generation: decided, built, drawn ​

Read-only research, 2026-10-30. Paths are relative to the repo root unless noted. "Board" means docs/twomore_design_system/wireframes/flat-v2-*.jsx. Every claim below was read in the file cited; guesses are marked Q (question for the lead).


0. The picture in six lines ​

  1. The canon decided one pipeline, 대상→구성→생성→검토→확정→시작→기록, read through a six-axis GenerationPolicy. Only recording varies in code (from session.isFriendly); every other axis is the default (docs/canon/match-generation.md:9-30).
  2. The code has a full, working host flow: a chooser (format-chooser-sheet.tsx) → the AddRoundSheet (add-round-sheet.tsx: config → 미리보기 → 저장, plus a manual composer) → the 대진표 (match-board-screen.tsx) → 경기 시작 → LIVE-tab 라운드 시작. Rounds are generated one at a time and committed as scheduled matches by the RPC generate_session_round.
  3. The boards redrew the 대진표 (organizer's console) and the 스코어카드, and drew one generation Sheet: mb-o-draw, four doubles rows and one action. They did not draw the 미리보기/검토 step, the singles/KDK/순위매칭전/코트승급전 rows, 라운드 추가, 팀 바꾸기, the 대진 다시 짜기 confirm, the pool/arrival view, any tournament set-up, or the 코트승급전 ladder tab.
  4. The owner decided (batch 4, 2026-10-29) that a tournament is stages of the session's own matches (예선 리그 groups, then 본선), so /tournament-board retires. This is decided and drawn for the console and the scorecard, and not built; the set-up (creation) step has no board at all.
  5. Three board ideas have no code under them: 대진 다시 짜기 (redo before round 1), 시작할 수 있는 경기 / 대기 (held matches in sequence), and 늦게 온 선수 추가 (a latecomer Sheet). The boards' 4라운드 시작 band also assumes the next round already exists, while the code generates the next round only when the host asks.
  6. Section 5 lists 12 questions that only the lead or the owner can settle. Questions 1 to 4 change the page count.

1. What is decided ​

1.1 Canon: docs/canon/match-generation.md ​

  • Pipeline (:9-15): ① 대상 Pool → ② 구성 Configure → ③ 생성 Generate → ④ 검토 Review → ⑤ 확정 Commit → ⑥ 시작 Activate → ⑦ 기록 Record. "These stages never change. What varies is who/when triggers them and what policy governs each."
  • Six axes (:17-28, type in packages/app/src/domain/entities/generation-policy.entity.ts):
AxisValuesActive in code
1 timingahead · at_start · duringat_start
2 triggermanual · auto_on_confirm · auto_on_deadlinemanual
3 lifecyclelive · frozen · rollingfrozen; rolling only as the INCREMENTAL_ROUND_STYLE_IDS fact below
4 poolModeall_confirmed · host_selected · opt_inall_confirmed
5 recordingrecorded · casualderived: isFriendly ? 'casual' : 'recorded'
6 compositionrotation · manual · seededrotation (the manual composer and tournaments bypass the axis, see 2.8)
+ styleIda registry id or nullonly americano/mexicano/king_of_court are persisted, on sessions.generation_style_id
+ lockAtdeadline · session_start · manualn/a while lifecycle is frozen

DEFAULT_GENERATION_POLICY = manual, frozen, all_confirmed, rotation, recorded, at_start. deriveGenerationPolicy(session) swaps only recording.

  • Use cases are coordinates (:32-43): 시작 시 생성 shipped (S2a); 사전 자동 라이브 (UC1); 진행 중 부분 생성 (UC3: host-selected pool + opt-out); 킹오브코트 · 래더 shipped (P6) through Session.generationStyleId → STRATEGY_REGISTRY, bypassing the trigger and composition axes; 시드 브라켓 (tournament); 기록·캐주얼 혼합 (per-match nature).
  • Activation order (:45-50): 1 Pool + Recording (UC3, "highest-value, lowest-cost, mostly UI"), 2 recurrence template, 3 Timing + Trigger (UC1), 4 seeded brackets.
  • Session completion (:54-56): sessions.auto_complete_matches (00683). generate_session_round sets it false in the same transaction, so a session whose rounds were committed through it ends only by the host (or the elapsed-session grace). INCREMENTAL_ROUND_STYLE_IDS = king_of_court, mexicano, americano is mirrored in SQL by a one-time backfill.
  • Copy (:58-60): preset and strategy copy lives only in the i18n registry, keyed by id (matchBoard.presetLabel / presetDescription, tournaments.strategyLabel / strategyDescription), read through packages/app/src/presentation/utils/matchmaking-copy.ts.
  • Banner at :5: "Decided, not yet built (owner review 48, batch 4, 2026-10-29): a tournament becomes stages of its session's own matches (예선 리그 groups, then a 본선 knockout), so its matches live in matches with a stage and a group … /tournament-board and its winner picks retire."

1.2 Decision log and owner-review syntheses ​

  • Naming (docs/design/terminology-guide.md:24): the CTA is 대진 짜기, replacing 경기 생성/경기 만들기. :45: 대진 방식 = how the draw is built (KOTC, Mexicano), "never 경기 방식, which is the session style". The code still headlines the chooser and the picker with common.sessionStyle = 경기 방식 (see 2.1, 2.2).
  • Page name (decision-log-autonomous-2026-09-17.md:584): the match board is 대진표, not 경기 보드.
  • Host CTA by play state (:530): after 마감 with no draw the band shows 대진 짜기 (it opens the 대진표 and its Sheet); a ready draw shows 경기 시작; play in progress shows 대진표 (the label became 대진표 보기, flat-v2-detail.jsx:253).
  • One intent, one tap (:587): "The session's 대진 짜기 opens the 대진표 with the method Sheet already up." Flow 6 is 5 taps (flat-v2-ia-and-flows-2026-10-09.md:553-577, drawn in flat-v2-flows.jsx:550-611). The IA doc's G2 also asked for "preselected to the club's last or default method" and options behind an ExpandToggle (ia-and-flows:563). The boards kept the preselection wording (flows.jsx:563) and dropped the ExpandToggle.
  • U-16 / U-16a / U-16b (usability-audit-2026-09-16.md:70): 경기 시작 needs a draw (canStartMatches), and the empty state explains 대진 짜기 ("경기 상대와 코트를 자동으로 배정해요").
  • Arrival window (owner review 46, M26; flat-v2-list-a.jsx:5722-5725, flat-v2-detail.jsx:10691): a player's planned hours are set on 일정 상세; "the host reads each window on 참석 확인 and 대진 짜기 reads it too", "reads both ends" (start and end).
  • Check-in (owner review 48; flat-v2-detail.jsx:10691): players stamp their own arrival so the host is not the only one doing it.
  • Tournament structure (owner review 48, batch 4; docs/design/flat-v2-component-library-2026-10-09.md "Live pages", ~:3691-3703; docs/design/research/owner-review-48-research-2026-10-28.md § 8): a tournament is ordered stages → groups → rounds → matches; a club session is one stage, one group, N rounds. 예선 리그 = groups of four pairs, a round robin of three rounds; then 본선 = 8강, 4강, 결승. 전적매칭 (swiss) and 전원부활전 (compass) are stages of rounds. No 3·4위전 (공동 3위). A group ranks 승수 → 승자승 → 게임 득실. The scope is one Choice ▾ in the first head (A조 ▾ … 본선). The 본선 is made behind one Dialog: 조 1·2위 8팀으로 8강, crossing 1A–2B, 1C–2D, 1B–2A, 1D–2C (flat-v2-detail.jsx:10761). "Replaces in code" (library): TournamentBoardScreen and /tournament-board; tournament matches move into matches with a stage and a group; get-default-tab.ts opens LIVE first; the round labels become 결승, 4강, N강.
  • Organizer's console (library "대진표, checked against the research's must-haves"; flat-v2-detail.jsx:10751-10773): a court queue where "a court that frees takes the next one and pings its players, so there is no button for it"; a 할 일 group (점수 입력 전, 검토, 시간 지난 경기); latecomer = one ⋯ row + one Sheet; "the next round is one press at the bottom"; re-seeding (대진 다시 짜기) allowed before round 1 only; held matches in sequence.
  • Research § 8 conclusion (research/owner-review-48-research-2026-10-28.md:147+): what the code held in 10-29: a tournament was a separate tournaments record whose matches never reached matches; TournamentBoardScreen showed flat standings and the current round's winner picks only; no court queue; late arrival only through AddRound's 선수별 도착; the submit RPC accepted walkover/abandoned with no UI.

2. What is implemented (code, today) ​

2.1 Entry points (who sees what, where) ​

EntryFile:lineWhoShown whenCopy
대진 방식 선택 row (a NavRow, in the 참석 section of 일정 상세) → FormatChooserSheetpackages/features/sessions/src/session-detail/sections/participation-section.tsx:250-258; gate use-generate-round-inputs.ts:143-147host OR canManageSchedule OR canManageMatchesstatus open, locked or in_progresscommon.chooseDrawFormat = "대진 방식 선택" (ko/common.ts:525)
토너먼트 이어보기 chipparticipation-section.tsx:238-246; format-chooser-sheet.tsx:403-445anyone who sees the sectionany session status while a tournament is setup/in_progress{방식} {n}라운드 진행 중 + "이어보기" (ko/tournaments.ts)
Host band (locked)session-detail/actions/primary-action.tsx:301-328hoststatus locked"경기 시작" (+ Dialog). No canStartMatches check here, so a host can start a locked session with no draw from 일정 상세. The 대진표's own button does check it.
Host band (in progress)primary-action.tsx:356-369hostin_progresscommon.matchBoard = "대진표"
Host band (open)primary-action.tsx:81-95hostopen"초대하기" only
Host ⋯ menusession-detail/actions/host-menu-actions.ts:51-125host/admin참석자 관리, 경기 규칙, 호스트 관리, 참석 확인, 경기 종료, 일정 취소, 일정 삭제. No 대진 door.
Club 관리 → operationspackages/features/clubs/src/settings/operations-section.tsx:93club managersnext managed sessionpushes routes.matchBoard
Push signal.matchup.publishedko/signals.ts:156-157; signal-routes.ts:100each player in a new matchon INSERT into matches (trigger signals_match_insert_trigger, 00109; function public.signals_on_match_insert, hardened in 00525), dedup key matchup_published:{session}:{player}title "대진표가 올라왔어요", body "{n}번 코트 배정"; host taps route to the 대진표
대진표 empty state CTAmatch-board/sections/match-list.tsx:51-64whoever opens the boardno matchesEmptyState "아직 대진이 없어요" / "경기 상대와 코트를 자동으로 배정해요" / button "대진 짜기"
대진표 "라운드 추가" pillmatch-board/sections/round-actions-row.tsx:63-74whoever opens the boardcanAddRound = isOpen || isLive (use-match-board-data.ts:318)"라운드 추가"
KOTC ladder "다음 라운드 시작"scorecard/king-of-court-ladder.tsx:225-228session host (or developer mode)active round fully scoredkotcLadder.nextRoundCta = "다음 라운드 시작"

Gate gap worth knowing (Q11): the route apps/mobile/app/match-board.tsx has no role guard, and canAddRound, canEditLiveRound and the MatchCard 교체 button (match-card.tsx:357-368, any scheduled match) are not role-gated on the client. The server is: generate_session_round accepts only the session creator or a club owner/admin/match_director (supabase/migrations/00683_competition_completion_and_live_substitution.sql:98-130, error "Not authorized to generate rounds"). The boards say 대진표 is "the host and officials only" (flat-v2-detail.jsx:10751).

2.2 FormatChooserSheet (the pure picker) ​

packages/features/sessions/src/format-chooser-sheet.tsx. ModalPanel with the title common.sessionStyle = "경기 방식" (note the term clash, 1.2), a meta line "코트 {n}면" / "참석 {n}명" (common.courtCountText, common.attendingCount), then two groups:

  • 라운드 방식 (matchBoard.formatChooserRoundGroupLabel): the format's presets (2.3), then the registry's round strategies other than round_robin (:119-130): KDK 복식/단식 (badge "개인전"), 순위매칭전 (badge "개인전"), 코트승급전 (badge "NEW"; if under 2 courts or 8 players the row is disabled with the warning badge "코트 2면, 8명 이상"; floor constants add-round/types.ts:63-64).
  • 토너먼트 (common.natureTournament): the four tournament strategies, shown only when canCreateTournament = host only AND tournament entitlement on AND session open/locked AND no active tournament (use-generate-round-inputs.ts:152-158). 전원부활전 is disabled below 16 entrants with the badge "16명 이상" (format-chooser-sheet.tsx:66).
  • CTA (dynamic, matchBoard.formatChooserCommitRound): "{선택한 방식}{으로/로} 대진 짜기" for rounds; tournaments.formatChooserCommitTournament: "{방식}{으로/로} 토너먼트 만들기" for tournaments. It re-seeds to the first preset every time it opens (:155-158) and never reads the session's saved 방식.

2.3 The 방식 list: rotation presets + registry styles (Korean labels, descriptions) ​

Presets: packages/app/src/domain/rules/rotation-presets.ts (carry only id + RotationConfig); copy: packages/app/src/config/i18n/ko/sessions.ts:305-343. presetsForSessionFormat: singles → the 3 singles presets, every other format → the 4 doubles presets (mixed_doubles uses them with requireMixedGender, applySessionFormat).

idLabel (ko)Description (ko, as shipped)What it does (config)
social소셜 복식매 게임마다 파트너가 바뀌는 KDK 방식partnerMode: rotate_all, no ELO balance
competitive실력별 복식매 게임마다 파트너가 바뀌는 KDK 방식skill_balanced, balanceByElo, tolerance 150
fixed_doubles고정 복식매 게임마다 파트너가 바뀌는 KDK 방식fixed_pairs, fair-rotation bench
random랜덤 복식매 게임마다 파트너가 바뀌는 KDK 방식random, no repeat-partner avoidance
singles_social소셜 단식다양한 상대와 경기하며 실력 향상rotate_all singles
singles_competitive실력별 단식실력이 비슷한 상대와 치열한 단식 경기ELO-balanced singles
singles_random랜덤 단식랜덤 상대 배정으로 다양한 경험random singles
(registry) americanoKDK 복식 / KDK 단식 (styleAmericanoName(format); bare "KDK" if format unknown)복식: 매 게임 파트너 교체, 점수는 개인 · 단식: 매 경기 상대 교체, 점수는 개인rotate_all + individual points (americano.strategy.ts)
(registry) mexicano순위매칭전점수 순위로 다음 대진 결정rank-adaptive from pointsMap (family: rank_adaptive)
(registry) king_of_court코트승급전 (common.courtClimb)이기면 윗코트, 지면 아랫코트ladder; needs ≥2 courts and ≥8 players; sets matchRules = kingOfCourtMatchRules() (draws allowed) when first used

Findings:

  • The four doubles descriptions are one identical string (owner-locked "naming v3", comment ko/sessions.ts:314-325). It is factually wrong under 고정 복식 (partners do not change). The board's draw Sheet drew four distinct lines instead (flat-v2-detail.jsx:4042-4058): 소셜 복식 "게임마다 파트너가 바뀌어요" · 실력별 복식 "비슷한 실력끼리 만나요" · 고정 복식 "정해진 짝으로 끝까지 쳐요" · 랜덤 복식 "라운드마다 무작위로 짝을 지어요". Q5.
  • 고정 복식 has no way to choose the pairs. fixed-pairs.ts pairs adjacent players in the playing list (packages/app/src/domain/rules/matchmaking/strategies/rotation/fixed-pairs.ts, the for (i += 2) loop), i.e. RSVP order, recomputed each round from whoever is playing. The board copy "정해진 짝" implies chosen pairs. Q6.
  • The preset choice is not saved. selectedStyle is local hook state (add-round/use-round-preview.ts:115-130; add-round-sheet.tsx:18-28 lists it as a DEFERRED gap). Only the three score-driven ids are written to sessions.generation_style_id, at commit (add-round/use-manual-round.ts:84-105). currentGenerationStyleId is passed to the sheet but never used to preselect. So "preselected to the club's last method" (flows board :563) has nothing behind it for the four doubles presets.
  • Other flows in the same registry (kind tournament): swiss 전적매칭 토너먼트 ("전적이 비슷한 상대와 매칭되고 탈락 없이 진행돼요"), bracket 토너먼트 ("한 번 지면 탈락"), compass 전원부활전 ("지더라도 탈락 없이 전원 끝까지 경기해요"), round_robin_groups 조별리그 ("조별예선 → 본선 토너먼트") (ko/tournaments.ts strategyLabel/strategyDescription). recommendStrategy(n): ≤8 → round_robin_groups; 9–16 → swiss; 17+ → bracket (matchmaking/recommend-format.ts).

2.4 AddRoundSheet: the generation flow (steps and states) ​

packages/features/sessions/src/add-round-sheet.tsx (a ModalPanel; three steps, add-round/types.ts:83 SheetStep = 'config' | 'preview' | 'manual'). Mounted three times: session detail (session-detail-overlays.tsx:164-195, on commit it closes and pushes routes.matchBoard), the 대진표 (match-board/sections/match-board-overlays.tsx:79-106, on commit it selects the new round and refetches), and the KOTC ladder (king-of-court-ladder.tsx, which chains useStartNextRound onto the new round, :236-247).

Step config (add-round-sheet.tsx:189-257), top to bottom:

  1. Title 대진 짜기 (matchBoard.makeMatchesTitle).
  2. "일정에서 설정됨" + three read-only chips: the format label (getFormatLabel), the court count, "참석 {n}명" (common.attendingCount(playerCount)). All are set on the 일정; none is editable here. Copy slip: the court chip calls t().common.court(courtCount) (add-round-sheet.tsx:205), which is the ordinal "{n}번 코트", so two courts read "2번 코트"; the chooser and the board use common.courtCountText = "코트 2면".
  3. 선수별 도착 roster (add-round/availability-roster.tsx), hidden when there are no players. Each row is a PersonRow with a tag: "도착" · "{n}라운드부터" · "{HH:mm} ({n}라운드부터)" (warning tone when late). For the host the tag is a button that opens an inline card of round chips ("도착", then the next 5 rounds) plus 시간으로 (a native time picker, 5-minute steps). Hint "탭해서 도착 라운드 설정 (총무 권한)" for the host; when anyone is late, "도착하면 바로 다음 라운드부터 합류해요". Editing is host-only (isHost = the literal session creator; RSVP policy rsvps_host_update, 00426); it writes rsvps.available_from_round / arrives_at optimistically, rolling back on error (use-arrival-availability.ts:147-163). The RSVP has only a start side (availableFromRound, arrivesAt), no departure, so "reads both ends" (the board decision) has no field to read (Q9).
  4. 경기 가능성 (add-round/feasibility-panel.tsx): three tiles ("사용 코트" ring, "예상 경기 수" "{min}~{max}경기", "지각 여유" "{n}명") and a horizontal strip "라운드별 코트 현황 / 선수 도착에 따라 달라져요" of 6 rounds (a round that cannot field a court reads "불가"; "+{n}" bench), then warning dots: "{n}명 더 필요해요" or "{round}라운드: {n}명 더 필요해요" (computeAttendanceViability, matchmaking/feasibility.ts).
  5. 방식 picker (add-round/style-picker.tsx), header common.sessionStyle = "경기 방식": the format's presets, then 아메리카노 as "KDK 복식/단식", 순위매칭전, and 코트승급전 (hidden below 2 courts or 8 players, :67-76). Selected row = a radio-style StyleOptionRow.
  6. Two buttons: primary 대진 미리보기 (matchBoard.previewDraw; pending label "만드는 중..."), and neutral 수동 구성.

Step preview (:258-306), title "대진 미리보기": per round a Card per court ("{n}번 코트" + the shared MatchScoreboard with the ELO tug-of-war bar), then a bench line "이번 판 휴식" with names or "전원 출전". Footer: primary "대진 저장" (matchBoard.saveManual; pending "저장 중..."; the file header and use-round-preview.ts:74 comment call this "경기 시작", the label is not), pill 다시 생성 (re-roll; the generator shuffles ties), pill 뒤로 (back to config). There is no per-court edit, swap or lock in the preview; to change names the host commits and then uses 교체 on the board (2.7), or goes back to 수동 구성.

Step manual (:307-371), title "수동 대진 구성 (코트 {n}면)": per court, slot rows "팀 1/팀 2" × "선수 1/2" (MetaSegments) open an inline picker of unplaced players ("예비 선택" label; "비우기"; "비어있음"/"빈 자리" for empty); buttons 자동 재배정 and 초기화; primary 대진 저장 (disabled until every slot is filled); 뒤로.

Generate logic (add-round/use-round-preview.ts:173-269):

  • Minimum players: doubles 4, singles 2. Below it the sheet closes and a dialog opens: title "알림" (common.notifications), message "최소 {n}명이 필요해요" (common.minPlayersRequired), button "확인".
  • The config goes through applySessionFormat (혼복 → requireMixedGender; singles → avoidRepeatOpponents). generateAllRounds(players, courtCount, 1, config, {genderMap, eloSnapshot, completedRounds, startRoundNumber: nextRoundNumber, availabilityMap, styleId, pointsMap, lastRoundCourts}) runs in computeAsync, one round per click, with full cross-round history rebuilt from played matches (buildRoundAssignmentsFromMatches).
  • Not enough courts / odd count: activeCourts = min(courtCount, floor(players/k)) with k = 4 doubles, 2 singles (rotation.rules.ts:~220, resolvePool). Everyone beyond the courts is the bench, chosen fairly (fewest rests first, earliest-rested next, then shuffled) and shown as "이번 판 휴식". A late arriver (fromRound > roundNumber) is benched without using a fairness slot. Too few players for even one court → no court is produced; the preview would be empty (only the min-players guard above prevents the sub-4 case).
  • Errors: any generator error closes the sheet and opens a dialog "문제가 생겼어요" with err.message or "라운드를 만들지 못했어요".
  • Commit (:271-316, add-round/use-manual-round.ts:74-122): builds scheduled match inputs (all scores null, nature: null) → useGenerateRound → matches.createBatch(…, sessionId) → RPC generate_session_round (00683). The RPC enforces: auth, creator-or-admin, session not terminal, 1–64 matches, round_number = max + 1, valid distinct slots, every player has a confirmed RSVP; it sets auto_complete_matches = false. Success toast "경기 배정이 완료됐어요" (sessions.toast.matchesGenerated). A score-driven pick also writes generationStyleId (+ KOTC matchRules). Errors: the settle-before-play message "정산이 끝나지 않은 참석자가 있어요. 정산 확인 후 다시 시도해 주세요." (matchBoard.settleBeforePlayError) or the generic dialog "라운드를 만들지 못했어요. 다시 시도해 주세요." (errors.roundGenerateFailed).
  • "Regenerate / redo the draw before round 1" does not exist as a flow. generateRoundMutation has an unused replaceExisting flag that calls matches.deleteBySession (generate-round.mutation.ts:12-22); nothing sets it. The 다시 생성 button only re-rolls a preview that has not been saved. RLS matches_delete (00011) lets club admins delete scheduled matches only; a 번개 host who is not a club admin is not covered (Q for 대진 다시 짜기).

2.5 Pool: who is in the draw ​

  • eligiblePlayerIds = confirmed RSVPs minus unsettled fee payers (host exempt): partitionSettledMatchPool (use-generate-round-inputs.ts:54-63, use-match-board-data.ts:246-255). Free sessions are unaffected.
  • The excluded count shows as UnsettledPoolNotice on the 대진표 (unsettled-pool-notice.tsx): a warning Banner "정산 대기 {n}명이에요. 확정 후 배정할 수 있어요." with the action "정산 확인" → routes.sessionPayments.
  • The tournament modal is fed confirmedPlayerIds (not the settled pool): session-detail-overlays.tsx:136. A fee session's unsettled payers can enter a tournament.
  • poolMode host_selected / opt_in is unbuilt. The only per-player control is the host's arrival round.

2.6 The 대진표 (match-board-screen.tsx), start and rounds ​

  • Header: SessionLiveAnchor, TypeHero (title = venue or the generated session title; facts 📅 date, 👥 "참석 {n}명", 🎾 format, 🏷 "코트 {n}면"; a CapacityRing completed/total once matches exist).
  • States: error → SessionErrorGate; cold offline → SessionOfflineGate; loading → skeleton; no row → NotFoundState ("일정을 찾을 수 없어요" family); then the three-way list: no matches (Well + EmptyState "아직 대진이 없어요" / "경기 상대와 코트를 자동으로 배정해요" + "대진 짜기"), round has no matches ("이 라운드에 경기가 없어요" / "경기 배정 후 점수를 입력할 수 있어요"), or MatchCards sorted by court.
  • Round row (round-actions-row.tsx): pill buttons "{n}라운드" per round; "라운드 추가" (neutral pill, canAddRound = status draft/open/locked/in_progress); "진행 중 라운드 조정" (canEditLiveRound = live and the round has a not-finished match).
  • 경기 시작 (start-session-button.tsx, in a BottomCtaBand): rendered only when the session is startable (draft/open/locked) and canManageLifecycle (creator or schedule/match manager); disabled when no scheduled match exists (canStartMatches, match-lifecycle.rules.ts:26-38), with the caption "대진을 먼저 확정해 주세요". Press → Dialog "경기를 시작할까요?" / "시작하면 참석 명단이 확정되고 되돌릴 수 없어요." with 취소 / 경기 시작 → useStartSession (offline-queued) flips only the session to in_progress. It does not start any match.
  • All done: banner "모든 경기가 끝났어요" / "일정을 종료할까요?" for a live session once every match is completed/cancelled (allDone), button → the end-session flow (a recorded session with owed scores is held: "점수를 먼저 입력해 주세요" / "점수를 입력하지 않은 경기가 {n}개 있어요…" / "대진표로 이동").
  • Round start is on the scorecard, not on the 대진표: the LIVE tab shows an admin callout "{n}라운드 시작 준비됨" / "{count}개 경기를 시작할 수 있어요" and the button "시작" → Dialog "{n}라운드 시작" / "{count}개 경기를 동시에 시작해요" → useStartNextRound (bulk start_match RPC for the lowest scheduled round above the last started one). Gate: club manager OR the session host, AND no match in progress (scorecard/live-tab/use-live-tab-state.tsx:366-407). getDefaultTab: KOTC session with matches → ladder; anything in_progress → live; score-driven with a result → standings; any completed → rounds; else draws (scorecard/get-default-tab.ts).
  • Next round = a new manual round. After a round finishes the host opens "라운드 추가" → config → 미리보기 → 대진 저장, then goes to the scorecard LIVE tab to start it. Rolling styles (americano/mexicano/king_of_court) need the previous round's scores for pointsMap / lastRoundCourts (use-match-board-data.ts:364-380).

2.7 Review and edit of a committed draw ​

  • 교체 (match-card.tsx:357-368, swap-sheet.tsx): a tertiary pill on every scheduled MatchCard. Sheet "선수 교체": four slot rows ("팀 1 · 선수 1" …), tap a slot → picker ("예비 선택", pool = settled confirmed players minus those placed, plus "비우기"), "팀 1 ↔ 팀 2 순서 바꾸기"/"팀 교체" (matchBoard.swapTeams), "교체 영향" card per changed slot (analyzeSubstitution): "{이름} {a}경기 → {b}경기"; tags "문제 없음", "같은 라운드 중복", "미확정 참석자", "파트너 중복: {이름}", "상대 중복: {이름}"; 저장 / 취소; error "저장에 실패했어요" or the settle-before-play message. Writes via matches.updateMatchTeams.
  • 진행 중 라운드 조정 (live-round-editor-sheet.tsx): one Sheet for the whole active round: "현재 라운드 조정" / "코트와 벤치를 한 번에 바꾸고 바로 반영해 보세요", all courts' slots, "이번 라운드 쉬는 인원" bench list ("현재 쉬는 인원이 없어요"), locked rows "진행 중이거나 완료되거나 취소된 경기는 수정할 수 없어요", validation "진행 중인 모든 코트의 인원을 채운 뒤 저장해 주세요", error "현재 라운드 저장에 실패했어요".
  • There is no code for 대진 다시 짜기, 팀 바꾸기 as a board ⋯ row, or 늦게 온 선수 추가 as a board Sheet (the arrival roster inside AddRound is the only late-arrival control).

2.8 King of the Court, and the tournament flow ​

  • 코트승급전 ladder tab (scorecard/king-of-court-ladder.tsx, king-of-court/court-card.tsx): court-primary live ladder (court 1 = 킹코트), chip "👑 코트승급전 개인전", "내 코트", an elapsed clock, movement badges "▲ 승격" / "▼ 강등" / "방어" / "유지", tiebreak reason "가위바위보", status "{n}라운드 · 점수 입력" while scoring, host CTA "다음 라운드 시작" when the active round is fully scored (opens the AddRoundSheet preselected to KOTC, then starts the new round on success), empty "아직 라운드가 없어요". The board library marks this tab "undrawn".
  • Tournament creation (create-tournament-modal.tsx): ModalPanel. Step select (title "대진 방식 선택"; rows with a "추천" badge on recommendStrategy's pick; empty "참가 확정된 인원이 없어요"; 다음/취소) is skipped when the chooser preselected a strategy; step setup (title "토너먼트 설정" + the format name; link "‹ 대진 방식 다시 선택"): for bracket with ≥9 entrants only, a one-line summary "{n}명씩 조를 나누고, 상위 {m}명이 진출해요" / "…전원이 진출해요" behind "설정 더보기/접기" → 조별 인원 (stepper 3–8), 진출 방식 (조별 상위 N명 / 전원 진출 (예선 성적 시드)), 진출 인원 (1–3), 조 편성 (실력 균형 / 무작위); a read-only 경기 시간 row ("끝까지" or "{n}분 제한", hint "경기 규칙에서 변경" only if the viewer can edit rules); CTA "토너먼트 시작", 취소. Doubles entrants are paired strong with weak (pairForDoubles); an odd player out silently sits out (:122-136).
  • Tournament board (tournament-board-screen.tsx, soon retired): host/creator only controls; flat standings ("승-무-패", "부흐홀츠 {n}" for swiss), the current round's winner picks ("승자 선택", plus "무승부" for swiss/round-robin formats), pinned CTA "다음 라운드" → Dialog "다음 라운드로 진행할까요?" / "진행 후에는 이번 라운드 결과를 되돌릴 수 없어요"; ⋯ "토너먼트 취소" → "토너먼트를 취소할까요?" / "지금까지의 대진과 기록이 모두 사라져요. 되돌릴 수 없어요"; no rounds → CTA "라운드 만들기" → "라운드를 다시 만들까요?" / "처음부터"; completed → Well "완료"; missing → "토너먼트를 찾을 수 없어요". Its matches never reach matches (research § 8).

2.9 What members see ​

  • 일정 상세: the 경기 section is hidden until a draw exists (B3, session-detail/sections/matches-section.tsx:58); after that it shows the viewer's own matches (header + scoreboard with the ELO tug-of-war) and the link "전체 대진표 보기 · {n}경기" → the scorecard (session-matchups-card.tsx). A spectator sees only the link. Nothing tells a member "the draw is not made yet".
  • 스코어카드 일정 tab empty: "대진이 아직 배정되지 않았어요" (scorecard.noDraws); LIVE empty "진행 중인 경기가 없어요".
  • Push: "대진표가 올라왔어요 / {n}번 코트 배정" (once per player per session, dedup key).
  • Settings → seeded persona "참석자 뷰" lists the states "대진 대기, 대진 확정(내 경기), 라이브(내 경기), 이번 판 휴식" (ko/settings.ts:449), i.e. the intended member-side states.

3. What the boards draw now (per pipeline stage) ​

Frames: 일정 상세 sd-h-* (flat-v2-detail.jsx:1543-1592), 대진표 mb-* (:4313-4480; sheets/dialogs DtlMbOver :3923-4220), 스코어카드 sc-* (:3120-3360), Flow 6 (flat-v2-flows.jsx:548-611). flat-v2-forms.jsx has nothing on generation except the 경기 유형 Choice set (정기 · 대회 · 리그 · 대항전, :945, :1262) in the create-session 종목 Sheet.

StageVerdictFrames and what they show
① 대상 (pool)Partly drawnmb-unsettled (warning Notice "입금 확인 전 3명", action "참석자 관리", "they are left out of the draw"; :4370, :3487-3493). The 참석 시간 window is decided (list-a :5722) and read by 대진 짜기, but no frame shows the pool, per-player arrival at draw time, or "fewer players than courts / zero courts" (the IA doc's G1 lists both as states to draw, ia-and-flows:561; only unsettled was drawn). The draw Sheet's footer helper "참석 6명 · 코트 2면" is the only pool readout (:4038). mb-o-late (Sheet "늦게 온 선수": Picker Cell 선수 + Choice set "몇 라운드부터" + footer "대진에 넣기") covers a latecomer between rounds, not the pool before round 1.
② 구성 (configure)Partly drawnmb-o-draw (:4027-4074): Sheet "대진 짜기", four radio rows with distinct one-liners (소셜/실력별/고정/랜덤 복식), footer button "대진 짜기" with helper "참석 6명 · 코트 2면". Not drawn: singles presets, KDK 복식/단식, 순위매칭전, 코트승급전 (and its 2-court/8-player floor), tournament methods, 수동 구성, 경기 가능성, "preselected to the club's last method", the format-aware list.
③ 생성 (generate)Partly drawnThe Sheet's one action. Not drawn: the pending state, the below-minimum state, the generic error, and the bench ("이번 판 휴식") readout.
④ 검토 (review)Missing as a step; partly drawn as a pre-start redoNo preview/다시 생성 frame exists. The board model is generate-then-see on the board (mb-draw "Draw ready: rounds listed; 경기 시작 is the one action") with 대진 다시 짜기 as a ⋯ row before round 1 (mb-o-menu-draw, :3936-3951) and 팀 바꾸기 as a ⋯ row (:3938). The Sheet behind 팀 바꾸기 and the Dialog behind 대진 다시 짜기 are not drawn. The code's per-match 교체 Sheet and the live round editor have no board.
⑤ 확정 (commit)Not a stage on the boardsThe code's 대진 저장 is the commit; the boards have no commit step because the Sheet's single action both generates and shows the draw. Whether a draw is "committed" before start (and re-notified on a redo) is Q2.
⑥ 시작 (activate)Drawnsd-h-draw (locked, band 대진 짜기), sd-h-start (band 경기 시작), sd-h-live (band 대진표 보기), mb-draw, mb-o-start (Dialog "경기를 시작할까요?" / "시작하면 참석 명단이 확정되고 되돌릴 수 없어요" — identical to the code), mb-next-round (band "4라운드 시작", Notice states only), mb-held + mb-o-start-ready (:4562-4566, Dialog :4108-4125: "경기 3개를 시작할까요?" / "대기 2경기는 점수가 정해지면 시작할 수 있어요"), mb-done / mb-o-end (일정 끝내기), mb-unscored, mb-ended, mb-loading, mb-offline, mb-error. Built differently or not built: see 4.6.
⑦ 기록 (record)Drawn (out of the missing set)sc-entry-player, sc-live-overtime, sc-entered, sc-review, sc-stopped, sc-void, sc-o-*, mb-own-match, mb-review*, mb-o-entry/typing/tiebreak/stop…, mb-stall, mb-o-run
TournamentDrawn for the console and scorecard; set-up missingConsole: mb-tn (코트 head over live Blocks, 점수 입력 전, 다음 경기 queue), mb-tn-late (warning Notice "A조 1번 코트 경기가 길어져 이후 경기가 20분 늦어져요", queue times move), mb-tn-stage (band "본선 대진 만들기"), mb-o-knockout (Dialog "본선 대진을 만들까요?" / "조 1·2위 8팀으로 8강을 만들어요. 만든 뒤에는 예선 결과를 바꿀 수 없어요"), mb-tn-ko (8강 1번 코트). Scorecard: sc-tn-live, sc-tn-draws, sc-tn-groups (Group table, "1·2위가 본선에 올라요. 같은 승수면 승자승, 게임 득실 순이에요"), sc-tn-bracket, sc-tn-semis, sc-tn-champion. Session page: sd-tournament. Not drawn: choosing the tournament method, the knobs (조 편성, 조별 인원, 진출 인원/방식), the entrant pairing for doubles, how a 대회 session starts.
Rolling stylesMissingNo frame for the 코트승급전 ladder tab (the library calls it "undrawn", ~:3703 "keeps its place after LIVE"). sc-standings (Podium, "score-driven formats only") exists for 순위매칭전/KDK.
Member side of the drawPartly drawnsc-empty ("No draw", first run), sc-ideal/sc-resting (my next match, court, how long the match before it has run), sc-draws/sc-held. The pre-draw state on 일정 상세 and the "draw posted" push have no frame.

Other board rulings that touch generation (so a new page must respect them): a Sheet never sits over a Sheet (flat-v2-detail.jsx:10691, the 도착 Sheet's second step); one coloured action per screen; a band verb is a verb ("대진표 보기"); a Notice states a state, never an instruction; ⋯ holds five rows at most (:10773); the round is the 경기 head's Choice, not page tabs.


4. Proposed page set for the missing stages ​

Scope: only what the code implements or the canon decided; everything else is marked Q. Page names follow the terminology guide (대진 짜기 / 대진표 / 대진 방식 / 라운드).

4.1 Doors (no new page) ​

  • 일정 상세 band: keep the drawn sd-h-draw / sd-h-start / sd-h-live order. Required fix: the band's 경기 시작 must be disabled with a reason until a draw exists (the code's detail band lacks the canStartMatches check, 2.1). Q1: the boards give a host of an open (not full) session only 초대하기, and the 관리 registry (FVK_SESSION_DOORS, flat-v2-kit.jsx) has no 대진 row, so there is no drawn way to 대진 짜기 until the session reads 마감. The code allows it from open.
  • 스코어카드 / 홈 live card: no change.

4.2 Page A: 대진 짜기 Sheet (stages ①②③) — extend mb-o-draw ​

One Sheet over the 대진표, one action. Content, from the code:

  1. Method group (radio rows, one StyleOptionRow shape): by session.format: doubles/혼복 → 소셜 복식, 실력별 복식, 고정 복식, 랜덤 복식; singles → 소셜 단식, 실력별 단식, 랜덤 단식. Then the three registry rows: KDK 복식/단식, 순위매칭전, 코트승급전 (disabled with "코트 2면, 8명 이상" under the floor). Descriptions: board per-preset lines (Q5) vs the shipped shared line.
  2. Readout in the footer helper (already drawn): "참석 {n}명" · "코트 {n}면". Add the pool facts the code can compute: players excluded for an unpaid fee (the Notice already exists on the page behind), players arriving late, and the feasibility warnings "N명 더 필요해요" / "{round}라운드: N명 더 필요해요".
  3. States to draw: ideal; below minimum (code: "최소 4명이 필요해요", singles 2; the Sheet should disable the action rather than close); fewer players than a full court set (bench shown after); unpaid players (Notice behind, already drawn); 코트승급전 under its floor; pending ("만드는 중..."); error ("문제가 생겼어요" / "라운드를 만들지 못했어요"); settle-before-play error; offline.
  4. Axis values this page covers: timing at_start; trigger manual; lifecycle frozen; poolMode all_confirmed (minus unsettled); composition rotation; recording derived from isFriendly (show nothing, or a quiet "친선 · 기록 안 함" fact — Q10); styleId: preset ids are not persisted, the 3 score-driven ids are.
  5. Secondary rows (code has them, boards do not): 선수별 도착 and 수동 구성. Proposal: keep both out of the first screen. 선수별 도착 becomes the pool facts above plus the 참석 시간 list the board already decided; 수동 구성 becomes a row in the ⋯ menu or a second step (Q3-adjacent, see 4.3 note on step 2).

4.3 Page B: 대진 검토 (stage ④) — decide before drawing (Q2) ​

Two coherent choices, both consistent with a board rule:

  • Option 1: keep the code's 미리보기 as step 2 of the same Sheet with a back arrow (precedent: 도착 Sheet's 직접 step, "a Sheet never sits over a Sheet"). Content from the code: per court a row with the two teams (MatchScoreboard with the ELO bar), the bench line "이번 판 휴식 {names}" / "전원 출전", buttons 저장 (primary; the code's label is "대진 저장"), 다시 생성, back. States: generating, error, empty (no court could be made). Axis: unchanged. This costs one page and keeps the draw an uncommitted proposal: no push, nothing for a member to see, nothing to delete.
  • Option 2: follow the boards (generate = commit, the draw is on the 대진표, redo until round 1). Then stage ④ lives on the 대진표: mb-draw plus ⋯ → 대진 다시 짜기 (Dialog needed: it deletes the scheduled matches; code needs replaceExisting wired and an RPC/permission for a non-admin host) and ⋯ → 팀 바꾸기 (Sheet needed). The preview and its ELO-balance bar disappear.

Either way, the review of one match needs a drawn Sheet: the code's 교체 (slots, pick a player, 팀 교체, the "교체 영향" card with 문제 없음 / 같은 라운드 중복 / 미확정 참석자 / 파트너 중복 / 상대 중복 tags, 저장). It is built and undrawn. 팀 바꾸기 in the board's ⋯ menu is this, for a whole round (the live editor).

4.4 Page C: 라운드 추가 (rolling or manual next round) — draw the missing Sheet ​

The code's only way to get round N+1. In the board's ⋯ (four rows once round 1 started: 라운드 추가, 늦게 온 선수 추가, 팀 바꾸기, 경기 규칙), 라운드 추가 has no Sheet. Proposal: Page A in "next round" mode: same Sheet, title "N라운드 대진 짜기" (Q wording), method row preselected to the session's saved 방식 (only reliable for the 3 score-driven ids; the 4 doubles presets are not persisted, Q), plus facts the next round needs: who is benched this time, who arrives later, for 순위매칭전/KDK/코트승급전 the standings it will read. Required state: rolling styles cannot generate round N+1 until round N is scored (they read pointsMap / lastRoundCourts); the Sheet should say so and disable the action. Axis: timing during, lifecycle rolling (effective, via INCREMENTAL_ROUND_STYLE_IDS), trigger manual.

4.5 Page D: tournament 대진 짜기 (set-up) — the largest hole ​

Decided shape (library + synthesis): stages of the session's own matches: 예선 리그 (groups of four pairs, three rounds) then 본선 (8강, 4강, 결승); no 3·4위전. Drawn: the console, the scorecard views, the 본선 Dialog. Not drawn: how a host sets it up. From the code (what the new Sheet can offer, all of which bracket.strategy.ts / round-robin-groups.strategy.ts / swiss.strategy.ts / compass.strategy.ts already implement):

  • Method rows (the code's four): 조별리그 ("조별예선 → 본선 토너먼트", also the recommendation for ≤8 entrants), 토너먼트 ("한 번 지면 탈락", single elimination ≤8 entrants, groups + knockout above 8), 전적매칭 토너먼트 (recommended 9–16), 전원부활전 (16+ in the UI).
  • Knobs (only for the group+knockout case, ≥9 entrants): 조별 인원 3–8, 진출 방식 (상위 N명 / 전원 진출), 진출 인원 1–3, 조 편성 (실력 균형 / 무작위). Default summary sentence behind a disclosure.
  • Doubles entrants are paired strong+weak automatically; an odd player out sits out silently (a state to draw or to fix).
  • Gates: host only, tournament entitlement, not after the session starts (code: open/locked), one tournament per session.
  • Q8: the boards' structure (groups of four pairs, three rounds, top two through, crossing 1A–2B …) fixes values the code leaves as knobs (advancersPerGroup 1–3, groupSize 3–8, seeding). Which knobs survive? The board's 8강/4강/결승 and fixed top-two are narrower than the code.
  • Axis values: composition seeded (new, today bypassed), timing ahead/at_start, lifecycle frozen per stage, poolMode all_confirmed.

4.6 Page E: starting play (stage ⑥) — mostly drawn; reconcile with the code ​

  • 경기 시작 (session) → mb-o-start: matches the code word for word.
  • Round start: the boards put it on the 대진표 band ("4라운드 시작", and "3경기 시작하기" when some matches are held). The code puts it on the scorecard LIVE tab ("시작", Dialog "{n}라운드 시작" / "{count}개 경기를 동시에 시작해요") and a 대진표 band does not exist for it. Q4: does 경기 시작 also start round 1's matches? The code does not (they stay scheduled until the LIVE button), the boards' Flow 6 ends at "Live".
  • Held matches (sequence, 대기): drawn (mb-held, library "Held and in sequence (D4)"), not built (no code derives it). Treat as a later page set, not part of this one.
  • Q3: mb-next-round / mb-held assume round N+1 already exists when round N is settled. In the code it exists only after the host generates it. Pre-generating rounds is canon UC1 (ahead), unbuilt, and the add-round header calls multi-round "a follow-on". The page set differs depending on the answer: (a) host taps 라운드 추가 first (Page C), then the band reads "N라운드 시작"; (b) the next round is generated automatically when the previous is settled (new trigger value).

4.7 Page F: the member side ​

  • 일정 상세 before a draw: today nothing shows (B3). Propose one quiet state in the 경기 section or a Notice-free line for a confirmed player ("대진이 아직 없어요" family, Q wording); the push covers the "draw posted" moment. sc-empty exists for the scorecard.
  • After the draw (sc-ideal/sc-resting/sc-draws): drawn; the data (court, round, partners, the match before) is already in the model.
  • Bench: "이번 판 휴식" exists in the preview and in the live editor's list; the member-facing bench state is sc-resting ("다음 경기" + court). No bench reason (late arrival, fairness rotation) is shown anywhere to a member.

4.8 Page G: 코트승급전 ladder tab — draw it ​

Built, undrawn. Elements from the code (use as the brief): court-primary ladder (court 1 = 킹코트 at the top), the chip "코트승급전 개인전", "내 코트", an elapsed clock, per-team movement badges (▲ 승격 / ▼ 강등 / 방어 / 유지) once the round is settled, a tiebreak reason "가위바위보" when needed, "점수 입력" while scoring, the host band "다음 라운드 시작" when the round is scored, empty "아직 라운드가 없어요". Axis: lifecycle rolling, composition via styleId = king_of_court.

4.9 Policy axis cheat sheet (what each page may offer) ​

AxisOffer in the pagesDo not offer (unbuilt)
timingat_start (a draw before 경기 시작) and during (라운드 추가 while live)ahead as an auto-pre-generated draw
triggermanualauto_on_confirm, auto_on_deadline
lifecyclefrozen; rolling for the 3 incremental styleslive (draft that regenerates until a lock)
poolModeall_confirmed; unsettled fee payers excluded; per-player arrival round (host-only)host_selected (pick a subset), opt_in
recordingderived from isFriendly; matches carry nature: null at creationa per-draw toggle, per-match nature at draw time
compositionrotation (7 presets + 3 registry styles), manual (수동 구성), seeded (tournament, once rebuilt)n/a
styleIdpersist and preselect the 3 score-driven ids; presets are not persisted (Q)a "club's last method" for presets

5. Questions for the lead (settle before drawing) ​

  1. Door for an open session. Boards: host band = 초대하기 until 마감, and the 관리 sheet lists no 대진 door. Code: 대진 방식 선택 appears on open/locked/in_progress. Which is right on game day with 6 of 8 seats filled?
  2. Preview or redo? Keep 미리보기/다시 생성 (an unsaved proposal) or follow the boards (generate = save, redo before round 1)? Redo needs replaceExisting wired, a delete permission for a non-admin host (RLS matches_delete is club-admin + scheduled only), a confirm Dialog, and a rule for the "대진표가 올라왔어요" push (dedup key is per session and player, so a redo would not re-notify).
  3. One round at a time, or pre-generated rounds? The boards' "4라운드 시작" and "대기" rows assume the next round exists; the code generates it on demand, and the rolling styles cannot generate before scores exist.
  4. Does 경기 시작 start round 1's matches? Code: no (session flips, matches stay scheduled until the LIVE-tab 시작 button). Boards: Flow 6 ends "Live".
  5. Preset copy. Keep the shared "매 게임마다 파트너가 바뀌는 KDK 방식" (wrong for 고정 복식) or adopt the board's four lines? Also: the chooser and picker headings use "경기 방식"; the terminology guide reserves 대진 방식 for this.
  6. 고정 복식 pairs. Who chooses them? Today they are consecutive players in RSVP order.
  7. Unpaid copy. Board Notice "입금 확인 전 3명" (action 참석자 관리) vs code "정산 대기 {n}명이에요. 확정 후 배정할 수 있어요." (action 정산 확인).
  8. Tournament knobs. Board = groups of four pairs, three rounds, top two, fixed crossing. Code = 조별 인원 3–8, 진출 1–3 or all, rating/random seeding, round_robin_groups = one exhaustive group then the top 4 (default). Which survive, and how does a host answer for an odd doubles field?
  9. Late and early windows. RSVP has availableFromRound and arrivesAt only (start side). The board decision "대진 짜기 reads both ends" has no departure field. Also, arrival edits are host-only in the code (RLS rsvps_host_update) while the boards have players set and stamp their own.
  10. Casual draws. recording derives casual from isFriendly; should the draw Sheet say so?
  11. Role gating on the 대진표. Client does not gate 라운드 추가, 대진 짜기 (empty state), 교체, 진행 중 라운드 조정; the server gates generation only.
  12. Persisting the last method. The IA "preselected to the club's last method" needs the preset id saved (today only score-driven ids are), plus where (session, club, or the recurrence template, canon step 2).

6. File index (everything cited) ​

  • Canon: docs/canon/match-generation.md
  • Policy: packages/app/src/domain/entities/generation-policy.entity.ts, domain/entities/session.entity.ts (SESSION_EVENT_TYPES :95-102, RSVP availableFromRound/arrivesAt :257-264, lateArrivalPolicy)
  • Engine: domain/rules/rotation.rules.ts (generateRound :196, generateAllRounds :283), rotation-presets.ts, matchmaking/registry.ts, matchmaking/resolve-pool.ts, matchmaking/feasibility.ts, matchmaking/recommend-format.ts, matchmaking/strategies/*.ts, matchmaking/strategies/rotation/fixed-pairs.ts, domain/rules/match-lifecycle.rules.ts, domain/rules/matchmaking/score-driven.ts
  • UI: packages/features/sessions/src/{format-chooser-sheet,add-round-sheet,swap-sheet,live-round-editor-sheet,match-card,match-board-screen,session-matchups-card,create-tournament-modal,tournament-board-screen,unsettled-pool-notice}.tsx; add-round/*; match-board/*; session-detail/{use-generate-round-inputs.ts,sections/participation-section.tsx,sections/matches-section.tsx,sections/session-detail-overlays.tsx,actions/primary-action.tsx,actions/host-menu-actions.ts}; scorecard/{live-tab/use-live-tab-state.tsx,get-default-tab.ts,king-of-court-ladder.tsx,draws-tab.tsx}
  • Hooks: packages/app/src/presentation/hooks/mutations/{generate-round.mutation.ts,use-generate-round.ts,use-start-session.ts,use-start-next-round.ts,use-update-match-teams.ts}; adapter adapters/supabase/match.supabase.ts (createBatch :~180-200, updateStatus)
  • Copy: packages/app/src/config/i18n/ko/{sessions.ts (matchBoard :219-412, scorecard :794, kotcLadder :865-889), tournaments.ts, common.ts, signals.ts :156-157, settings.ts :449}
  • SQL: supabase/migrations/00683_competition_completion_and_live_substitution.sql (generate_session_round :98-197), 00011_rls_indexes_triggers.sql (matches_* policies), 00109_signals_remaining_triggers.sql + 00525_secdef_search_path_hardening.sql (matchup push), 00690_cron_prelocks_session_roster.sql
  • Boards: docs/twomore_design_system/wireframes/flat-v2-detail.jsx (sd :1543-1617, spec :10667-10773, sc :3120-3358, mb :3370-4480, DtlMbOver :3923-4220), flat-v2-flows.jsx:548-611, flat-v2-forms.jsx:945,1262, flat-v2-list-a.jsx:5722-5725, flat-v2-kit.jsx (FVK_SESSION_DOORS)
  • Docs: docs/design/{terminology-guide.md,decision-log-autonomous-2026-09-17.md,flat-v2-ia-and-flows-2026-10-09.md,flat-v2-component-library-2026-10-09.md,usability-audit-2026-09-16.md,owner-review-48-synthesis-2026-10-28.md,owner-review-49-synthesis-2026-10-30.md,research/owner-review-48-research-2026-10-28.md}

C. The wizards, the visitor club page and the practice log ​

Read-only research, 2026-10-30. Nothing in the repo was edited.

Path prefixes used below

  • CL = packages/features/clubs/src
  • SE = packages/features/sessions/src
  • ON = packages/features/onboarding/src
  • I18N = packages/app/src/config/i18n/ko
  • BD = docs/twomore_design_system/wireframes
  • Board files: BD/flat-v2-forms.jsx (cc-, cs-, jc-), BD/flat-v2-auth.jsx (onboarding, sign-in), BD/flat-v2-hub.jsx (club-visitor-, rec-, prof-), BD/flat-v2-list-b.jsx (pl-*, the 연습 기록 page). flat-v2-flows.jsx does not draw any of these wizards.

Reading the "mismatch" lists: each is tagged

  • [BOARD-OMITS] the app does it, the board does not draw it
  • [BOARD-ADDS] the board draws something the app does not have
  • [COPY] same thing, different words
  • [DEFAULT] different default/initial state
  • [CONTROL] different control for the same field
  • [ORDER] different order
  • [ACK] the board's own spec text acknowledges the deviation as a deliberate redesign (so it is a decision to port, not drift)

0. Inventory: which flows are wizards ​

Real WizardShell wizards (canon: docs/canon/wizards.md; shell: packages/ui/src/wizard-shell.tsx):

WizardRoute / screenStepsBoard
클럽 만들기apps/mobile/app/create-club.tsx → CL/create-club-screen.tsx9 (quick path 4)flat-v2-forms.jsx cc-* (10 steps full, 3-4 quick)
일정 만들기apps/mobile/app/create-session.tsx → SE/create-session/create-session-screen.tsx7flat-v2-forms.jsx cs-w1..cs-w7 ("first-timer path"); the board's ideal (cs-ideal) is a review PAGE that does not exist in the app
온보딩ON/onboarding-wizard-screen.tsx (+ value-props pager before it)6 progress segments + pre-wizard 3-card pagerflat-v2-auth.jsx vi-/co-/fs-* (3 steps)

Not wizards (checked, no WizardShell):

  • Sign-up: there is NO sign-up wizard in the app. packages/app/src/presentation/screens/auth/login-screen.tsx is email+password sign-in only (docstring says "+ sign-up" but code only has signInWithEmail, lines 35-66). The board's sign-in/OAuth/email-sign-up (si-, em-, iv-*) is target design, not a port of a wizard.
  • SE/create-tournament-modal.tsx: two-state modal (ModalStep = 'select' | 'setup', line 138), no WizardShell. Not on these boards.
  • SE/venue-contributions/*, SE/venue-owner-claims/*: single forms, no WizardShell.
  • CL/join-club-screen.tsx: single invite-code form (see Task 2).

Shell chrome difference that touches every wizard (theme-migration item, not per-wizard):

  • App WizardShell (packages/ui/src/wizard-shell.tsx:242-383): ONE header row = 40pt back/close square + N flex progress segments (always segments, any N) + "n / N" text (:210) + 건너뛰기 secondary Button; page title lives in the step body (Text textRole="pageTitle").
  • Board FvkWizardHeader (BD/flat-v2-kit-chrome.jsx:~765): 56 bar + 20 progress row ("10단계 중 3단계"; more than 7 steps = one continuous bar) + title row 24/32; 건너뛰기 is a sm tonal Button in the bar. So: continuous bar for >7 steps and the "n단계 중 m단계" text are new.
  • Armed-detour copy: app back pill backToReviewShort(n) = ${n}단계 검토 (I18N/common.ts:380), primary backToReview = '검토로 돌아가기' (common.ts:281; used CL/create-club-screen.tsx:442,457-459). Board: back label '확인' and primary '확인 화면으로 돌아가기' (BD/flat-v2-forms.jsx:2390,2402; session :1182,1338). [COPY]
  • Leave dialog: app title '나갈까요?' / body '작성 중인 내용이 삭제돼요. 처음부터 다시 작성해야 해요.' / buttons '계속 작성' + '삭제하고 나가기' (common.ts:276-279, 278 keepEditing). Board FvkLeaveGuard = '나갈까요?' / '지금 나가면 입력한 내용이 사라져요' / '계속하기' + '나가기' (BD/flat-v2-kit-controls.jsx:2562-2567). [COPY]
  • Resume toast: app '이어서 작성 중이에요' + action '처음부터' (common.ts:275,451; CL/create-club-screen.tsx:220-228). Board RESUMED = '쓰던 내용이 있어요' + '처음부터 쓰기' (BD/flat-v2-forms.jsx:1070). [COPY]

1. 클럽 만들기 (create-club) ​

1.0 Structure ​

App step sequence (CL/create-club-screen.tsx:1-34, renderStep 286-366; STEP_COUNT = 9 at CL/create-club-helpers.ts:44). Index → file → title (I18N/clubs.ts):

idxStepFileTitle copy
0이름 (+ 꾸미기, 로고, 빠르게 만들기)create-club-step1.tsx + create-club/step1/decor-section.tsx'클럽 이름을 정해요' (clubs.ts:659)
1지역 (courts)create-club-step2.tsx'어디서 활동하나요?' (:698)
2일정create-club-step3.tsx + create-club/step3/*'언제 활동하나요?' (:764)
3스타일create-club-step4.tsx'어떻게 치나요?' (:766)
4인원create-club-step5.tsx'몇 명과 함께할까요?' (:987)
5회비 (skippable)create-club-step6.tsx'회비를 계산해 볼까요?' (:1034)
6소개 (skippable)create-club-step7.tsx'클럽을 소개해요' (:1130)
7멤버십·게스트 (required fork)create-club-step-guests.tsx'멤버십과 게스트, 어떻게 할까요?' (:1112)
8확인create-club-step8.tsx'확인해 주세요' (common.ts:268)

Skippable: idx 5, 6 only (create-club-screen.tsx:393). Quick path (빠르게 만들기): QUICK_START_SEQUENCE = [0, 1, 2, STEP_COUNT-1] = 이름 → 지역 → 일정 → 확인 (4 segments) (CL/create-club/quick-start.ts:24).

Board sequence (BD/flat-v2-forms.jsx CC_TITLE 1770-1781, CcStep 2049-2397): 10 steps: 클럽 이름을 정해요 / 어디서 활동하나요? / 언제 활동하나요? / 어떻게 치나요? / 몇 명과 함께할까요? / 회비는 얼마인가요? / 클럽을 소개해 주세요 / 가입은 어떻게 받을까요? / 게스트도 받을까요? / 확인해 주세요. Skippable: n=6, n=7 (:2382). Quick path = 3 steps (이름, 일정 with 요일 only, 확인), 4 when the profile has no region (:2381).

Structure mismatches:

  1. 9 vs 10 steps. The app merges 가입 방식 + 게스트 into ONE step (멤버십·게스트, create-club-step-guests.tsx:293-331). The board splits them into step 8 (가입 방식 alone) and step 9 (게스트). Step titles 6, 7, 8/9 differ in copy: 회비 '회비를 계산해 볼까요?' vs board '회비는 얼마인가요?'; 소개 '클럽을 소개해요' vs '클럽을 소개해 주세요'; app has no '가입은 어떻게 받을까요?' / '게스트도 받을까요?'. [COPY] [ORDER]
  2. Quick path. App: 4 steps incl. 지역 and full 일정 step (fork + 활동 장소 + 요일 + 반복 + 시간). Board: 3 steps, 지역 dropped when the profile has a region (SPEC_CC Route :2887, "new: today the quick path still asks it" :2895), quick 일정 has no fork and no 반복 (CcStep :2132,2158). There is no profile-region seeding in the app. [BOARD-ADDS] [ACK]
  3. Progress bar. See section 0 (continuous bar past 7 steps).

1.1 Step 0 / board step 1: 이름 (+ 꾸미기) ​

App (create-club-step1.tsx:226-315, decor-section.tsx):

  • LabeledField '클럽 이름' (common.ts:149) required size lg + StyledInput, maxLength={CLUB_NAME_MAX} (hard cap), placeholder '예: 강남 테니스 크루' (clubs.ts:660), blur-commit via useEditScope. Error on blur: '클럽 이름은 2자 이상 30자 이하여야 해요' (:661). CTA hint: '클럽 이름을 2~30자로 입력해 주세요' (:734). (step1.tsx:248-261)
  • Same-name soft Callout tone="info": '같은 이름의 클럽이 N곳 있어요. 그대로 사용해도 돼요' (:664; step1.tsx:266-272).
  • RevealStack item decor (self-confirming) reveals at 2+ typed chars (step1.tsx:139,276-282) and holds:
    1. '대표 색상' HueSlider (hue default 152, sat/light defaults), small round swatch in the label's right slot (decor-section.tsx:214-253). No visible Stepper (a11y increment/decrement only, packages/ui/src/hue-slider.tsx).
    2. '클럽 로고' required fork of two SelectionCards '로고 업로드' | '문양 사용' (:259-357). Upload tile: tap launches the image picker with 1:1 crop (usePickImages, resize preset avatar), shows a spinner then the thumbnail in the tile, a trash RemoveIconButton at the tile corner removes it (no confirm), re-tapping the selected tile re-picks, switching fork never destroys the other path's draft (:166-210).
    3. AttachedPanel notchRight="23%" only under 문양 사용 (:367-486): '모노그램' (max 4 chars, label-right hint '최대 4자' then 'n / 4자', placeholder = initials of the name), '글자 크기' ChoiceSet 작게/보통/크게 (default 보통), '모양' swatches 원/둥근 네모/네모/방패 (default 원), '문양 선택' = wrapped grid of all FLAG_PRESETS at 48px, each wearing the 대표 색상 (hueFlagPalette).
    4. '미리보기' ClubCardMiniPreview (banner strip in the hue + mark + name) (:490-504).
  • '빠르게 만들기' toggle: hand-rolled Pressable + TogglePill, label '기본 설정으로 빠르게 만들기', caption '실력, 구성, 인원, 게스트는 기본값으로 시작해요. 만든 뒤 클럽 설정에서 바꿀 수 있어요' (clubs.ts:693-695). Rendered OUTSIDE the RevealStack, visible from the first render even with an empty name (step1.tsx:286-314). ON seeds 구성 '혼성', 종목 '복식', 실력 '실력 무관', 성격 '균형형', 게스트 '허용' (quick-start.ts:86-96).
  • Gate: step1Valid && logoChoiceSettled (screen/step-validity.ts:23); 'upload' needs logoDraftUri (step1.tsx:327-331); hint '로고를 정해 주세요' (clubs.ts:670).

Board (CcStep n=1 :2056-2081, CcName :1964-2013, CcDecor :1889-1960):

  • Same title. Fields: 클럽 이름 (required), 대표 색상 (required, FvkSlider hue with stepper {value 6, min 1, max 12}), 클럽 로고 (required, 2 Choices), nested generator, 미리보기, 빠르게 만들기 Cell.
  • Mismatches
    • Name placeholder '예: 강남 라켓클럽' vs app '예: 강남 테니스 크루'. [COPY]
    • Name errors: board has THREE messages ('클럽 이름을 입력해 주세요' pressed-empty, '2자 이상 입력해 주세요' short, '30자 이하로 입력해 주세요' over, :1974-1990) and a soft limit ("typing past 30 is kept and flagged, never blocked", counter '−4'); app has ONE message and a hard maxLength. [COPY] [CONTROL]
    • Same-name soft warning Callout: not drawn. [BOARD-OMITS]
    • 대표 색상 has a Stepper (WCAG 2.5.7) on the board, none in the app. [BOARD-ADDS]
    • Upload path: board shows the picked photo as a MediaRow ('사진 로고' / '내가 고른 사진이에요') whose tap opens a MenuSheet '사진 찍기' / '앨범에서 고르기' / destructive '문양 쓰기' (switches fork back + Toast '로고를 문양으로 바꿨어요' + 되돌리기) (:1932-1943, cc-logo :2684-2701, cc-logo-undo :2703-2711). App: tile thumbnail, re-tap re-picks, corner trash removes, no camera option, no undo toast. [CONTROL] [BOARD-ADDS]
    • 문양 선택: board = ONE horizontal row of 8 presets, next peeking (CcMarks :1875-1887); app = wrapped grid. 모노그램 counter board '2/4' vs app '2 / 4자' + '최대 4자' hint when empty. [CONTROL] [COPY]
    • 미리보기: board MediaRow club row, l2 '나머지는 다음 단계에서 채워져요' (matches clubs.ts:687 previewPlaceholderLine); app mini card with banner strip (board spec admits the strip is dropped, SPEC_CC :2907). [ACK]
    • Toggle: board label '빠르게 만들기' with no caption, only appears after the name has 2 chars (:2065-2079); app label '기본 설정으로 빠르게 만들기' + 2-line caption, always visible. [COPY] [BOARD-OMITS] (caption) / [ORDER] (reveal timing)

1.2 Step 1 / board step 2: 지역 (courts) ​

App (create-club-step2.tsx:184-453):

  • There is no region picker field in this step. Region/district are derived from court picks (deriveBasesFromPicks, header :8-14). Region as its own field exists only as the fallback inside step 3.
  • Zero picks: LabeledField '자주 가는 테니스장' (clubs.ts:703), NOT required, containing a search-field-shaped Pressable (placeholder '테니스장 이름을 검색해 보세요' :706) that opens VenueSearchTakeover (commitMode="immediate", closes after every pick).
  • With picks: collapsible LabeledField header '자주 가는 테니스장' + right meta 'N곳' (:703-705, toggle :208-222), then one PickedVenueCard per pick, then AddCard '코트 추가' (common.ts:265).
  • Each PickedVenueCard row: venue facts + certification chip; trailing cluster = HomeCourtToggle (house; first pick = 홈 코트, tapping house on another pick promotes it, toast '이미 홈 코트예요'/'홈 코트가 변경됐어요' :715-716), expand chevron, 변경 (swap) ActionIconButton (replace-in-place, hint ''{name}' 대신 사용할 테니스장을 선택해 주세요' :717, toast '변경한 테니스장의 일정, 비용 설정이 초기화됐어요' :719), trash RemoveIconButton (:282-311).
  • Row tap opens the attached court-usage panel (court-usage-panel.tsx): known total (≤12): '사용하는 코트' chip grid + per-chip rename pencil, hint '예약하는 코트를 골라 주세요'; unknown total: Stepper '사용 코트 수' + '코트 이름 (선택)' rename chips (clubs.ts:953-964); title row has pencil '정보 수정' to the venue detail correction flow (:338-346).
  • Gate: always valid (step-validity.ts:25-28); courts optional.

Board (CcStep n=2 :2082-2121, CcCourt :2020-2032):

  • '활동 지역' FvkPickerCell required, seeded '서울' from the profile, opens a value Sheet of 시/도 (cc-region :2712-2722); '테니스장' PickerCell optional ('2곳'); section head '선택한 테니스장'; MediaRow per court = leading radio ring (home court) + trash (control {icon trash, danger}) + l2 ['홈 코트', facts]; trash removes at once + Toast '잠실 테니스장을 지웠어요' + 되돌리기 (cc-court-remove :2758-2766); 테니스장 PickerCell → FvkSearchTakeover in immediate mode (cc-venue :2750-2757).
  • Mismatches
    • Board adds a required 활동 지역 PickerCell (profile-seeded, 17 시/도 Sheet). App has no such field in step 2 (region derived from courts; region fallback lives in step 3 and is only required when there are no courts). [BOARD-ADDS] [ACK] (SPEC_CC :2895 "new")
    • Board omits per-pick 사용 코트 수 / 사용하는 코트 / 코트 이름 panel (needed downstream: step 3 사용 코트 subset, step 6 court-cost math). [BOARD-OMITS]
    • Board omits 변경 (replace-in-place) and 정보 수정 pencil. [BOARD-OMITS]
    • 홈 코트 control: app house icon; board radio ring (board spec: "a ⋯ needs three commands; a court has two" :2907). [CONTROL]
    • Collapse toggle + 'N곳' meta: board uses PickerCell value '2곳' instead. [CONTROL]
    • Row delete: app = RemoveIconButton (built-in confirm per canon/components.md "Item removes"); board = immediate remove + 되돌리기 Toast. [CONTROL]
    • Label: app '자주 가는 테니스장', board '테니스장' / '선택한 테니스장'. [COPY]

1.3 Step 2 / board step 3: 일정 ​

App (create-club-step3.tsx:312-478, create-club/step3/*):

  1. Required fork LabeledField '정기 일정' (common.ts:123) with two SelectionCards '있어요' | '아직 없어요' (has-meets-fork.tsx); hint '정기 일정 여부를 정해 주세요' (clubs.ts:762). Always rendered, reversible.
  2. 아직 없어요 + no court picks → required region fallback LabeledField '활동 지역' → RegionDistrictFilter ('지역' + '시/군/구', placeholder '선택해 주세요') (region-fallback-field.tsx; hint '활동 지역을 선택해 주세요' :745). With picks → step done.
  3. 있어요 → per-venue "round": place picker = LabeledField '활동 장소' required listing every pick as VenueRow variant="card" (or the region fallback when there are no picks) (step3.tsx:266-291); picking pushes a draft entry and opens an AttachedPanel (under the entry's VenueMeetCard) running a sequential RevealStack:
    • '요일' required, multi ChoiceSet 월요일…일요일 (label + daySuffix '요일') (days-chips-section.tsx);
    • '반복' required DropdownChip standalone fullWidth allowClear={false} placeholder '선택해 주세요', 7 options: 매주, 격주, 매월 첫째 주, 둘째 주, 셋째 주, 넷째 주, 마지막 주 (clubs.ts:216-232; recurrence-section.tsx) — starts UNSET, and the 시간 section waits for it (recurrenceTouched, use-active-schedule.ts:75-90);
    • '시간' TimeBandField (label '시간'), default 19:00-21:00 seeded when 반복 is first chosen, domain = the pick's operating hours (time-section.tsx, use-active-schedule.ts:60-90);
    • optional '사용 코트 (선택)' multi ChoiceSet of the pick's configured courts + hint '선택 안 하면 모든 코트를 써요' (used-courts-subset-section.tsx; only when the pick has ≥2 courts).
  4. VenueMeetCard summary per venue with per-schedule edit (opens/folds the panel) and remove; AddCard '다음 일정 추가' (clubs.ts:749) for additional schedules (multiple days at one court allowed; region-fallback clubs get exactly one).
  • Gate: step3Valid = fork answered; 아직없어요 → court pick OR region; 있어요 → ≥1 complete schedule, none incomplete, place OK (step3-helpers.ts step3Valid).

Board (CcStep n=3 :2122-2171):

  • 정기 일정이 있나요? ChoiceSet ['있어요','아직 없어요'] required (full path only); 활동 테니스장 PickerCell with default '올림픽공원 테니스장'; '요일' ChoiceSet ['월'…'일'] required, single-select (selected={[5]}); '반복' ChoiceSet ['매주','격주','매월'] with 매주 pre-selected; '시간' TimeBand. Frames cc-f3-unset/-day/cc-f3/cc-f3b/cc-q2/cc-q2-done.
  • Mismatches
    • Fork legend '정기 일정이 있나요?' vs app label '정기 일정'. 활동 테니스장 vs app '활동 장소'. [COPY]
    • 반복: app = 7 options, required, starts unset (gates 시간); board = 3 options (매주/격주/매월), pre-answered default 매주 that "appears with the section above" (:2128, caption cc-f3 "반복 and 시간 appear with their defaults"). The 매월 N째 주 / 마지막 주 variants (stored as weekOfMonth) cannot be expressed. [BOARD-OMITS] [DEFAULT] [CONTROL]
    • 요일 is multi-select in the app (ChoiceSet multiple, toggleDay); board draws a single selected day. [CONTROL]
    • Board omits: 다음 일정 추가 (multi-schedule), per-venue schedule summary cards with edit/remove, 사용 코트 subset, the region fallback (지역 + 시/군/구 for court-less clubs) and the 아직 없어요 region requirement. [BOARD-OMITS]
    • Board's 활동 테니스장 is a PickerCell (single value) vs app card list of all picks. [CONTROL]
    • Quick path: board has no fork and no 반복 (:2132,2158); app quick path still shows the full step. [ACK]

1.4 Step 3 / board step 4: 스타일 ​

App (create-club-step4.tsx:86-285), RevealStack, every required enum starts null:

  1. '구성' (common.ts:152) required, caption glossary.composition '남성, 여성, 혼합 중 누가 참석하는지예요' (glossary.ts:25), ChoiceSet 남성 / 여성 / 혼성.
  2. '종목' required ChoiceSet from getFormatOptions(composition): 복식, 단식, 혼복 (only when 구성 = 혼성), 무관 (packages/app/src/presentation/components/style-axis-options.ts:27-41; common.ts:71,72,167,234). Choosing a non-혼성 구성 clears a picked 혼복.
  3. '실력' required (clubs.ts:767) with InfoHintButton → TierInfoSheet; fork SelectionCards '실력 무관' | '범위 설정' (common.ts:410, clubs.ts:940); under 범위 설정 an AttachedPanel with TierRangeSelector (discipline follows 종목; credential lines keyed market × discipline × composition, clubs.ts:820-932).
  4. '성격' (common.ts:264) required ChoiceSet 균형형 / 친목형 / 경쟁형 / 성장형 (option-lists.ts:51-58).
  • Gate: all four non-null (step4Valid). Hints: '구성을 정해 주세요', '종목을 정해 주세요', '실력 범위를 정해 주세요', '성격을 정해 주세요' (clubs.ts:946-949).

Board (CcStep n=4 :2172-2221): 구성 ['남성','여성','혼성'] → 종목 ['단식','복식','단식+복식'] → '수준 범위' ['수준 무관','범위 정하기'] + LevelBand nested → 성격 ['친목형','경쟁형','성장형','균형형'].

  • Mismatches
    • 종목 options/order/labels: board 단식, 복식, 단식+복식 (no 혼복; label '단식+복식' while the app label for singles_doubles is '무관', although common.ts:425 formatSinglesDoubles = '단식+복식' exists unused here); app order 복식, 단식, [혼복], 무관. [COPY] [ORDER] [BOARD-OMITS] (혼복, 혼성-gating)
    • Label 실력 vs 수준 범위; option labels '실력 무관'/'범위 설정' vs '수준 무관'/'범위 정하기'. Board spec says labels were unified by the lead 2026-10-20 (SPEC_CC :2907), so this is a copy migration the app has not taken (no '수준 범위' string exists in I18N). [COPY] [ACK]
    • 성격 order: app 균형형 first; board 균형형 last. [ORDER]
    • Board omits the 구성 caption, the 실력 info door (TierInfoSheet), and discipline-aware credential lines. [BOARD-OMITS]
    • Board draws a Stepper under the level band readout (WCAG 2.5.7, like 시간 band). [BOARD-ADDS]

1.5 Step 4 / board step 5: 인원 ​

App (create-club-step5.tsx; calc basis = ≥1 complete schedule):

  • With calc basis (:266-288): RevealStack: '1인당 경기 수' required Stepper fullWidth min 1 max 6, format '${n}경기', unset shows 0 (floor sentinel), caption '정기 일정 총 {N}분 [(종료 시간 미정, 가정)] 중 약 {M}분 플레이해요' (clubs.ts:994-999); changing it clears the pinned 모집 목표. Then '모집 목표' (common.ts:266) SuggestedCountField (TRACK/PIN Stepper, min/max MIN/MAX_RECRUIT_TARGET, format 'N명'), AttachedPanel receipt: headline '모집 목표 N명 → 예상 출석률 약 P%' as an ExpandToggle ('왜 이 숫자죠?', common.ts:20) revealing '코트 C면 × M분 ÷ 30분 = 경기 슬롯 S개', '1인당 k경기 기준', '일정마다 약 X명이 필요해요', optional '종료 시간이 없어 2시간 기준으로 계산했어요' (clubs.ts:1023-1033), then caption lines '제안/고정 N명' + '일정당 K명 수용' (clubs.ts:1014-1017).
  • Without calc basis (아직 없어요 / no complete schedule) (:238-264): Card tone="flat" note '정기 일정이 있으면 코트와 시간 기준으로 적정 인원을 계산해 드려요' (clubs.ts:1002) + one plain '모집 목표' Stepper.
  • Gate: step5Valid = true without calc basis else both non-null.

Board (CcStep n=5 :2222-2236): '1인당 경기 수' FvkStepper min 1 max 6 starting at 0; '정원' required FvkInput value '24' unit '명' (typed).

  • Mismatches
    • Label 모집 목표 vs 정원; control TRACK/PIN Stepper with live suggestion vs typed Number field. (Board label unification is a lead ruling, SPEC_CC :2907.) [COPY] [CONTROL] [ACK]
    • Board omits: suggestion/pin behavior, the receipt panel (왜 이 숫자죠? accordion), the play-minutes caption, the 일정당 수용 caption, the no-schedule fallback frame. [BOARD-OMITS]
    • Board's review shows a '1인당 경기 수' row; the app's review has no such row (see 1.9). [BOARD-ADDS]

1.6 Step 5 / board step 6: 회비 (skippable) ​

App (create-club-step6.tsx), RevealStack:

  1. Only with a calc basis: '{venue} 시간당 코트비 (원)' WonAmountField commitMode="blur" per scheduled venue, placeholder '예: 8,000' (clubs.ts:1035-1036; :238-261). Confirmed when all entered OR any touched.
  2. Only with a calc basis: '운영비 (공값 등)' (:1037) editable Stepper fullWidth min -100, step 5, format '없음' / '+N%' / '-N%', then an AttachedPanel suggestion receipt: headline '제안 월 회비 / 제안 분기 회비 / 제안 6개월 회비 / 제안 연 회비' + won, ExpandToggle '왜 이 숫자죠?' revealing per-venue monthly-cost lines ('{venue} 월 코트비 (월 N회)', '미입력') and '월 예산 ÷ N명', micro line '월 {won}원이면 코트비와 운영비가 충당돼요', and a secondary small ActionButton '제안 금액 적용' (:394-411, :327-382).
  3. Always: '회비 주기' (:1078) ChoiceSet 매월 / 분기마다 / 6개월마다 / 매년 (default 매월, pre-answered) → '월 회비 (원)' (label follows cycle: 월/분기/6개월/연 회비 (원)) StyledInput number-pad, placeholder '예: 30,000', buffered edit scope → if amount > 0 an AttachedPanel '납부일 (며칠)' Stepper 1-28 (DUES_DAY_MAX), caption '최대 28일 (납부월 말일 안전 기준)', value format '매월 N일' etc. (:413-480; clubs.ts:1085-1093).
  • Skippable; defaults: duesCycle 'monthly', duesDay 1, duesAmount 0 (create-club-draft.ts:388-391).

Board (CcStep n=6 :2237-2253): '납부 주기' FvkPickerCell value '매월' → value Sheet of six (매월, 분기, 6개월, 매년, 일정마다, 한 번) (CC_CADENCES :1798, cc-cadence :2741-2749); '금액' FvkInput '30,000' unit '원'; '매월 납부일' FvkInput '5' unit '일' (typed Number field, "1 to 28").

  • Mismatches
    • Whole cost calculator missing: 시간당 코트비 per venue, 운영비 stepper, 제안 월 회비 receipt + 제안 금액 적용. [BOARD-OMITS] (high value: this is what makes the app's step 6 a calculator)
    • 납부 주기 options: board 6 values incl. '일정마다' and '한 번'; app domain DUES_CYCLES = ['monthly','quarterly','semiannual','yearly'] (packages/app/src/domain/entities/club/discovery.ts:109); I found no per-session/one-time dues plan in the club entity or club settings (grep of per_session/once in club entities + settings returned nothing). Labels differ: app '분기마다 / 6개월마다 / 매년' vs board '분기 / 6개월 / 매년'. [BOARD-ADDS] [COPY] [CONTROL]
    • Amount label: app follows the cycle ('월 회비 (원)' etc.), board '금액'. 납부일: app Stepper in an AttachedPanel shown only when amount > 0, label '납부일 (며칠)' and value '{cycle} N일'; board always-visible Number field '매월 납부일'. [CONTROL] [COPY]
    • Board shows 납부 주기 as a separate-cell Picker (+Sheet); app defaulted-axis law (canon/wizards.md "Defaulted axis") requires the real control visible, which a Picker Cell showing the value satisfies. No violation, just a control change.

1.7 Step 6 / board step 7: 소개 (skippable) ​

App (create-club-step7.tsx:133-249), RevealStack:

  1. '클럽 소개 (선택)' (clubs.ts:1131) multiline StyledInput, maxLength 500, label-right counter 'n / 500', placeholder '클럽의 분위기, 활동 방식, 가입 조건 등을 자유롭게 적어 주세요' (:1132); buffered scope.
  2. '공개 범위' (common.ts:267) two SelectionCards '공개' | '비공개', neither selected until tapped (visibilityTouched &&), hint '공개하면 탐색에서 발견될 수 있어요' (:1133); underlying default isDiscoverable: true.
  3. '사진 정책' (common.ts:168) DropdownChip standalone fullWidth options '운영진 선별' / '멤버 업로드 후 승인' / '운영진만 업로드'; default member_uploads_approval = '멤버 업로드 후 승인' (create-club-draft.ts:402).

Board (CcStep n=7 :2254-2266): '소개' FvkTextArea counter '0/500' placeholder '예: 강남에서 매주 토요일 아침에 복식을 쳐요'; '클럽 찾기에 공개' ChoiceSet ['공개','비공개'] with 공개 pre-selected; '사진 정책' PickerCell value '운영진이 골라요' → Sheet ['운영진이 골라요','멤버가 올리면 운영진이 승인','운영진만 올려요'], "운영진이 골라요 is the default" (cc-photo :2724-2740).

  • Mismatches
    • Labels: '클럽 소개 (선택)' vs '소개'; '공개 범위' vs '클럽 찾기에 공개'. [COPY] [ACK] (label unification, SPEC_CC :2907)
    • Placeholder differs. [COPY]
    • Photo-policy option copy: app '운영진 선별' / '멤버 업로드 후 승인' / '운영진만 업로드' vs board '운영진이 골라요' / '멤버가 올리면 운영진이 승인' / '운영진만 올려요'. [COPY]
    • Photo-policy default: app member_uploads_approval; board states 운영진이 골라요 (= admin_curated) is the default. [DEFAULT]
    • 공개 범위 initial selection: app unselected-until-touched; board 공개 pre-selected. [DEFAULT]
    • Board omits the visibility hint line. [BOARD-OMITS]

1.8 Step 7 / board steps 8 + 9: 멤버십·게스트 ​

App (create-club-step-guests.tsx), ONE step:

  1. '가입 방식' (common.ts:247) DropdownChip standalone fullWidth allowClear={false}: 바로 가입 / 승인 후 가입 / 초대 전용 (option-lists.ts:60-66, clubs.ts:1101, common.ts:490,248); default open = 바로 가입 (create-club-draft.ts:383); pre-confirmed, never gates.
  2. '게스트 허용' required fork SelectionCards '허용' | '허용 안 함' (common.ts:454); hint '게스트 허용 여부를 정해 주세요' (clubs.ts:1115); choosing 허용 안 함 clears fee + limit.
  3. 허용 → '게스트 참가비 (원)' WonAmountField (save-scope), placeholder '예: 10,000', caption '가입하지 않은 게스트가 공개 일정에 참석할 때 내는 금액이에요' or the live hint '지금 기준 1인당 회당 비용은 약 {n}원이에요' (:1094-1100); 0 = 무료 (no 무료/유료 fork).
  4. 허용 → '방문 횟수 제한' Stepper fullWidth 0-10, format '제한 없음' / 'N회', caption '이 횟수를 넘으면 가입을 권해요' (:1116-1122) — defaulted, never gates.

Board:

  • Step 8 n=8 (:2267-2289): title '가입은 어떻게 받을까요?'; '가입 방식' as radio rows with meaning on line 2 (CLUB_JOIN :1790-1794): 바로 가입 '누구나 바로 멤버가 돼요' / 승인 후 가입 '총무가 확인하면 멤버가 돼요' / 초대 전용 '초대 코드가 있어야 멤버가 돼요'; frame draws 승인 후 가입 checked (checked={i === 1}).
  • Step 9 n=9 (:2290-2320): title '게스트도 받을까요?'; '게스트' ChoiceSet ['허용','허용 안 함'] required; under 허용: '게스트 참가비' ChoiceSet ['무료','유료'] with nested required '1인당 참가비' Number field (10,000 원); '방문 횟수 제한' (optional marker) FvkStepper 0-10 value 3.
  • Mismatches
    • Two steps vs one. [ORDER] (see 1.0)
    • 가입 방식 control: Dropdown vs radio rows with meanings (the meaning strings exist only on the board; the app has labels only). [CONTROL] [COPY]
    • Default shown: board frame pre-checks 승인 후 가입 and calls it "a defaulted axis shown with its answer"; app default is 바로 가입. Quick-path review also reads '바로 가입' (:2357). [DEFAULT]
    • Guest fee: app = single amount field (0 = free) with explainer caption/live per-seat hint; board = 무료/유료 fork + nested amount (copies 일정 만들기's 참가비). Board omits the caption and live hint. [CONTROL] [BOARD-OMITS]
    • 방문 횟수 제한: no caption on the board; board value 3 vs app default 0 (제한 없음). [BOARD-OMITS] [DEFAULT]
    • Fork label: app '게스트 허용', board '게스트'. [COPY]

1.9 Step 8 / board step 10: 확인 (review) ​

App rows in order (create-club-step8.tsx:208-438; each row = FactRow with emoji glyph, editCue '수정', whole-row tap → step): 이름 / 대표 색상 (swatch; value '대표 색상') / 로고 ('문양' or '업로드됨') / 지역 / [테니스장 — only draft.courtInfo, i.e. the HOME pick's name] / 정기 일정 (first line + secondaryValue lines per extra schedule + '사용 코트: …') / 구성 / 종목 / 실력 / 성격 / 모집 목표 ('N명') / 회비 ('아직 안 정했어요' when skipped, else '{cycle} N일 {amount}') / 가입 방식 / 게스트 ('허용' / '허용 안 함') / [참가비 — only if 허용] / [방문 횟수 제한 — only if 허용] / [클럽 소개 — only if written] / 공개 범위 ('탐색에 공개' / '비공개') / 사진 정책. 기본값 Tag shown only on 구성, 종목, 실력, 성격, 게스트 and only while the value still equals the quick-start seed (:190-199,310-345,380-382).

Board rows (CcStep else-branch :2321-2380): 이름 / 로고 / 활동 지역 / [테니스장 (line2 '홈 코트' + '외 1곳'; omitted on quick)] / 정기 일정 / 구성 / 종목 / 수준 범위 / 성격 / 1인당 경기 수 / 정원 / 회비 / 가입 방식 / 게스트 / 게스트 참가비 / 방문 횟수 제한 / [소개 — omitted on quick] / 클럽 찾기에 공개 / 사진 정책. 기본값 Tag on every seeded row on the quick path (로고, 활동 지역, 구성…사진 정책).

  • Mismatches
    • 대표 색상 row: app has it; board omits it (spec: "the review shows the mark as 로고 and has no row of its own" :2907). [BOARD-OMITS] [ACK]
    • 1인당 경기 수 row: board has it; the app has no such row (it is not shown on review at all). SPEC_CC (:2891,2907) claims the board lists "every answer the app's review shows" and counts 19 rows; the app has 15 always-on rows plus up to 4 conditional ones (테니스장, 참가비, 방문 횟수 제한, 클럽 소개) and no 1인당 경기 수. [BOARD-ADDS]
    • Labels: 지역 → 활동 지역; 실력 → 수준 범위; 모집 목표 → 정원; 참가비 → 게스트 참가비; 클럽 소개 → 소개; 공개 범위 → 클럽 찾기에 공개 with values '탐색에 공개' → '공개'. [COPY] [ACK] for the unified labels
    • 테니스장 row: app = home pick name only, hidden when empty; board = home + '외 N곳'. [CONTROL]
    • 기본값 Tag coverage: app badges 5 rows (and only while equal to the seed); board badges up to 13 rows on quick. [BOARD-ADDS]
    • 회비 '아직 안 정했어요' for skipped: both. ✓ 방문 횟수 '제한 없음': both ✓.
    • Row glyph emojis (🏷️ 🎨 🛡️ 📍 …) not drawn on the board (flat Cells). [COPY]

1.10 Receipt / recovery (post-submit) ​

App CL/create-club/screen/club-setup-receipt.tsx: DetailShell titled '클럽은 만들어졌어요. 설정을 마저 완료해 주세요' / '클럽 생성 결과를 확인해 주세요' (clubs.ts:1167-1168), Banner with body, a failure line per stage (court/home court/schedule/logo/other, :1189-1193), retained-inputs sentence, three footer buttons: retry '완료되지 않은 설정 다시 시도' / '생성 결과 확인 후 계속', end '클럽 유지하고 추가 설정 종료' / '이번 클럽 만들기 종료' (confirm dialog), and '클럽 설정에서 마무리'. Board (CcReceipt :2419-2436, cc-receipt* :2809-2842): EmptyState titles '클럽은 만들었어요' / '클럽을 만들었는지 확인하지 못했어요', one primary ('다시 시도' default / '다시 확인'), header × asks '여기서 끝낼까요?' ('끝내면 이 기기에 남은 진행 상황만 지워요'; '계속 확인' / '끝내기').

  • Board omits the per-failure list, the retained-input sentence and the '클럽 설정에서 마무리' door; end action moves to the header. [BOARD-OMITS] [CONTROL] [COPY]

1.11 Other create-club frames and states ​

  • Submit label '클럽 만들기' both ✓; submitting '만드는 중…' (board :2404) vs app primaryLoading spinner with no label change (create-club-screen.tsx:476). Error: board danger helper '클럽을 만들지 못했어요. 다시 시도해 주세요' (:2408); app string errorCreateFailed = '클럽을 만들지 못했어요' (clubs.ts:1194); how/where it is surfaced (toast vs receipt) was not traced. [COPY]
  • Board success: "클럽 만들기 → the hub with the owner Notice 멤버를 초대해 보세요" (SPEC_CC :2899). Not checked in app code (outside wizard).

2. 일정 만들기 (create-session) ​

2.0 Structure ​

App (SE/create-session/create-session-screen.tsx:1-31, STEP_COUNT = 7 at SE/create-session-draft.ts:219, step-renderer.tsx). Titles from I18N/sessions.ts:

idxStepFileTitle
0언제create-session/step1/create-session-step1.tsx'언제 만나나요?' (sessions.ts:419)
1어디서create-session/step2/create-session-step2.tsx'어디서 하나요?' (:428)
2어떻게 치나요create-session-step3.tsx'어떻게 치나요?' (:472)
3몇 명이서create-session-step-players.tsx'몇 명이서 뛰나요?' (:497)
4옵션 (skippable)create-session-step4.tsx'추가 옵션' (:527)
5경기 규칙create-session-step5.tsx'경기 규칙' (common.ts:341) + sub '한국 표준 규칙' / '이전에 사용한 규칙' (sessions.ts:950-951)
6확인create-session-step6.tsx'확인해 주세요'

Skippable: idx 4 only (create-session-screen.tsx:348-361). Primary labels: 다음 / step 5 '이대로 진행' (sessions.ts:952, wizard-copy.ts:nextLabel) / last '일정 만들기' (common.ts:105).

Board (CS_STEP_TITLE :1170-1178, CsStep :1194-1345): '언제 만나나요?' / '어디서 하나요?' / '어떻게 치나요?' / '몇 명이 치나요?' / '더 정할 게 있나요?' / '이런 규칙으로 할게요' / '확인해 주세요'; skip on n=5 (:1337); step-6 primary '이대로 하기' (:1182).

  • Title mismatches: step 4 '몇 명이서 뛰나요?' vs '몇 명이 치나요?'; step 5 '추가 옵션' vs '더 정할 게 있나요?'; step 6 '경기 규칙' vs '이런 규칙으로 할게요' (app's card title is '이런 규칙으로 진행할게요', sessions.ts:949); primary '이대로 진행' vs '이대로 하기'. [COPY]
  • Bigger structural mismatch: the board's ideal (cs-ideal, CsReview, csEntry) is a review page reached from an entry MenuSheet ('어떤 일정인가요?' with 정기 일정 rows, 지난 일정 불러오기, 새로 만들기), whose Cells open Sheet editors (IA ruling 9, SPEC_CS :1738,1758); the wizard steps are the "first-timer path". The app has NO review-first page and NO entry sheet: it always opens the 7-step wizard, and prefill is inline on step 1 (see 2.1). [BOARD-ADDS] [ACK]

2.1 Step 0 / board step 1: 언제 ​

App (step1/create-session-step1.tsx):

  • Above the stack, optional prefill block, club flow: '정기 일정에서 불러오기 (선택)' (sessions.ts:12) SelectionChip radio list of active regular meets + '직접 입력' (common.ts:452) (regular-meet-prefill-section.tsx); picking one seeds 시작/종료, 어디서 (step 2) and 최대 인원 silently, date still needs a pick (:188).
  • Pickup flow only: '지난 일정 불러오기' list + '새로 만들기' (sessions.ts:17-18; past-session-prefill-section.tsx; gated isPickup && seeds.length > 0 in use-past-session-prefill.ts:61). Prefill law (canon/wizards.md): confirm only when it would overwrite edited fields.
  • '날짜' (common.ts:101) required: club = SessionDateCalendar (:229, restricted to the chosen meet's rrule when one is picked); pickup = PickerRow + native DateTimePickerModal ('날짜 선택') (:218-228). Picking a date that already has a session asks '이 날짜에 이미 일정이 있어요' / '같은 날짜에 일정을 하나 더 만들까요?' / '하나 더 만들기' (sessions.ts:424-426; :111-124).
  • '시간' TimeBandField (overnight domain), defaults 19:00-21:00 written when a valid date is first picked (:80-81, effect). Errors: '오늘 이후의 날짜를 선택해 주세요'.

Board (n=1 :1201-1217): '날짜' FvkCalendar mode="month" required, past disabled; '시간' TimeBand revealed once the date is answered. Entry sheet cs-entry* (csEntry :824-867) replaces the inline prefill chips.

  • Mismatches: prefill chips are not on the step (moved to the entry sheet) [BOARD-OMITS]/[ACK]; the board offers 지난 일정 불러오기 to club hosts too (csEntry :851-855), the app offers it to pickup hosts only [BOARD-ADDS]; pickup path uses a native date modal in the app, the board's calendar for all [CONTROL]; duplicate-date confirm not drawn [BOARD-OMITS] (grep of the board finds no '이미 일정이 있어요'); error copy '오늘 이후의 날짜를 선택해 주세요' vs board '날짜를 골라 주세요' / '오늘 이후의 날짜를 골라 주세요' [COPY].

2.2 Step 1 / board step 2: 어디서 ​

App (step2/create-session-step2.tsx):

  • Unpicked: LabeledField '테니스장 이름' (common.ts:332) required + search-trigger Pressable (placeholder '장소 이름을 검색해 보세요' sessions.ts:435) → VenueSearchTakeover (select-then-confirm default). Hint '테니스장을 선택해 주세요'.
  • Picked: collapses to PickedVenueCard (name, district, court count, owner-certification badge) with a 변경 ActionPill (clears the venue, court usage and meet link, toast '정기 일정 연결이 해제됐어요') and the attached court-usage section (courtUsage, :143-160): same CourtUsagePanel as club step 2 (chips for ≤12 courts; Stepper '사용 코트 수' + names otherwise), caption '장소에 설정된 코트 수를 사용해요' when from the club's config (sessions.ts:485), pencil '정보 수정' to venue detail. A venue with no court count: '이 장소는 등록된 코트 수 정보가 없어요' (:464).
  • Gate step2Valid = spineVenueId !== null || courtVenueId !== null (step2-valid.ts).

Board (n=2 :1218-1247): '테니스장' FvkPickerCell required (error '테니스장을 골라 주세요'); when answered: map face + '사용하는 코트' multi ChoiceSet ['1번','2번','3번','4번'] (board spec: no court count → Stepper '사용 코트 수', >7 courts → Picker Cell into a Sheet of check rows, SPEC_CS :1758).

  • Mismatches: label '테니스장 이름' (trigger) / card vs '테니스장' [COPY]; 변경 pill, certification badge, 정보 수정 pencil, court rename, the ≤12 threshold (board 7) not drawn [BOARD-OMITS]/[CONTROL]; board's map face [BOARD-ADDS] (app card has coordinates but I did not verify a map thumbnail).

2.3 Step 2 / board step 3: 어떻게 치나요 ​

App (create-session-step3.tsx, RevealStack :401-445):

  1. '종목' (common.ts:379) required ChoiceSet from getFormatOptions(composition): 복식, 단식, [혼복 when 혼성], 무관. Seeded from the club's preferredFormat (club flow).
  2. Club flow only: '경기 유형' (common.ts:208) DropdownChip 정기 / 대회 / 리그 / 대항전 (style-axis-options.ts:55-62; common.ts:215-221), default 정기.
  3. '구성' ChoiceSet 남성/여성/혼성 + caption glossary.composition; seeded from the club (pickup: 'any').
  4. '실력' with InfoHintButton → TierInfoSheet; fork '클럽 기준 그대로' (club) / '실력 무관' (pickup) | '범위 설정' (sessions.ts:479) + TierRangeSelector in an AttachedPanel.
  5. '경기 분위기' (sessions.ts:486) two SelectionCards '친선' | '경쟁' (common.ts:99,82), default 친선 (isFriendly: true).
  • Only 종목 gates (step3Valid; 혼복 requires 혼성). Hint '종목을 선택해 주세요'.

Board (n=3 :1248-1274): '종목' ['단식','복식'] required → (answered) '경기 유형' ChoiceSet ['정기','대회','리그','대항전'] → '구성' ['남성','여성','혼성'] → '수준' ['클럽 기준 그대로','범위 정하기'].

  • Mismatches: 종목 options (board lacks 혼복 and 무관; order 단식, 복식 vs 복식, 단식) [BOARD-OMITS] [ORDER]; 경기 유형 as Choice set vs app DropdownChip, and the board draws it for pickups too while the app shows it for clubs only [CONTROL]; 경기 분위기 (친선/경쟁) not drawn at all [BOARD-OMITS]; 실력 vs 수준, 범위 설정 vs 범위 정하기 [COPY]; no 구성 caption, no tier info door [BOARD-OMITS]; 수준 band Stepper [BOARD-ADDS].

2.4 Step 3 / board step 4: 몇 명이서 ​

App (create-session-step-players.tsx):

  1. '한 경기 시간' (sessions.ts:502) required, caption '코트 하나에서 한 판이 걸리는 시간이에요': ChoiceSet '시간 미정', 20분, 30분, 40분, 60분, '직접 입력' (DURATION_PRESET_MINUTES = [20,30,40,60] :88; sessions.ts:504), '직접 입력' reveals '직접 입력 (분)' numeric field (min 5 → '경기 시간은 최소 5분 이상으로 입력해 주세요'). 미정 counts as an answer. Hint '경기 시간을 선택해 주세요 (미정도 괜찮아요)'.
  2. '최대 인원' (common.ts:109): when a duration is set → AttachedPanel receipt '인원 계산' (+ '추천' Tag, summary '{duration} 동안 총 N개 경기 슬롯, 1인당 a~b경기 예상', tags '동시 경기 N명' / '추천 참석 a~b명', feedback line, and a '최대 N명으로 맞추기' button) above a SuggestedCountField (TRACK/PIN Stepper, step by format, min by format, max 16; captions '제안/고정 N명', '코트 N면 기준', '1인당 약 N경기'); when 시간 미정 → plain Stepper.
  3. '최소 인원 (시작 기준)' Stepper + caption '최소 인원 미달 시 일정이 자동 취소돼요' (sessions.ts:480-481).

Board (n=4 :1275-1296): '한 경기 시간' ChoiceSet ['20분','30분','40분','시간 미정'] required; '최대 인원' FvkInput '8' unit '명'; '최소 인원' FvkStepper 2-8.

  • Mismatches: 60분 and 직접 입력 not drawn; '시간 미정' placed last vs first [BOARD-OMITS] [ORDER]; 최대 인원 typed vs TRACK/PIN stepper with the calculator receipt and recommended-range feedback [BOARD-OMITS] [CONTROL]; '(시작 기준)' and the auto-cancel caption not drawn (board only moves the auto-cancel note onto the review row) [COPY]; board spec: "최대 인원 (2 to 32) is typed" vs app max 16 [CONTROL].

2.5 Step 4 / board step 5: 옵션 (skippable) ​

App (create-session-step4.tsx, RevealStack all pre-confirmed :536-562):

  1. 참가비: club = Pressable row '별도 참가비' + TogglePill (help '회비에 참가비가 포함돼요. 추가 비용이 있는 일정만 켜세요') → when ON '참가비 (1인)' numeric field (placeholder '예: ₩ 10000', hint '금액은 저장을 눌러야 반영돼요' and, if > 0, '참가비가 있는 일정은 입금 확인 후 참석이 확정돼요'); pickup = '참가비 (1인)' field always shown with '비워두면 무료예요' (sessions.ts:529-535; :202-300).
  2. '응답 기한' (common.ts:132) DropdownChip: 없음 (default), 1시간 전, 3시간 전, 12시간 전, 1일 전, 2일 전, '직접 설정' → ModalPanel (PillNav 시간/일 + Stepper + receipt '… 까지 응답' + 확인); caption '참석자에게 이 시간까지 응답을 요청해요. 기한이 지나도 자리가 남아 있으면 참석할 수 있어요' (create-session-helpers.ts:233-242, step4.tsx:365-445, sessions.ts:559). Default rsvpDeadlineMinutes: null.
  3. '반복 일정' (common.ts:333) DropdownChip '단일 일정' (common.oneTime, common.ts:334) / '매주 반복' / '매월 반복', default once; when repeating, '반복 종료' DropdownChip 4주 / 8주 / 12주 / ∞ with caption '총 N회 만들어져요' / '최대 1년까지 자동 반복돼요' (create-session-helpers.ts:312-326).
  4. '공지 (선택)' (sessions.ts:571) multiline, counter 'n / {SESSION_DESCRIPTION_MAX}', placeholder '참석자에게 알릴 내용을 적어 주세요'.

Board (n=5 :1297-1325): '참가비' ChoiceSet ['무료','유료'] (무료 default) with nested required '1인당 참가비'; '신청 마감' FvkPickerCell value '1일 전' (Sheet deadline options 1시간 전, 3시간 전, 12시간 전, 1일 전, 2일 전, '직접 정하기'); '반복' ChoiceSet ['반복 안 함','매주','매월']. No 공지 in the step (공지 exists only as a Sheet editor on the review page, csEditor :1050-1058).

  • Mismatches
    • 공지 missing from the board's step 5 [BOARD-OMITS].
    • 신청 마감 label vs app '응답 기한'; default 1일 전 on board vs app 없음 (null); board Sheet has no '없음' row; '직접 정하기' vs '직접 설정'; the 직접 설정 modal (hours/days + Stepper + receipt) not drawn; the caption not drawn [COPY] [DEFAULT] [BOARD-OMITS].
    • 반복: label '반복' vs '반복 일정'; options 반복 안 함/매주/매월 vs 단일 일정/매주 반복/매월 반복 [COPY]; 반복 종료 (4주/8주/12주/∞ + count caption) is in the board's review Sheet (csEditor 'repeat', '반복 끝' options 4주/8주/12주/계속 반복) but not in step 5 [BOARD-OMITS] (step) [COPY].
    • 참가비 control: app club = Switch row + amount; board explicitly bans the Switch (Choice set 무료/유료) [CONTROL] [ACK] (SPEC_CS :1758). Pickup path (always-on amount field, '비워두면 무료예요') not drawn separately [BOARD-OMITS].
    • Board adds the "fee needs an account" warning Notice + gated primary (noaccount, noAcctTitle '입금 계좌를 등록하면 참가비를 받을 수 있어요' / '계좌 등록하기', :598-599); the app wizard has no such notice at this step (participationFeeGateNote text only). [BOARD-ADDS]

2.6 Step 5 / board step 6: 경기 규칙 ​

App (create-session-step5.tsx): page title '경기 규칙', subtitle '한국 표준 규칙' / '이전에 사용한 규칙'; whole-card Pressable Card titled '이런 규칙으로 진행할게요' with MetaSegments summary (format, N게임, 차이 N, 타이브레이크/없음, 무승부 허용/없음) and detail lines 시작 점수 / 듀스·노애드 / 타이브레이크 (trigger + points) / 시간 제한 / 무승부 허용; tap opens MatchRulesEditorSheet (ModalPanel, header X, MatchRulesEditor, footer reset-to-default + 취소 + 저장). 저장 commits to the draft and toasts '경기 규칙을 수정했어요' WITHOUT advancing; the shell primary '이대로 진행' advances (create-session-screen.tsx:224-246,339-340). MatchRulesEditor (SE/components/match-rules-editor.tsx): 시작 점수 Stepper, 세트 방식 DropdownChip, 한 세트 게임 수 Stepper, 차이로 승리 Stepper, 듀스 TogglePill, 타이브레이크 TogglePill + trigger/score(7|10)/winBy(1|2) PillNav, 시간 제한 TogglePill + minutes (30/45/60/90/120) + 시간이 지나면 (서든데스 1게임 | 현재 점수로 종료), 무승부 허용 TogglePill, derived caption '세트 방식이 세부 규칙을 정해요'. Board (n=6 → MrFields BD/flat-v2-forms.jsx:~3732-3820): the rules form drawn INLINE in the step: '세트 수' PickerCell ('한국 표준 단판'), '시작 점수' Stepper (0-0), '한 세트 게임 수', '차이로 승리', '게임 방식' ChoiceSet [듀스, 노애드], check-rows 타이브레이크 (expand: 시작 시점 6-6|5-5, 점수 7점|10점, 점수 차 1점|2점), 시간 제한 (제한 시간 30–120분 + 시간이 지나면 radio rows), 무승부 허용.

  • Mismatches: summary-card-plus-editor-sheet (app) vs inline form (board) [CONTROL]; toggles vs check rows / Choice sets [CONTROL]; labels '세트 방식' vs '세트 수', '듀스' toggle vs '게임 방식' 듀스/노애드, 타이브레이크 trigger copy 6-6/5-5 [COPY]; '현재 점수로 종료' vs '그때 점수로 끝내기' [COPY]; app's "이전에 사용한 규칙" provenance + reset-to-default footer not drawn [BOARD-OMITS].

2.7 Step 6 / board step 7: 확인 ​

App (create-session-step6.tsx, session-record/session-record.tsx:~25-105): FactList of the session record, one FactRow per field in this order: 날짜, 시간, 반복 ('반복 일정'), 응답 기한, 장소, 코트, 종목, 한 경기 시간, 경기 규칙, 실력, 구성, 최대 인원 (with note '최소 인원 미달 시 일정이 자동 취소돼요'), 참가비, 취소 규정, 공지 (labels from common.ts: date 날짜, timeRangeLabel 시간, recurrence 반복 일정, rsvpDeadline 응답 기한, venue 장소, courts 코트, format 종목, matchRules 경기 규칙, maxPlayers 최대 인원, fee 참가비, notice 공지). Fixed-after-create rows (응답 기한, 종목, 한 경기 시간) carry '만든 뒤에는 바꿀 수 없어요'. Pickup: extra Card '번개로 등록돼요. 누구나 참석 신청할 수 있어요.' (sessions.ts:580). Board (CsRecord :717-782): 일시, 반복, 신청 마감, 테니스장, 코트, 종목 (+ event type fact), 한 경기 시간, 경기 규칙, 수준, 구성, 최대 인원 (line2 '최소 4명'), 참가비, 취소 규정 (locked; absent for pickups), 공지.

  • Row ORDER is identical apart from the date/time split (both: recurrence, deadline, place, courts, format, duration, rules, tier, composition, capacity, fee, [cancel], notice).
  • Mismatches: 날짜 + 시간 are two rows in the app, one '일시' row on the board [ORDER]; labels 응답 기한→신청 마감, 장소→테니스장, 실력→수준, 반복 일정→반복 [COPY]; 취소 규정: the app's wizard review has NO such row ("취소 규정 is the club's rule, not a wizard answer: no review row", SE/session-record/record-from-wizard.ts:46), the board's step-7 record (CsRecord v="fresh", :1329) draws it as a locked row for club sessions [BOARD-ADDS]; pickup notice Card: app shows it, board explicitly drops it (SPEC_CS :1758 "a pickup's 매치로 등록돼요 is gone") [BOARD-OMITS] [ACK]; consequence notes: app puts '만든 뒤에는 바꿀 수 없어요' on 3 rows (session-record.tsx FIXED_AFTER_CREATE = deadline, format, rotation) and the min-players note on 최대 인원; the board draws only the '최소 4명' second line, no locked-after-create notes [BOARD-OMITS]; 반복 row: app shows the count (recurrenceCount '총 N회 만들어져요') as second line, board 'until date' ('10월 17일까지') [COPY].

2.8 Header / provenance ​

  • Board header subtitle 'club | 정기 일정에서 / 지난 일정에서 / 클럽 기본값 / 매치' (csProv :689-709). The app wizard header has no subtitle or provenance (WizardShell takes none). [BOARD-ADDS]

3. 온보딩 (onboarding wizard) ​

App (ON/onboarding-wizard-screen.tsx; step constants ON/onboarding-validation.ts:11-19; copy I18N/onboarding.ts): value-prop pager FIRST, then WizardShell with 6 segments.

#StepComponentContent / copy
pre3-card value-prop pager (header 건너뛰기, CTA '시작하기')onboarding-value-props.tsx'번개 한 판, 두 명만 더' / '근처 테니스장에서 바로 합류할 수 있어요'; 'ELO·매너로 믿고 치는 상대' / '실력과 매너 검증된 플레이어와 함께해요'; '총무 잡일은 앱이' / '출석·회비·일정을 자동으로 관리해 드려요' (onboarding.ts:6-11)
0Welcomeonboarding-step1-welcome.tsx'다붓이에 오신 걸 환영해요!' / '테니스 클럽 활동을 더 쉽고 즐겁게 만들어 드릴게요.'; CTA '시작하기'
1출생 연도onboarding-step-birth-year.tsx'출생 연도를 알려주세요' + caption '테니스 실력 등급, 적합한 클럽 추천, 그리고 만 14세 미만 가입 제한을 위해 사용돼요.'; required LabeledField '출생 연도' 4-digit number-pad (1900-this year), placeholder '예) 1990', error '1900년 이후의 유효한 연도를 입력해 주세요', live '만 N세'; hint '출생 연도를 입력해 주세요'
2약관 동의 (or under-14 gate)onboarding-step-consent.tsx, onboarding-step-under-14-gate.tsx'이용약관 동의' / '서비스 이용을 위해 아래 약관에 동의해 주세요.'; one checkbox row per KR consent with showAtSignup: '[필수] 서비스 이용약관에 동의합니다', '[필수] 개인정보 처리방침에 동의합니다', '[필수] 개인정보 국외 이전에 동의합니다', '[선택] 위치 정보 수집·이용에 동의합니다', '[선택] 마케팅 푸시 알림 수신에 동의합니다' (onboarding.ts:68-88), each with a '보기' link (pushes the policy URL). Hint '필수 약관에 동의해 주세요'. Under 14 → gate '보호자 동의 절차는 아직 지원하지 않아요' / '다붓이는 만 14세 이상부터 이용할 수 있어요…' + '뒤로 가기'
3이름onboarding-step2-display-name.tsx'어떻게 불러드릴까요?' / '클럽 멤버들에게 표시되는 이름이에요.'; required '이름' with counter 'n / DISPLAY_NAME_MAX', placeholder '이름을 입력해 주세요', error '이름은 {min}~{max}자여야 해요'
4이용 방식onboarding-step3-play-style.tsx'주로 어떻게 테니스를 즐기세요?' / '맞춤 경험을 위해 알려 주세요. 나중에 변경할 수 있어요.'; required '이용 방식' three SelectionCards: '클럽' ('정기 클럽 중심으로 활동해요'), '번개' ('자유롭게 번개 경기를 즐겨요'), '둘 다' ('클럽과 번개 모두 즐겨요'); hint '이용 방식을 선택해 주세요'
5완료onboarding-step4-done.tsx'준비 끝! 🎾' / '홈 피드에서 주변 번개와 클럽을 확인해 보세요.'; CTA '시작하기' → writes profile + consents, completeOnboarding(), replaces to home

Nav: header X → confirm '가입을 그만두시겠어요?' / '지금까지 입력한 내용은 저장되지 않고, 로그아웃돼요.' ('계속 진행' / '로그아웃'); jump-back via completed segments capped by firstInvalidOnboardingStep (validation.ts:30-45).

Board (BD/flat-v2-auth.jsx, section AuSecOnboarding :971-1170, specs AU_SPEC.verify/stops/consent/start :2199-2298): 3 steps "본인인증 → 약관 동의 → 첫 시작" (owner review 4, confirmed 2026-10-28):

  1. 1/3 본인인증 AuPgVerify (:555-687): title '본인인증을 해 주세요', reason line '한 사람이 한 계정만 쓰도록 확인해요. 클럽 회비와 경기 기록을 믿을 수 있게요.', quiet facts '주민등록번호는 받지 않아요' + '받는 정보는 이름, 생년월일, 휴대폰 번호, 중복 가입 확인용 정보예요'; CTA band = four provider buttons PASS / 토스 / 네이버 / 카카오 + quiet link '다른 방법으로 인증' (PortOne v2 / KG이니시스 통합인증). States: busy, checking, cancel, offline, fail, 본인 명의 휴대폰이 필요해요, Danal fallback, under 14 stop ('만 14세부터 이용할 수 있어요' + 로그아웃), 이미 가입한 계정이 있어요 (dup CI). Exit dialog '가입을 그만둘까요?' / '입력한 내용은 저장되지 않고 로그아웃돼요.' ('계속하기' / '로그아웃').
  2. 2/3 약관 동의 AuPgConsent (:787-814): title '약관에 동의해 주세요', FvkConsentGroup with a 전체 동의 master row, four rows, hairline, marketing row; the same five legal labels; primary '동의하기', soft-disabled when one required row remains, inline danger line on the missing row; '보기 ›' pushes a doc page.
  3. 3/3 첫 시작 AuPgStart (:915-967): '어떻게 시작할까요?' with three nav Cells '클럽에 가입할래요' / '매치로 칠래요' / '클럽을 만들래요' (each with a line 2), no CTA band, header 건너뛰기; skipped when sign-up began from an invite code/share link.

Mismatches (almost everything; the board is a redesign, not a re-skin):

  • Dropped from the board entirely: value-prop pager, Welcome, 출생 연도, 이름 (display name), 이용 방식 (play style), 완료. Board rationale: "The verified name and birth date fill the display name and the age that the old name and birth-year steps asked" (AU_SPEC.verify :2210); "The first-action chooser replaces the play-style step" (AU_SPEC.start :2296). [BOARD-OMITS] [ACK]
  • 本人인증 (본인인증) is new: nothing in the app implements it (grep of packages/apps/supabase for inicis/identity-verification/본인인증 finds no app code; blocked on EXT-19 per spec :2206). The app collects birth year by typing. [BOARD-ADDS] [ACK]
  • Under-14: app = gate inside consent step (computed from typed birth year); board = stop on step 1 from verified age. 14-18 simply continue in both. [CONTROL]
  • 약관 동의: app has no 전체 동의 master row, no 동의하기 label (app uses 다음), no inline per-row error, '보기' is a text link (board '보기 ›' cell). Same five labels, same required/optional split. [BOARD-ADDS] [COPY] [CONTROL]
  • Final destination: app writes profile + consents at the 완료 step then goes home; board writes at step 3 ("Each writes first") and routes by the chosen first action (클럽 tab / 경기 › 매치 찾기 / 클럽 만들기) via resolveOnboardingDestination. [BOARD-ADDS]
  • Exit dialog copy: app '가입을 그만두시겠어요?' / '지금까지 입력한 내용은 저장되지 않고, 로그아웃돼요.' / '계속 진행' + '로그아웃'; board '가입을 그만둘까요?' / '입력한 내용은 저장되지 않고 로그아웃돼요.' / '계속하기' + '로그아웃'. [COPY]
  • Progress: app 6 segments ("n / 6"); board "1/3…3/3". [CONTROL]
  • App name on the board is deliberately absent (owner review 47); app copy still says 다붓이 (onboarding.ts:13, under14.body).

4. Task 2 lookups ​

4a. Non-member view of a club and the join flows ​

A visitor never sees the member club page. ClubDetailScreen redirects non-members to a separate route.

  • CL/club-detail-screen.tsx:173-181: if (ctx.ready && club && !isMember) appRouter.replace(routes.clubPreview(clubId)) (comment :175 "Non-members bounce to the restricted preview").
  • Route apps/mobile/app/clubs/[clubId]/preview.tsx → PublicClubProfileScreen (CL/public-club-profile-screen.tsx); routes.clubPreview = /clubs/{id}/preview (packages/app/src/navigation/routes.ts:88). Members who land on it are bounced back to routes.club (:145-149). Data gate: RLS clubs_discoverable_select (non-discoverable clubs return null → '클럽 정보를 찾을 수 없어요').

What the visitor page shows (public-club-profile-screen.tsx:330-546, header doc :11-39), top to bottom:

  1. DetailShell header="collapsing" with header ⋯ menu = '클럽 공유' only.
  2. MediaHero kind="album" (album pager of up to 10 club photos, else the club cover), logo/flag avatar, title, inline ClubJoinabilityBadge (clubList.joinOpen '가입 가능' / '승인 후 가입' / '초대 전용'), up to 3 glyph facts (실력 range, 성격 if not balanced, 지역).
  3. Leading row: face stack + capacity pill ('N/M명'), passive for non-members.
  4. Archived Banner '보관된 클럽이에요' when archived.
  5. SectionBlock '클럽 정보' FactList: 종목, 실력 (or '실력 무관'), 가입 방식, 홈 코트 (if any), 회비 (:439-458).
  6. '정기 일정' (DayDotsRow + next session, "+N more").
  7. '참석 가능한 일정' — only for an active guest row.
  8. '소개' (auto-linked) when present; '클럽 사진' 3-up gallery when photos exist.
  9. Application panel (Well) only after 가입 신청하기 is tapped.
  10. Pinned BottomCtaBand = JoinCtaBand (public-club-profile/join-cta-band.tsx), by joinPolicy:
    • open → '가입하기' (common.ts:150) → useJoinOpenClub instant join, then replace(routes.club).
    • approval → '가입 신청하기' → inline panel '가입 신청' / '총무가 확인할 수 있도록 간단한 소개를 남겨 주세요.' + StyledInput multiline (placeholder '자기소개를 적어주세요 (선택)', max 500) + '총무가 승인하면 알려드릴게요' → CTA becomes '신청 보내기' (useApplyToClub, optional message). Request states: pending → disabled '신청 검토 중' + hint line; rejected → '다시 신청하기'. Server rule: zero-attendance applicants get '먼저 공개 일정에 참석해 주세요' (clubs.ts:449, migration 00415).
    • invite_only → '초대 코드로 가입하기' → pushes routes.joinClub({ clubId }) (separate page).
    • No application questions: only the one optional free-text message. Growth events profile_view/join_tap/application_start/application_submit recorded (:190-261).

/join-club is a separate page, not a state of the club page (apps/mobile/app/join-club.tsx, apps/web/app/join-club/page.tsx, both → JoinClubScreen CL/join-club-screen.tsx). It is invite-code ENTRY only: DetailShell title '{club} 가입하기' (when clubId is in the route) else '클럽 가입'; optional origin-club avatar+name row; hint '초대 코드를 입력해 가입해보세요' / ''{club}' 클럽의 초대 코드를 입력해 주세요'; big letter-spaced code readout + StyledInput (maxLength 10, autoFocus, uppercase, placeholder '초대 코드 입력'); error Banner; CTA '가입하기' disabled under 4 chars; signed-out tap → loginReturnTo(joinClub({clubId, code})). Errors: 'join_unexpected_club' → dialog '다른 클럽의 초대 코드예요'; already member '이미 가입된 클럽이에요.'; bad code '초대 코드가 맞지 않아요. 다시 확인해 주세요.'. It does not resolve the code to a club or show fee/policy before the tap (the repo port has findByInviteCode, club.repository.port.ts:28, but the screen only calls joinByInviteCode). The same flow also serves web /join-club?code=….

Board, club home visitor states (BD/flat-v2-hub.jsx, group "Non-member (review 41)" :3399-3443; HubPgClubPins :2997-3038; HubPgClub v='visitor' :2943-2969, feed visitor :2107-2265):

  • club-visitor (open), club-visitor-approval, club-visitor-invite, club-visitor-pending, club-visitor-rejected, club-visitor-apply (Sheet). Board rule: "review 41: the separate 클럽 미리보기 retires" — the non-member sees the SAME route/page as members (/clubs/[clubId]) with no Tabs, no 관리, the public 소식 feed (public sessions + photos, counts with no names), and a pinned CTA band instead of the FAB (SPEC :3903,3907,3911).
  • CTA copy: open '가입하기'; approval '가입 신청하기'; invite '초대 코드로 가입하기'; pending '승인 대기' (disabled, no line under); rejected '다시 신청하기'. Helper under open/approval = '월 회비 30,000원' (the price of joining; no other meta lines).
  • Apply Sheet (applySheet :3099-3108): title '가입 신청', FvkField '자기소개' optional, counter 0/200, placeholder '예: 구력 3년, 주말 오전에 칠 수 있어요', footer '신청 보내기'.
  • Mismatches vs app: separate preview route (app) vs same page (board) [ACK]; hero/FactList sections of the app (클럽 정보, 정기 일정, 소개, 클럽 사진, 참석 가능한 일정) replaced by a 홈-pane Feed [CONTROL]; pending label '신청 검토 중' + hint vs '승인 대기' no line [COPY]; apply = inline Well panel + pinned submit (500 chars, placeholder '자기소개를 적어주세요 (선택)') vs Sheet (200 chars, different placeholder) [CONTROL] [COPY]; dues shown as helper under the CTA (board) vs in the 클럽 정보 FactList (app) [CONTROL]; app's '먼저 공개 일정에 참석해 주세요' server rejection has no board frame [BOARD-OMITS].

Board jc-* (BD/flat-v2-forms.jsx JcPage :2936-3020, jcBand :3025-3039, JcSection :3041-3120, SPEC_JC :3123): a separate page '클럽 가입하기', "Join with a code: see the club, its fee and its policy, then one tap." jc-ideal = opened from an invite link, code already in hand, no Input: FvkMediaRow club row (logo 56, name, 강남구 | NTRP 3.0–4.0 | 멤버 42명), Cells '월 회비 30,000원' and '가입 방식 바로 가입' (or '승인 후 가입'), CTA '가입하기' (approval: '가입 신청하기'; signed out: '로그인하기'; busy '가입하는 중…'; error '가입하지 못했어요. 다시 시도해 주세요'). Edge states: member ('이미 가입한 클럽이에요'), full ('정원이 다 찼어요'), archived ('보관된 클럽이에요'), other club's code ('다른 클럽의 초대 코드예요' + '원래 보던 클럽에는 가입하지 않았어요'), no-code (Field '초대 코드' placeholder '예: TNK-4821', soft-disabled CTA), unknown code ('이 코드로 찾은 클럽이 없어요. 코드를 다시 확인해 주세요').

  • So: jc-ideal is a separate page in both (app /join-club, board /join-club), but the app's page has no club row, no fee/policy Cells, no resolving/member/full/archived states (the app resolves nothing before the tap), an always-visible code input with a large code readout, and CTA copy '가입하기' (common.ts:150). [BOARD-ADDS] (resolve-by-code, club row, fee/policy, full/archived/member Notices) [BOARD-OMITS] (the large letter-spaced code readout; dev origin-club row is in the board's club row). The 가입하기 in jc-ideal is NOT a state of the club page; the club page's own join CTA (visitor frames above) is a different surface.

4b. 연습 기록 (practice log) ​

App:

  • Screen packages/features/profile/src/practice-log-screen.tsx; route apps/mobile/app/profile/practice.tsx → PracticeLogScreen; routes.practice (packages/app/src/navigation/routes.ts).
  • Only entry point: ProfileMenuCard row '연습 기록' (practice.menuRow, I18N/home.ts:206), packages/features/profile/src/profile-menu-card.tsx:46-50 (appRouter.push(routes.practice)), rendered by profile-screen.tsx:425. The file's own comment (:36-41): "Practice logging (routes.practice) has no other entry point anywhere in the app". Menu = 통계 공유 · 연습 기록 · 환경설정 · 로그아웃.
  • What it records: PracticeLog { id, date, type, durationMinutes }; usePracticeLogs(userId, 50) newest first; useCreatePracticeLog writes { userId, date: getLocalToday(), type, durationMinutes }. Create only — no edit/delete hook (board spec says the same: "Owed: an edit and a delete").
  • Layout: DetailShell '연습 기록'; StatStrip cells '횟수' (list length) and '총 시간' (sum, 'N분') (practice.countLabel/totalLabel); FeedList of ListRows (accent bubble icon, title = type label, sub = date yyyy.mm.dd, trailing Tag '30분'); empty EmptyState '아직 연습 기록이 없어요' / '코트 밖에서의 노력도 실력으로 쌓여요'; pinned BottomCtaBand '연습 추가'.
  • Add flow: ModalPanel '연습 기록 추가' with '연습 종류' PillNav (서브, 랠리, 벽치기, 레슨, 드릴, 연습 경기, 기타 — defaults 랠리) and '시간 (분)' PillNav 30/60/90/120분 (default 30), buttons '취소' + '저장' (home.ts:204-219); date is always today (no date picker).

Board:

  • The page: BD/flat-v2-list-b.jsx section PracticeSection :1218-1280+ (frames pl-ideal, pl-first, pl-overflow, pl-loading, pl-error, pl-cold, pl-stale, pl-sheet, pl-entry, + delete Dialog), components PracticePage :1048-1128, PracticeSheet :1134-1164, PracticeEntry :1167-1178, PracticeDelete :1180-1189, SPEC_PRACTICE :1191-1216.
  • Reached from 기록 › 분석 (not 프로필): the 기록 hub page draws the door Cell '연습 기록' (FvkTaskCell) under the 전적 activity grid (BD/flat-v2-hub.jsx:1526; frames rec-ideal, rec-few (it is also in the unrated state), spec :3845,3853,3857); the practice spec states "pushed from 기록 › 분석 (the hub board: 프로필 no longer holds it)" (list-b.jsx:1194). No prof-* frame (flat-v2-hub.jsx:86-101) draws a 연습 기록 or menu row.
  • Mismatches vs app
    • Entry point moves from the 프로필 menu card (app) to a Cell on 기록 (board); the profile menu (통계 공유 / 환경설정 / 로그아웃) is redrawn elsewhere. [CONTROL] [ACK]
    • List grouped by day with FvkDayHeader ('9월 18일 (금)', '오늘'/'3일 전') and value Cells (type | minutes); app = flat rows with a per-row date and a Tag. [CONTROL]
    • Strip tiles '횟수' + '총 시간' ✓ same labels; board numbers '2,340분' formatting.
    • Create entry: FAB '연습' (spoken '연습 추가') instead of a pinned CTA band '연습 추가'; add Sheet titled '연습 기록 추가' with Choice sets (연습 종류, 시간 (분) — 30/60/90/120분) and ONE 저장 button + × (no 취소 pair); dirty × asks '나갈까요?' [CONTROL] [COPY]. Type list identical (서브, 랠리, 벽치기, 레슨, 드릴, 연습 경기, 기타). ✓
    • Board adds edit + delete (row → MenuSheet '9월 18일 서브' with 수정 / 삭제 → Dialog '이 연습 기록을 삭제할까요?' / '횟수와 총 시간에서 빠져요'); no hook exists for them in the app. [BOARD-ADDS]
    • First-run copy: app '코트 밖에서의 노력도 실력으로 쌓여요' vs board '코트 밖에서 한 노력도 실력이 돼요'. [COPY]

5. Cross-wizard patterns the board gets "wrong" relative to the app (for a single fix list) ​

  1. Copy not ported: every board label rename listed [COPY]+[ACK] (실력→수준 범위, 모집 목표→정원, 클럽 소개→소개, 공개 범위→클럽 찾기에 공개, 지역→활동 지역, 참가비→게스트 참가비, 장소→테니스장, 응답 기한→신청 마감, 반복 일정→반복). The app copy has not been changed. These need an I18N migration if the board is the target.
  2. Defaulted-axis facts that contradict app defaults: 사진 정책 default (app 멤버 업로드 후 승인 vs board 운영진이 골라요), 가입 방식 default (app 바로 가입 vs board frame 승인 후 가입), 반복 (app unset-required vs board 매주 default), 신청 마감 (app 없음 vs board 1일 전), 공개 범위 (app unselected-until-touched vs board 공개 pre-selected), 방문 횟수 제한 (app 0 vs board 3).
  3. Fields the board never draws that the app requires/uses downstream: club step 2 court-usage panel (사용 코트 수/이름) → step 3 사용 코트 subset and step 6 court-cost math; club 반복 monthly variants; region fallback; the full dues calculator (시간당 코트비, 운영비, 제안 월 회비); the 모집 목표 receipt; session 경기 분위기; session 한 경기 시간 60분/직접 입력; session 인원 calculator; session 공지 in step 5; 응답 기한 없음 + 직접 설정; 반복 종료 in step 5.
  4. Board-only additions needing new work (not a port): 본인인증 (EXT-19), profile-seeded 활동 지역, review-first create-session page + entry MenuSheet, delete/edit for practice logs, 무료/유료 fork replacing the 별도 참가비 switch, resolve-code-before-join page, 전체 동의 master row, 첫 시작 chooser, slider Steppers (hue/time/level), 6-value 납부 주기 (일정마다, 한 번) — the domain currently has 4 (discovery.ts:109), a continuous progress bar past 7 steps, soft name-length limit with three messages.
  5. Control changes that look like theme work but alter behavior: Dropdown/DropdownChip → Picker Cell + value Sheet; multi-select days → single-select draft; TogglePill rows → check rows / Choice sets; app's hard maxLength → soft limit; app RemoveIconButton confirm → immediate remove + undo Toast.

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