o2o-triple-pick/lib/mockData.ts

260 lines
11 KiB
TypeScript

// 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-2.5-flash)
// 결과: GPT 무 1-1(conf45) / Claude 무 1-1(conf55) / Gemini 한국 2-1(conf60) — 둘은 무승부, Gemini만 한국 승리
const FEATURED_REASONS: Record<Lang, Record<ModelName, string>> = {
ko: {
GPT: "전력과 압박 강도 모두 팽팽",
Claude: "양 팀 전력 비슷, 박빙 무승부",
Gemini: "이강인·손흥민 공격력, 체코 수비에 근소 우세",
},
en: {
GPT: "Power and pressing — dead even",
Claude: "Evenly matched, a tight draw likely",
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 (conf 45)
outcome: "DRAW",
scoreA: 1,
scoreB: 1,
confidencePct: 45,
reasonShort: FEATURED_REASONS.ko.GPT,
generatedAt: "2026-06-10",
},
{
matchId: MATCH.matchId,
model: "Claude", // claude-opus-4-8: 무승부 1-1 (conf 55)
outcome: "DRAW",
scoreA: 1,
scoreB: 1,
confidencePct: 55,
reasonShort: FEATURED_REASONS.ko.Claude,
generatedAt: "2026-06-10",
},
{
matchId: MATCH.matchId,
model: "Gemini", // gemini-2.5-flash: 한국 승 2-1 (conf 60)
outcome: "TEAM_A_WIN",
scoreA: 2,
scoreB: 1,
confidencePct: 60,
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<ModelName, string> = {
GPT: "gpt-5.5",
Claude: "claude-opus-4-8",
Gemini: "gemini-2.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<Outcome, [number, number][]> = {
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<string, Curated[]> = {
// 개막전 — 멕시코 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 };