Futbol Veritabanı
8대 리그 클럽·선수·통계—Transfermarkt 데이터
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A local-first training app that tracks whether your week is actually complete.
Log a set in a basement with no signal; it syncs when you surface.
Working on it? Start with docs/ — the architecture map, the
invariants that must not be broken, and the traps that have already cost
somebody a day.
@athletic/domain has no runtime dependencies at all. That is enforced
structurally rather than by convention — React, Next and the DOM simply are not
installed there — which keeps the rules testable in isolation and runnable on
either side of the wire.README 전문 보기 ↗A local-first training app that tracks whether your week is actually complete. Log a set in a basement with no signal; it syncs when you surface.
cp .env.example .env # then fill in AUTH_* values
docker compose up -d db # Postgres on :5433
npm ci
npm run db:migrate
npm run dev # http://localhost:3000
Working on it? Start with docs/ — the architecture map, the
invariants that must not be broken, and the traps that have already cost
somebody a day.
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16, App Router, React 19 | Server routes for auth and sync, static shell for the rest |
| Language | TypeScript, strict + noUncheckedIndexedAccess | An unchecked array index is a real source of runtime bugs |
| Styling | Tailwind v4, CSS custom properties | Tokens live in one @theme block; dark by default |
| Database | Postgres — Neon in production, Docker locally | The driver switches on the hostname; same Drizzle code either way |
| ORM | Drizzle + drizzle-kit migrations | SQL-shaped, no generated client, migrations are readable |
| Auth | Auth.js v5, Google, database sessions | A session row can be revoked; a JWT cannot until it expires |
| Local store | Dexie (IndexedDB) | The client's source of truth, not a cache |
| Validation | Zod, at the env and the wire | The client is never trusted |
| Tests | Vitest (domain), Playwright (flows) | Pure logic unit-tested; everything else through a real browser |
apps/web Next.js app — UI, auth, sync endpoint, database schema
packages/domain the training logic: types, model, coach, seed
e2e user-flow write-ups and Playwright tests
legacy/ the pre-Next vanilla build: gitignored, so never in a clone
@athletic/domain has no runtime dependencies at all. That is enforced
structurally rather than by convention — React, Next and the DOM simply are not
installed there — which keeps the rules testable in isolation and runnable on
either side of the wire.
A split decides how the week is organised and what a complete week means. Those are the same question: a push/pull week is complete once you have pushed and pulled. Scoring it against seven movement patterns would mark it down for work it never set out to do, and a coverage view that is permanently red is one people stop reading.
So the seven-movement-pattern method is one option among several rather than a rule imposed on everyone. It is the option that demands rotation and carries, because that is what it is for.
| Split | A complete week | Days |
|---|---|---|
| Seven movement patterns | squat, hinge, lunge, push, pull, rotate, carry | 2–4 |
| Push / Pull / Legs | push, pull, squat, hinge, lunge | 3–6 |
| Upper / Lower | push, pull, squat, hinge, lunge | 2–6 |
| Custom | inherited from the split it grew out of | — |
Each preset materialises into ordinary Slot rows, so nothing downstream knows
a preset was involved — which is what lets a hand-edited split behave exactly
like a built-in one.
Settings → Build your own opens the slot editor, which is the only way to
produce a custom split. It starts from the week you already train, because
arranging one from nothing is a much harder question than adjusting one. Each
slot is pinned to specific movements or constrained to a role, days can be added
and removed, and the editor shows what the arrangement will make a complete week
before it is saved: editing slots can genuinely put a movement out of reach,
and coversFor will then stop asking for it rather than leave a box that can
never be ticked.
The Progress tab shows two, because they answer different questions and one number cannot answer both honestly.
gymmy's strength index — "am I getting stronger?" The sum of your best estimated one-rep max in each of the five loaded patterns (squat, hinge, lunge, push, pull), divided by bodyweight to the power of two thirds and adjusted for age from 40. Only real weights count — barbell, dumbbell and kettlebell lifts, plus pull-ups, chin-ups and dips at bodyweight plus what was added; a machine's stack setting is not a mass and never enters it. Shown to one decimal, tracked week by week. It is only ever compared with itself, which is what lets it count every pattern you train rather than three competition lifts.
DOTS — "how do I compare?" The published powerlifting score, computed from back squat, bench press and deadlift only, exactly as any DOTS calculator would. It needs all three lifts, a bodyweight and an answer about sex, and shows nothing rather than a guess when one is missing.
The choices behind them:
ALLOMETRIC_EXPONENT in strength.ts.Bodyweight is its own dated record rather than a profile field — the score divides by what you weighed that week, and one current value would silently rewrite what every past week meant every time you stepped on a scale.
Changing split must not rewrite the past. Three months of seven-pattern weeks still read as seven-pattern weeks after you move to push/pull; the new split applies from the switch onward and no further back.
A switch therefore appends a SplitPeriod rather than editing a field.
Each period records the split, the day count and a frozen copy of the coverage
goal, so a week is scored against what was in force at the time — even if a
preset's definition changes in a later release, or the user renames a pattern.
Periods start on a Monday, because a week is the unit of coverage, and a second
switch in the same week replaces that week's period rather than adding another.
Opening the first period on an account that pre-dates them backfills the old one first; without that, a first-ever switch would hand every earlier week the new split and silently rescore all of it.
This is the reason for most of the architecture's complexity, so it is worth being explicit about what it buys.
Reads never touch the network. Writes land in IndexedDB and are queued in an
outbox; the UI updates from the local write and never awaits a request. One
endpoint, POST /api/sync, does both directions in a single round trip — push
what is queued, pull anything newer than the cursor.
Two details that are easy to get wrong:
seq from change_seq, which gives changes across all
tables a single total order. Comparing wall clocks between devices could not:
they drift.Conflicts resolve last-write-wins per record. That is a deliberate choice, not a shortcut: the dominant write is a set log — an immutable fact, appended once, from one phone. A CRDT would be substantial machinery for a problem this data shape does not have.
Deletes are soft. A hard delete cannot replicate to a device that is currently offline; it would simply reappear on that device's next push.
npm run dev # app against the local database
npm run build # production build
npm run typecheck # every workspace
npm test # domain, web and e2e suites (needs db + build)
npm run test:e2e # Playwright flows only; E2E_PORT picks the port
npm run lint
npm run format
npm run db:generate # new migration from schema changes
npm run db:migrate
docker compose --profile full up --build # everything containerised
.env at the repo root is read by Docker Compose and drizzle-kit.
apps/web/.env.local is read by Next — it does not read the monorepo root.
Both are gitignored; .env.example documents every variable.
src/env.ts parses the environment once at import and fails with the offending
variable named, rather than surfacing later as an undefined connection string
mid-request. SKIP_ENV_VALIDATION=1 bypasses it for builds that only typecheck.
Signing in as [email protected] (override with DEMO_EMAIL) creates an
account that arrives already onboarded, on the seven-movement-pattern split,
with five months (22 weeks) of history behind it. An empty account demonstrates nothing: no
coverage ticks, no progress line, no strength numbers, and no goal that can be
judged without history to judge it against.
Seeding happens at account creation, through the same seedNewUser path as
everyone else — a demo running on code you do not ship is a demo of the wrong
thing. It also means resetting the demo is DELETE FROM "user" for that address,
followed by signing in again.
apps/web.DATABASE_URL: the pooled connection string, for the app. The driver
switches to Neon's HTTP transport automatically.DATABASE_URL_UNPOOLED: the direct connection string, for
migrations. A pooler can reject or reorder schema changes, and without
this variable migrations fall back to the pooled URL.AUTH_SECRET, AUTH_GOOGLE_ID and AUTH_GOOGLE_SECRET. AUTH_URL is
set by Vercel.https://your-domain/api/auth/callback/google as an authorised redirect
URI in Google Cloud Console.vercel-build runs drizzle-kit migrate over
DATABASE_URL_UNPOOLED before next build, so every deploy applies pending
migrations and a failed one fails the deploy.packages/domain covers the rules — 133 assertions, including every preset at
every allowed day count against both equipment settings and every bias, each
asserted to produce a week with no uncovered pattern, no empty slot and no role
violation; plus the historised coverage, which is checked by reading the same
week before and after a split change and requiring the verdict to be identical.
e2e covers the flows a person actually performs, written up in
e2e/flows before the tests that cover them. It runs in two
layers, and the split is deliberate.
Most specs bypass authentication, seeding a session row rather than driving Google's consent screen — fast, independent, and no test-only code path in the production build. But a suite that always starts from a session row can never see the front door, and that cost came due: email sign-in was broken from the day it shipped (the code was posted in the request body; Auth.js reads it from the query string) while the suite stayed green, because asking for a code was covered and entering one was not.
So flow 08 seeds nothing but the code that would have arrived by email and walks the whole thing: sign up, get set up, train, sign out, come back, and find the same training on a second device. The rule is if a step only ever happens on the way in, a fixture cannot cover it — account creation, the server-side seeding of a new account's rows, the first sync onto an empty device, the sign-out wipe.
userId on every row and index), and a person
can delete their own account (Settings → Delete your account). But nothing is
billed yet, and a trainer is made by a manual database change.sign-in-throttle.ts). Sync and the plans API have no limit.middleware.ts triggers a Next 16 deprecation warning in favour of proxy.ts.
It still works; the rename is untested against the live OAuth flow, so it is
deliberately not done blind.sw-register.tsx says why).