Overview
Compare
Inspect
Creating a session is a 7-step WizardShell wizard — 언제 → 어디서 → 어떻게 치나요 → 몇 명이서 → 옵션 → 경기 규칙 → 확인 — and editing one happens on the session page itself: the header 수정 → 완료 toggle turns a BARE page (no hero, no hint — EDIT-PAGE-BARE) into the session RECORD's rows (session-record.tsx), the same rows the wizard's own review shows, each trailing a pencil (never 수정 text) and opening its field's BOTTOM edit sheet in place (EDIT-SHEET); 응답 기한·종목·한 경기 시간 lock outright once made (a lock glyph, not a pencil), 참가비 locks once the first payment hold exists, 취소 규정 is always the club's own rule, and 경기 규칙 opens its own screen for a host with the role, else locks with the reason.
route · apps/mobile/app/create-session.tsx → sessions/src/create-session-screen.tsx (wizard) · sessions/[sessionId]/index.tsx → sessions/src/session-detail-screen.tsx (edit state, no separate route)
grounded in 18 source file(s)
packages/features/sessions/src/create-session-screen.tsxpackages/features/sessions/src/create-session/steps/wizard-host.tsxpackages/features/sessions/src/create-session/steps/step-table.tsxpackages/features/sessions/src/create-session/editors/index.tspackages/features/sessions/src/create-session/review/review-page.tsxpackages/features/sessions/src/create-session-draft.tspackages/features/sessions/src/session-record/session-record.tsxpackages/features/sessions/src/session-record/record-values.tspackages/features/sessions/src/session-record/record-from-wizard.tspackages/features/sessions/src/session-record/record-from-session.tspackages/features/sessions/src/session-detail/page/session-info-group.tsxpackages/features/sessions/src/session-detail/editing/use-session-fact-editing.tspackages/features/sessions/src/session-detail/header/use-session-header.tsxpackages/ui/src/cell.tsxpackages/app/src/presentation/components/style-axis-options.tspackages/ui/src/wizard-shell.tsxpackages/app/src/config/i18n/ko/sessions.tspackages/app/src/config/i18n/ko/clubs.ts
Spec
1
Foundations · 디자인 시스템
Spacing · 4px grid
$1 – $4
4 · 8 · 12 · 16 px
$5 – $10
20 · 24 · 32 · 40 · 48 · 64 px
Radius
$2 8 · $3 12 · $4 16 · $7 9999 (pill)
Color · Classic Grass (light)
Surface
$background #F8FBF9 · $card #FFFFFF · $surfaceTertiary #E5FAEA
Brand
$primary #006D37 · $primarySubtle #E5FAEA
Text
$text #0D100E · $textSecondary #4F5851 · $textTertiary #626C64
Border
$border #CAD4CC · $borderSubtle #DFE7E1
Status
$success #14804A · $warning #946200 · $error #B8332E
Badge intents
accent #E0F8F3/#007B6B · warning #FFF4D6/#946200 · error #FDECEC/#B8332E
Court
$courtClay #B84A2E · $courtHard #247BA0 · $courtGreen #006633
Typography · Pretendard
cardTitle · navTitle
16 / 24 · 600 · 17 / 22 · 600
cardBody · cardMeta
14 / 22 · 400 · 13 / 18 · 500
badge · micro
12 / 18 · 600 · 12 / 16 · 600
display Sm/Md/Lg/Xl
20/28 · 28/36 · 36/44 · 48/56 · 700
Primitives · @twomore/ui
Layout
YStack · XStack · Card (default/flat) · Well · Divider
Controls
Pressable · Badge · Icon · PillNav · MainTabShell · BottomCtaBand
Data viz
DateTile · CapacityRing · ProgressBar · AvatarStack · SegmentedDonutChart
Domain cards
SessionCard · ClubCard · VenueHero · SessionCompactRow · member row
Box sizes
AVATAR XS 28 · SM 32 · MD 40 · LG 44 · 2XL 88 · TOUCH_TARGET 44
Rationale
Every part is built from these — one 4px grid, the Pretendard type scale, the Classic-Grass palette, and the shared @twomore/ui primitives + domain cards.
2
Wizard chrome · NAV LAW (2026-08-15)
Structure
Header
WizardShell (wizard-shell.tsx) — NO title. Left: ✕ on step 0, a ChevronLeft back button once step > 0. Center: tap-navigable progress segments + "n / N". Right: 건너뛰기 (secondaryLabel) ONLY when the current step is skippable
Footer
owns exactly ONE job — the single primary CTA (+ optional primaryHint), never a back or secondary slot, on EVERY step including 6/7 경기 규칙 — the step-owned two-button footer that step used to be the sanctioned exception for is RETIRED (owner 2026-08-27); it keeps only its own CTA label override (이대로 진행 instead of 다음)
Step title
each step's own heading renders as the FIRST LINE OF THE BODY (Text role="pageTitle", e.g. "언제 만나나요?" on step 1) — never shell chrome
3
1/7 · 언제 — 정기 일정 불러오기 + 날짜 (club: calendar)
Structure
Sequential reveal
날짜 confirmed by a pick, THEN 시간 reveals — never both fields shown flat together (create-session-step1.tsx RevealStack)
날짜 (club)
SessionDateCalendar — inline month grid marked with the club's existing sessions (dot marker), $primary fill on the selected date; pickup uses a plain PickerRow instead
4
1/7 · 언제 — 시간
Structure
시간
ONE TimeBandField band — overnight-capable 00:00→30:00 domain
5
2/7 · 어디서 — picked venue + 사용 코트 수
Structure
Component
Step2 — VenuePickPanel (favorites/search/browse) collapses to PickedVenueCard (name + 확인됨/미확인 trust chip + 변경) once answered, with an attached CourtUsagePanel (which courts, numbered chips) tied to the SAME answer
Answer rule
다음 stays disabled until a picked court OR explicitly-committed free text (typed search text alone is not an answer)
6
3/7 · 어떻게 치나요 — 형식 → [클럽만] 경기 유형 → 구성 → 실력 → 성격
Structure
Component
Converges onto create-club-step4.tsx's axis grammar, RevealStack order per create-session-step3.tsx:366-404+ (2026-09-18): 형식 (the ONE required axis — club-seeded from club.preferredFormat once that query resolves, U-33 2026-09-17, same as 구성 below; pickup keeps the true null-until-answered start, no chip pre-selected) → 경기 유형 (session-event-type-plan.md D3/D5, slice 4 — CLUB flow only, DropdownChip standalone fullWidth, real default 정기, options 정기/대회/리그/대항전 — never 번개, which the pickup flow stamps itself and has no picker for) → 구성 (club-seeded default, SelectionChip row + a first-encounter glossary caption, gates 형식's 혼복 option) → 실력 (two-tile fork + TierRangeSelector AttachedPanel, default "클럽 기준 그대로") → 성격 (친선/경쟁 SelectionCard pair, default 친선) — the instant 형식 answers, all four ride-along sections become visible together (three for pickup, which skips 경기 유형 entirely)
No tier field
there is no bare numeric tier-range — only the fork
7
4/7 · 몇 명이서 — 경기 시간 → 최대/최소 인원 (live suggestion)
Structure
Component
DURATION-FIRST redesign (owner 2026-08-27, create-session-step-players.tsx): 경기 시간 leads as a SelectionChip row (시간 미정 / 20 / 30 / 40 / 60분 / 직접 입력) — NO 1인당 경기 수 Stepper anymore (matchesPerPlayer retired from this UI). A duration pick unlocks calculateSessionNeeds receipt AttachedPanel + SuggestedCountField 최대 인원 (live suggestion, pin/unpin) + 최소 인원; picking 미정 instead swaps in plain min/max Steppers with no math, no receipt (RevealStack: 경기 시간 gates the 최대/최소 인원 ride-along)
8
5/7 · 옵션 — 참가비 / 참석 마감 / 반복 / 소개
Structure
Component
Step 4 (skippable) — 참가비 (club: toggle reveal / pickup: plain amount) + 참석 마감 DropdownChip standalone (default 없음) + 반복 일정 DropdownChip standalone (default 반복 없음, with 반복 종료 ride-along) + 공지 (Now 2026-09-28: was 한 줄 소개 here, 공지 on the page — one name now; 200-char, save-then-proceed edit scope)
Skip
건너뛰기 is the header-RIGHT secondary button (secondaryLabel, create-session-screen.tsx:348-361, gated on nav.step === 4) — the footer keeps its normal single primary CTA (다음); this is the ONLY skippable step
9
6/7 · 경기 규칙 — the whole card IS the edit affordance
Structure
Component
Step 5 (create-session-step5.tsx) — a hero summary Card (이전에 사용한 규칙 OR 한국 표준 규칙), the WHOLE card Pressable (opens MatchRulesEditorSheet on tap, trailing chevron) — IA repair owner 2026-08-27: no separate 수정하기 row/button anymore. The facts line converged onto MetaSegments (diff since 2026-09-12) — all 5 facts (세트/게임/차이/타이브레이크/무승부), each its own hairline-broken segment, never a "·"-glued string; 듀스 OFF now reads 노애드 (was "듀스 ✕")
Shell primary restored
proceeding is back to the shell's normal single bottom-pinned primary CTA, keeping this step's own label (이대로 진행, wizard-copy.ts nextLabel step-5 special-case) instead of 다음 — the step-owned two-CTA footer this step used to be canon's cited exception for is retired
10
7/7 · 확인 · 리뷰
Structure
Component
Step 6 (create-session-step6.tsx) — a flat FactList of the session RECORD's rows (buildSessionRecordRows + recordFromWizardDraft, session-record/), NO Card wrapper; every row's access is reviewRecordAccess — always 수정, jumping back to the owning step (WIZARD_STEP_OF). 응답 기한/종목/한 경기 시간 carry the note "만든 뒤에는 바꿀 수 없어요" (fixed after create); 최대 인원 carries "최소 인원 미달 시 일정이 자동 취소돼요" when a minimum is set. 반복 일정 is review-only (omitted once the session is made) and appears only for a series. 종목 folds 경기 유형 into its OWN one-line segments (["복식","정기","친선"], recordFormat) — no separate 경기 유형 row. 경기 규칙 shows only its ONE headline fact (recordMatchRules = summaryFacts(...)[0]) — the full breakdown is one 수정 tap away. A pickup session keeps its own small notice Card BELOW the rows, unrelated to the record.
CTA
primary 일정 만들기 (loading → 처리 중…)
11
수정 상태 · 세션 페이지 (헤더 취소·완료, EDIT-PAGE-BARE)
Structure
Component
There is no separate edit route — the host's header 수정 toggle (useSessionFactEditing) turns SessionFactsSection (right under the page title) into the SAME session-RECORD rows the wizard review draws (recordFromSession instead of recordFromWizardDraft), each editable one trailing a pencil (EditCue) and opening its field's BOTTOM edit sheet (EditFieldModal/EditControlModal, ModalPanel placement="bottom", 취소/적용) in place; the footer's bottom CTA band steps aside for the duration (no footer at all while editing).
Header — Now (2026-09-28, EDIT-PAGE-BARE)
"an edit page probably doesn't need a thumbnail photo" — the header is the PLAIN DEFAULT bar (never the collapsing one), titled 일정 수정 (t().sessionDetail.editTitle, was a blank title="") — 취소 leading (SessionEditCancel, regular weight, $primary, in place of the back chevron), 완료 trailing (semibold): the bar offers the mode's two ways out, never the page's. 취소 with no change leaves the mode at once; with a change pending it asks 수정을 그만둘까요? first (confirmDiscard) — same as Android back (useWizardBackHandler, focus-gated).
Page while editing
Now (2026-09-28): NO hero (the map band + club eyebrow + venue title is gone outright, factEditing.editing ? null : <SessionHeroSection .../>) and NO hint line (editModeHint deleted from i18n) — the body is ONLY the record rows. Every other section (alerts, notice, the live participant bar, 참석, 경기, and — since ONE-BOTTOM-CTA folded them into the ⋯ menu — 호스트 도구/위험 구역, which no longer exist as page sections even in the read state) is gone outright, not hidden-empty (!factEditing.editing gates each one in session-detail-screen.tsx).
Leaving the page
Leaving the PAGE itself (not the mode) with changes pending — the iOS swipe (locked while pending, useLeaveGuard's gestureEnabled: false) or any other removal (Android back through the OS, a deep link, a tab switch) — asks the app's one leave question instead (나갈까요? / 계속 작성 · 삭제하고 나가기, useLeaveConfirm); with none, the page just goes.
Locked rows
a lock glyph + a reason line INSTEAD of a pencil, never a dimmed value: 응답 기한/종목/한 경기 시간 (fixed after create, same reason as the review's note), 참가비 CONDITIONALLY once the first session_payments hold exists (feeLockedByPayments, mirrors DB trigger trg_block_fee_edit_with_holds), 취소 규정 always (the club's own attendance rule, editCancelPolicyClubRule), 경기 규칙 for a host without the role that can change it (editMatchRulesByRole) — WITH that role it is a chevron instead (editRecordAccess: opens sessionMatchRules, which saves itself).
반복
has no row here at all — recordFromSession never sets a recurrence field once a session is made (a series is a create-time-only choice).
12
편집 시트 — 필드 하나 (EDIT-SHEET)
Structure
Component
Owner 2026-09-28: "the edit doesn't open a modal but render a bottom edit sheet" — every field editor is ModalPanel placement="bottom" (modal-panel.tsx, modal-panel-visual.ts): full width, flush to the bottom edge, rounded TOP corners only, rises into place (animation: 'quick', enterStyle: { y: 48 }) — NO drag handle, NO snap points (STYLE-3 stays retired), NO X close (the sheet's own 취소 is the close). EditControlModal/EditFieldModal (presentation/components/edit-field-modal.tsx) render a title + the field's own control (here, a Stepper for 최대 인원) + a full-width [취소 (secondary, filled $surfaceSecondary + $borderStrong) | 적용 (primary)] pair at the bottom — commit="apply" for the session page's per-field editors (writes into the page draft; the page's own 완료 saves it), commit="save" for club settings (writes straight to the server, one field at a time).
Keyboard
a text-field sheet (공지, 참가비 amount) rides up with the keyboard — ModalPanel's root bakes in KeyboardAvoidingView, no per-consumer handling needed.
Centered dialogs unchanged
confirms and pickers (TierEditModal, VenueSearchTakeover, date/time pickers, useConfirm) stay placement="center" — only a FIELD editor moved to the bottom.