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-clubMyClubCard— compact card showing club name, region, format badge, member count, tier range badgeDiscoverTab— horizontal chip rows for format (전체/복식/단식/혼복) and skill level (전체/초급/중급/상급) above a discover feedDiscoverClubCard— discover card with join CTA; join currently shows an alert ("초대 코드가 필요해요") because the invite-code modal is deferredFilterChip— inline styledPressablechip (not yet extracted topackages/ui)- FAB (raw
Pressablecircle) visible only on 내 클럽 tab; positionedbottom: 32, right: 24
Data hooks wired:
useMyClubs(userId)— joined clubs listuseDiscoverClubs(filter)— paginated discover feed;DiscoverClubsFilteracceptspreferredFormat; skill filter state is tracked but not yet passed to the hookuseJoinClub()— 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 monthStatHeroCard— elevated card: total matches / W-L / win rate; showsEmptyState(compact) when no historyMatchRow— compact card: opponent names, score, format badge, ELO delta, win/loss badge; taps through to session detailPickupFinderTab— 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 FilterChipin this file usesButton(not raw Pressable) — slight inconsistency with 모임's version
Data hooks wired:
useMatchHistory(userId)— match history entries; drives stat hero and grouped historyuseUpcomingSessions(userId)— upcoming confirmed sessionsuseOpenPickups()— public pickup feeduseRsvp()— 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 fromeloToNextTier()andgetTierDefinition())FormatStatsCard— bar chart (RNViewwidth-percentage trick) for singles/doubles/mixed win ratesTopPartnersCard— top-3 partners ranked by win rate; medal badges (gold/silver/bronze) usingBadgevariantsEloTrendCard— mini bar chart of last 10 matches; bar height proportional to ELO delta magnitude; win=primary, loss=textSubtleRecentMatchRow— flat-tone card row: opponent, date, win/loss badge, ELO delta
Data hooks wired:
useProfile(userId)— ELO rating and matches played countuseMatchHistory(userId)— source for ELO trend bars and recent matches listusePlayerStatsDetail(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 layoutAvatarPlaceholder— 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 monthTierInfoSheet— bottom sheet listing all 7 tiers with ELO ranges; opened by tapping the tier badge in the hero. Implemented atpackages/features/profile/src/components/tier-info-sheet.tsx- Settings menu —
CardwithMenuRowrows: 내 기록, 환경설정, 공지사항, 문의하기, 로그아웃
Data hooks wired:
useProfile(userId)— ELO, tier, display nameuseMatchHistory(userId)— drives stat grid (total, win rate, streak, this-month count)useAuth().signOut()— wired to 로그아웃 withAlert.alertconfirmation
Known gaps / stubbed:
- All settings menu items except 로그아웃 fire
Alert.alertstubs. 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:
- 언제 — date (YYYY-MM-DD) + start/end time (HH:MM) with inline validation and blur-triggered error messages
- 어디서 — venue name (required, ≥2 chars) + optional region + district
- 누구 — format tiles (복식/단식/혼복), max-players stepper (steps of 4 for doubles, 2 for singles, cap 16), court-count stepper (1–4), friendly/competitive toggle
- 옵션 — cost per person (numeric), description (200 chars); entire step skippable
- 확인 — 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:
- 이름 — club name (2–30 chars, blur-validated) + optional tagline (50 chars)
- 지역 — 17-region chip grid (서울…제주), optional district text input; region selection required to advance
- 스타일 — format tiles, tier range using
TierStepperstepper pairs (min/max independently adjustable across all 7 tiers), member composition tiles (남성/여성/혼성) - 소개 — description textarea (500 chars) + discoverable toggle pill; entire step skippable
- 확인 — 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 numberMatchCard— per-match card: team 1 vs team 2 names, twoRNTextInputscore fields, submit button, status badge; completed matches show stored scores read-onlyMatchStatusBadge— mapsMatch.statusto 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-sidegetDateRange()that translates 오늘/내일/이번 주 to ISO ranges. An active-filter clear chip appears when any filter is on. - New
/join-clubroute: full-screen form with auto-uppercase invite-code input, disabled CTA until valid; looks up the club viafindByInviteCodethen callsaddMember. Triggered from a new "초대 코드로 바로 가입하기" row in 탐색 and from 가입하기 on discover cards (replacing the previous alert stub). ClubDetailScreenmembership gating is real:isMembernow checksm.isActiveagainst 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
/leaderboardroute, 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
$primarySubtlewith 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.BannerPresetis 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 (
generateAllRoundsviauseGenerateRound) 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
tiebreakScoretouseSubmitScore. - 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 viauseEndSessionwhen all matches complete.
d4e724d — feat(profile): settings + edit-profile — replace alert stubs
- New
/settingshub with four sections: 계정 (프로필 수정 → edit-profile; 비밀번호 변경 stubbed), 알림 (push + reminder toggles wired touseUpdatePreferencesoptimistically), 앱 정보 (version fromexpo-constants; 공지/문의/약관/정책 remain alert stubs), 위험 영역 (로그아웃 viauseAuth; 회원 탈퇴 stubbed). - New
/settings/edit-profilescreen: avatar block (upload stubbed),displayName(2–20 chars), gender chips (남성/여성/비공개), region chips (17 시도). Save wired touseUpdateProfile; only changed fields submitted; CTA disabled when nothing has changed. ProfileScreengear icon and 환경설정 row now route to/settingsinstead 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
WizardShellextracted topackages/ui: wrapsWizardTopBar,BottomBar,LabeledField, andStyledInputinto 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_DRAFTreset moved to explicit "discard" action triggered only on success or user confirmation. WizardTopBardismiss button now shows a confirmation alert when a draft has unsaved changes.
5252843 — feat(legal): build out 공지/문의/약관/정책 screens
- 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,Cardsize="default" throughout; previously some cards used size="compact" inconsistently. SessionCardin the home feed updated to show RSVP status badge alongside date/venue.
c64b356 — feat(ui): loading skeletons across all tabs
SkeletonCard,SkeletonRow, andSkeletonHeroprimitives added topackages/ui; built with Reanimated 2useSharedValuepulse animation using$borderSubtle→$cardoscillation.- 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(notisFetching) gates skeleton display so pull-to-refresh doesn't re-trigger the full skeleton.
f365c57 — feat(ui): error boundaries + TierInfoSheet promoted to @twomore/app
ErrorBoundarywrapper component added topackages/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. TierInfoSheetmoved frompackages/features/profile/src/components/topackages/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;updatePasswordwired tosupabase.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-pickerwired inedit-profile; on pick, image uploaded toavatarsbucket via signed URL fromuseAvatarUpload; note: requires a native rebuild sinceexpo-image-pickeris a native module — OTA alone is not sufficient. - Session email exposure fix:
SessionCardand session detail no longer display the host's raw email; display name is shown instead. Adapter column projection updated to excludeauth.users.emailfrom 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.
useSwapTeamPlayersmutation wired to a newswap_match_playersRPC; 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(ExpoDateTimePickerModal) for date + time — no more freeform HH:MM text.minPlayersstepper added to step 3 (옵션 group).rsvpDeadlinedate-only picker added to step 4.recurrencechip row (없음 / 매주 / 격주) added to step 4.rotationMinutesstepper (15/20/30 min) conditional on recurrence ≠ 없음. - Create-club wizard:
lateCancelHoursstepper (0–48 h) added to the 회비 step.courtInfomultiline text area added to a new step 6 (코트 정보, skippable). Step count updated from 7 to 8; progress bar recalculated. - Both wizards:
WizardShellstep 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-mediamutation: takes{ clubId, kind: "logo" | "banner", imageUri }, uploads to theclub-mediaStorage bucket at<clubId>/<kind>.<ext>(upsert), then writes the public URL back toclubs.logoUrl/clubs.bannerUrl. Invalidates detail + list query keys. - New
/club/[clubId]/settingsroute —ClubSettingsScreen: admin-gated viauseClubRole.isAdmin; 이미지 section (16:9 banner preview + 80pt logo circle, upload buttons with loading overlay); 정보 section (inline modals for 이름/소개/시군구/코트 정보 + 공개 여부 toggle); 위험 영역 (클럽 삭제, owner-only,Alertdouble-confirm). ClubDetailScreenheader gains a Settings gear icon that routes to the new screen when the viewercanManageSchedule.- Create-club wizard Step 1 gains a "로고 업로드" button; selected URI is held in the Zustand draft and uploaded non-blockingly after
useCreateClubsucceeds.
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 oneCardper 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 inapps/mobile/package.json): three rows — 날짜 (min=today, showsyyyy-MM-dd 요일), 시작 시간, 종료 시간 (5-min intervals). Stored as"YYYY-MM-DD"/"HH:MM"strings — submit + draft contract unchanged. - Requires a native rebuild (
yarn build:dev) —datetimepickeris a native module; OTA will not activate it.
e7347df — feat(auth): password re-auth + soft-delete account + notifications audit
AuthServicePort.changePasswordnow takes{ currentPassword, newPassword }. Supabase adapter callssignInWithPasswordfirst; on failure throwsDomainError(INVALID_CURRENT_PASSWORD)with a Korean message; only on success callsupdateUser({ password }). Mock adapter throws the same error whencurrentPassword === "wrong".change-password-screenpasses both fields and renders a focused red-bordered inline error beneath 현재 비밀번호 on the new error code.ProfileRepositoryPort.softDeleteProfile(userId)added; Supabase adapter is a// TODOno-op pending theprofiles.deleted_atcolumn; the mutation still callssignOutso the UX works. New/settings/delete-accountroute: type-to-confirm"탈퇴합니다"input, amber consequence card, doubleAlertconfirm.- Notifications preferences audit confirmed:
preferences.supabase.tsupsertsuser_preferenceson 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 wherecreated_by = userIdandcourt_venue_idis non-null, joinscourt_venues, dedupes by venue id, orders by date desc.use-recent-venuesquery 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/districtand pinscourtVenueIdon the draft; typing in the name field clearscourtVenueId(freeform override).useCreateSessionnow receives the non-nullcourtVenueId— the hardcodednullTODO is gone. - New
ProfileRepositoryPort.getRegionalLeaderboard(region, limit=50): Supabase impl filtersprofilesby region, orders byelo_ratingdesc, computes tier client-side viagetEloTier.use-regional-leaderboardhook. - Leaderboard 지역 tab: percentile hero card stays; below it the same
Podium+RankRowcomponents 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/anden/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.
Real legal copy
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 dashboardHow 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_idvalues; 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)