Skip to content

Owner review 46 — lane C: dues, members, the club 기록, participants with fees, participation windows (2026-10-19) ​

Status: Reference · Date: 2026-10-19

Read-only research for owner review 46, marks M10, M11, M16, M17, M25, M26. Paths are repo-relative; hub = docs/twomore_design_system/wireframes/flat-v2-hub.jsx, la = flat-v2-list-a.jsx. Two claims re-checked by the lead on 2026-10-19: expo-clipboard is in no package.json (a native copy button needs a native module and a build), and rsvps has an arrival (available_from_round, arrives_at, 00426) but no departure column.

1. Now ​

Dues ​

  • The ledger is dues, one row per (club, user, year, month) (00009_dues.sql:2-15); status unpaid|paid|partial|waived (00001_enums.sql:10). due_date and a paid_amount rollup of payment_history receipts came later (00405:96-149). A member's 입금 완료 is a pending receipt a manager confirms (00414:184-263); the payer never confirms their own (00536); bulk confirm exists (00648).
  • Frequency is a label only. clubs.dues_amount, dues_day (00003:11-12) and dues_cycle monthly|quarterly|semiannual|yearly, "labels/wizard only" (00490:10-16). No dues screen reads the cycle; the UI is a month picker; generation is manual, one month, a flat amount (packages/features/clubs/src/club-dues/use-club-dues-actions.ts:307-337). Reminders exist (signals_tick_dues, 00405:517-590); auto-generation does not.
  • Account: club_bank_accounts, one per club, readable by active members (00412:15-37). Copy is web-only; native gets selectable text and a hint (packages/app/src/presentation/components/copyable-value.tsx:1-16, 85-97).
  • Absent: an expense/income ledger, a club balance, escrow. The nearest is the legacy sessions.cost_total split (docs/canon/payments.md:11). PortOne checkout is scaffolded; club settlement is "a separate design" with a 3% fee (docs/guides/portone-payments.md:76-81). The retention store covers dues, payment_history and session_payment only (00542:42-46).
  • The Strip's 확인 대기/미납/완납 counts (hub 2479-2486; la 2016-2033) repeat the section-head counts right below them; production already dropped its StatStrip for that reason (packages/features/clubs/src/club-dues/sections/dues-list-headers.tsx:8-19). Review 29 faulted a Strip as "how much, not what to do or for whom" (owner-review-29-rulings-2026-10-15.md:53). Empty states hub 2446-2468; 멤버를 초대해 보세요 is a lone Notice above an empty feed (hub 2212-2227).

Session fees and participants ​

  • session_payments: one hold per (session, user), pending|paid|waived (00059:1-13); method manual|portone and submitted_at (00301:32-36); refund_state (00307:31); a settle-before-play match gate (00384:45-87).
  • Participants and fees are separate routes (la 3697-3788, 3795-3857); the fee row is hand-built with no avatar (packages/features/sessions/src/session-payments/payment-row.tsx:47-83).

Members and 복식 파트너 추천 ​

  • The pane: caption, a 복식 파트너 추천 task cell, role groups, then the officials' 멤버 관리 (2) cell (hub 2414-2433; packages/features/clubs/src/club-detail/members-pane.tsx:225-236).
  • The full page stacks 심사 대상, 추천할 만한 게스트, two tool cells and the roster as peer sections (la 1607-1654), against the page grammar's "Modules… three rows at most" (hub 383).
  • The suggester is rankPartnerCandidates: ELO proximity 40, chemistry 30, trust 15, attendance 15 (packages/app/src/domain/rules/partner-matching.rules.ts:1-9, 178-201).

순위 and 조 ​

  • RankingTab shows the global rating filtered to members; 조별 only partitions that list and never renumbers within a group (packages/features/clubs/src/club-detail/ranking-tab.tsx:153-204). 조 is an ELO rank partition into club_groups with a manual pin (00476:51-66, 133-138; club-groups.rules.ts:156-209). No per-조 results exist.
  • season_records and get_season_standings exist (00065, 00099) with no writer outside seed scripts and no UI hook found. A standings engine exists for tournaments (tournament.rules.ts:278).
  • 00692 added club-scoped player_matchups_by_format and player_partner_records, readable by club members (00692:70-92); friendlies are skipped. The adapter reads one player at a time, 200 rows max (player-format-records.supabase.ts:18-46).

Attendance and each player's times ​

  • Attendance is derived after completion: appearing in a match = attended; confirmed with matches but none played = a no_show strike; a zero-match session = everyone attended; only when attendance_tracking_enabled (00444:201-338). GPS check-in was retired (00384:1-38).
  • confirm_attendance is owner/admin only (00405:1222-1257); the 출석 확인 entry shows for in_progress|completed (host-tools-gates.ts:71-74); the board's job reads "After a session" (la 4177).
  • Arrival exists, departure does not: rsvps.available_from_round, arrives_at, sessions.late_arrival_policy (00426:37-46), host-written only (00426:72-87), inside the add-round sheet (use-arrival-availability.ts:147-167). arrives_at is informational; its round is an estimate (arrives-at-to-round.ts:34-42); the default equal_rest_forward is unimplemented (late-arrival-hint.ts:1-16). Early exit is reactive: forfeit_session swaps in a substitute (00683:499-675).
  • Generation reads windows as availabilityMap into isAvailable(id, round) (rotation.rules.ts:227-233); unavailable players bench without taking a fairness slot (resolve-pool.ts:43-45); computeAttendanceViability forecasts per round (feasibility.ts:120-177).

2. References (2025–2026) ​

3. Proposal ​

(a) Dues: two pages, one grammar. 내 회비 (member): one Block for the open period ("분기 회비 90,000원 · 7/1~9/30 · 7/5까지 · D-3 · 미납"); 입금 계좌 as one row with a trailing copy icon button (needs expo-clipboard and a build; until then the share sheet); CTA 입금 완료 알리기; history one row per period. 클럽 회비·장부 (officials see everything, members an aggregate): a 잔액 Block with a 월/분기/연 Choice; one progress line "₩510,000 / ₩600,000 · 17/20명" replaces the Strip; rows with verbs (확인 대기 → 확인; 미납 → 알림 보내기, "마지막 알림 9/5"); a 장부 Feed of dated income and expense rows (category, receipt photo, running balance) with 지출 추가 for officials; CSV export; a transparency setting (members see totals and the unpaid count; names go to officials only). Cadence: club_dues_plans (name, amount, cadence monthly|quarterly|semiannual|yearly|one_time|every_n_months|per_session, due rule, grace, proration, applies to role/조); dues gains plan_id, period_start/end, label; rows generated at period start; per-session dues reuse session_payments. Escrow later extends the hold (settlement_state held|released|refunded, payout batches; release on attended, refund on cancel, forfeit on no-show per club policy; needs a PG sub-merchant or 지급대행 and legal review). Empty states become setup steps for officials (계좌 등록, 회비 설정, 첫 일정, 멤버 초대) and a calm line for members.

(b) Members. The pane is the roster only (role groups, 조 tag, search). Verbs leave the list: one bounded tonal 할 일 row appears when the viewer can act ("가입 신청 2건 · 심사하기") and opens a 가입 심사 page (심사 대상, 추천할 만한 게스트, 게스트 지명); 단톡방 and 초대 go to the header ⋯. An action is time-bound, disappears when done and carries a verb; the list is persistent people with ⋯ for rare actions. 복식 파트너 추천 leaves the pane; rankPartnerCandidates becomes a 파트너 module in 기록 or the 대진 flow.

(c) The club 기록. Same grammar as the personal 기록 (hub 3511-3539): a season Block (경기 수, 활동 멤버, 평균 출석); Modules 순위 전체|조별 (a per-조 table 경기 승 패 승률, reusing calculateStandings with each 조 as a pool), 베스트 파트너 (≥ 5 together), 라이벌/천적 (most played, most lopsided, ≥ 5), 출석·꾸준함, 상승세; doors 선수별 전적 and 통계; Feed 경기 기록. Data: club-scoped bulk reads of the 00692 tables (an RPC beyond 200 rows), a decision on friendlies, a per-match 조 snapshot and a season window, a season_records writer or compute on read.

(d) Participants + fees: one route, PersonRow only (avatar, name, one sub line of state and time, a badge only for 호스트/게스트/예비); sections follow what the host must do (입금 확인 중 with bulk confirm, 입금 필요, 참석 확정, 예비, 취소 with refund state); managers see money, others names and times; the Strip becomes one line ("6명 · 입금 확인 3 · 필요 2"); footer 정산 완료; the split moves to the header ⋯. No new tables: join rsvps and session_payments.

(e) Confirmation with time windows. 출석 확인 becomes 참석 확인, open from T-2h (reuse the session_starting_soon cron, 00385:545-573) to the end; states 미응답, 확정, 도착함, 먼저 감; post-completion derivation (00444) stays as the audit trail. rsvps gains leaves_at, available_until_round, checked_in_at, checked_out_at (the participant declares their window and taps 도착 or 먼저 갈게요; the host edits anyone; 00433's allowlist and confirm_attendance widen accordingly); wall-clock is canonical, rounds derive from the actual round start. Engine: availabilityMap becomes {fromRound, untilRound}; the pre-start screen shows the per-round forecast ("09:00 4명 → 1면 / 09:30 8명 → 2면"); fairness per available round; implement or drop equal_rest_forward. On the day: a T-2h prompt "오늘 몇 시부터 칠 수 있나요?" (정시 / 30분 늦게 / 일찍 가야 해요 / 시간 선택); rows read "09:30 도착 예정"; 도착 enters the next round; 먼저 갈게요 removes the player after their time; at start + 15 m the host sees 미도착 and marks 불참.

Migration gaps: dues plans and periods; a club ledger (opening balance, aggregate RPC, retention record type); a reminder log; escrow settlement and payouts; rsvps windows and check-in; a match 조 snapshot and season writer. Not migrations: the expo-clipboard build, bulk record reads, the session-fee bulk-confirm RPC.

Not verified: Smash's hold and release rules; the legal basis for holding funds; Band's current dues UI; 플랩풋볼 rules; whether season_records has a writer outside seed or edge code.

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