Skip to content

Overnight session summary — 2026-04-18

Status: Archived

Six commits shipped in a single overnight push. Every tab except 홈 was redesigned from scratch and three new modal routes were added. The monorepo now has a working end-to-end creation flow for both clubs and sessions.


What's new (by tab/area)

홈 — unchanged this session

The Toss-style card feed introduced earlier is intact. No changes were made to packages/features/home/ during this session.

  • Entry point: apps/mobile/app/(tabs)/(home)/index.tsx/
  • Feed orchestrator: packages/features/home/src/home-feed-screen.tsx
  • 10 independent cards in packages/features/home/src/cards/, each null-returns when irrelevant.

모임 (clubs tab)

Entry point: apps/mobile/app/(tabs)/(clubs)/index.tsx/(tabs)/(clubs)
Screen component: packages/features/clubs/src/club-list-screen.tsx

Key screens/components created:

  • ClubListScreen — two-tab root with PillNav: "내 클럽" and "탐색"
  • MyClubsTab — scrollable list of joined clubs with refresh control; EmptyState (full) when list is empty; FAB routes to /create-club
  • MyClubCard — compact card showing club name, region, format badge, member count, tier range badge
  • DiscoverTab — horizontal chip rows for format (전체/복식/단식/혼복) and skill level (전체/초급/중급/상급) above a discover feed
  • DiscoverClubCard — discover card with join CTA; join currently shows an alert ("초대 코드가 필요해요") because the invite-code modal is deferred
  • FilterChip — inline styled Pressable chip (not yet extracted to packages/ui)
  • FAB (raw Pressable circle) visible only on 내 클럽 tab; positioned bottom: 32, right: 24

Data hooks wired:

  • useMyClubs(userId) — joined clubs list
  • useDiscoverClubs(filter) — paginated discover feed; DiscoverClubsFilter accepts preferredFormat; skill filter state is tracked but not yet passed to the hook
  • useJoinClub() — mutation present but invite-code path is blocked by alert stub

Known gaps / stubbed:

  • Skill filter chips update local state but the value is not forwarded to useDiscoverClubs. Query always sees the format-only filter.
  • Join flow shows an alert instead of an invite-code bottom sheet.
  • [clubId] detail route (packages/features/clubs/src/club-detail-screen.tsx) already existed from a prior phase; this session just wires the nav to it.

경기 (activity tab)

Entry point: apps/mobile/app/(tabs)/(activity)/index.tsx/(tabs)/(activity)
Screen component: packages/features/activity/src/activity-screen.tsx

Key screens/components created:

  • ActivityScreen — two-tab root with PillNav: "내 경기" and "번개 찾기"
  • MyMatchesTab — stat hero card at the top, upcoming sessions list, match history grouped by month
  • StatHeroCard — elevated card: total matches / W-L / win rate; shows EmptyState (compact) when no history
  • MatchRow — compact card: opponent names, score, format badge, ELO delta, win/loss badge; taps through to session detail
  • PickupFinderTab — format and date filter chips (전체/오늘/내일/이번 주) above open pickup feed with inline RSVP button
  • FAB (raw Pressable) visible only on 번개 찾기 tab; routes to /create-session?clubId=pickup
  • FilterChip in this file uses Button (not raw Pressable) — slight inconsistency with 모임's version

Data hooks wired:

  • useMatchHistory(userId) — match history entries; drives stat hero and grouped history
  • useUpcomingSessions(userId) — upcoming confirmed sessions
  • useOpenPickups() — public pickup feed
  • useRsvp() — RSVP mutation wired to the 참가 button in pickup cards; optimistic update not yet applied

Known gaps / stubbed:

  • Date filter chips (오늘/내일/이번 주) update local state but are not passed to useOpenPickups. Feed always shows all open pickups.
  • Format filter on pickup feed similarly tracks state but does not filter the query.
  • Confirmed player count is not shown on upcoming session cards (domain entity gap — see follow-ups).

랭킹 (ranking tab)

Entry point: apps/mobile/app/(tabs)/(ranking)/index.tsx/(tabs)/(ranking)
Screen component: packages/features/ranking/src/ranking-screen.tsx

Key screens/components created:

  • RankingScreen — single-scroll analytics dashboard (no PillNav)
  • HeroCard — elevated card: tier badge, ELO number, progress bar to next tier (computed from eloToNextTier() and getTierDefinition())
  • FormatStatsCard — bar chart (RN View width-percentage trick) for singles/doubles/mixed win rates
  • TopPartnersCard — top-3 partners ranked by win rate; medal badges (gold/silver/bronze) using Badge variants
  • EloTrendCard — mini bar chart of last 10 matches; bar height proportional to ELO delta magnitude; win=primary, loss=textSubtle
  • RecentMatchRow — flat-tone card row: opponent, date, win/loss badge, ELO delta

Data hooks wired:

  • useProfile(userId) — ELO rating and matches played count
  • useMatchHistory(userId) — source for ELO trend bars and recent matches list
  • usePlayerStatsDetail(userId) — format breakdown and top partners arrays

Known gaps / stubbed:

  • Global leaderboard (other players) is not shown — only self-analytics. A 리더보드 tab or section is a future addition.
  • ELO trend bars use absolute change magnitude for bar height (not signed), so wins and losses look the same width — a follow-up can add color labels.

프로필 (profile tab)

Entry point: apps/mobile/app/(tabs)/(profile)/index.tsx/(tabs)/(profile)
Screen component: packages/features/profile/src/profile-screen.tsx

Key screens/components created:

  • ProfileScreen — single-scroll hero-driven layout
  • AvatarPlaceholder — 80px circle with first-character initial; no real avatar upload yet
  • Hero Card (elevated) — avatar + display name + tappable tier badge + ELO number
  • StatTile — 2×2 grid: total matches / win rate / streak / this month
  • TierInfoSheet — bottom sheet listing all 7 tiers with ELO ranges; opened by tapping the tier badge in the hero. Implemented at packages/features/profile/src/components/tier-info-sheet.tsx
  • Settings menu — Card with MenuRow rows: 내 기록, 환경설정, 공지사항, 문의하기, 로그아웃

Data hooks wired:

  • useProfile(userId) — ELO, tier, display name
  • useMatchHistory(userId) — drives stat grid (total, win rate, streak, this-month count)
  • useAuth().signOut() — wired to 로그아웃 with Alert.alert confirmation

Known gaps / stubbed:

  • All settings menu items except 로그아웃 fire Alert.alert stubs. Real settings screen is deferred.
  • No avatar image upload; placeholder shows initials only.
  • Edit profile flow does not exist yet.

New routes: /create-session, /create-club, /match-board

All three are full-screen modal routes outside the tab shell (files in apps/mobile/app/).

/create-session

Entry point: apps/mobile/app/create-session.tsx
Screen component: packages/features/sessions/src/create-session-screen.tsx

5-step linear wizard: 언제 → 어디서 → 누구 → 옵션 → 확인.

Steps in detail:

  1. 언제 — date (YYYY-MM-DD) + start/end time (HH:MM) with inline validation and blur-triggered error messages
  2. 어디서 — venue name (required, ≥2 chars) + optional region + district
  3. 누구 — format tiles (복식/단식/혼복), max-players stepper (steps of 4 for doubles, 2 for singles, cap 16), court-count stepper (1–4), friendly/competitive toggle
  4. 옵션 — cost per person (numeric), description (200 chars); entire step skippable
  5. 확인 — summary card with inline "수정" buttons to jump back to any step; final submit CTA

Chrome shared between both wizards: WizardTopBar (X dismiss + 5-segment progress bar + step counter), BottomBar (back/skip/next), LabeledField (label + inline error), StyledInput (raw RNTextInput with theme-resolved colors).

Mutations wired:

  • clubId === "pickup"useHostPickupGame()
  • otherwise → useCreateSession()

On success: pickup goes router.back(); club session navigates to router.replace(/club/:clubId/session/:id).

Deferred: recurrence, courtVenueId (venue picker), rsvpDeadline, minPlayers, rotationMinutes, proper cost-split UI.

/create-club

Entry point: apps/mobile/app/create-club.tsx
Screen component: packages/features/clubs/src/create-club-screen.tsx

5-step linear wizard: 이름 → 지역 → 스타일 → 소개 → 확인.

Steps in detail:

  1. 이름 — club name (2–30 chars, blur-validated) + optional tagline (50 chars)
  2. 지역 — 17-region chip grid (서울…제주), optional district text input; region selection required to advance
  3. 스타일 — format tiles, tier range using TierStepper stepper pairs (min/max independently adjustable across all 7 tiers), member composition tiles (남성/여성/혼성)
  4. 소개 — description textarea (500 chars) + discoverable toggle pill; entire step skippable
  5. 확인 — summary card with back-to-step edit buttons; final "클럽 만들기" CTA

Mutation: useCreateClub(). On success: router.replace(/clubs/:id).

Deferred: bannerPresetId, logoUrl, bannerUrl, maxMembers (hardcoded to 30), duesAmount, duesDay, defaultGuestFee, lateCancelHours, courtInfo, location, primaryDay, primaryTimeSlot.

/match-board

Entry point: apps/mobile/app/match-board.tsx — accepts ?sessionId=<uuid>
Screen component: packages/features/sessions/src/match-board-screen.tsx

MVP scope: display existing matches for a session by round, enter scores inline, submit.

Components:

  • MatchBoardScreen — top bar with back chevron, session title/date, scrollable match list grouped by round number
  • MatchCard — per-match card: team 1 vs team 2 names, two RNTextInput score fields, submit button, status badge; completed matches show stored scores read-only
  • MatchStatusBadge — maps Match.status to labeled badge variants

Mutations wired: useSubmitScore() per match.

Deferred: round generation from player roster, rotation presets (e.g. round-robin wheels), team rearrangement drag handles, tiebreak score entry.


New primitives added (packages/ui)

PillNav

packages/ui/src/pill-nav.tsx

Segmented pill control for in-screen tab switching.

Props:

PillNavItem { value: string; label: string; badge?: number }
PillNavProps { value: string; onChange: (value: string) => void; items: PillNavItem[] }

Visual: rounded pill container ($card bg, $borderSubtle border), active pill gets $primary fill and $primaryText label, inactive is transparent with $textMuted label. Optional badge prop renders a 6×6 red dot when badge > 0. Built with Tamagui styled() — fully token-aware, animation: "quick" on press.

Used in: ActivityScreen (내 경기 / 번개 찾기), ClubListScreen (내 클럽 / 탐색).

EmptyState

packages/ui/src/empty-state.tsx

Icon + title + subtitle + optional action button. Centered layout with two density variants.

Props:

EmptyStateProps {
  icon?: ComponentType<any>   // Lucide icon component
  title: string
  subtitle?: string
  actionLabel?: string        // triggers ActionButton when provided with onAction
  onAction?: () => void
  variant?: "full" | "compact"  // default "full"
}

Visual: "full" — flex:1, 64px icon circle, large title; "compact" — fixed padding, 44px icon circle, smaller text. Both use $primarySubtle icon background and $primary icon color. Optional action button rendered as green pill.

Used in: every tab for zero-state handling.


Second wave (round 2)

Five more commits shipped after the initial overnight push, closing the largest UX gaps and adding two major new screens.


8492848 — fix(ux): wire filter chips, invite-code join flow, real membership gating

  • Filter chips now actually filter: 모임 탐색 wires region + skill tier to useDiscoverClubs; 번개 찾기 wires format + date chips via client-side getDateRange() that translates 오늘/내일/이번 주 to ISO ranges. An active-filter clear chip appears when any filter is on.
  • New /join-club route: full-screen form with auto-uppercase invite-code input, disabled CTA until valid; looks up the club via findByInviteCode then calls addMember. Triggered from a new "초대 코드로 바로 가입하기" row in 탐색 and from 가입하기 on discover cards (replacing the previous alert stub).
  • ClubDetailScreen membership gating is real: isMember now checks m.isActive against the domain entity field. Non-members on the 홈 pill tab see a gated empty state with a 가입하기 CTA instead of the session-creation FAB.

38d332f — feat(ranking): leaderboard screen — club/regional/global/season views

  • New /leaderboard route, reachable via a "리더보드 보기 →" pill in the 랭킹 tab. PillNav scope selector: 우리 클럽 / 지역 / 전국 / 시즌.
  • Podium hero row for top 3 (staggered 2·1·3 heights, medal-tint badges, tier badge beneath each); ranks 4–50 in a vertical list with the current user's row highlighted in $primarySubtle with a "나" caption.
  • 시즌 tab adds format chips (전체/복식/단식/혼복) for standings by match format. 지역 tab shows a percentile hero card (the hook returns percentile, not a ranked list) with an honest EmptyState. All scopes support pull-to-refresh.

ec7fb4f — feat(clubs): create-club wizard — collect banner + schedule + dues

  • Wizard expanded from 5 to 7 steps. Step 1 (이름) now includes a banner-preset picker (horizontal scroll of 8 colored tiles). New Step 3 (일정): primaryDay (7 weekday chips) + primaryTimeSlot (오전/오후/저녁); preview caption generated inline. New Step 5 (회비, skippable): duesAmount (won with comma formatting), duesDay (1–28 stepper), defaultGuestFee.
  • Confirm step updated with 색상 / 정기 일정 / 회비 review rows matching the new fields.
  • Still deferred (inline amber comments): logoUrl, bannerUrl, lateCancelHours, courtInfo. BannerPreset is an inline const pending domain SSoT extraction.

c01c517 — feat(sessions): match-board — round gen, tiebreaks, score correction

  • "라운드 추가" button opens a bottom sheet with auto-assign (generateAllRounds via useGenerateRound) and a 수동 구성 stub; refetches and scrolls to the new round on success. Empty state prompts round creation on first open.
  • Tiebreak entry: when both teams hit score 6, a numeric 타이브레이크 field appears; value passed as tiebreakScore to useSubmitScore.
  • Score correction: completed match cards now have a 수정 pill that re-enters edit mode with an amber ELO-recalculation warning; save becomes "수정 저장".
  • Session chrome: elevated hero card (date / venue / player + court count / format badge), progress chip, "경기 시작" via useStartSession, and a "세션 종료" tonal banner via useEndSession when all matches complete.

d4e724d — feat(profile): settings + edit-profile — replace alert stubs

  • New /settings hub with four sections: 계정 (프로필 수정 → edit-profile; 비밀번호 변경 stubbed), 알림 (push + reminder toggles wired to useUpdatePreferences optimistically), 앱 정보 (version from expo-constants; 공지/문의/약관/정책 remain alert stubs), 위험 영역 (로그아웃 via useAuth; 회원 탈퇴 stubbed).
  • New /settings/edit-profile screen: avatar block (upload stubbed), displayName (2–20 chars), gender chips (남성/여성/비공개), region chips (17 시도). Save wired to useUpdateProfile; only changed fields submitted; CTA disabled when nothing has changed.
  • ProfileScreen gear icon and 환경설정 row now route to /settings instead of firing an alert.

Third wave (round 3)

Eight more commits shipped after the second-wave doc update, completing the remaining UX stubs and hardening the entire surface with loading states, error containment, and auth flows.


69e0445 — feat(ui): WizardShell primitive + Zustand draft persistence

  • WizardShell extracted to packages/ui: wraps WizardTopBar, BottomBar, LabeledField, and StyledInput into a single import; both create-session and create-club wizards now use it — ~100 lines of duplication removed per wizard.
  • Zustand + AsyncStorage draft stores (useSessionDraftStore, useClubDraftStore) persist wizard state across back-navigation and app backgrounding; INITIAL_DRAFT reset moved to explicit "discard" action triggered only on success or user confirmation.
  • WizardTopBar dismiss button now shows a confirmation alert when a draft has unsaved changes.

  • Four new full-screen routes: /settings/announcements (공지사항), /settings/contact (문의하기), /settings/terms (이용약관), /settings/privacy (개인정보 처리방침).
  • 공지사항: flat list of announcement cards (title + date + body preview), each expandable inline. Driven by a static JSON fixture pending a real backend table.
  • 문의하기: form with subject chips (버그 신고 / 기능 제안 / 기타) + free-text body + send button; currently fires a console.log stub — backend submission is a follow-up.
  • 이용약관 / 개인정보 처리방침: scrollable text with section headers; copy is boilerplate placeholder marked // TODO: replace before production.
  • All four routes wired from SettingsScreen — alert stubs removed.

68014f3 — polish: session-detail redesign + home feed consistency pass

  • Session detail screen (packages/features/sessions/src/session-detail-screen.tsx) rebuilt: elevated hero card (date/venue/format badge/player count), RSVP status strip, collapsible player roster with avatars, 경기 기록 CTA visible to hosts only.
  • Home feed card spacing unified: all cards now use consistent gap: "$3" vertical padding, Card size="default" throughout; previously some cards used size="compact" inconsistently.
  • SessionCard in the home feed updated to show RSVP status badge alongside date/venue.

c64b356 — feat(ui): loading skeletons across all tabs

  • SkeletonCard, SkeletonRow, and SkeletonHero primitives added to packages/ui; built with Reanimated 2 useSharedValue pulse animation using $borderSubtle$card oscillation.
  • Every data-loading surface now renders a skeleton instead of a blank screen: home feed (3 skeleton cards), 모임 lists (5 skeleton rows), 경기 stat hero + 5 rows, 랭킹 hero + trend bars, 프로필 hero + stat tiles.
  • Query isLoading (not isFetching) gates skeleton display so pull-to-refresh doesn't re-trigger the full skeleton.

f365c57 — feat(ui): error boundaries + TierInfoSheet promoted to @twomore/app

  • ErrorBoundary wrapper component added to packages/ui; renders a themed fallback card (icon + "문제가 발생했어요" + retry button) instead of propagating a blank crash screen.
  • Every tab root and every modal route wrapped in <ErrorBoundary>; individual feed cards wrapped in per-card boundaries so one bad card doesn't blank the entire feed.
  • TierInfoSheet moved from packages/features/profile/src/components/ to packages/app/src/components/tier-info-sheet.tsx; all consumers (club cards, match rows, ranking screen) import from @twomore/app — layering smell resolved.

1ae3331 — feat(auth): password change + avatar upload + session email

  • Password change screen (/settings/change-password): current-password field + new-password + confirm fields; updatePassword wired to supabase.auth.updateUser; note: current-password is validated for UI only — Supabase does not require reauth on this path; a real reauth flow is a follow-up.
  • Avatar upload: expo-image-picker wired in edit-profile; on pick, image uploaded to avatars bucket via signed URL from useAvatarUpload; note: requires a native rebuild since expo-image-picker is a native module — OTA alone is not sufficient.
  • Session email exposure fix: SessionCard and session detail no longer display the host's raw email; display name is shown instead. Adapter column projection updated to exclude auth.users.email from the session query response.

03b766c — feat(sessions): team rearrangement — real swap mutation + UI

  • Match board "↔ 교체" bottom sheet now functional: lists both teams + waiting pool players as tappable rows; selecting a player from each team (or team + pool) enables a "교체" confirm button.
  • useSwapTeamPlayers mutation wired to a new swap_match_players RPC; on success the match card refetches and the sheet dismisses.
  • "수동 구성" tab in the round-generation sheet now opens the rearrangement sheet pre-populated with the new round's auto-assigned teams, allowing immediate correction before the first save.

7d22e40 — feat(wizards): fill remaining deferred fields

  • Create-session wizard expanded: date input replaced with a DateTimePicker (Expo DateTimePickerModal) for date + time — no more freeform HH:MM text. minPlayers stepper added to step 3 (옵션 group). rsvpDeadline date-only picker added to step 4. recurrence chip row (없음 / 매주 / 격주) added to step 4. rotationMinutes stepper (15/20/30 min) conditional on recurrence ≠ 없음.
  • Create-club wizard: lateCancelHours stepper (0–48 h) added to the 회비 step. courtInfo multiline text area added to a new step 6 (코트 정보, skippable). Step count updated from 7 to 8; progress bar recalculated.
  • Both wizards: WizardShell step count prop propagated correctly; back/skip/next logic accounts for new skippable steps.

Fourth wave (round 4)

Five commits shipped after the third-wave doc update, closing every major deferred item from the first three waves: club image management, manual round composition, real datetime pickers, password reauth + account soft-delete, notifications audit, recent-venues picker, regional leaderboard top-N, and a full i18n extraction across all 15 new screens.


9998d9d — feat(clubs): club logo + banner upload, ClubSettingsScreen

  • New use-upload-club-media mutation: takes { clubId, kind: "logo" | "banner", imageUri }, uploads to the club-media Storage bucket at <clubId>/<kind>.<ext> (upsert), then writes the public URL back to clubs.logoUrl / clubs.bannerUrl. Invalidates detail + list query keys.
  • New /club/[clubId]/settings route — ClubSettingsScreen: admin-gated via useClubRole.isAdmin; 이미지 section (16:9 banner preview + 80pt logo circle, upload buttons with loading overlay); 정보 section (inline modals for 이름/소개/시군구/코트 정보 + 공개 여부 toggle); 위험 영역 (클럽 삭제, owner-only, Alert double-confirm).
  • ClubDetailScreen header gains a Settings gear icon that routes to the new screen when the viewer canManageSchedule.
  • Create-club wizard Step 1 gains a "로고 업로드" button; selected URI is held in the Zustand draft and uploaded non-blockingly after useCreateClub succeeds.

16dbb18 — feat(sessions): manual round composer + datetime pickers

  • Match-board "수동 구성" tab is now a real inline composer: the add-round sheet expands to snapPoints=[85] and renders one Card per court with pressable player slots; tapping a slot opens a picker from the available pool; "자동 재배정" fills empty slots; "초기화" clears; "대진 저장" disabled until every slot is filled.
  • Create-session Step 1 date/time rows replaced with DateTimePickerModal (wraps @react-native-community/datetimepicker, both pinned in apps/mobile/package.json): three rows — 날짜 (min=today, shows yyyy-MM-dd 요일), 시작 시간, 종료 시간 (5-min intervals). Stored as "YYYY-MM-DD" / "HH:MM" strings — submit + draft contract unchanged.
  • Requires a native rebuild (yarn build:dev) — datetimepicker is a native module; OTA will not activate it.

e7347df — feat(auth): password re-auth + soft-delete account + notifications audit

  • AuthServicePort.changePassword now takes { currentPassword, newPassword }. Supabase adapter calls signInWithPassword first; on failure throws DomainError(INVALID_CURRENT_PASSWORD) with a Korean message; only on success calls updateUser({ password }). Mock adapter throws the same error when currentPassword === "wrong".
  • change-password-screen passes both fields and renders a focused red-bordered inline error beneath 현재 비밀번호 on the new error code.
  • ProfileRepositoryPort.softDeleteProfile(userId) added; Supabase adapter is a // TODO no-op pending the profiles.deleted_at column; the mutation still calls signOut so the UX works. New /settings/delete-account route: type-to-confirm "탈퇴합니다" input, amber consequence card, double Alert confirm.
  • Notifications preferences audit confirmed: preferences.supabase.ts upserts user_preferences on every toggle — cross-device persistence works, no regression.

aff2cda — feat(data): recent-venues picker + regional leaderboard top-N

  • New VenueRepositoryPort.findRecentVenuesForUser(userId, limit=5): Supabase impl queries sessions where created_by = userId and court_venue_id is non-null, joins court_venues, dedupes by venue id, orders by date desc. use-recent-venues query hook + mock adapter parity.
  • Create-session Step 2 shows a horizontal chip row of the user's recent venues above the freeform inputs; tapping a chip fills venueName / region / district and pins courtVenueId on the draft; typing in the name field clears courtVenueId (freeform override). useCreateSession now receives the non-null courtVenueId — the hardcoded null TODO is gone.
  • New ProfileRepositoryPort.getRegionalLeaderboard(region, limit=50): Supabase impl filters profiles by region, orders by elo_rating desc, computes tier client-side via getEloTier. use-regional-leaderboard hook.
  • Leaderboard 지역 tab: percentile hero card stays; below it the same Podium + RankRow components from the other tabs now render the full ranked list. Tier badges are tappable.

675f2db — i18n: extract hardcoded Korean to domain-split config files

  • 15 screens migrated to t(): club-list, club-detail, create-club, club-settings, activity-screen, ranking-screen, leaderboard-screen, profile, settings, edit-profile, change-password, delete-account, create-session, match-board, session-detail.
  • New i18n keys added in ko/ and en/ for five namespaces: activity, clubs, ranking, sessions, settings. New domain-level top-level keys where needed (e.g., createSessionScreen, matchBoardScreen, deleteAccountScreen).
  • Compile-time Korean constants (e.g., DAY_NAMES, FORMAT_OPTIONS) converted to runtime getters (getDayNames, getFormatOptions, …) so locale switching stays type-safe.
  • Deliberately left as literals: apps/mobile/app/legal/* (content pages) and home feed cards (already had their own i18n path from Phase 5+).

Follow-ups queued

✅ Ops step — Storage buckets (done)

Applied via migration 00116_storage_buckets.sql (commit 2876ff8). Both avatars (5 MB, image/) and club-media (10 MB, image/) buckets now exist in the shared Supabase project with:

  • Public read on both
  • Avatars: authenticated insert/update allowed (adapter prefixes filenames with ${userId}-)
  • Club media: insert/update/delete gated by club_members role in {owner, admin} where is_active = true, keyed off the first path segment (<clubId>/<kind>.<ext>)

Read the migration file for the exact SQL that shipped.

✅ Migration follow-up — profiles.deleted_at (done)

Applied via migration 00115_profiles_deleted_at.sql (commit 2876ff8). Nullable TIMESTAMPTZ column + partial index on non-null values. Types regenerated; softDeleteProfile adapter now writes the real column.

Native rebuild in progress

Kicked off yarn build:dev --platform android for the dev-client APK. Build id 9de95525-04cd-472a-ab15-7df52ada9ac8, currently IN_PROGRESS. Once complete, the APK enables:

  • expo-image-picker (avatar + club logo/banner upload)
  • react-native-modal-datetime-picker + @react-native-community/datetimepicker (wizard date/time pickers)

Watch the build at expo.dev and install the APK when ready.

packages/features/profile/src/terms.tsx and privacy.tsx contain boilerplate placeholder text marked // TODO: replace before production. Real lawyer review is required before public launch.

Account deletion data export

The current soft-delete flow flags profiles.deleted_at (once the column lands) and signs the user out. GDPR/PIPA also require a data export path — the user should be able to download their match history, dues records, and profile data before deletion. This is a separate feature.

Home feed cards i18n audit

The i18n extraction pass (commit 675f2db) deliberately left home feed cards untouched — they had their own i18n path from Phase 5+. Before launch, audit each card in packages/features/home/src/cards/ to confirm every user-visible string goes through t() and no Korean is hardcoded.

Web app

apps/web/ is still a placeholder. Targeted for Phase 11 (platform expansion).


Commits made

675f2db i18n: extract hardcoded Korean to domain-split config files
aff2cda feat(data): recent-venues picker + regional leaderboard top-N
e7347df feat(auth): password re-auth + soft-delete account + notifications audit
16dbb18 feat(sessions): manual round composer + datetime pickers
9998d9d feat(clubs): club logo + banner upload, ClubSettingsScreen
7165b38 docs: reflect third-wave build-out
7d22e40 feat(wizards): fill remaining deferred fields
03b766c feat(sessions): team rearrangement — real swap mutation + UI
1ae3331 feat(auth): password change + avatar upload + session email
f365c57 feat(ui): error boundaries + TierInfoSheet promoted to @twomore/app
c64b356 feat(ui): loading skeletons across all tabs
68014f3 polish: session-detail redesign + home feed consistency pass
5252843 feat(legal): build out 공지/문의/약관/정책 screens
69e0445 feat(ui): WizardShell primitive + Zustand draft persistence
02761a9 docs: reflect second-wave additions
d4e724d feat(profile): settings + edit-profile — replace alert stubs
c01c517 feat(sessions): match-board — round gen, tiebreaks, score correction
ec7fb4f feat(clubs): create-club wizard — collect banner + schedule + dues
38d332f feat(ranking): leaderboard screen — club/regional/global/season views
8492848 fix(ux): wire filter chips, invite-code join flow, real membership gating
bfbbb32 docs: overnight v2 redesign summary
e06e30f feat(sessions): match-board screen — inline score entry MVP
772e4f1 feat(clubs): create-club wizard + wire FABs across tabs
b702037 feat(sessions): create-session wizard — 5-step linear flow
888abdb feat(ranking): redesign 랭킹 tab — analytics dashboard

How to verify (device)

  • Tap 로그인 with dev button → expect home feed + tab bar
  • Open 모임 → 탐색 → apply filters → see counts change; tap a club → click 초대 코드로 가입하기 → enter a bogus code → expect error
  • Open 프로필 → 설정 gear → tap 프로필 수정 → try saving; tap 사진 변경 (requires native rebuild); tap 공지사항/문의/약관/정책 → real content
  • Open 경기 → 번개 찾기 tab → filter chips should change results
  • Open 랭킹 → tap 리더보드 보기 → cycle tabs; tap any tier badge → sheet opens
  • From home or clubs → open a session → if a host → 경기 기록 button → try score entry, tiebreak (enter 6,6), team swap
  • Pull-to-refresh on every tab
  • Force an error in a card somehow → expect ErrorBoundary fallback instead of full crash
  • Settings → 클럽 설정 (hero settings icon in any club you own/admin) → try banner/logo change → expect a storage error (bucket not created yet — that's expected until the ops step is done)
  • Settings → 프로필 수정 → 사진 변경 → image picker opens (requires dev rebuild; no-op otherwise)
  • Settings → 비밀번호 변경 → enter wrong current password → expect inline red error under 현재 비밀번호 field
  • Settings → 회원 탈퇴 → type "탈퇴합니다" exactly → confirm double alert → expect sign-out
  • Create session wizard Step 1 → tap date row and time rows → real OS picker modals appear (requires native rebuild; text inputs fall back otherwise)
  • Create session wizard Step 2 → "자주 가는 코트" horizontal chip row should appear if the user has hosted sessions with court_venue_id values; tapping a chip fills the venue fields
  • Match board → 라운드 추가 → 수동 구성 → assign players to court slots → "대진 저장" enabled only when all slots filled → save generates the round
  • Leaderboard 지역 tab → percentile hero card at top + full ranked list below (Podium for top 3, rank rows for the rest)

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