o2o-triple-pick/lib/mockData.ts

153 lines
5.2 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";
export const MATCH = FEATURED; // 대표 경기 = 한국 vs 체코 (schedule SSOT)
// 데모용: 실제 운영은 투표 오픈 = 킥오프 D-2(opensAt). 팀 데모에서 픽이 막히지 않게
// true면 오픈 게이트를 무시하고 즉시 참여 가능. 운영 배포 시 false 로.
export const DEMO_FORCE_OPEN = true;
// 체코전 큐레이션 예측 (D3: reason 짧고 또렷이)
const FEATURED_PREDICTIONS: AIPrediction[] = [
{
matchId: MATCH.matchId,
model: "GPT",
outcome: "TEAM_A_WIN",
scoreA: 2,
scoreB: 1,
confidencePct: 62,
reasonShort: "한국 측면 스피드가 한 끗 위",
generatedAt: "2026-06-08",
},
{
matchId: MATCH.matchId,
model: "Claude",
outcome: "DRAW",
scoreA: 1,
scoreB: 1,
confidencePct: 41,
reasonShort: "중원 힘싸움이 끝까지 팽팽",
generatedAt: "2026-06-08",
},
{
matchId: MATCH.matchId,
model: "Gemini",
outcome: "TEAM_B_WIN",
scoreA: 0,
scoreB: 2,
confidencePct: 28,
reasonShort: "체코 역습 한 방이 매섭다",
generatedAt: "2026-06-08",
},
];
// ── 결정론적 생성 (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"];
// 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): string {
const A = match.teamA.shortName;
const B = match.teamB.shortName;
const win = outcome === "TEAM_A_WIN" ? A : outcome === "TEAM_B_WIN" ? B : null;
const winPool = win
? [
`${win} 측면 스피드가 한 끗 위`,
`${win} 세트피스 한 방이면 끝`,
`${win} 초반 기세에서 앞선다`,
`${win} 결정력이 승부를 가른다`,
]
: [];
const drawPool = [
"중원 힘싸움이 끝까지 팽팽",
"양 팀 다 한 방 있는 진검승부",
"first goal이 모든 걸 가른다",
];
const pool = win ? winPool : drawPool;
return pool[seed % pool.length];
}
function generatePredictions(match: Match): 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),
generatedAt: "2026-06-08",
};
});
}
export function getPredictions(match: Match): AIPrediction[] {
if (match.matchId === FEATURED.matchId) return FEATURED_PREDICTIONS;
return generatePredictions(match);
}
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 };