// 1차 mock 데이터 — Firebase 연결 전까지 사용. // ⚠️ AI 예측은 플레이스홀더. 발행 전 GPT/Claude/Gemini 실제 출력(JSON)으로 교체할 것. // 경기 일정은 lib/schedule.ts(Group A 정식 일정, KST)에서 자동 반영. // // 멀티 경기(06.09 회의): 모든 경기에 3 AI 예측 + Crowd 가 있어야 대시보드/상세가 깨지지 않는다. // 체코전(FEATURED)은 큐레이션 값, 그 외 경기는 matchId 시드 결정론적 생성 (Math.random 미사용). import type { AIPrediction, CrowdStats, Match, ModelName, Outcome } from "./types"; import { FEATURED, GROUP_A } from "./schedule"; import { type Lang, teamShort } from "./i18n"; // 체코전 — 실제 3모델 API 출력 (2026-06-10 생성: gpt-5.5 / claude-opus-4-8 / gemini-3.5-flash) const FEATURED_REASONS: Record> = { ko: { GPT: "한국 역습과 체코 제공권이 팽팽", Claude: "손흥민·이강인 측면 창의성이 체코 블록을 연다", Gemini: "이강인·손흥민 공격력, 체코 수비에 근소 우세", }, en: { GPT: "Korea's counters vs Czech aerial — dead even", Claude: "Son & Lee's wing play cracks the Czech block", Gemini: "Korea's attack edges a stubborn Czech defense", }, }; export const MATCH = FEATURED; // 대표 경기 = 한국 vs 체코 (schedule SSOT) // 데모용: 실제 운영은 투표 오픈 = 킥오프 D-2(opensAt). 팀 데모에서 픽이 막히지 않게 // true면 오픈 게이트를 무시하고 즉시 참여 가능. 운영 배포 시 false 로. export const DEMO_FORCE_OPEN = true; // 체코전 — 실제 3모델 API 출력 (2026-06-10). reasonShort는 getPredictions에서 FEATURED_REASONS로 언어별 대체. const FEATURED_PREDICTIONS: AIPrediction[] = [ { matchId: MATCH.matchId, model: "GPT", // gpt-5.5: 무승부 1-1 outcome: "DRAW", scoreA: 1, scoreB: 1, confidencePct: 54, reasonShort: FEATURED_REASONS.ko.GPT, generatedAt: "2026-06-10", }, { matchId: MATCH.matchId, model: "Claude", // claude-opus-4-8: 한국 승 2-1 outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 1, confidencePct: 57, reasonShort: FEATURED_REASONS.ko.Claude, generatedAt: "2026-06-10", }, { matchId: MATCH.matchId, model: "Gemini", // gemini-3.5-flash: 한국 승 2-1 outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 1, confidencePct: 55, reasonShort: FEATURED_REASONS.ko.Gemini, generatedAt: "2026-06-10", }, ]; // ── 결정론적 생성 (matchId 시드) ────────────────────────────── function hash(s: string): number { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; } const MODELS: ModelName[] = ["GPT", "Claude", "Gemini"]; // 예측에 사용한 실제 모델 버전 (UI 투명성 표기용). 갱신 시 .env·AI-PREDICTIONS.md와 함께 변경. export const MODEL_VERSIONS: Record = { GPT: "gpt-5.5", Claude: "claude-opus-4-8", Gemini: "gemini-3.5-flash", }; // AI 셋이 갈리도록 만든 결과 조합 (홈/원정 균형) const OUTCOME_SETS: Outcome[][] = [ ["TEAM_A_WIN", "DRAW", "TEAM_B_WIN"], ["TEAM_A_WIN", "TEAM_A_WIN", "DRAW"], ["TEAM_B_WIN", "DRAW", "TEAM_A_WIN"], ["TEAM_A_WIN", "DRAW", "TEAM_A_WIN"], ["DRAW", "TEAM_B_WIN", "TEAM_A_WIN"], ]; const SCORE_BY_OUTCOME: Record = { TEAM_A_WIN: [[2, 1], [1, 0], [2, 0], [3, 1]], DRAW: [[1, 1], [0, 0], [2, 2]], TEAM_B_WIN: [[0, 1], [1, 2], [0, 2], [1, 3]], }; function reasonFor(match: Match, outcome: Outcome, seed: number, lang: Lang): string { const win = outcome === "TEAM_A_WIN" ? teamShort(match.teamA, lang) : outcome === "TEAM_B_WIN" ? teamShort(match.teamB, lang) : null; const pools = lang === "en" ? { win: win ? [ `${win}'s wing speed edges it`, `${win} just needs one set piece`, `${win} starts on the front foot`, `${win}'s finishing decides it`, ] : [], draw: [ "A tight midfield battle to the end", "Both sides carry a real threat", "The first goal decides everything", ], } : { win: win ? [ `${win} 측면 스피드가 한 끗 위`, `${win} 세트피스 한 방이면 끝`, `${win} 초반 기세에서 앞선다`, `${win} 결정력이 승부를 가른다`, ] : [], draw: [ "중원 힘싸움이 끝까지 팽팽", "양 팀 다 한 방 있는 진검승부", "first goal이 모든 걸 가른다", ], }; const pool = win ? pools.win : pools.draw; return pool[seed % pool.length]; } function generatePredictions(match: Match, lang: Lang): AIPrediction[] { const base = hash(match.matchId); const outcomes = OUTCOME_SETS[base % OUTCOME_SETS.length]; return MODELS.map((model, i) => { const seed = hash(match.matchId + model); const outcome = outcomes[i]; const scores = SCORE_BY_OUTCOME[outcome]; const [scoreA, scoreB] = scores[seed % scores.length]; const confidencePct = outcome === "DRAW" ? 33 + (seed % 13) : 46 + (seed % 25); // draw 33~45, win 46~70 return { matchId: match.matchId, model, outcome, scoreA, scoreB, confidencePct, reasonShort: reasonFor(match, outcome, seed, lang), generatedAt: "2026-06-08", }; }); } // 나머지 5경기 — 실제 3모델 API 출력 (2026-06-10, gpt-5.5 / gemini-3.5-flash / claude-opus-4-8 직접) // scoreA=teamA(앞 팀), scoreB=teamB(뒤 팀). outcome: TEAM_A_WIN / DRAW / TEAM_B_WIN. type Curated = { model: ModelName; outcome: Outcome; scoreA: number; scoreB: number; confidencePct: number; ko: string; en: string; }; const CURATED: Record = { // 개막전 — 멕시코 vs 남아공 A_MEX_RSA_20260612: [ { model: "GPT", outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 1, confidencePct: 62, ko: "멕시코 전력 우위, 남아공 수비 불안", en: "Mexico's edge vs a shaky South Africa defense" }, { model: "Claude", outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 0, confidencePct: 64, ko: "개최국 멕시코의 홈 압박과 측면 우위", en: "Host Mexico's home press and wing edge" }, { model: "Gemini", outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 0, confidencePct: 75, ko: "멕시코의 홈 이점과 객관적 전력 우세", en: "Mexico's home advantage and clear quality" }, ], // 체코 vs 남아공 A_CZE_RSA_20260619: [ { model: "GPT", outcome: "TEAM_A_WIN", scoreA: 1, scoreB: 0, confidencePct: 56, ko: "체코의 조직력·결정력이 근소 우위", en: "Czechia's structure & finishing just edge it" }, { model: "Claude", outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 0, confidencePct: 60, ko: "체코의 세트피스와 피지컬 우위", en: "Czechia's set pieces and physicality" }, { model: "Gemini", outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 1, confidencePct: 65, ko: "체코의 전력 우세와 골 결정력 차이", en: "Czechia's quality and finishing decide it" }, ], // 멕시코 vs 한국 — 모델 갈림 A_MEX_KOR_20260619: [ { model: "GPT", outcome: "TEAM_A_WIN", scoreA: 2, scoreB: 1, confidencePct: 55, ko: "홈 압박으로 멕시코가 한 골 차 우세", en: "Home press gives Mexico a one-goal edge" }, { model: "Claude", outcome: "DRAW", scoreA: 1, scoreB: 1, confidencePct: 50, ko: "홈의 멕시코, 역습의 한국 — 균형", en: "Mexico's press vs Korea's counters — even" }, { model: "Gemini", outcome: "DRAW", scoreA: 1, scoreB: 1, confidencePct: 65, ko: "홈 멕시코와 한국 유럽파의 접전 무승부", en: "Home Mexico vs Korea's Europe stars — a draw" }, ], // 체코 vs 멕시코 A_CZE_MEX_20260625: [ { model: "GPT", outcome: "TEAM_B_WIN", scoreA: 1, scoreB: 2, confidencePct: 58, ko: "멕시코의 홈 이점과 공격력 우세", en: "Mexico's home edge and attack prevail" }, { model: "Claude", outcome: "TEAM_B_WIN", scoreA: 1, scoreB: 2, confidencePct: 57, ko: "멕시코의 템포와 개인기가 체코를 넘는다", en: "Mexico's tempo and flair edge Czechia" }, { model: "Gemini", outcome: "TEAM_B_WIN", scoreA: 1, scoreB: 2, confidencePct: 60, ko: "멕시코의 경험이 승리를 견인", en: "Mexico's experience drives the win" }, ], // 남아공 vs 한국 — 16강 길목 A_RSA_KOR_20260625: [ { model: "GPT", outcome: "TEAM_B_WIN", scoreA: 1, scoreB: 2, confidencePct: 58, ko: "한국의 전방 결정력과 큰 무대 경험 우세", en: "Korea's finishing and big-stage edge" }, { model: "Claude", outcome: "TEAM_B_WIN", scoreA: 1, scoreB: 2, confidencePct: 62, ko: "16강 길목, 한국의 화력이 남아공을 압도", en: "With the last-16 on the line, Korea's firepower tells" }, { model: "Gemini", outcome: "TEAM_B_WIN", scoreA: 1, scoreB: 2, confidencePct: 75, ko: "스쿼드 전력 우세한 한국이 경기 주도", en: "Korea's stronger squad dictates the game" }, ], }; export function getPredictions(match: Match, lang: Lang = "ko"): AIPrediction[] { if (match.matchId === FEATURED.matchId) { return FEATURED_PREDICTIONS.map((p) => ({ ...p, reasonShort: FEATURED_REASONS[lang][p.model], })); } const cur = CURATED[match.matchId]; if (cur) { return cur.map((c) => ({ matchId: match.matchId, model: c.model, outcome: c.outcome, scoreA: c.scoreA, scoreB: c.scoreB, confidencePct: c.confidencePct, reasonShort: lang === "en" ? c.en : c.ko, generatedAt: "2026-06-10", })); } return generatePredictions(match, lang); } export function getCrowd(match: Match): CrowdStats { if (match.matchId === FEATURED.matchId) { return { matchId: MATCH.matchId, total: 1284, teamAWin: 616, draw: 347, teamBWin: 321 }; } const base = hash(match.matchId + "crowd"); const total = 240 + (base % 1400); const a = 28 + (base % 40); // 28~67% const d = 18 + ((base >> 3) % 22); // 18~39% const teamAWin = Math.round((total * a) / 100); const draw = Math.round((total * Math.min(d, 100 - a - 5)) / 100); const teamBWin = total - teamAWin - draw; return { matchId: match.matchId, total, teamAWin, draw, teamBWin }; } // 경기별 공유 딥링크 (D8) const ORIGIN = "https://triplepick-web.vercel.app"; export function matchUrl(matchId: string): string { return `${ORIGIN}/match/${matchId}`; } // ── 하위 호환 (체코전 단일 참조용) ── export const AI_PREDICTIONS: AIPrediction[] = FEATURED_PREDICTIONS; export const CROWD: CrowdStats = getCrowd(FEATURED); export const LANDING_URL = matchUrl(FEATURED.matchId); // schedule re-export (페이지에서 한 곳에서 import) export { GROUP_A };