Skip to content

feat: UFC 경기 일정 페이지 추가 및 AI 메인 이벤트 승부 예측 연동#16

Merged
umsungjun merged 1 commit into
mainfrom
feat/ufc-schedule
Apr 20, 2026
Merged

feat: UFC 경기 일정 페이지 추가 및 AI 메인 이벤트 승부 예측 연동#16
umsungjun merged 1 commit into
mainfrom
feat/ufc-schedule

Conversation

@umsungjun
Copy link
Copy Markdown
Owner

@umsungjun umsungjun commented Apr 20, 2026

  • /schedule 페이지: CloudFront CDN → HTML 폴백 크롤링으로 예정 이벤트 최대 8개 표시
  • Gemini analyzeMainEvent()로 메인 이벤트 AI 승부 예측 (eventId 중복 체크로 불필요한 호출 방지)
  • 크론 4단계에 일정 크롤 + 예측 생성 + ufc_schedule 테이블 저장 추가
  • 메인 페이지에 SchedulePreview 섹션 추가 (다음 이벤트 1개 + 예측)
  • Header 네비에 경기 일정 항목 추가

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • UFC 경기 일정 페이지 출시: 예정된 경기 목록과 AI 기반 메인 이벤트 승패 예측 결과 표시
    • 상단 네비게이션에 경기 일정 메뉴 항목 추가
    • 경기 일정 데이터 매일 자동 새로고침 지원
  • 문서

    • README 및 프로젝트 설명서 업데이트

- /schedule 페이지: CloudFront CDN → HTML 폴백 크롤링으로 예정 이벤트 최대 8개 표시
- Gemini analyzeMainEvent()로 메인 이벤트 AI 승부 예측 (eventId 중복 체크로 불필요한 호출 방지)
- 크론 4단계에 일정 크롤 + 예측 생성 + ufc_schedule 테이블 저장 추가
- 메인 페이지에 SchedulePreview 섹션 추가 (다음 이벤트 1개 + 예측)
- Header 네비에 경기 일정 항목 추가
Copilot AI review requested due to automatic review settings April 20, 2026 03:40
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lets-ko Ready Ready Preview, Comment Apr 20, 2026 3:40am

@umsungjun umsungjun merged commit 80f9c4e into main Apr 20, 2026
4 of 5 checks passed
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 003080e4-9ff4-4ac3-a1b8-af61ee991fee

📥 Commits

Reviewing files that changed from the base of the PR and between 60c7db3 and 41187d6.

📒 Files selected for processing (17)
  • CLAUDE.md
  • README.md
  • src/app/[locale]/page.tsx
  • src/app/[locale]/schedule/page.tsx
  • src/app/api/cron/crawl/route.ts
  • src/components/layout/Header.tsx
  • src/components/schedule/EventCard.tsx
  • src/components/schedule/SchedulePreview.tsx
  • src/components/schedule/ScheduleView.tsx
  • src/data/cached-schedule.json
  • src/lib/crawl/schedule-crawler.ts
  • src/lib/crawl/schedule-prediction-generator.ts
  • src/lib/crawl/ufc-image-scraper.ts
  • src/lib/gemini.ts
  • src/messages/en.json
  • src/messages/ko.json
  • src/types/schedule.ts

📝 Walkthrough

Walkthrough

UFC 경기 일정 및 AI 승부 예측 기능을 추가하는 PR입니다. 일정 크롤링, 예측 생성, 캐싱, 페이지 렌더링 파이프라인을 구축하고 관련 UI 컴포넌트, 타입 정의, 메시지 문자열을 신규 추가합니다.

Changes

Cohort / File(s) Summary
문서 및 설정
CLAUDE.md, README.md
프로젝트 아키텍처 문서 업데이트: 신규 /schedule, /predictions 라우트, ufc_schedule Supabase 테이블, 크롤러 4단계 체인, Gemini 연동 명시화. README에 일정 기능 및 디렉토리 구조 추가.
일정 페이지 및 라우트
src/app/[locale]/schedule/page.tsx, src/app/[locale]/page.tsx
일정 조회 페이지 신규 추가: Supabase에서 최신 ufc_schedule 데이터 조회, 캐시 폴백. 메인 페이지에 SchedulePreview 컴포넌트 통합 및 getSchedule() 함수 추가.
일정 UI 컴포넌트
src/components/schedule/EventCard.tsx, src/components/schedule/SchedulePreview.tsx, src/components/schedule/ScheduleView.tsx
일정 페이지용 세 개 컴포넌트 추가: EventCard (개별 경기 카드 + AI 예측), SchedulePreview (메인 페이지 미리보기), ScheduleView (전체 일정 목록).
일정 크롤링 및 예측 생성
src/lib/crawl/schedule-crawler.ts, src/lib/crawl/schedule-prediction-generator.ts
UFC 일정 크롤링 (CloudFront API/HTML 폴백), 선수 이미지 병렬 수집. Gemini를 통한 경기 결과 예측 생성 및 기존 예측 재사용.
타입 정의 및 캐시 데이터
src/types/schedule.ts, src/data/cached-schedule.json
UfcEvent, EventPrediction, UfcSchedule 등 타입 인터페이스 정의. 폴백용 캐시된 일정 및 예측 데이터 추가.
Gemini AI 통합
src/lib/gemini.ts, src/lib/crawl/ufc-image-scraper.ts
analyzeMainEvent() 함수 추가로 경기 예측 생성. 이미지 스크레이퍼에 8초 타임아웃 추가 및 JSDoc 보강.
API 및 캐시 무효화
src/app/api/cron/crawl/route.ts
cron 핸들러에 일정 크롤링 및 예측 생성 단계 추가, /schedule 경로 캐시 워밍, maxDuration 60초 설정.
네비게이션 및 다국어 지원
src/components/layout/Header.tsx, src/messages/en.json, src/messages/ko.json
헤더에 schedule 네비게이션 항목 추가. 영문/한글 메시지 파일에 일정 페이지 UI 문자열 및 네비게이션 라벨 추가.

Sequence Diagram(s)

sequenceDiagram
    participant User as 사용자
    participant NextApp as Next.js App
    participant CronJob as Cron Job
    participant Crawler as Schedule Crawler
    participant Gemini as Gemini API
    participant Supabase as Supabase
    participant Cache as 캐시 JSON
    
    User->>NextApp: /[locale]/schedule 페이지 방문
    NextApp->>Supabase: ufc_schedule 최신 데이터 조회
    alt Supabase 데이터 있음
        Supabase-->>NextApp: 일정 + 예측 반환
    else 없음/에러
        NextApp->>Cache: 캐시된 일정 로드
        Cache-->>NextApp: 폴백 데이터
    end
    NextApp->>NextApp: 선수 이미지 enrichment
    NextApp-->>User: 일정 페이지 렌더링
    
    CronJob->>Crawler: crawlUfcSchedule() 호출
    Crawler->>Crawler: CloudFront API에서 이벤트 조회
    alt API 실패
        Crawler->>Crawler: UFC.com HTML 파싱 (cheerio)
    end
    Crawler-->>CronJob: UfcEvent[] 반환
    
    CronJob->>Gemini: generateSchedulePredictions(events) 호출
    Gemini->>Gemini: 각 경기별 analyzeMainEvent() 호출
    Gemini-->>CronJob: EventPrediction[] 반환
    
    CronJob->>Supabase: UfcSchedule 블로브 삽입 (events + predictions)
    Supabase-->>CronJob: 저장 완료
    CronJob->>NextApp: revalidatePath() 캐시 무효화
    NextApp->>NextApp: /, /schedule, /predictions 재생성
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature/ai predictions #9: Gemini 통합 및 cron 예측 워크플로우와 직접 연계된 AI 예측 기능 (src/lib/gemini.ts, src/app/api/cron/crawl/route.ts 공통 수정).

Poem

🐰 일정을 크롤하고, 예측은 Gemini로,
캐시는 폴백 준비, 선수 이미지도 병렬로!
경기 카드 반짝반짝, UI는 깔끔하고,
라우트도 추가되고 메시지도 갖춰졌으니,
UFC 일정이 이제 완성되네! 🥊✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ufc-schedule

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new UFC schedule feature that crawls upcoming events, generates Gemini-based main event predictions, persists them to Supabase, and surfaces the result via a new /schedule page plus a home page preview section.

Changes:

  • Introduces schedule domain types, crawler (CloudFront → HTML fallback), and prediction generation pipeline.
  • Extends the cron crawl chain to store ufc_schedule (events + predictions) and revalidates/warms schedule routes.
  • Adds schedule UI (page + home preview) and updates navigation + i18n strings/docs.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/types/schedule.ts Adds shared types for events, fights, and AI predictions stored in Supabase.
src/messages/ko.json Adds schedule translations and new nav item label.
src/messages/en.json Adds schedule translations and new nav item label.
src/lib/gemini.ts Adds analyzeMainEvent() for Gemini JSON prediction output.
src/lib/crawl/ufc-image-scraper.ts Adds fetch timeout and improves JSDoc.
src/lib/crawl/schedule-prediction-generator.ts Generates/preserves schedule predictions with duplicate avoidance.
src/lib/crawl/schedule-crawler.ts Crawls upcoming schedule (CloudFront first, HTML fallback) and enriches images.
src/data/cached-schedule.json Adds cached schedule+predictions fallback blob.
src/components/schedule/ScheduleView.tsx New schedule page view rendering upcoming events + predictions.
src/components/schedule/SchedulePreview.tsx New home page “next event + prediction” preview section.
src/components/schedule/EventCard.tsx Event card UI including expandable analysis.
src/components/layout/Header.tsx Adds Schedule link to header nav.
src/app/api/cron/crawl/route.ts Adds schedule crawl+prediction stage and revalidation/warmup for schedule routes.
src/app/[locale]/schedule/page.tsx New schedule page with Supabase → cached JSON fallback.
src/app/[locale]/page.tsx Adds schedule loading + SchedulePreview on home.
README.md Documents new schedule feature and folder structure.
CLAUDE.md Updates architecture/docs for schedule crawler + cron chain + data fallback pattern.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +163 to +169
/**
* 영문 지명에서 도시명을 추출해 한국어로 변환
*/
function localizeCity(location: string): string {
for (const [en, ko] of Object.entries(CITY_KO_MAP)) {
if (location.includes(en)) {
// 주(state) 등 나머지 텍스트 제거 후 한국어로 대체
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localizeCity() iterates CITY_KO_MAP in insertion order, which causes partial matches to win over more specific ones (e.g., "New York" matches before "New York City", and "Kuala" matches before "Kuala Lumpur"), producing broken localized strings. Consider matching the longest keys first (sort entries by key length desc) or remove ambiguous short keys; also the comment about removing state/extra text doesn’t match the current replace() behavior.

Suggested change
/**
* 영문 지명에서 도시명을 추출해 한국어로 변환
*/
function localizeCity(location: string): string {
for (const [en, ko] of Object.entries(CITY_KO_MAP)) {
if (location.includes(en)) {
// 주(state) 등 나머지 텍스트 제거 후 한국어로 대체
const CITY_KO_ENTRIES = Object.entries(CITY_KO_MAP).sort(
([a], [b]) => b.length - a.length,
);
/**
* 영문 지명에서 도시명을 추출해 한국어로 변환
*/
function localizeCity(location: string): string {
for (const [en, ko] of CITY_KO_ENTRIES) {
if (location.includes(en)) {
// 매칭된 도시명만 한국어로 대체하고 나머지 위치 정보는 유지

Copilot uses AI. Check for mistakes.
Comment on lines +232 to +235
const id = eventName
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchFromCloudFront() derives id from a slugified eventName, even though the API provides a numeric EventId. Slug-based IDs can change when titles change and can collide across events, which will break prediction de-duping (eventId) and mapping predictions back to events. Prefer using EventId (or EventId + date) as the stable UfcEvent.id for the CloudFront source.

Suggested change
const id = eventName
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
const eventSlug = eventName
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
const id =
raw.EventId != null ? String(raw.EventId) : `${dateStr}-${eventSlug}`;

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +75
// 기존 예측 + 신규 예측 합산해서 반환
return [...existingPredictions, ...newPredictions];
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateSchedulePredictions() always returns existingPredictions plus newPredictions without pruning to the current events list. Because the cron inserts a new ufc_schedule row each run, this will cause the JSON blob (and predictions.length) to grow over time with stale predictions for events no longer in the 8 upcoming events. Filter existingPredictions to events.map(e => e.id) before merging (or rebuild the predictions array per crawl) to keep the stored payload bounded.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +88
// 캐시 데이터도 렌더링 시 이미지 보완
const base = cachedSchedule as UfcSchedule;
const enriched = await enrichFighterImages(base.events);
return { ...base, events: enriched };
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cached fallback path calls enrichFighterImages(base.events), but cached-schedule.json currently has no imageUrls, so this will trigger up to ~20 external scrape requests during ISR/page generation whenever Supabase isn’t available (or before cron runs). Consider embedding headshot URLs in the cached JSON, skipping enrichment for cached data, or caching enrichment results to avoid slow/fragile runtime scraping.

Suggested change
// 캐시 데이터도 렌더링 시 이미지 보완
const base = cachedSchedule as UfcSchedule;
const enriched = await enrichFighterImages(base.events);
return { ...base, events: enriched };
// 캐시 fallback은 외부 스크래핑 없이 그대로 반환하여 ISR/렌더링 시 지연과 실패를 방지
return cachedSchedule as UfcSchedule;

Copilot uses AI. Check for mistakes.
Comment thread src/app/[locale]/page.tsx
Comment on lines +178 to +180
const base = cachedSchedule as UfcSchedule;
const enriched = await enrichFighterImages(base.events);
return { ...base, events: enriched };
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSchedule() enriches fighter images even for the cached fallback (cached-schedule.json), which currently contains no imageUrls. That means the home page ISR generation can fan out to ~20 external scrape requests when Supabase isn’t available (or before cron runs), impacting latency and reliability. Consider including image URLs in the cached data, skipping enrichment for cached fallback, or persisting enrichment results.

Copilot uses AI. Check for mistakes.
/**
* @description UFC 이벤트 단건 카드 클라이언트 컴포넌트.
* 어두운 헤더(이벤트명·날짜·파이터 좌우 대결)와 밝은 AI 예측 섹션으로 구성.
파이터 이미지 없으면 플레이스홀더 SVG 표시.
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc formatting is broken on the line "파이터 이미지 없으면…": it’s missing the leading * so it won’t be treated as part of the block comment. Please align it with the surrounding JSDoc style.

Suggested change
파이터 이미지 없으면 플레이스홀더 SVG 표시.
* 파이터 이미지 없으면 플레이스홀더 SVG 표시.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants