Screen audit — unclutter, consistency, efficiency (every screen)
Status: Active · Date: 2026-09-23 · Owner brief: "apply the same rigor to other screens … a thorough audit of other screens and bring back the report that will be analyzed, researched, planned and implemented for improvement." Direction agreed the same day: remove before adding; one grammar per entity; one number per concept; laws enforced by shared components and rendered-output tests, not by longer docs.
This report is the input to the next phases (research → plan → build). It is organised by class (the pattern to fix once) with every instance listed under it, then a per-screen index. Nothing here has been fixed yet except the build-hygiene items in §7.
1. Method, coverage and severity
- On device. Android emulator (Pixel 7, 1080×2400 @ 420 dpi), preview build 0.8.0 with the OTAs of 2026-09-23 (
a85228e1, thena39f0311). 78 routes opened by deep link (twomore://…) with real ids from the hosted QA world, screenshotted at every scroll position (≈190 shots), and each screen's native layout tree dumped, so spacing is measured (px ÷ 2.625 = dp), not eyeballed. - In code. Three read-only sweeps (clubs · sessions/venues/home · records/profile/messaging/settings) reported findings with
file:line. Every device observation was traced to code; every code claim was either reproduced on the device or is marked code-verified. Where a sweep and the device disagreed, the device won (see D1, where both sweeps said "not reproducible"). - Not covered. Login and onboarding (would need logging out of the QA account; code-reviewed only), the web share pages (
/s/[id],/m/[id]on web), livein_progressstates (none exist in the QA world today). The emulator's GPS sits in the US and its fonts are Noto, so distances and emoji metrics differ from a Korean phone. - Already audited, excluded. The session list card and the session-detail top block (2026-09-20 → 22). They appear here only where another screen contradicts them.
Severity. P0: shows wrong information or asks the user for a wrong action (money, results); fix first. P1: visible on a primary surface on most visits, or a class the owner has already flagged. P2: a secondary surface or a specific state. P3: polish or engineering only.
Source ids in brackets point at the evidence: DV- device observations, CL- / SE- / RP- the three code sweeps, EV- the 2026-09-23 morning evaluation. Code sweep reports and screenshots live in the session scratchpad; the facts needed to act are restated here.
2. Headline
The app still shows people things that are wrong. A payment banner asks users who owe nothing to transfer money, including the host of their own session, members who haven't joined, and even a cancelled session. The ELO chart draws a falling red line for a player who won twice. Several numbers have two or three values depending on the screen (the same user's ELO reads 1,213, 1,109 and 1222 on one day).
The biggest unclutter win is subtraction by rule. Most visual noise comes from four repeatable patterns:
- sections that render only to say "nothing yet";
- labels that repeat on every row;
- chrome that is always on (the floating scroll pill has a real bug);
- walls of primary buttons.
Each is fixable once, in a shared component.
The spacing complaint has a single root cause. Every tappable
ListRowignores the first-row trim, so a section title sits 16 dp above a tappable first row but 8 dp above anything else. Measured: 참석 8 · 정보 11 · 플레이 15 · 클럽 16 · 운영 16 · 도전 과제 20 dp.One entity, many drawings. A person is drawn by at least seven different row components. Matches, session rows, heroes, dates and numbers also each have two to four forms.
Process. Two AI agents wrote to the same working tree today. One committed the other's unfinished work under its own message, and an OTA was published from a commit that failed
yarn check. That is how laws drift even when every gate is green.
3. Findings by class
A · Wrong or contradictory information (correctness) — fix first
| ID | Sev | Screen | What the user sees | Evidence | Class fix |
|---|---|---|---|---|---|
| A1 | P0 | Session detail (any paid session) | "입금 필요 · 국민은행 … · 예금주 … · [입금 완료]" shown to the host of the session, to a member who hasn't joined (above the "참석 확정하기" CTA), and on a cancelled session. Tapping 입금 완료 fails server-side ("No pending hold to confirm"), so it's a wrong prompt, not corrupted data. [DV-01] | session-detail/use-session-detail-model.ts:122 gateState = sessionPaymentGateState(fee, myPayment?.status, …): no payment row ⇒ status undefined ⇒ 'reserved' (domain/session-info-model/payment-gate.ts:18-26) ⇒ showPaymentGateBanner (:446). The gate never checks that the viewer is a payer. | The gate is a function of the viewer's payment row. No row means no banner. Host, browsers, waitlist and cancelled sessions get none. Add the missing domain cases to payment-gate.ts and a screen test per viewer role. |
| A2 | P0 | 기록 → 최근 ELO 추이 | Two wins (+18, +12) draw a falling red sparkline under a "+30" summary. [RP-01, EV-05] | records/src/cards/elo-trend-card.tsx:46-49,102-118 feeds changes (per-match deltas) to SparkLine, which colours by first-vs-last (ui/src/charts/SparkLine.shared.ts:147-153). | Plot the rating after each match (running sum or elo_history). Test that a sequence of wins renders rising and green. |
| A3 | P1 | Profile · 기록 · club leaderboard · 일일 요약 | The same user's ELO reads 1,213 (profile, 기록), 1,109 (club leaderboard, labelled just "ELO"), and 1222 (일일 요약 "마지막 ELO", no thousands separator). [DV-05, CL-07] | Global profiles.elo_rating vs club useClubEloLeaderboard vs ratingAfterForEntries (record-day-detail-screen.tsx:94-95); Partner Suggester also reads global ELO under a bare "ELO" (partner-suggester-screen.tsx:62,170). | A metric registry (§5 W3): every rating is labelled by scope ("클럽 ELO" vs "ELO") and formatted by formatElo only. |
| A4 | P1 | 성장 분석 vs 출석 현황 / 클럽 설정 | Same club: 출석률 88% (분석) vs 평균 출석률 37% (출석 현황). 프로필 완성도 86% (분석) vs 6/8 (설정). [DV-04, CL-09, CL-13] | Three attendance sources (club-attendance-screen.tsx:290-293, member-stats-modal.tsx:365, club-analytics-screen.tsx:229-233). Two completeness formulas (shared/club-card-model.ts:216-249 vs the analytics RPC). | One definition per metric, computed once (server) and read everywhere. If two numbers are truly different metrics, give them different names. |
| A5 | P1 | Public profile | "총 경기" silently switches to a count of sessions attended when the player has 0 matches. [RP-02] | public-profile-screen.tsx:399-406 | One source per label, no silent fallback. |
| A6 | P1 | Profile vs 기록 | "연승" on the profile is the longest streak in the last 50 matches. 기록's "현재 연승" is the current one. [RP-03] | profile-hero-card.tsx:41-50 vs personal-records-section.tsx:201-213 | Pick one meaning per label; rename the other. |
| A7 | P1 | 성장 분석 | "주요 경로" lists raw keys unknown · share_link · club_card. "전환율 150%" (3 completes ÷ 2 starts). [DV-03] | club-analytics-screen.tsx (source label map missing; conversion not clamped or defined) | Enum → i18n label map with an exhaustiveness test. A conversion is only shown when numerator ≤ denominator by construction. |
| A8 | P2 | 알림 | Filter chips read 전체 15 but 관리 69, so a category exceeds "all". [DV-07] | notification-center/helpers.ts:30,39: 전체 counts visible rows (visibleCounts.total). 관리 sums the attention quantities (adminAttentionTotal, e.g. 미납 12 + 신고 1 + …), a different unit. | Every chip counts the same thing (rows). |
| A9 | P2 | 클럽 대항전 · 동의 이력 · completed session | A challenge that ended 09-22 is still under 진행 중 on 09-23. Dates are shown as raw ISO text ("2026-08-25 ~ 2026-09-22"). Consent history shows invented 00:00 times. A completed session's roster still says 참석 확정. [DV-12] | club-challenges-screen.tsx, consent history rows, participation-section.tsx | State words derive from dates (ended ⇒ 종료). Date-only values never render a time. Completed ⇒ 참석 완료 (canon already has the softened word). |
| A10 | P2 | 메시지 | Inbox preview for 최민아 shows "서브 폼 많이 늘었어요!", but opening a thread with 최민아 shows no messages ("첫 메시지를 보내 보세요"). Needs check: a duplicate 1:1 thread or a load miss. [DV-26] | dm-thread-screen.tsx / inbox row source | Verify; if duplicates exist, enforce one 1:1 thread per pair. |
| A11 | P2 | Legal | 개인정보 처리방침 (§1③, §2, §3④) still lists GPS 출석 체크 as an active feature — CONFIRMED dead, not device-only speculation: migration 00384 (settle-before-play) drops public.check_in_attendance_gps, and two independent code comments (live-participant-bar.tsx, use-session-detail-actions.tsx) both state "GPS 체크인 ... was retired by migration 00384 ... attendance is now match-derived and automatic." The clause's other two GPS uses (코트 주변 동호인 검색, 지역 기반 일정 추천) remain live and unaffected — only the GPS 출석 체크 phrase and its two tied PII/retention entries (§2's "GPS 위치 정보 ... 즉시 파기", §3④) are stale, not the whole clause. 이용약관 defines "RSVP … 대기" where the app says 참석 / 예비. [device, code-verified 2026-09-24] | packages/app/src/config/legal/documents/privacy.ko.ts:21,44,63, terms.ko.ts:31, supabase/migrations/00384_settle_before_play.sql, packages/features/sessions/src/session-detail/sections/live-participant-bar.tsx, packages/features/sessions/src/session-detail/use-session-detail-actions.tsx | Legal copy follows the terminology guide and current features. Re-read before launch. |
B · Empty and placeholder rendering (unclutter)
| ID | Sev | Screen | What the user sees | Evidence |
|---|---|---|---|---|
| B1 | P1 | Public profile (new player) | 3 of 4 sections are "nothing yet" wells: 경기 기록 · 받은 매너 태그 · 업적 0/74. The stat strip above reads 0 · — · —. [RP-06, device 김소희] | recent-matches-section.tsx:37-54, manner-tags / achievements sections |
| B2 | P1 | Own profile (new player) | 친구 · 업적 · 받은 매너 태그 stack as three titled empty blocks. [RP-05] | profile-screen.tsx:419-458; only AttendanceSection hides itself when empty |
| B3 | P1 | Session detail (before start) | A full 경기 section whose only content is "일정이 시작되면 여기에 내 경기가 표시돼요". [EV-02] | session-detail/sections/matches-section.tsx |
| B4 | P1 | 기록 | 개인 기록 renders three greyed "— — —" rows plus a teaser sentence. [RP-07] | personal-records-section.tsx:82-121 |
| B5 | P1 | Home 주 / 월 | A 30%-opacity recap card with a fake full donut and an overlay pill "아직 이번 주 기록이 없어요". [SE-02] | recap-summary-card.tsx:168-209,296-317 |
| B6 | P1 | 업적 gallery | 54 of 74 achievements are rows reading "준비 중 · 곧 만나볼 수 있어요", advertising features that don't exist. [device] | achievements-gallery-screen.tsx:176-178,217-223 |
| B7 | P2 | 참석자 (viewer, empty) · 참석자 관리 (all cancelled) | Two empty sections, "참석 확정 (0)" and "예비 (0)", each with a "아직 이 상태의 참석자가 없어요" well, above the only real content. [DV-18] | participants-screen.tsx (pseudo-empty rows so headers render) |
| B8 | P2 | 호스트 관리 (no applicants) | The page is an empty 신청 대기 well plus a big red 번개 취소 (already in the detail's 위험 구역). [DV-17] | pickup-host-dashboard-screen.tsx |
| B9 | P2 | 가입 심사 (guest with no history) | 근거 lists four empty values ("기록 없음", "아직 없어요" ×3). The hero chip states a zero ("공개 일정 0회 참석"). [DV-13] | join-review-detail-screen.tsx:186-208 |
| B10 | P2 | 관리 (admin hub) | The attention strip shows three zeros out of five cells (입금 확인 0 · 가입 신청 0 · 출석 주의 0). [DV-10] | club-detail/admin-snapshot-strip.tsx |
Class fix. Nothing to say ⇒ say it once, or not at all.
- A section with no content either hides itself or, if it's the page's primary list, shows one empty state. A
SectionBlockempty contract makes the default "hide". - Pages whose sections are mostly empty for new users get one "new player" state instead of N wells: profile, public profile, join review.
- Teaser and ghost content is removed and banned. This covers "— — —" rows, low-opacity fake cards and "준비 중" entries.
- Zero-valued attention counts collapse into one "all clear" line.
C · Repetition noise (unclutter)
| ID | Sev | Screen | What repeats | Evidence |
|---|---|---|---|---|
| C1 | P1 | Session detail roster | A solid 참석 확정 badge on every row under a title that already says 참석. Now 12 px since the sm default. [EV-01] | participation-section.tsx trailing |
| C2 | P1 | 회비 관리 | Every row repeats 20,000원 and 메모 추가, plus a badge, a solid 납부 확인 and 면제 처리, so ten solid primary buttons stack in a column. [DV-08] | dues/dues-row.tsx |
| C3 | P1 | 알림 | Every 할 일 item is a warning-yellow card with ⚠️ and a category tag. The same kind of item appears once per club (회비 장기 미납 × 9) instead of grouped. [DV-07] | notification-center-screen.tsx + row components |
| C4 | P2 | 리더보드 | The same 🥈 NTRP 3.0–3.5 badge on every row. [DV-25] | leaderboard-rank-row.tsx |
| C5 | P2 | 알림 설정 | An identical 알림함 badge on all 13 category rows (in-app is always on). [RP-17] | settings-signal-preferences-screen.tsx:80-107 |
| C6 | P2 | 스코어카드 | Each match row inside the "1라운드" group restates 1라운드 and 복식. Every player carries the same 🥈 medal. [DV-20] | scorecard match rows |
| C7 | P2 | 출석 확인 | 16 outlined buttons (참석/불참 on every row), while the explainer says "예외만 수정해 주세요". [DV-16] | components/participant-row.tsx (attendance variant) |
| C8 | P1 | Home | The title 일간/주간/월간 브리핑 restates the 일/주/월 pills directly below it. [SE-01] | home-feed-screen.tsx:43-49,101-106 |
| C9 | P2 | 일일 요약 / 기간 기록 | Each match row prints the format twice ("복식 / 복식"): sessionTitle is now the format word. [DV-06] | record-detail/match-timeline-row.tsx:59-63 |
| C10 | P3 | 가입 심사 · 경기 규칙 · profile | Join review states 첫 참석 twice [CL-06]. Match rules uses the label "차이로 승리" for both set and tiebreak. The profile's "출석률" section shows no rate, only 연속 참석 [RP-14]. | as cited |
Class fix. State the exception, not the default.
- A per-row badge appears only when the row differs from the list's default. The roster marks 호스트 · 예비 · 나 only; the leaderboard marks only bands that differ.
- Constants go in the header once: the dues amount, the in-app delivery.
- A list has at most one primary action per row, shown only on rows that need it. Bulk actions go in the header.
- Notifications of the same kind are grouped ("회비 장기 미납 · 9개 클럽").
D · Chrome that adds nothing
| ID | Sev | What | Evidence |
|---|---|---|---|
| D1 | P1 | "아래로 스크롤해 더보기" appears on short lists, even a single row with 1,641 px of empty list below it (클럽 테니스장 관리: list 1,799 px, content 158 px). Both code sweeps judged this "not reproducible". The device plus the code found the bug. [DV-02] | ui/src/feed-list/hooks.ts useScrollHint: reset() runs on every itemCount change and zeroes the measured viewport height. The list's onLayout never fires again (its size didn't change), so layoutHeight stays 0 and showHint = contentHeight > 0 + 24 + padding is true for any content. Fix: don't reset the viewport height (it doesn't depend on item count). Keep the pill only for long lists, or retire it. |
| D2 | P2 | Persistent explainers: 멤버 관리 "조는 클럽 순위로 나눈 실력 그룹이에요", 리더보드 "실력자들과 비교해보세요", 테마 disclaimer, 관리 row sub-lines under every destination, 복식 파트너 추천 intro. [device] | Show once (first visit) or behind a "?" (the U-07b first-encounter pattern). |
| D3 | P2 | Every club card on 클럽 spends ~40% of its height on the same default court illustration. [DV-14] | shared/club-card.tsx photo seam (the default photo set is EXT-16). Until real photos exist, a compact card without the band. |
| D4 | P2 | 클럽 만들기 is a permanently pinned full-width band on 내 클럽, including for members who will never create a club. [DV-14] | The FAB-for-creation pattern, or keep the band only when the member has no clubs. |
E · Spacing and rhythm (the recurring complaint)
| ID | Sev | What | Evidence |
|---|---|---|---|
| E1 | P1 | Root cause. A tappable ListRow pads top and bottom 8 unconditionally and ignores edge, so RowList's first/last-row trim is silently off for every tappable row. Any non-flush section title above one sits 16 dp from its content instead of 8. Measured on device: 관리 운영 16 dp, public profile 클럽 16 dp, session 참석 8 dp (flush). [EV-03, RP-13, CL-01..03] | ui/src/list-row.tsx pressableRowStyle overrides edgePadding (introduced 2026-09-22, ab9d3790) |
| E2 | P1 | Instances of E1: 관리 운영/설정 · 클럽 설정 출석 추적 · 상대 전적 주요 상대 · public profile 클럽 (and any other section over a tappable first row). | sweep inventories |
| E3 | P2 | Hand-rolled rows that never read edge: 도전 과제 blocks (20 dp), tournament 현재 라운드 (20 dp), participants (16 dp, and GroupedFeedList offers no flush header). Also ApplicationRow, FriendRow and the DM participants modal. [CL-04, SE-04, SE-05] | club-challenges-screen.tsx:305-365, tournament-board-screen.tsx:101-116, feed-list.tsx:399-404 |
| E4 | P2 | The gate that should catch E1 can't. seam-contracts.test.ts asserts resolveListRowMetrics() (a helper), not the rendered row. | ui/src/__tests__/seam-contracts.test.ts:42-48 |
Class fix.
- Make a tappable
ListRowhonouredge(keep the side inset of the press layer). - Give
GroupedFeedLista flush header option. - Move the hand-rolled rows onto
ListRow. - Replace helper assertions with a rendered-output seam test (render title + first row, assert the gap).
F · One entity, many grammars (consistency)
| ID | Sev | Entity | Forms in use | Evidence |
|---|---|---|---|---|
| F1 | P1 | Person | At least seven row components: ListRow (roster preview, head-to-head, add friend, blocked), RsvpParticipantRow / AttendanceParticipantRow / MannerTagParticipantRow, ApplicationRow, FriendRow, the DM participants modal row, leaderboard rows. Leaderboard and head-to-head show initials for users who have photos elsewhere. Partner suggester leads with a sparkle icon instead of the face. [SE-06, RP-19, RP-21, DV-09, DV-25] | sweep inventories 2b |
| F2 | P1 | Session row | Club home rows show "오늘 · 올림픽공원 테니스장" with no time, so three sessions on one day read identically (seen on two clubs). Other session rows carry time. [CL-05, EV-07] | club-detail/club-schedule-rows.tsx:111-122 |
| F3 | P2 | Match | MatchTimelineRow (boxed score, not tappable) vs MatchHistoryRow (tappable → session). Opponent rows are tappable on 상대 전적 but inert on 기간 기록. [RP-04, RP-12] | record-detail/match-timeline-row.tsx, records-history-screen.tsx:108-158, opponent-context-card.tsx:44-61 |
| F4 | P2 | Session hero | The match board's hero is a chip row (📅 · 👥 · 🎾 · 🏷️). The session detail is page-native (eyebrow + title + fact rows). [DV-21] | match-board hero section |
| F5 | P2 | Navigation row | Boxed LinkCards on 기록 vs flat ListRow + chevron everywhere else. A full-width outline button as navigation (장소 정보 제안 → 소유권 신청 내역). [RP-08, device] | records/src/cards/link-card.tsx |
| F6 | P2 | Destructive action | A red outline 삭제 per row on 테니스장 관리, while canon says a muted RemoveIconButton. Big red buttons in 위험 구역 and 호스트 관리. [DV-11] | club-venues-screen.tsx |
| F7 | P2 | Dates & numbers | Dates appear as "2026-08-25" (대항전), "2026.08.19" (동의), "9/8" and "9월 8일 (화)". ELO as "1222" vs "1,213". Money as "12만원" and "15,000원" on one screen. Distance as "8817.7km". [device] | formatDate / formatElo / formatKRW bypassed at the cited screens |
| F8 | P3 | Save model | 일정 수정 has a per-row 수정 and a page-level 변경사항 저장. [DV-29] | edit-session-screen.tsx |
Class fix.
- One
PersonRowbuilt onListRow(avatar photo, then initials; name; one sub; exception badge; optional action). - One match row (tappable to its session everywhere).
- A time on every session row.
- One nav-row grammar.
- The item-remove law applied to venue rows.
- A lint that bans hand-formatted dates and numbers in features (the formatters exist).
G · Truncation and wrapping
| ID | Sev | What | Evidence |
|---|---|---|---|
| G1 | P1 | The 경기 tab's 4th lens reads "테니스장 ..." on every visit. [SE-03, device] | ui/src/pill-nav.tsx:60,303: the scrollable variant triggers at > 4, so exactly 4 items stay in the shrinking track. |
| G2 | P2 | Header subtitle on 출석 확인 is cut ("…X 버튼을 눌러 ..."). The 조용 시간 subtitle on 알림 설정 is cut. [DV-16, device] | DetailShell subtitle numberOfLines; row sub |
| G3 | P2 | Partner suggester "29/40" wraps to "29/4" + "0". [DV-09] | partner-suggester-screen.tsx:222-241 fixed-width value slot |
H · Flow and information architecture
| ID | Sev | What | Evidence |
|---|---|---|---|
| H1 | P1 | Home 일 answers the wrong question. It lists only sessions the member has already joined, then says "오늘 예정된 경기가 없어요 / 새로운 번개를 찾아보세요". Meanwhile the member's own club has three open sessions today (same device, same minute). The CTA opens 내 경기, not 번개 찾기. [DV-28, device] | home-today-view.tsx:201-211 (useRsvpdUpcomingSessions) and :362 (routes.activity, not routes.activityPickups) |
| H2 | P2 | Key actions buried: 문의하기 (contact the host) is now inside the session ⋯ menu. 공유 sits with 신고/차단 in the public profile ⋯. [EV-08, RP-09] | session-detail-header-right.tsx, public-profile-screen.tsx:241-256 |
| H3 | P2 | Dead ends: the admin strip shows 신고 1 in red with no review screen (admin-snapshot-strip.tsx:66). A tournament with no rounds shows only "순위 미정" and 토너먼트 취소. Period match rows don't open anything. [DV-10, DV-22, RP-04] | as cited |
| H4 | P2 | Misplaced entries. 설정 is one undivided list of 21+ rows that includes 없는 장소 추가 and 제안 내역 (content actions, not settings). The club new-member mission (가입인사 남기기 …) is shown to the club's owner. [device] | settings-screen.tsx; club-detail/home/mission-header.tsx:51-55 gates on isMember only |
| H5 | P2 | Form friction: the venue contribution date is typed as "YYYY-MM-DD" text. Region is 17 chips. The 회원 탈퇴 input placeholder is the confirmation phrase itself, so it looks pre-filled. The owner-claim form hand-builds a venue search with duplicate results instead of VenueFinder (canon: the one venue search). [DV-23, DV-24, device] | venue-contributions/new-screen.tsx, venue-owner-claims/new-screen.tsx, delete-account-screen.tsx |
I · Engineering drift and process
| ID | Sev | What | Evidence |
|---|---|---|---|
| I1 | P2 | Canon says HeaderActionsMenu opens a centered ModalPanel. Since 3c18d142 it's an anchored popover on a raw RN Modal. The code is the better UX, but the contract and the code disagree. Correction to the morning evaluation: its elevation is one instance and only while open, which the Android-flattening lesson explicitly allows. [RP-10] | ui/src/header-actions-menu.tsx:106-146; docs/canon/components.md |
| I2 | P2 | Canon's session-detail IA still lists the deleted HostCalloutSection and the old order. The Terms blueprint still describes Cards. The public-profile blueprint says "최근 승률" (the label is 승률). The public-profile header comment says "no titleBadge" while the code uses one. [RP-15, RP-16] Resolved — verified 2026-09-24: all three are fixed. docs/canon/sessions.md "Detail-screen overview IA" now documents the shipped order with an explicit note that HostCalloutSection was deleted; screen-blueprints/legal.md states twice that the flat migration "retired the boxed Card per row" / "every per-section Card box"; screen-blueprints/profile.md reads "승률" (not "최근 승률") and shows titleBadge=[TrustTierBadge] on the hero. | docs/canon/sessions.md, screen-blueprints/legal.md, profile.md |
| I3 | P2 | Unswept corner: venue-contribution and owner-claim detail screens still use text-as-header, "label: value" strings glued into a Badge, and loose stacked buttons. Their list screens wire the CTA outside DetailShell footer. [SE-07, SE-08] | venue-contributions/{detail-screen,contribution-row}.tsx, venue-owner-claims/{receipt-details,claim-summary}.tsx |
| I4 | P3 | Hardcoded bits: a 1200 ELO fallback in a feature (partner-suggester-screen.tsx:62); a typed "›" instead of the chevron icon (club-mission-card.tsx:79-81); an i18n key named …Empty that means "tap to view". [CL-10, CL-11, RP-22] | as cited |
| I5 | P1 | Two agents, one tree. Today, another agent session (Gemini/Antigravity was running on this machine) committed Claude's unfinished work under its own message (414591de). It published OTA a85228e1 from a commit that failed yarn check (size, blueprint, dead-key and clone gates; fixed in 102980d0). Device list 20 described a layout that was changed again an hour later. The pre-push hook doesn't run check:sizes, check:clones or the i18n gates, so the push succeeded. | git log 2026-09-22/23 |
4. Top 20 by user impact
- A1 payment banner for people who owe nothing (P0)
- A2 ELO chart falls for a winner (P0)
- D1 scroll pill on every short list (P1, one-line root cause)
- E1 tappable-row spacing root cause (P1)
- H1 home 일 hides the member's own club sessions; CTA goes to the wrong lens (P1)
- A3 three ELO values for one user (P1)
- A4 two attendance rates / two completeness values for one club (P1)
- C1 roster repeats 참석 확정 on every row (P1)
- B1 / B2 profiles made of "nothing yet" sections for new players (P1)
- F2 club home session rows without a time (P1)
- G1 "테니스장 ..." on the 경기 tab (P1)
- C2 dues rows: repeated amount + ten primary buttons (P1)
- C3 notifications: a wall of identical warning cards (P1)
- B5 / B4 ghost recap card and "— — —" rows (P1)
- B6 54 "준비 중" achievements (P1)
- A7 raw keys and a 150% conversion in 성장 분석 (P1)
- F1 seven person-row components; photos missing on the leaderboard (P1)
- C8 home title restates the pills (P1)
- A5 / A6 총 경기 and 연승 each mean two things (P1)
- I5 single-writer rule for agents + gates on push (P1)
5. Proposed shape of the plan (for the next phase)
Waves are ordered so each one removes a cause, not a symptom. Every wave ships with its prevention (test, lint or shared-component contract) in the same commit.
| Wave | Content | Nature | Needs owner? |
|---|---|---|---|
| W0 — correctness & roots | A1 payment gate · A2 ELO chart · D1 scroll hint · E1 tappable-row edge (+ E4 rendered seam test) · G1 PillNav threshold · H1 CTA route · C9 duplicate format line · F2 time on club rows | Mechanical, small, high impact | No |
| W1 — subtraction laws | B (empty sections, ghosts, 준비 중) · C (exception-only badges, constants in headers, one primary per row, grouped notifications) · D2–D4 | Rules in shared components (SectionBlock empty contract, list badge rule, action budget) | Yes: new-player profile state, notification grouping, dues row action model, hiding 준비 중 achievements, 클럽 만들기 placement |
| W2 — one grammar per entity | F1 PersonRow · F3 match row · F4 hero · F5 nav rows · F6 destructive rows · F7 formatter lint · E3 row migrations · I3 venue receipts | Consolidation; deletes code | No (canon exists) |
| W3 — one number per concept | A3–A9 metric registry: ELO (global vs club, labelled), attendance, completeness, win rate, streak, conversion; clamp and define; enum label maps | Research first (what should each number mean?), then one server source each | Yes: which ELO a club screen shows; which attendance definition |
| W4 — flow & IA | H1 home 일 content · H2 buried actions · H3 dead ends (신고 review, tournament next step) · H4 settings grouping, mission role-gating · H5 form friction | Product decisions + builds | Yes: H1, H3 신고 |
| W5 — process | I5 single-writer rule and handoff (pushed + OTA'd + yarn check green) · pre-push runs the gates yarn check has · top ~10 visual laws in AGENTS.md (the file every agent reads) · I1/I2 canon sync | Workflow + hooks | Confirm the rule |
Research questions for W1–W4 (benchmarks to fetch, not opinions):
- How do Toss, 당근 and Strava treat new-user profiles and "nothing yet" states?
- How do 네이버 밴드 and 카카오 club tools present dues rows and bulk marking?
- What does a notification centre with grouped duplicates look like (iOS / Android / 토스)?
- Which rating do club apps show on club screens (club vs global), and how do they label it?
6. Per-screen index
| Screen | Findings |
|---|---|
| Home (일/주/월) | H1, C8, B5, D1 |
| 알림 | A8, C3, D1 |
| 클럽 (list) | D3, D4 |
| Club detail — 홈 | F2, H4 (mission to owner), D1 |
| Club detail — 멤버 · 멤버 관리 | D2 |
| 관리 (admin hub) | E1/E2, B10, H3 (신고), D2 |
| 회비 관리 | C2 |
| 출석 현황 | A4 |
| 성장 분석 | A4, A7 |
| 클럽 설정 | A4 (6/8 vs 86%), E2 (출석 추적), large blank banner preview with only a trash icon |
| 테니스장 관리 (club venues) | D1 (measured), F6 |
| 클럽 대항전 / 도전 과제 | A9, E3 |
| 가입 심사 | B9, C10 |
| 복식 파트너 추천 | A3, F1 (sparkle icon), G3, I4 |
| 게시판 / 글쓰기 | Category and visibility chip rows look identical and are unlabelled (P3) |
| 경기 tab (all lenses) | G1, D1 |
| 테니스장 (directory) | F7 (8817.7km), category counts don't add up (전체 3,292 vs 3,483 across chips: overlap to verify) |
| Create session / create club | No findings at step 1 (wizards are consistent) |
| Session detail (states) | A1 (host, browser, cancelled), C1, B3, A9 (완료 roster), H2 |
| 참석자 / 참석자 관리 | B7, E3 |
| 호스트 관리 | B8 |
| 출석 확인 | C7, G2 |
| 일정 수정 | F8, hairlines between peer rows |
| 경기 규칙 | C10 (label twice) |
| 참가비 / 정산 | F7 (money formats), raw "15000" in the amount input |
| 스코어카드 | C6 |
| 대진표 (match board) | F4 |
| 토너먼트 보드 | H3, E3 |
| Venue contributions / owner claims | I3, H5 (date text, hand-rolled venue search with duplicates) |
| 기록 | A2, B4, F5 |
| 경기 기록 (history) · 일일 요약 · 기간 기록 | A3 ("1222"), C9, F3 |
| 리더보드 | C4, F1 (initials), A3 |
| 상대 전적 | E2, F1 (initials) |
| 프로필 (own) | B2, A6, C10 (출석률 title) |
| Public profile | A5, B1, H2, E2, I2 |
| 업적 | B6 |
| 메시지 / DM thread | A10 |
| 설정 | H4 (21 rows, misplaced entries), D2 (theme disclaimer) |
| 동의 관리 / 동의 이력 | F7 ("2026.08.19"), A9 (00:00 times) |
| 알림 설정 | C5, G2 |
| 프로필 수정 · 회원 탈퇴 · 공개 범위 | H5 |
| Legal | A11 |
7. Already done while auditing (build hygiene, not audit fixes)
HEAD was red after 3c18d142, and the audit had to run against pushed, shipped code. Commit 102980d0 (pushed, OTA a39f0311) made it green:
- Split the oversized test mock (
ui-mock-render.tsx). - Re-dated three blueprints after re-reading them against the code.
- Pruned four dead i18n keys plus one stale baseline entry.
- Replaced the two identical ELO-progress copies in the profile screens with
EloFactRow showTierProgress.
No audit finding above has been fixed yet.