尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗게임 × IP 콜라보 트래커의 Next.js 프론트엔드. PRD의 M2 범위(F-01~F-08)와 디자인 초안 4개 화면(목록·상세·작품·관리자)을 구현했다.
/ko, /en 경로src/schema, 나중에 packages/schema로 이동)corepack enable # pnpm 12
pnpm install
cp .env.example .env.local
pnpm dev # http://localhost:3000/ko
API_BASE_URL을 비워 두면 메모리 목(mock) API로 동작한다. 목 데이터는 PRD에 나온 콜라보 3건(태고의 달인 × 자가리코, Blood Strike / WePlay × 진격의 거인)과 검수용 초안 1건뿐이고, 확인되지 않은 값은 비워 두었다. 관리자에서 수정한 내용은 서버를 재시작하면 사라진다. MOCK_LATENCY_MS=800처럼 지정하면 목 API 호출마다 그만큼 지연을 넣어, 로딩·대기 화면을 실제 API와 비슷하게 확인할 수 있다. MOCK_QUEUE_PAGE_SIZE=1처럼 검수 큐 한 페이지 크기(기본 100, 백엔드와 같음)를 줄이면 “더 보기”를 확인할 수 있다.
개발 중에는 ADMIN_DEV_BYPASS=1이면 로그인 없이 /ko/admin에 들어갈 수 있다(production에서는 무시).
pnpm typecheck # next typegen + tsc (TS 7)
pnpm test # vitest: 진행 상태 계산, URL 필터, 목 API 규칙
pnpm build
| 경로 | 내용 | PRD |
|---|---|---|
/[locale] | 홈: 현황 한 줄(등록된 콜라보 전체 수, 진행 중·예정·종료 수) → 대표 콜라보 큰 카드와 진행 중 번호 목록 → IP로 찾아보기(관리자가 고른 작품 타일) → 새로 등록됨 카드(위에 나온 콜라보 제외) → 일정표(곧 시작·곧 종료). 옛 목록 주소(/[locale]?필터…)는 /[locale]/collabs로 301 | — |
/[locale]/collabs | 목록·검색·필터(카테고리, 파트너 분류, 권역, 플랫폼, 유형, 기간, 진행 상태). 필터 상태는 URL 쿼리에 그대로 들어가 공유 가능. 상위 키를 고르면 하위 항목 포함. 필터를 하나만 건 목록(진행 상태 하나 또는 분류 항목 하나, 결과 1건 이상)은 랜딩 페이지로 색인하고 sitemap에 넣는다. 그 외 필터 조합은 noindex | F-01 |
/[locale]/collabs/[slug] | 상세: 기간·진행 상태, 요약(ko/en), 참여 작품·회사(역할), 출처(확인일), 관련 콜라보. ISR | F-02 |
/[locale]/properties/[slug] | 작품(게임·IP) 페이지: 연도별 타임라인, 건수, 협업 작품, 유형 분포. ISR | F-03 |
/[locale]/companies/[slug] | 회사 페이지: 참여 콜라보와 역할. ISR | F-03 |
/[locale]/admin | 검수 대기열 + 편집기. draft → in_review → published → archived, 발행 조건 체크리스트, 중복 후보 비교, 분류 매핑 실패 표시, If-Match: rev 충돌 처리 | F-04 |
/[locale]/admin/entities | 작품·회사 자동완성(별칭 일치 표시), 생성·수정, 병합. 작품은 홈 ‘IP로 찾아보기’ 노출 순서와 대표 이미지를 정한다(백엔드 명세: docs/api/featured-properties.md) | F-05 |
/[locale]/admin/taxonomy | 통제 어휘 조회·추가(ko/en 라벨, 기존 표기) | F-06 |
/[locale]/stats | 통계: 요약 숫자(전체, 진행 상태별, 올해 시작) → 수집 시작(2026년 4월)부터 월별 막대(최대 24개월, 시작일 기준, 이번 달 강조, 예정은 옅게, 막대마다 툴팁·표 보기) → 분류별 분포(파트너 분류, 콜라보 유형, 카테고리, 권역, 플랫폼. 각 항목은 필터 목록으로) → 순위(게임, 파트너 IP, 역할별 회사, 각 5개) → 집계 기준. Dataset JSON-LD, OG 이미지, sitemap. ISR | — |
/[locale]/stats/[year] | 연도 리포트: 그해 시작한 콜라보만 센다. 전년 대비, 가장 많은 달, 1~12월 막대, 분포·순위(분류 링크는 그해 기간 목록). 시작한 콜라보가 없는 해는 404. ISR | — |
/[locale]/report | 콜라보 제보: 아직 없는 콜라보의 출처 링크(필수)와 메모. 상세 페이지의 “오류 제보” 버튼은 같은 폼을 대화상자로 열어 그 콜라보의 틀린 내용(필수)과 근거 링크를 받는다. 푸터에서 연결, noindex | F-15 |
/[locale]/admin/submissions | 제보 큐: 미처리·반영함·버림, 오류 제보/새 콜라보로 거르기. 오류 제보는 편집기에서 고친 뒤 “반영함”, 새 콜라보는 등록한 뒤 “반영함”. 메뉴에 미처리 건수 | F-15 |
/[locale]/changelog | 변경 내역: 관리자가 공개한 릴리스 노트를 최신순으로(버전·날짜·마크다운 본문, 버전별 앵커 #v1-4-0). 푸터에서 연결, sitemap 포함, changelog 태그로 갱신. ISR | — |
/[locale]/admin/changelog | 변경 내역 작성·수정·삭제·공개(관리자 전용, 편집자는 안내만). 본문은 ko/en 마크다운, 작성/미리보기 전환 | — |
/[locale]/rss.xml | 최신 발행 콜라보 피드(ko/en 분리) | F-11 |
/sitemap.xml, /robots.txt, OG 이미지, /tools/game-ip-collab-tracker/* → 301 | SEO | F-08 |
다국어(F-07): 모든 콜라보·작품·회사는 한국어와 영어를 모두 갖고 올라온다(백엔드가 둘 다 필수로 검증). 그래서 언어 대체나 “기계 번역” 표시는 없다. 관리자 화면은 운영자 전용이라 한국어만 제공한다.
src/
app/
[locale]/(site)/ 공개 페이지 (헤더·고지·푸터 레이아웃)
[locale]/admin/ 관리자 (login, (protected)/…, actions.ts)
api/ export, revalidate 웹훅, GitHub OAuth
components/{site,collab,entity,admin}/
i18n/ next-intl 라우팅
lib/
api/ PublicApi·AdminApi 계약, http.ts(Express), mock/
auth/session.ts 관리자 JWT
phase.ts filters.ts format.ts taxonomy.ts
schema/ Zod 스키마 (공유 대상)
messages/{ko,en}.json
브라우저는 Express를 직접 부르지 않는다. 모든 호출은 Next.js 서버(서버 컴포넌트, Server Action, Route Handler)에서 나간다.
Authorization: Bearer $API_SERVICE_TOKEN, 응답은 태그 캐시(collabs, collab:<slug>, property:<slug>, company:<slug>, taxonomies, 홈 노출 작품은 featured).POST /api/revalidate를 호출한다: Authorization: Bearer $REVALIDATE_SECRET, 본문 { "tags": ["collabs", "collab:<slug>"] }. 작품의 홈 노출 순서·대표 이미지를 바꾸면 featured를 보낸다.AUTH_SECRET, iss=gamecollabs-web, aud=gamecollabs-api, sub=GitHub 로그인, 8시간)를 그대로 Bearer로 넘긴다. Express는 같은 값인 ADMIN_JWT_SECRET으로 검증하고, users 컬렉션에 등록된 GitHub 계정만 통과시킨다(cli create-user --github <login> --role admin).{ data, meta }, 이름은 요청 언어의 문자열 하나)은 src/lib/api/adapt.ts가 화면 스키마(src/schema/index.ts)로 바꾸고, http.ts가 그 스키마로 다시 검증한다. 형태가 어긋나면 조용히 깨지지 않고 바로 오류가 난다.화면이 쓰는 엔드포인트와 변환 (adapt.ts):
| 화면 | Express | 변환 |
|---|---|---|
| 목록·RSS | GET /v1/collabs | 필터는 쉼표 목록, 월 범위 YYYY-MM은 그 달 첫날·마지막 날, sort=recent는 발행일순 |
| 상세 | GET /v1/collabs/:slug | 관련 콜라보의 same_host/same_partner는 호스트 작품 공유 여부로 판정 |
| 작품·회사 | GET /v1/properties/:slug, /v1/companies/:slug | 상대 작품·유형 분포·회사 역할은 타임라인에서 집계 |
| 필터·편집기 분류 | GET /v1/taxonomies | 트리 → 평면 목록 |
| 홈 요약 | GET /v1/stats | total, byPhase |
| 홈 IP로 찾아보기 | GET /v1/properties?featured=true (예정, 명세) | 대체 텍스트가 없으면 작품 이름. 404·형태 불일치면 섹션을 숨긴다 |
| sitemap | GET /v1/sitemap | 그대로 |
| 검수 대기열 | GET /v1/admin/collabs?status=draft,in_review[&cover=missing] | meta.counts(상태별 건수), 항목별 duplicateCount. cover=missing(커버 없음만)을 백엔드가 모르면 받은 100건 안에서 프론트가 거른다 |
| 편집기 | GET /v1/admin/collabs/:id + /duplicates | origin.unmapped → 필드별 미해결 값 |
| 저장 | POST·PATCH /v1/admin/collabs[/:id] (If-Match: <rev>) | 빈 메모는 null, 월 단위 날짜는 YYYY-MM |
| 엔티티 | GET /v1/admin/{properties,companies}[?q=] | q가 없으면 전체 목록 |
| 병합 | POST /v1/admin/{…}/:targetId/merge { from } | from(원본)이 :targetId에 흡수된다 |
| 커버 업로드 | POST /v1/admin/media (multipart, file) | 응답 url이 없으면(null) MEDIA_BASE_URL + storageKey |
편집기의 “커버 이미지”에서 끌어다 놓기·클릭·붙여넣기로 올린다. 브라우저가 긴 변 1600px 이하 WebP(안 되면 JPEG)로 줄인 뒤
POST /api/admin/media(Next Route Handler, 세션·Origin·파일 형식·4MB 확인)로 보내고, 여기서 관리자 JWT를 붙여 Express로 넘긴다.
관리자 토큰이 httpOnly 쿠키라 브라우저가 Express로 직접 보낼 수 없어서다. 목 모드에서는 메모리에 data: 주소로 둔다.
Express(gamecollabs-backend의 POST /v1/admin/media)가 형식 확인(JPEG·PNG·WebP, 5MB, 4천만 픽셀), sharp 재인코딩(메타데이터 제거,
긴 변 1600px, WebP), covers/<sha256>.webp 저장을 맡고 { storageKey, url, width, height }를 돌려준다. 편집기는 저장할 때
PATCH의 cover로 연결하고, 7일 넘게 어느 콜라보도 쓰지 않는 업로드는 백엔드 주간 작업이 지운다.
<img src>(링크로 감싼 이미지면 링크가 아니라 이미지) 주소를 originalUrl로 두고 미리 보기만 하며, 저장 직후
POST /v1/admin/collabs/:id/cover/mirror로 백엔드가 우리 저장소로 복사한다(백엔드의 safeFetch가 사설 주소 등을 막는다).
복사에 실패하면 원본 링크로 저장된 채 편집기에 이유가 보이고, 백엔드 시간별 mirror-covers 작업도 다시 시도한다.cover 필드가 없으면(이전 백엔드) 편집기는 현재 커버를 “모름”으로 보고, 사용자가 커버를 건드리지 않는 한
저장 본문에서 cover를 빼서 기존 값을 지우지 않는다.storage_unavailable)을 준다..env.example 참고. 백엔드(홈서버 deploy/.env)와 짝이 맞아야 하는 값:
| 프론트 (Vercel) | 백엔드 (deploy/.env) |
|---|---|
API_BASE_URL=https://api.<도메인> (/v1 없이) | Cloudflare Tunnel의 api 호스트 |
API_SERVICE_TOKEN | SERVICE_TOKENS에 들어 있는 값 하나 |
AUTH_SECRET (32자 이상) | ADMIN_JWT_SECRET (같은 값) |
REVALIDATE_SECRET | WEB_REVALIDATE_SECRET (같은 값), WEB_REVALIDATE_URL=https://<사이트>/api/revalidate |
MEDIA_BASE_URL | MEDIA_BASE_URL (같은 값, 커버 이미지 주소) |
제보 폼의 Cloudflare Turnstile은 백엔드에 TURNSTILE_SECRET을 넣었을 때 프론트에 짝이 되는 사이트 키 NEXT_PUBLIC_TURNSTILE_SITE_KEY를 넣으면 켜진다(빌드 때 들어가므로 바꾸면 다시 배포). 둘 다 비우면 honeypot과 IP당 시간당 10건 제한만 쓴다.
방문자는 두 가지를 보낼 수 있다. 이름·연락처는 받지 않는다.
/[locale]/report): 출처 링크(필수)와 메모.폼은 서버 액션(src/app/[locale]/(site)/actions.ts)으로 보내고, 서버 액션이 입력을 다시 검증한 뒤 서비스 토큰과 방문자 IP(X-Forwarded-For의 첫 값 → X-Client-IP)를 붙여 Express로 넘긴다. 봇 거르기는 숨은 website 칸(채워져 있으면 받은 척하고 버림), 백엔드의 IP당 시간당 10건 제한, 설정했을 때 Turnstile. 아직 열린 같은 제보(같은 URL, 같은 콜라보·같은 내용)는 한 번만 쌓인다. 운영자는 /admin/submissions에서 처리한다. 목 모드에서도 같은 규칙으로 메모리에 쌓인다.
그 밖에 NEXT_PUBLIC_SITE_URL, GITHUB_CLIENT_ID·GITHUB_CLIENT_SECRET(콜백 https://<사이트>/api/auth/github/callback), ADMIN_GITHUB_LOGINS.
tsc(TS 7)와 next build가 맡는다.| 제보 | POST /v1/submissions (서비스 토큰, X-Client-IP) | kind: tip(url 필수) 또는 correction(collab slug, 5자 이상 note). 429·bot_check_failed·404는 폼에 안내 문구로 |
| 제보 큐 | GET /v1/admin/submissions?status=&kind=, PATCH …/:id { status } | meta.counts(종류별 미처리 수)가 없으면 받은 항목으로 센다. kind가 없는 이전 문서는 tip |