Futbol Veritabanı
8대 리그 클럽·선수·통계—Transfermarkt 데이터
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Fire up a golf tournament in a couple of minutes. Tørny is a mobile-first PWA for running golf tournaments. It scales from four friends on a Saturday round to a club event with 150 players. You create the game, invite the group, and everyone taps their strokes while they walk the course. The app handles the rest: scoring, the live leaderboard, side tournaments, and the mail that goes out when a game ends. Live at tornygolf.no (also tørny.no).README 전문 보기 ↗
Fire up a golf tournament in a couple of minutes.
Tørny is a mobile-first PWA for running golf tournaments. It scales from four friends on a Saturday round to a club event with 150 players. You create the game, invite the group, and everyone taps their strokes while they walk the course. The app handles the rest: scoring, the live leaderboard, side tournaments, and the mail that goes out when a game ends.
Live at tornygolf.no (also tørny.no).
It's invite-only: players sign in with a one-time code by mail, with no open signup and no password to forget. Any signed-in player can set up a game, run it, and finish it themselves, and manage their own games end to end: edit or delete before play starts, add or remove players, invite new people by mail, withdraw someone mid-round, and approve a scorecard on the flight's behalf when a co-player can't. Everyone gets a Klubbhuset tab at the bottom of the screen: your own games gather there, and it's where you set up a new game or add a course. For admins it also holds the full secretariat, where they run club-scale tournaments. Tørny is a solo project, built and run by Jørgen, and it runs in production for real tournaments.
Tørny ships more than twenty scoring modes. Each one comes with a short rules card in the app, so a player can pick something they've never tried and still know how to score it.
Any game can carry a side tournament next to the main result. Turn it on and it runs as an automatic points race across the round, and you can switch off any category you don't care about.
The package counts things like the best net front and back nine, King of the par 3s, 4s and 5s, and most birdies and eagles. It also hands out named badges: Turkey for three birdies in a row, Solid for five, and Snowman for a blow-up hole (that last one costs you points).
Then there are the two hole contests, longest drive and closest to the pin, with up to two of each. Those winners can't be read off the scorecard, so you pick them yourself when you end the game.
| Framework | Next.js 16 (App Router) + React 19 + TypeScript |
| Styling | Tailwind v4, forest-and-champagne palette |
| Database and auth | Supabase (Postgres + Auth + Realtime, EU region) |
| Offline sync | Dexie (IndexedDB) with a last-write-wins RPC |
| i18n | next-intl — Norwegian default on unprefixed URLs, other locales under /<locale>/, catalogs in messages/ |
Resend, through the verified tornygolf.no domain | |
| Testing | Vitest + Testing Library + Playwright |
| Hosting | Vercel, auto-deploy on push to main |
Auth uses a one-time code by mail. Magic links went in the bin because iOS PWA handoff and mail scanners each broke the flow in their own way at the same time.
npm install
npm run dev
Open http://localhost:3000. You'll need a .env.local with Supabase and Resend keys. They're not in the repo, so a fresh clone won't boot on its own. This isn't wired up for outside contributors.
npm test # vitest (2000+ unit + integration)
npm run e2e # playwright
npm run typecheck # tsc --noEmit (Supabase clients are typed, so schema drift fails here)
npm run lint
npm run build
npm run dup # jscpd — copy-paste / duplication report (on demand, doesn't gate)
CI (GitHub Actions) runs typecheck + tests + lint on every PR to main, along with the Expo app's own gate — its separate npm ci, its jest suite, tsc --noEmit, and an expo export, which is the only check that catches a shared lib/ module importing a package the app itself doesn't declare. Its limit is worth knowing: it resolves imports, so it says nothing about a shared module reaching for a JS global the app's engine lacks — Hermes has no crypto, and that only surfaced when the round-start code ran on a device (#1855). A daily schema-drift job regenerates the Supabase types from prod and fails if lib/database.types.ts is stale — run npm run gen:types to refresh it (needs the Supabase CLI + a SUPABASE_ACCESS_TOKEN). A sibling daily job (migration-ledger.yml) reads prod's applied-migrations ledger and fails — opening a prod-vakt issue that closes itself once green — when a file in supabase/migrations/ on main has never been applied; the few known renames and out-of-ledger applies live in docs/loops/migration-ledger-baseline.txt. A monthly job (tavle-arkiv.yml, 1st–3rd of the month) copies last month's comments from the loop boards #1110 and #1208 into docs/loops/logg/ via a docs PR, and deletes a board comment only once a byte-identical copy is on main. An optional authenticated-e2e gate (the scoring golden path, cup/liga smoke, and the signup/invite flows) is wired but off by default; set the RUN_E2E repo variable and the Supabase service-role + E2E_* secrets to activate it. A local pre-push git hook (.githooks/pre-push, enabled automatically by ) runs the same typecheck + lint + tests before every push, so red code never leaves your machine; branch protection on then enforces the same gate server-side — the , and checks must be green before any merge, with docs-only PRs satisfied by the no-op twins in . Companion hooks in and block direct pushes to , guard the local Dexie database name, and require an issue reference in each commit and a note file on user-visible changes — a weekly Monday workflow () rolls the notes into a single version bump and CHANGELOG update. Once that PR is merged, tags the version () and publishes the week's CHANGELOG block as a GitHub release. Main verify starts it with a each time it runs on (every merge except docs-only ones) — the Discord card merges with , which fires neither push nor workflows, but a dispatch always runs — with a push trigger for human merges and a daily schedule as a backup. Releases from before the automation are backfilled locally, because may not tag a commit whose workflow files differ from every branch tip. Native store builds get a tag of their own () on the exact commit that was built, set by the build script after a confirmed upload.
The scoring logic (lib/scoring/) is plain TypeScript with no Supabase dependency. It has its own tests and its own TDD discipline, so don't touch it without writing a new test first. This is where the WHS formula, stroke allocation, best-ball aggregation, the five-tier tiebreaker, and all twenty-odd game modes live.
Offline sync (lib/sync/) writes to Dexie first and drains the queue against Supabase once the phone has signal again. Last write wins, keyed on client_updated_at. The Dexie database is named 'golf-app' for historical reasons. Don't rename it, or you'll wipe local data for every existing user.
RLS is enforced strictly in Postgres. You see your own scores, all scores from any flight while a game is running in live mode (and your own flight's scores in reveal mode), and every score once the admin has ended the game. Single-flight games — four players or fewer, matchplay, Wolf — expose all scores throughout. A game can also be given a public spectate token; anyone with the URL can follow the leaderboard without logging in, via a dedicated /spectate/[token] route that uses the service-role client rather than opening any anon RLS. Realtime channels prime their auth with a no-argument await supabase.realtime.setAuth() before subscribing: the join payload is snapshotted synchronously, so a channel built before priming joins without an access token at all. The token itself is never passed in — an explicitly set token switches off supabase-js' own token upkeep, which used to kill the channel when the access token expired mid-round. lib/sync/realtimeChannel.ts owns both, and rebuilds the channel after three consecutive failures. Postgres Changes are never replayed, so once a channel is back after an error or a rebuild it calls the consumer's onResubscribed, and every screen reads its state again instead of waiting for the next event.
The Expo app is the one client that cannot lean on RLS for everything. Anything needing Node — the notification fan-out, Resend mail, the service-role key — reaches the server over a small set of route handlers under app/api/ that authenticate a Authorization: Bearer <access_token> against GoTrue and take the user id from that token alone, never from the body or query. Those routes sit outside the proxy matcher, so they own their own auth: the shared check in lib/api/appAuth.ts is the only place that decides who is calling and whether they organise the game in question. The helpers behind them run on the service-role client with no authorization of their own, which is the part worth remembering — the gate in the route is the security, and there is no RLS underneath it to catch a missing one.
Migrations live in supabase/migrations/ (120+ files, chronological).
docs/, read when that event comes up.app/globals.css, and a test fails if the two drift apart. The design skills in .claude/skills/ and when to use each are in docs/style-and-brand.md.docs/ holds the launch checklist, mail templates, and the original design notes.npm installmainverifye2escanmain.changes/v1.236.0workflow_dispatchmainGITHUB_TOKENworkflow_runGITHUB_TOKENnative-ios/v1.1.0-3