尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
모든 축구팬들을 위한 모바일 전용 이적시장 보드 색인 대상 화면은 전부 SSR입니다 — 보드와 딜 상세 모두 서버가 딜·보도 타임라인을 조립해 초기 HTML에 담습니다(로그인·프로필처럼 색인하지 않는 화면만 클라이언트 쿼리). 프리페치는 최적화라 실패하면 클라이언트 조회로 폴백합니다 — 자세한 규약은 docs/conventions/nextjs.md. 데이터 접근에 Route Handler를 두지 않고 브라우저가 Supabase를 직접 호출합니다. 그래서 RLS와 컬럼 권한이 유일한 방어선이며, 마이그레이션이 곧 보안 설계입니다 — 자세한 규칙은 docs/conventions/api-and-db.md. 이적 데이터(transfer_*)는 앱에 쓰기 경로가 없고 service_role로 도는 수집 스크립트만 씁니다.檢視完整 README ↗
모든 축구팬들을 위한 모바일 전용 이적시장 보드
| 영역 | 내용 |
|---|---|
| 이적시장 | 프리미어리그·유럽 5대 리그 이적 딜 보드(/transfers)와 딜 상세(/transfers/[id], 보도 타임라인). 기자·매체 보도를 매시 수집해(scripts/sync-transfer-news.mjs, GitHub Actions) 딜을 파생하고, 딜에 붙은 보도만 LLM이 한국어로 한두 문장 요약한다. 리그·정렬·구단·관심 필터는 쿼리 파라미터 + canonical이고, 첫 화면만 SSR이며 필터 전환은 서버를 부르지 않는다(주소만 바꾼다). /는 이 보드로 리다이렉트된다 |
| 관심 딜 · 응원 구단 | 로그인 사용자가 딜을 관심 목록에 담고(낙관적 업데이트, 복합 PK로 멱등) 보드의 관심 칩으로 모아 본다. 응원 구단을 고르면 보드의 구단 칩 맨 앞에 놓인다 |
| 알림 | 웹 푸시 — 관심 딜의 상태가 바뀌거나 응원 구단에 새 딜이 생기면 수집 스크립트가 보낸다. 아이폰은 홈 화면에 추가한 앱에서만 된다(app/manifest.ts · public/sw.js) |
| 딜 댓글 | 딜 상세의 댓글·답글(깊이 1)과 좋아요/싫어요 |
| 인증 | 카카오 · 구글 소셜 로그인(로그인 = 가입) · 로그아웃. 에러는 한국어로 매핑 |
| 프로필 | 닉네임(가입 시 랜덤 배정 → 본인이 변경) · 프로필 사진 업로드 · 응원 구단 · 알림 · 로그인 수단 연결 |
| 권한 | 2중 방어 — 클라이언트 가드(AuthRequired·GuestOnly, 안내) → RLS + 컬럼 권한(실제 차단). proxy.ts는 세션 쿠키 갱신만 하고 라우트 가드를 두지 않는다(판정자가 둘이면 무한 리다이렉트가 된다 — docs/conventions/nextjs.md) |
| 검색 유입 · 공유 | 보드·딜 상세 SSR · 정렬·리그는 쿼리 + canonical · sitemap.xml · robots.txt · 딜마다 그리는 공유 카드(opengraph-image) |
| 분석 | GA4(NEXT_PUBLIC_GA_ID가 있을 때만) — 이벤트는 track(@/shared/lib) 하나로 보낸다 |
색인 대상 화면은 전부 SSR입니다 — 보드와 딜 상세 모두 서버가 딜·보도 타임라인을 조립해 초기 HTML에 담습니다(로그인·프로필처럼 색인하지 않는 화면만 클라이언트 쿼리). 프리페치는 최적화라 실패하면 클라이언트 조회로 폴백합니다 — 자세한 규약은
docs/conventions/nextjs.md.
데이터 접근에 Route Handler를 두지 않고 브라우저가 Supabase를 직접 호출합니다. 그래서 RLS와 컬럼 권한이 유일한 방어선이며, 마이그레이션이 곧 보안 설계입니다 — 자세한 규칙은
docs/conventions/api-and-db.md. 보도에서 파생하는 이적 데이터(딜·구단·보도)는 앱에 쓰기 경로가 없고 service_role로 도는 수집 스크립트만 씁니다 — 사용자가 쓰는 것은 자기 행(관심·응원 구단·댓글·표·알림 구독)뿐입니다.
docs/legacy/v1-inventory.md에, 코드 실물은 커밋 5d02657 이전 이력에 있습니다.20260926000001_drop_community_survey_match.sql이
지우고, 코드 실물은 그 커밋 이전 이력에 있습니다.| 카테고리 | 기술 |
|---|---|
| Framework | Next.js 16.2 (App Router, Turbopack) |
| Language | TypeScript 5 (strict) |
| UI | React 19 |
| Styling | Tailwind CSS v4 (@theme 디자인 토큰) |
| Data Fetching | TanStack Query v5 |
| Global State | zustand (세션) |
| Backend / DB | Supabase (PostgreSQL, RLS, 카카오·구글 OAuth) |
| Validation | 손으로 쓴 순수 함수(validateNickname 등) — 스키마 라이브러리 없음. DB CHECK가 실제 방어선 |
| Icons | lucide-react |
| Architecture | FSD (Feature-Sliced Design) |
pnpm install
supabase start # 로컬 스택 기동 (643xx 포트)
supabase db reset # 마이그레이션 적용 + seed.sql 자동 실행
# 개발 계정(alice/bob)은 supabase/seed.sql이 db reset 때 자동으로 넣는다
node scripts/sync-transfer-news.mjs # 이적 소식 수집 → 딜 파생 → 한국어 요약 (로컬 스택에 쓴다)
# 구단 엠블럼(public/crests/)은 이미 커밋돼 있어 따로 받을 필요가 없다.
# 새 구단이 생겨 빈 자리가 보이면: node scripts/fetch-team-crests.mjs --team <id> --code <code>
# (API_FOOTBALL_KEY 필요 — 사용법은 스크립트 머리 주석)
pnpm dev
http://localhost:3000 에서 확인합니다.
로컬 Supabase 스택은 포트 충돌을 피하려고 표준(543xx)이 아닌 643xx를 씁니다 (
supabase/config.toml). API 64321 / DB 64322 / Studio 64323 / Mailpit 64324(발송 메일 확인).
⚠
supabase/config.toml의[auth]값을 바꾸면db reset이 아니라supabase stop && supabase start가 필요합니다.
.env.example을 복사해 .env.local을 만들고 값을 채웁니다.
환경 파일은 둘입니다 — 소셜 로그인의 Redirect URI가 supabase 주소에서 파생되어 환경마다 다르기 때문입니다.
| 파일 | 바라보는 곳 | 읽히는 방법 | 콘솔에 등록할 Redirect URI |
|---|---|---|---|
.env.local | 로컬 스택(643xx) | 자동 (pnpm dev·pnpm build) | http://127.0.0.1:64321/auth/v1/callback |
.env.prod | 원격 프로젝트 | pnpm build:prod / pnpm start:prod | https://<project-ref>.supabase.co/auth/v1/callback |
⚠
.env.prod는 이름을.env.production으로 바꿔도 자동으로 읽히지 않습니다 — Next 우선순위에서.env.local이 위라 로컬 값이 이깁니다. 그래서 스크립트가 명시적으로 싣습니다.
| 변수명 | 설명 | 어느 파일 | 필수 |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase 프로젝트 URL | 둘 다 | ✅ |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase anon(publishable) 키 | 둘 다 | ✅ |
NEXT_PUBLIC_SITE_URL | og:image 절대 URL 기준(빌드 시점에 인라인) | 둘 다 | 배포 시 |
NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION / NEXT_PUBLIC_NAVER_SITE_VERIFICATION | Search Console·네이버 서치어드바이저 소유권 확인 메타(비우면 태그를 내보내지 않음). 등록 뒤 /sitemap.xml 제출 | .env.prod | 검색 등록 시 |
NEXT_PUBLIC_GA_ID | GA4 측정 ID(G-…). 비우면 분석 스크립트를 싣지 않는다 — 운영 빌드에만 넣는다 | .env.prod | 분석 사용 시 |
NEXT_PUBLIC_VAPID_PUBLIC_KEY | 웹 푸시 공개키. 비우면 화면에 알림 기능을 그리지 않는다 | 둘 다 | 알림 사용 시 |
VAPID_PRIVATE_KEY / VAPID_SUBJECT | 웹 푸시 비밀키·연락처 — 발송 스크립트(scripts/sync-transfer-news.mjs)만 쓴다. 런타임에는 필요 없다 | .env.local | 알림 사용 시 |
SUPABASE_SERVICE_ROLE_KEY | 운영 스크립트(scripts/sync-transfer-news.mjs) — 런타임에는 필요 없다 | .env.local | 원격 수집 시 |
ANTHROPIC_API_KEY | 이적 소식 한국어 요약(LLM). 없으면 로컬 실행은 요약을 건너뛰고, --remote 실행은 실패로 끝난다 | .env.local | 원격 수집 시 |
API_FOOTBALL_KEY | 구단 엠블럼 내려받기(scripts/fetch-team-crests.mjs) — 런타임에는 필요 없다 | .env.local | 엠블럼 추가 시 |
SUPABASE_PROJECT_REF | supabase link용 프로젝트 ref | .env.local | 배포 시 |
SUPABASE_DB_PASSWORD | supabase db push용 DB 비밀번호 |
SUPABASE_AUTH_EXTERNAL_*·SUPABASE_PROJECT_REF·SUPABASE_DB_PASSWORD·SUPABASE_ACCESS_TOKEN은.env.local전용입니다 — supabase CLI가 읽는 값이라.env.prod에 적어도 아무 데도 가지 않습니다. 원격의 소셜 로그인 키는 파일이 아니라 대시보드 Authentication → Providers가 소유합니다.
env가 비어 있어도 빌드는 성공합니다. 런타임에 쿼리 훅이 한국어 안내 에러를 던집니다. 다만 소셜 로그인 키는 로그인 자체가 유일한 인증 수단이라 필수입니다 → 소셜 로그인 설정.
⚠ CLI가 원격 프로젝트에 링크돼 있습니다. 확인 프롬프트 없이 원격을 바꾸는 명령이 셋입니다 —
supabase db push(플래그 없음) ·db reset --linked(원격 초기화) ·config push(로컬config.toml의[auth]를 통째로 덮어써 Site URL이 localhost가 됩니다). 로컬 작업에는supabase db reset만 쓰세요.
호스팅은 Vercel, 데이터는 원격 Supabase 프로젝트입니다. Auth 설정은 파일이 아니라
Supabase 대시보드가 소유합니다 — supabase config push는 절대 쓰지 마세요(위 경고).
supabase login # SUPABASE_ACCESS_TOKEN 발급
supabase link --project-ref <ref> # .env.local의 SUPABASE_PROJECT_REF
supabase db reset # ① 로컬에서 먼저 마이그레이션 건전성 확인
bash supabase/tests/run-rls.sh # (러너는 로컬 스택 전용입니다 — 127.0.0.1:64322 고정)
supabase db push # ② 원격에 마이그레이션 적용 (Storage 버킷도 여기서 생성)
supabase migration list --linked # ③ Local/Remote 열이 일치하는지 대조
🔴
supabase db reset --linked를 쓰면 원격에서seed.sql이 돕니다 — 비밀번호가test1234인 테스트 계정이 생깁니다. 시드는 로컬 전용이고,db push는 시드를 실행하지 않습니다.
⚠
20260926000001_drop_community_survey_match.sql은 되돌릴 수 없습니다 — 원격에 적용하는 순간 글·댓글·투표·예측·공지 행이 전부 사라집니다. 적용 전에 원격 DB를 백업합니다. 이 마이그레이션은post-images·survey-images버킷의 정책만 걷습니다(호스팅 Supabase가 버킷 직접 삭제를 막습니다) — 버킷과 남은 파일은 대시보드(Storage)에서 비운 뒤 지웁니다. 공개 버킷이라 그 전까지는 옛 파일의 공개 URL이 열려 있습니다.
| 항목 | 값 |
|---|---|
| Framework | Next.js (자동 감지) |
| Build Command | 기본 next build — ⚠ build:prod를 쓰면 안 됩니다(.env.prod는 저장소에 없습니다) |
| Install Command | 자동 (pnpm-lock.yaml) |
| Function Region | 서울(icn1) — vercel.json이 정합니다. Supabase 리전과 같아야 서버 조회가 태평양을 왕복하지 않습니다(docs/conventions/nextjs.md) |
환경변수는 아래 표가 전부입니다. 앱에서 process.env를 읽는 곳은 src/shared/config/env.ts 하나이고,
운영 스크립트는 scripts/lib/sync-db.mjs가 읽습니다(Vercel이 아니라 GitHub Actions 시크릿으로 받습니다).
| 키 | 범위 | 없으면 |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | Production + Preview | 모든 조회가 "서비스 설정이 완료되지 않았어요."(빌드는 성공합니다) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Production + Preview | 〃 |
NEXT_PUBLIC_SITE_URL | Production만 | Preview는 비워 둬야 VERCEL_URL 폴백이 배포별 도메인을 잡습니다 |
NEXT_PUBLIC_GA_ID | Production만 | 분석이 꺼집니다(스크립트를 싣지 않습니다). Preview에 넣으면 시험 클릭이 운영 수치에 섞입니다 |
NEXT_PUBLIC_VAPID_PUBLIC_KEY | Production | 화면에 알림 기능이 그려지지 않습니다. 수집 워크플로의 VAPID_PUBLIC_KEY 시크릿과 같은 값이어야 합니다 |
⚠
NEXT_PUBLIC_*는 빌드 시점에 인라인됩니다 — 값을 바꾸면 반드시 재배포해야 합니다.NEXT_PUBLIC_SITE_URL이 도메인 확정 전에 정해져야 하므로, Import 화면에서 프로젝트 이름을 먼저 정하고 그 자리에서https://<이름>.vercel.app을 넣은 뒤 첫 배포를 돌립니다.
⚠
SUPABASE_AUTH_EXTERNAL_*·SUPABASE_PROJECT_REF·SUPABASE_DB_PASSWORD·SUPABASE_ACCESS_TOKEN·SUPABASE_SERVICE_ROLE_KEY·VAPID_PRIVATE_KEY는 Vercel에 넣지 않습니다 — 소셜 로그인 키는 대시보드가, CLI 값은 로컬이, service_role은 수집 워크플로의 시크릿이 소유합니다. 앱 런타임에는 service_role이 필요 없습니다.
빌드 로그에서 확인할 것: /transfers·/transfers/[id]가 ƒ(동적) 이어야
합니다. ○면 unstable_rethrow 가드가 cookies()의 내부 에러를 삼킨 것입니다
(docs/conventions/nextjs.md).
/sitemap.xml만 ○ + 30s가 정상입니다 — 쿠키를 읽지 않아 통째로 프리렌더되고
ANON_REVALIDATE가 ISR 주기가 됩니다.
| 위치 | 값 |
|---|---|
| Authentication → URL Configuration → Site URL | https://<이름>.vercel.app |
| 〃 → Redirect URLs | https://<이름>.vercel.app/** (프리뷰도 쓰려면 https://<이름>-*.vercel.app/**) |
| Authentication → Providers | Kakao · Google 활성화 + 키 4개 직접 입력 |
| 비활성 (이 앱은 소셜 전용입니다) | |
〃 설정의 Manual Linking (config.toml의 enable_manual_linking에 대응) | 활성 — /profile의 "로그인 수단 연결"(features/link-identity)이 이 값에 의존합니다 |
각 프로바이더 콘솔에는 앱 주소가 아니라 Supabase 콜백(https://<ref>.supabase.co/auth/v1/callback)을
등록합니다 → 소셜 로그인 설정.
⚠ 앱 복귀 주소가 Redirect URLs에 없으면 GoTrue가 조용히 Site URL로 되돌려 보냅니다 —
?next=가 통째로 사라져 "딜 상세에서 로그인했는데 보드로 떨어지는" 증상이 됩니다.
.github/workflows/sync-transfer-news.yml이 매시 node scripts/sync-transfer-news.mjs --remote를 돌린다
(수집 → 딜 파생 → 한국어 요약 → 알림 발송). 저장소 시크릿에 SUPABASE_URL·SUPABASE_SERVICE_ROLE_KEY·
ANTHROPIC_API_KEY를 넣는다. 웹 푸시 알림을 켜려면 VAPID_PUBLIC_KEY·VAPID_PRIVATE_KEY·VAPID_SUBJECT를 더한다
(pnpm exec web-push generate-vapid-keys로 만든다 — 공개키는 Vercel의 NEXT_PUBLIC_VAPID_PUBLIC_KEY와 같은 값).
셋 중 하나라도 없으면 발송만 건너뛴다. 주기의 상한과 무료 한도는 docs/conventions/api-and-db.md의
이적 소식 동기화 절에 있다.
| 항목 | 상태 |
|---|---|
| 구단 엠블럼 | public/crests/에 커밋된 것만 뜹니다. 없는 구단은 약칭 모노그램으로 떨어지고, 채우려면 node scripts/fetch-team-crests.mjs 후 커밋·재배포입니다 |
| 이적 창 일정·보도 주체 표기 | scripts/lib/transfer/windows.json·reporters.json을 시즌마다 사람이 갱신합니다 |
| 옛 스토리지 버킷 | post-images·survey-images는 대시보드에서 비우고 지웁니다(위 1번의 경고) |
⚠ 무료 플랜의 Supabase 프로젝트는 무활동이 이어지면 정지됩니다 — 정지되면 사이트 전체가 데이터를 잃은 것처럼 보입니다.
라우팅은 얇게(app/), 구현은 FSD 레이어(src/)로 분리합니다. 의존 방향은
shared ← entities ← features ← widgets ← views 단방향이며, 각 슬라이스의 index.ts가 public API입니다.
상세 규칙은 docs/conventions/.
app/ # Next.js 라우팅 전용 (view만 마운트)
├── page.tsx # / → /transfers 리다이렉트
├── (auth)/ # sign-in (GuestOnly 셸). 소셜 로그인 복귀 지점이기도 하다
├── transfers/ # 이적 보드 · [id] (딜 + 보도 타임라인 · 딜마다 그리는 공유 카드 opengraph-image)
├── profile/ # 닉네임·사진 수정 + 응원 구단 + 알림 + 계정 연결. 계정 연결의 복귀 지점
├── manifest.ts # 홈 화면 앱(웹 푸시의 전제 — 아이폰은 설치한 앱에서만 알림이 된다)
└── sitemap.ts / robots.ts # 색인 신호 (Route Handler는 없다 — 이들은 Next 특수 파일이다)
public/sw.js # 서비스 워커 — 웹 푸시 알림 전용(캐시·오프라인 없음)
proxy.ts # 세션 쿠키 리프레시 (Next 16의 middleware). 라우트 가드는 없다
src/
├── app/ # providers(QueryClient + AuthProvider), fonts, globals.css
├── views/ # 화면 조립 (⚠ pages 금지)
├── widgets/ # app-bar · bottom-tab-bar · sub-header · tab-scroll-area · auth-shell · auth-status
├── features/ # 사용자 액션 1개 = 슬라이스 1개
├── entities/ # session · profile · transfer · comment · push
├── shared/ # ui / api / lib / config
└── types/ # database.types.ts (supabase 생성 — 손으로 고치지 않는다)
supabase/
├── migrations/ # 스키마 = 보안 설계
├── seed.sql # db reset이 자동 실행 (개발 계정 alice/bob)
└── tests/ # run-rls.sh · rls.sql
scripts/ # 운영 스크립트 — 이적 소식 수집·파생·요약·알림 발송, 구단 엠블럼, 규약 검사
docs/
├── conventions/ # 코딩 컨벤션
├── oauth-setup.md # 카카오·구글 앱 등록 → 키 → 검증 절차
└── legacy/ # v1 인벤토리 (청산 전 스냅샷)
pnpm dev # 개발 서버 실행
pnpm build # 프로덕션 빌드 (.env.local — 로컬 supabase를 바라본다)
pnpm start # 그 빌드 실행
pnpm build:prod # .env.prod를 실어 빌드 (원격 supabase를 바라본다)
pnpm start:prod # 그 빌드 실행
pnpm lint # ESLint 실행
pnpm lint:fix # ESLint 자동 수정
pnpm db:types # 로컬 스키마 → src/types/database.types.ts 재생성 (마이그레이션 추가 후 필수)
pnpm check:conventions # FSD 레이어·배럴·스타일 화이트리스트 검사
pnpm test:transfer # 이적 파이프라인 회귀(추출·조립·파생·요약·이름·소스·알림 — DB 없이 돈다)
⚠
build와build:prod는 같은.next/를 쓴다.build:prod뒤에pnpm start를 부르면 원격을 바라보는 빌드가 그대로 뜬다. 로컬로 돌아올 때는pnpm build를 다시 돌릴 것.
자동 테스트 프레임워크는 없습니다. 대신 DB 계층에 실행 가능한 검증 스크립트를 둡니다 — RLS가 유일한 방어선이라 정책을 고칠 때마다 돌려야 합니다.
# RLS · 컬럼 권한 · 함수 · 회귀 검사 (전체 rollback이라 DB에 흔적을 남기지 않는다)
bash supabase/tests/run-rls.sh
# 이적 파이프라인 회귀 (DB·네트워크 없이 돈다)
pnpm test:transfer
.env.local| 배포 시 |
SUPABASE_ACCESS_TOKEN | CLI/MCP 인증 토큰 | .env.local | 배포 시 |
SUPABASE_AUTH_EXTERNAL_KAKAO_CLIENT_ID / _SECRET | 카카오 로그인 | .env.local | ✅ |
SUPABASE_AUTH_EXTERNAL_GOOGLE_CLIENT_ID / _SECRET | 구글 로그인 | .env.local | ✅ |