Skip to content

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, then a39f0311). 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), live in_progress states (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 ​

  1. 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).

  2. 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.

  3. The spacing complaint has a single root cause. Every tappable ListRow ignores 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.

  4. 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.

  5. 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 ​

IDSevScreenWhat the user seesEvidenceClass fix
A1P0Session 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.
A2P0기록 → 최근 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.
A3P1Profile · 기록 · 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.
A4P1성장 분석 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.
A5P1Public profile"총 경기" silently switches to a count of sessions attended when the player has 0 matches. [RP-02]public-profile-screen.tsx:399-406One source per label, no silent fallback.
A6P1Profile 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-213Pick one meaning per label; rename the other.
A7P1성장 분석"주요 경로" 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.
A8P2알림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).
A9P2클럽 대항전 · 동의 이력 · completed sessionA 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.tsxState words derive from dates (ended ⇒ 종료). Date-only values never render a time. Completed ⇒ 참석 완료 (canon already has the softened word).
A10P2메시지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 sourceVerify; if duplicates exist, enforce one 1:1 thread per pair.
A11P2Legal개인정보 처리방침 (§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.tsxLegal copy follows the terminology guide and current features. Re-read before launch.

B · Empty and placeholder rendering (unclutter) ​

IDSevScreenWhat the user seesEvidence
B1P1Public 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
B2P1Own profile (new player)친구 · 업적 · 받은 매너 태그 stack as three titled empty blocks. [RP-05]profile-screen.tsx:419-458; only AttendanceSection hides itself when empty
B3P1Session detail (before start)A full 경기 section whose only content is "일정이 시작되면 여기에 내 경기가 표시돼요". [EV-02]session-detail/sections/matches-section.tsx
B4P1기록개인 기록 renders three greyed "— — —" rows plus a teaser sentence. [RP-07]personal-records-section.tsx:82-121
B5P1Home 주 / 월A 30%-opacity recap card with a fake full donut and an overlay pill "아직 이번 주 기록이 없어요". [SE-02]recap-summary-card.tsx:168-209,296-317
B6P1업적 gallery54 of 74 achievements are rows reading "준비 중 · 곧 만나볼 수 있어요", advertising features that don't exist. [device]achievements-gallery-screen.tsx:176-178,217-223
B7P2참석자 (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)
B8P2호스트 관리 (no applicants)The page is an empty 신청 대기 well plus a big red 번개 취소 (already in the detail's 위험 구역). [DV-17]pickup-host-dashboard-screen.tsx
B9P2가입 심사 (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
B10P2관리 (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 SectionBlock empty 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) ​

IDSevScreenWhat repeatsEvidence
C1P1Session detail rosterA solid 참석 확정 badge on every row under a title that already says 참석. Now 12 px since the sm default. [EV-01]participation-section.tsx trailing
C2P1회비 관리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
C3P1알림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
C4P2리더보드The same 🥈 NTRP 3.0–3.5 badge on every row. [DV-25]leaderboard-rank-row.tsx
C5P2알림 설정An identical 알림함 badge on all 13 category rows (in-app is always on). [RP-17]settings-signal-preferences-screen.tsx:80-107
C6P2스코어카드Each match row inside the "1라운드" group restates 1라운드 and 복식. Every player carries the same 🥈 medal. [DV-20]scorecard match rows
C7P2출석 확인16 outlined buttons (참석/불참 on every row), while the explainer says "예외만 수정해 주세요". [DV-16]components/participant-row.tsx (attendance variant)
C8P1HomeThe title 일간/주간/월간 브리핑 restates the 일/주/월 pills directly below it. [SE-01]home-feed-screen.tsx:43-49,101-106
C9P2일일 요약 / 기간 기록Each match row prints the format twice ("복식 / 복식"): sessionTitle is now the format word. [DV-06]record-detail/match-timeline-row.tsx:59-63
C10P3가입 심사 · 경기 규칙 · profileJoin 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 ​

IDSevWhatEvidence
D1P1"아래로 스크롤해 더보기" 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.
D2P2Persistent explainers: 멤버 관리 "조는 클럽 순위로 나눈 실력 그룹이에요", 리더보드 "실력자들과 비교해보세요", 테마 disclaimer, 관리 row sub-lines under every destination, 복식 파트너 추천 intro. [device]Show once (first visit) or behind a "?" (the U-07b first-encounter pattern).
D3P2Every 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.
D4P2클럽 만들기 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) ​

IDSevWhatEvidence
E1P1Root 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)
E2P1Instances of E1: 관리 운영/설정 · 클럽 설정 출석 추적 · 상대 전적 주요 상대 · public profile 클럽 (and any other section over a tappable first row).sweep inventories
E3P2Hand-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
E4P2The 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 ListRow honour edge (keep the side inset of the press layer).
  • Give GroupedFeedList a 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) ​

IDSevEntityForms in useEvidence
F1P1PersonAt 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
F2P1Session rowClub 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
F3P2MatchMatchTimelineRow (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
F4P2Session heroThe match board's hero is a chip row (📅 · 👥 · 🎾 · 🏷️). The session detail is page-native (eyebrow + title + fact rows). [DV-21]match-board hero section
F5P2Navigation rowBoxed LinkCards on 기록 vs flat ListRow + chevron everywhere else. A full-width outline button as navigation (장소 정보 제안 → 소유권 신청 내역). [RP-08, device]records/src/cards/link-card.tsx
F6P2Destructive actionA red outline 삭제 per row on 테니스장 관리, while canon says a muted RemoveIconButton. Big red buttons in 위험 구역 and 호스트 관리. [DV-11]club-venues-screen.tsx
F7P2Dates & numbersDates 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
F8P3Save model일정 수정 has a per-row 수정 and a page-level 변경사항 저장. [DV-29]edit-session-screen.tsx

Class fix.

  • One PersonRow built on ListRow (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 ​

IDSevWhatEvidence
G1P1The 경기 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.
G2P2Header subtitle on 출석 확인 is cut ("…X 버튼을 눌러 ..."). The 조용 시간 subtitle on 알림 설정 is cut. [DV-16, device]DetailShell subtitle numberOfLines; row sub
G3P2Partner 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 ​

IDSevWhatEvidence
H1P1Home 일 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)
H2P2Key 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
H3P2Dead 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
H4P2Misplaced 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
H5P2Form 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 ​

IDSevWhatEvidence
I1P2Canon 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
I2P2Canon'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
I3P2Unswept 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
I4P3Hardcoded 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
I5P1Two 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 ​

  1. A1 payment banner for people who owe nothing (P0)
  2. A2 ELO chart falls for a winner (P0)
  3. D1 scroll pill on every short list (P1, one-line root cause)
  4. E1 tappable-row spacing root cause (P1)
  5. H1 home 일 hides the member's own club sessions; CTA goes to the wrong lens (P1)
  6. A3 three ELO values for one user (P1)
  7. A4 two attendance rates / two completeness values for one club (P1)
  8. C1 roster repeats 참석 확정 on every row (P1)
  9. B1 / B2 profiles made of "nothing yet" sections for new players (P1)
  10. F2 club home session rows without a time (P1)
  11. G1 "테니스장 ..." on the 경기 tab (P1)
  12. C2 dues rows: repeated amount + ten primary buttons (P1)
  13. C3 notifications: a wall of identical warning cards (P1)
  14. B5 / B4 ghost recap card and "— — —" rows (P1)
  15. B6 54 "준비 중" achievements (P1)
  16. A7 raw keys and a 150% conversion in 성장 분석 (P1)
  17. F1 seven person-row components; photos missing on the leaderboard (P1)
  18. C8 home title restates the pills (P1)
  19. A5 / A6 총 경기 and 연승 each mean two things (P1)
  20. 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.

WaveContentNatureNeeds owner?
W0 — correctness & rootsA1 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 rowsMechanical, small, high impactNo
W1 — subtraction lawsB (empty sections, ghosts, 준비 중) · C (exception-only badges, constants in headers, one primary per row, grouped notifications) · D2–D4Rules 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 entityF1 PersonRow · F3 match row · F4 hero · F5 nav rows · F6 destructive rows · F7 formatter lint · E3 row migrations · I3 venue receiptsConsolidation; deletes codeNo (canon exists)
W3 — one number per conceptA3–A9 metric registry: ELO (global vs club, labelled), attendance, completeness, win rate, streak, conversion; clamp and define; enum label mapsResearch first (what should each number mean?), then one server source eachYes: which ELO a club screen shows; which attendance definition
W4 — flow & IAH1 home 일 content · H2 buried actions · H3 dead ends (신고 review, tournament next step) · H4 settings grouping, mission role-gating · H5 form frictionProduct decisions + buildsYes: H1, H3 신고
W5 — processI5 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 syncWorkflow + hooksConfirm 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 ​

ScreenFindings
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 clubNo 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 claimsI3, 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 profileA5, B1, H2, E2, I2
업적B6
메시지 / DM threadA10
설정H4 (21 rows, misplaced entries), D2 (theme disclaimer)
동의 관리 / 동의 이력F7 ("2026.08.19"), A9 (00:00 times)
알림 설정C5, G2
프로필 수정 · 회원 탈퇴 · 공개 범위H5
LegalA11

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.

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