feat: UFC 경기 일정 페이지 추가 및 AI 메인 이벤트 승부 예측 연동#16
Conversation
- /schedule 페이지: CloudFront CDN → HTML 폴백 크롤링으로 예정 이벤트 최대 8개 표시 - Gemini analyzeMainEvent()로 메인 이벤트 AI 승부 예측 (eventId 중복 체크로 불필요한 호출 방지) - 크론 4단계에 일정 크롤 + 예측 생성 + ufc_schedule 테이블 저장 추가 - 메인 페이지에 SchedulePreview 섹션 추가 (다음 이벤트 1개 + 예측) - Header 네비에 경기 일정 항목 추가
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (17)
📝 WalkthroughWalkthroughUFC 경기 일정 및 AI 승부 예측 기능을 추가하는 PR입니다. 일정 크롤링, 예측 생성, 캐싱, 페이지 렌더링 파이프라인을 구축하고 관련 UI 컴포넌트, 타입 정의, 메시지 문자열을 신규 추가합니다. Changes
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 재생성
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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.
| /** | ||
| * 영문 지명에서 도시명을 추출해 한국어로 변환 | ||
| */ | ||
| function localizeCity(location: string): string { | ||
| for (const [en, ko] of Object.entries(CITY_KO_MAP)) { | ||
| if (location.includes(en)) { | ||
| // 주(state) 등 나머지 텍스트 제거 후 한국어로 대체 |
There was a problem hiding this comment.
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.
| /** | |
| * 영문 지명에서 도시명을 추출해 한국어로 변환 | |
| */ | |
| 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)) { | |
| // 매칭된 도시명만 한국어로 대체하고 나머지 위치 정보는 유지 |
| const id = eventName | ||
| .toLowerCase() | ||
| .replace(/[^a-z0-9]+/g, "-") | ||
| .replace(/^-|-$/g, ""); |
There was a problem hiding this comment.
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.
| 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}`; |
| // 기존 예측 + 신규 예측 합산해서 반환 | ||
| return [...existingPredictions, ...newPredictions]; |
There was a problem hiding this comment.
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.
| // 캐시 데이터도 렌더링 시 이미지 보완 | ||
| const base = cachedSchedule as UfcSchedule; | ||
| const enriched = await enrichFighterImages(base.events); | ||
| return { ...base, events: enriched }; |
There was a problem hiding this comment.
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.
| // 캐시 데이터도 렌더링 시 이미지 보완 | |
| const base = cachedSchedule as UfcSchedule; | |
| const enriched = await enrichFighterImages(base.events); | |
| return { ...base, events: enriched }; | |
| // 캐시 fallback은 외부 스크래핑 없이 그대로 반환하여 ISR/렌더링 시 지연과 실패를 방지 | |
| return cachedSchedule as UfcSchedule; |
| const base = cachedSchedule as UfcSchedule; | ||
| const enriched = await enrichFighterImages(base.events); | ||
| return { ...base, events: enriched }; |
There was a problem hiding this comment.
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.
| /** | ||
| * @description UFC 이벤트 단건 카드 클라이언트 컴포넌트. | ||
| * 어두운 헤더(이벤트명·날짜·파이터 좌우 대결)와 밝은 AI 예측 섹션으로 구성. | ||
| 파이터 이미지 없으면 플레이스홀더 SVG 표시. |
There was a problem hiding this comment.
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.
| 파이터 이미지 없으면 플레이스홀더 SVG 표시. | |
| * 파이터 이미지 없으면 플레이스홀더 SVG 표시. |
Summary by CodeRabbit
릴리스 노트
새로운 기능
문서