Screen Blueprints — Matches
Status: Active Last reviewed: 2026-09-11
Current-state blueprints for the matches screens, read from the components. Conventions and the screen index live in screen-blueprint.md. History lives in git and rebuild-log.md — not here.
Match Result Share Preview
- Route:
/m/[matchId] - Screen:
MatchSharePreviewScreen—packages/features/sessions/src/share-receivers/match-share-preview-screen.tsx(route fileapps/mobile/app/m/[matchId].tsxonly passes the param) - Shell: DetailShell · title empty (back-only Header) · no footer
- Design: unreviewed
- Verified: 2026-11-13
- Batch 63 (L1/N2), Wide windows (web): the tab root sits in the reading column (
MainTabShellis alwaysreading, 680) and the day-sectioned session rows are one column at every width; the 2-column packing was removed with theFeedListgrid.
Purpose. What a shared match-result link opens to a person who cannot read the match: the venue, the club, the result, the format and the day, with nothing to act on. A member or a player is replaced to the session. The venue shown is the linked court venue's name only (migration 00758); a match whose session has a typed place and no linked venue shows the derived title (the format word, else 경기) in the Hero, never the typed place. The preview payload no longer carries the session's stored title.
Anatomy.
├─ Header (back, no title)
├─ [Hero] eyebrow = club · title = venue
├─ [SectionHead] 결과
└─ [CellGroup]
├─ 점수 6-4 (labelTag Tag 검토 중, warning, while a score review is open)
│ └─ or a tag Cell with the Tag 기록 제외 and no numerals (a match that does not count)
├─ 종목
└─ 완료 day and timeStates.
Offline (F1c, the app's root strip says it): offline a warm link keeps the cached preview (rc-m-stale).
loading → Hero and four Cell skeletons
result (the score stands)
under review → the 점수 Cell keeps its numerals and carries 검토 중 (the word 분쟁 is never printed)
not counted (cancelled or non-competitive) → 기록 제외, no numerals; it wins over a review
not found → 이 경기를 찾을 수 없어요 / 삭제됐거나 주소가 바뀌었어요 · 홈으로 가기
offline cold start → the offline EmptyState with retry, never not found
error → the error EmptyState naming 경기, with retry
a member or a player (match and session both readable) → replaced to
routes.clubSession2026-11-13 (UX-7, the state helper): Offline, error and loading from the one
PageStateView.
Actions.
- back (Header)
- retry (offline / error); 홈으로 가기 (not found)
Gaps. The match read carries no event type, so a pickup match shows its personal pickup club's fixed name as the eyebrow; team names, ELO and set scores are never shown by rule; the web page is L0-07; an old-shaped payload (with sessionTitle) still parses and the title goes nowhere; nothing ran on a device.
Manage Scorecard (스코어카드 운영, L3-39)
- Route:
/match-board— opened with a ?sessionId= query param (retired name 대진표; the helper is routes.matchBoard) - Screen:
OperatorScorecardScreen—packages/features/sessions/src/operator-scorecard-screen.tsx: the staff guard in front ofScorecardPage(scorecard-page/scorecard-page.tsx) withuseOperatorLayer(scorecard-operator/use-operator-layer.tsx); the same page withPLAYER_LAYERis L3-38 - Shell:
DetailShell(the shared page's frame) · title 스코어카드 운영 (layer.title) ·subthe venue and the day ·subheadthe Tabs strip · actions 할 일 (badge: the inbox count) and ⋯ · footer the band (CtaBand) ·refreshControl - Design: unreviewed
- Verified: 2026-11-13
Purpose. The host's and the club officials' page for running a session as it plays: the one next step of the phase (the band), the exceptions that need a person (the 할 일 inbox), the structure behind ⋯, and on each card its one state action; built W4 batch 12 to flat-v2-detail#mb-ideal (L3-39).
Now. The page is L3-38's (DtlScPage with role operator), never forked: useOperatorLayer(data) fills the ScorecardRoleLayer slots and renders its own Sheets and Dialogs once through layer.element. Everything it decides is the app's pure rules (scorecard-operator.rules.ts: operatorActionFor, deriveOperatorInbox, deriveOperatorBand, deriveOperatorNotice, deriveOperatorHeader, operatorMenuRows, readiness, queueHeadId), so a Block, a row, the inbox and the band cannot disagree.
- Who operates (ruling O1): the server's staff set (
private.is_session_staff: the host, or an active owner, admin or match director); the client mirror isderiveSessionToolAccess(...).canRunAttendance. The route's guard (use-operator-guard.ts) replaces anyone else with/sessions/{id}/scorecard, once the club's members have been read (a failed or paused read never sends an official away). A staff member who plays a match is a player on it: 점수 입력, never 대신 입력, and never resolves its review. - The band (ruling O11), one step per phase, round numbers derived from the matches: no draw → a soft 경기 시작 with the helper 대진을 먼저 확정해 주세요 (does nothing; the empty state's 대진 짜기 is the way on) · draw made → 경기 시작 (Dialog 경기를 시작할까요?,
start_session_play) · a round with held matches → N라운드 시작 (Dialog 경기 N개를 시작할까요? with the held count,start_round) · a settled round and another due → N라운드 대진 짜기 (today's draw flow) · the next round drawn → N라운드 시작 (one press) · groups over → 본선 대진 만들기 (Dialog,make_knockout) · every match settled → 일정 끝내기 (Dialog; 그래도 끝내기 withcancelUnsettledwhen matches are owed, counted byderiveSessionCloseGate); a round in play, an ended session and every failure have no band. A refused write keeps its Dialog open and says why in place of the body (HINT → words,operator-refusal.ts); success is silent; a step with no question (the next round drawn) fails into a one-button Dialog titled with the step. Never a toast. - 할 일 (ruling O10): rows derived, never counted, in the order unscored (점수 입력 전 · 대신 입력), review (점수 검토 요청 · 검토), late (N분째 [· 이후 경기 N분 늦어짐 where a match carries a planned start] · 코트 보기), unpaid (미납 N명 · 대진에서 빠져 있어요 · 참석자 관리; only before the draw and only for a viewer who sees money). 대신 입력 runs the host entry through the unscored matches the operator does not play, in order; 검토 opens the reviews; an ended session keeps only the icon while a review is open (ruling O8).
- ⋯ (the MenuSheet 스코어카드 운영): from round 1 started 라운드 추가 · 늦게 온 선수 추가 · 팀 바꾸기 · 경기 규칙; before it 팀 바꾸기 · 경기 규칙 · 대진 다시 짜기; with no draw only 경기 규칙. 라운드 추가 and 대진 다시 짜기 open the same 경기 방식 chooser and flows 일정 상세 opens behind its
openDrawseam (use-operator-draw.tsxmountsuseDrawFlowandSessionDetailOverlayson demand) until 대진 짜기 (L3-47) replaces them; the Sheets for 늦게 온 선수, 팀 바꾸기, the review, 대신 입력 and 교체 arescorecard-operator/sheets/(lane U2). - On a card: 시작 on the first queued match of a free court (
start_queued_match, the court the lowest with no match in play), 검토 on a review the operator does not play, 대신 입력 on a match nobody scored, none on a match the operator plays; a live Block on another court opens the host entry when pressed. A refused 시작 shows in the Notice slot as a danger Notice. - The state Notice (a state, never an instruction): N라운드가 모두 끝났어요, 모든 경기가 끝났어요, 예선이 모두 끝났어요, 끝난 일정이에요.
- Whether another round is due (no column says it): the time left before
end_timeagainst the session'srotation_minutes, else the median of the rounds played; with neither, none is due and a settled session ends.
Anatomy.
├─ [OperatorScorecardScreen] guard: pending → page · staff → page · stranger → replace(/sessions/{id}/scorecard)
└─ [ScorecardPage useLayer=useOperatorLayer]
├─ [DetailShell] title=스코어카드 운영 sub=[venue, day] actions=[할 일 badge=N ▸ not ended or a review open, ⋯ ▸ not ended] footer=band
│ ├─ subhead ▸ [Tabs]
│ ├─ ▸ layer.notice → [Notice meaning=done | tone=neutral | tone=danger ▸ a refused 시작]
│ ├─ ▸ no draw → [EmptyState first compact calendar] ▸ action 대진 짜기 (draw flow)
│ ├─ [LiveTab] Blocks (▸ press → 대신 입력 entry, not for a match the operator plays) · queue rows ▸ [sm 시작 | 검토 | 대신 입력]
│ └─ [ScheduleTab | BracketTab | StandingsTab | LadderTab] rows ▸ [sm 시작 | 검토 | 대신 입력]
└─ layer.element
├─ [Sheet bleed 할 일] ▸ [TextRow l1 l2 control sm Button]* | [EmptyState first compact check] 처리할 일이 없어요
├─ [MenuSheet 스코어카드 운영] one group of the phase's rows
├─ [Dialog] the band's question ▸ [닫기 | 경기 보기] + [경기 시작 | 일정 끝내기 | 그래도 끝내기 danger | 본선 만들기]
├─ the Sheets of lane U2 (review, 대신 입력 run, 교체, 늦게 온 선수, 팀 바꾸기)
└─ the draw flow ▸ asked (chooser · 라운드 추가 · tournament)States.
Offline (F1c, the app's root strip says it): offline only the draw door stays live; starting, rounds, knockout and ending wait.
Loading: the title and a disabled ⋯ over the skeleton (the layer hears only "no data yet", so error, offline and not found show the same disabled ⋯ where the board draws none). Error and offline are the page's EmptyStates with retry.
A viewer who is not staff (or not yet known to be) gets the title alone: no 할 일, no ⋯, no band; the guard replaces them once the club's members are read.
Ended: no band, no ⋯, the Notice 끝난 일정이에요, 할 일 alone while a review is open.
Nothing to do: 처리할 일이 없어요 / 점수 입력이나 검토가 필요하면 여기에 모여요, and the icon carries no badge.
2026-11-13 (UX-7, the state helper):
PageStateView(offline, error, loading, not found with 경기 보기, no-permission).
Actions.
- 할 일 → 대신 입력 (host entry through the unscored matches), 검토 (the review Sheet, or the list when there are several), 코트 보기 (closes the list), 참석자 관리 (L3-35).
- ⋯ → 라운드 추가 / 대진 다시 짜기 (the draw flow), 늦게 온 선수 추가, 팀 바꾸기 (Sheets), 경기 규칙 (L4-06).
- The band → the step's Dialog or the draw flow, then
start_session_play,start_round,make_knockoutorcomplete_session. - A row's 시작 →
start_queued_match; a Block's press → the host entry.
Gaps.
- The header registry has no clipboard glyph (the board draws
clipboardCheck): 할 일 is drawn with the alert bell, named 할 일 for the screen reader, until the registry gains it (INBOX_GLYPHinoperator-header.ts). - 코트 보기 only closes the list: the page owns its tab and the layer has no way to select LIVE.
- A 할 일 row for a match the operator plays has no 점수 입력 button: the player's score entry belongs to the page's overlays, which the layer does not reach (they enter it from their Block).
- The no-draw empty state shows the viewer's body (호스트가 대진을 짜면 여기에 보여요); the board's operator body (경기 상대와 코트를 자동으로 배정해요) needs a
layer.emptyBodyslot. - 라운드 추가's commit pushes
/match-boardonto itself (the shared overlay was written for 일정 상세); L3-47 replaces it. - A court that frees does not take the next queued match on its own (the
mb-tncaption); the operator's 시작 is the only way. - The pace rule (
isMatchDelayed) reads the wall clock, not the layer's tick; it matters only where the session sets norotation_minutes. - 팀 바꾸기, 교체 and the unscored row's ⋯ are lane U2's Sheets; their gaps are in their own entry.
Tournament Board (legacy link resolver)
- Route:
/tournament-board— opened with a ?tournamentId= query param - Screen:
TournamentBoardRedirectScreen—packages/features/sessions/src/tournament-board-redirect-screen.tsx - Shell: none (draws nothing; a failed read draws
QueryErrorState) - Design: unreviewed
- Verified: 2026-11-13
Purpose. The tournament board retired with the match board (W4 batch 12): a tournament is played on its session's scorecard, standings and bracket under their tabs (L3-38, L3-39). This route is the legacy link resolver: a tournament id needs one read to find its session, so it stays a small screen rather than a static redirect (R3, the lead's call for the owner's veto, 2026-11-10).
Anatomy.
└─ [TournamentBoardRedirectScreen] useTournament(tournamentId)
├─ ▸ tournament read → replace(/match-board?sessionId={tournament.sessionId}) (the operator route; its guard sends anyone who is not staff to the player's scorecard)
├─ ▸ tournament gone, or an empty id → replace(경기 tab)
└─ ▸ read failed → [QueryErrorState] retryStates. pending: nothing drawn · found: replaced · gone: replaced with 경기 · failed: the retry state (a failed read is shown, never skipped past).
- 2026-11-13 (UX-7, the state helper): Offline, error and loading from the one
PageStateView; the redirect's offline blank is gone.
Actions. 다시 시도 on a failed read; nothing else.
Gaps. The mobile route file sends a missing tournamentId to 홈 itself; the web page passes an empty id on, which leads to 경기.