"use client"; import { useEffect, useMemo, useState } from "react"; import { DEMO_FORCE_OPEN, MODEL_VERSIONS } from "@/lib/mockData"; import { outcomeLabel, pct, kickoffDisplay, winProb } from "@/lib/format"; import { type Lang, dict, teamShort } from "@/lib/i18n"; import type { Outcome, CrowdStats, ModelName, Match, AIPrediction, Team, } from "@/lib/types"; import Countdown from "./Countdown"; import TeamFlag from "./TeamFlag"; import { fetchMyPredictions, saveMyPrediction, rememberEmail, recallEmail, type MyPrediction, } from "@/lib/myPredictions"; type Step = "form" | "done"; const MODEL_ICON: Record = { GPT: "/icons/gpt.png", Claude: "/icons/claude.jpg", Gemini: "/icons/gemini.jpeg", }; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 제출 시각 ISO → "6.18" (내 예측 목록 우측 라벨) function fmtSubmitted(iso: string): string { const m = iso.match(/\d{4}-(\d{2})-(\d{2})/); return m ? `${+m[1]}.${+m[2]}` : ""; } export default function Arena({ match, predictions, crowd: initialCrowd, shareUrl, lang = "ko", }: { match: Match; predictions: AIPrediction[]; crowd: CrowdStats; shareUrl: string; lang?: Lang; }) { const t = dict(lang); const aShort = teamShort(match.teamA, lang); const bShort = teamShort(match.teamB, lang); const genDate = (predictions[0]?.generatedAt ?? "").replace(/-/g, "."); const modelVersions = predictions.map((p) => MODEL_VERSIONS[p.model]).join(" · "); const [outcome, setOutcome] = useState(null); const [scoreA, setScoreA] = useState(2); const [scoreB, setScoreB] = useState(1); const [step, setStep] = useState("form"); const [email, setEmail] = useState(""); const [notify, setNotify] = useState(true); const [crowd, setCrowd] = useState(initialCrowd); const [copied, setCopied] = useState(false); const [myPreds, setMyPreds] = useState([]); const [votesExpanded, setVotesExpanded] = useState(false); // 재방문 시: 기억된 이메일 복원 → 내 지난 예측 로드 (로그인 대체) useEffect(() => { const saved = recallEmail(); if (!saved) return; setEmail(saved); fetchMyPredictions(saved).then(setMyPreds); }, []); // 이 경기에 대한 내 기존 픽 (있으면 스코어 프리필 + 강조) const myThisMatch = useMemo( () => myPreds.find((p) => p.matchId === match.matchId), [myPreds, match.matchId], ); useEffect(() => { if (myThisMatch) { setScoreA(myThisMatch.scoreA); setScoreB(myThisMatch.scoreB); } }, [myThisMatch]); // 이메일 입력이 유효해지면 그 즉시 내 기록 조회(다른 기기/세션 흔적 표시) const onEmailChange = (v: string) => { setEmail(v); if (EMAIL_RE.test(v.trim())) fetchMyPredictions(v).then(setMyPreds); }; // 점수 선택 시 승/무/패 자동 선택 (스코어가 결과의 소스) useEffect(() => { setOutcome( scoreA > scoreB ? "TEAM_A_WIN" : scoreA < scoreB ? "TEAM_B_WIN" : "DRAW", ); }, [scoreA, scoreB]); // 투표 창: 오픈(D-2) ≤ now < 마감(킥오프 정각). 종료 = 결과 존재. const now = Date.now(); const finished = !!match.result; const notOpen = !DEMO_FORCE_OPEN && now < new Date(match.opensAt).getTime(); const locked = now >= new Date(match.lockAt).getTime(); const disabled = finished || locked || notOpen; const ctaLabel = finished ? t.ctaResult : notOpen ? t.ctaOpens(kickoffDisplay(match.opensAt, lang)) : locked ? t.ctaLocked : t.ctaBeat; const matched = useMemo(() => { if (!outcome) return []; return predictions .filter((p) => p.outcome === outcome) .map((p) => ({ model: p.model, exact: p.scoreA === scoreA && p.scoreB === scoreB })); }, [outcome, scoreA, scoreB, predictions]); const emailValid = EMAIL_RE.test(email.trim()); const confirmSubmit = async () => { if (!emailValid || !outcome) return; // 군중 분포 증분은 새 투표일 때만 (이미 투표한 경기 수정 시 중복 카운트 방지) if (!myThisMatch) { setCrowd((c) => ({ ...c, total: c.total + 1, teamAWin: c.teamAWin + (outcome === "TEAM_A_WIN" ? 1 : 0), draw: c.draw + (outcome === "DRAW" ? 1 : 0), teamBWin: c.teamBWin + (outcome === "TEAM_B_WIN" ? 1 : 0), })); } // 내 예측 저장(경기당 1건 upsert) + 이메일 기억 → 재방문 자동 복원 await saveMyPrediction(email, { matchId: match.matchId, teamAShort: aShort, teamBShort: bShort, kickoffKst: match.kickoffKst, outcome, scoreA, scoreB, submittedAt: new Date().toISOString(), }); rememberEmail(email); setMyPreds(await fetchMyPredictions(email)); setStep("done"); }; const shareText = useMemo(() => { if (!outcome) return ""; const me = `${aShort} ${scoreA}-${scoreB} ${bShort}`; const sameAI = matched.map((m) => m.model).join("·"); if (lang === "en") { return sameAI ? `I picked ${me}, same as ${sameAI}! You? — TriplePick` : `I picked ${me} — different from all 3 AIs! You? — TriplePick`; } return sameAI ? `나는 ${me}. ${sameAI}와 같은 선택! 당신은? — TriplePick` : `나는 ${me}. AI 셋과 다 다른 선택! 당신은? — TriplePick`; }, [outcome, scoreA, scoreB, matched, aShort, bShort, lang]); const copyLink = async () => { try { await navigator.clipboard.writeText(`${shareText}\n${shareUrl}`); setCopied(true); setTimeout(() => setCopied(false), 1800); } catch { setCopied(false); } }; const nativeShare = async () => { if (navigator.share) { try { await navigator.share({ title: `${aShort} vs ${bShort} — TriplePick`, text: shareText, url: shareUrl, }); } catch { /* cancelled */ } } else copyLink(); }; return ( <> {/* ===== 카운트다운 (D7) — 투표 진행 중일 때만 ===== */} {!finished && !notOpen && (
)} {/* ===== 레이어드 화이트 시트 (002) ===== */}

{t.aiBattle}

{t.genLabel} · {genDate} 00:00 KST
{modelVersions}
{predictions.map((p) => { const wp = winProb(match, p); const wpLabel = wp.team ? t.winProb(teamShort(wp.team, lang)) : t.drawOdds; return (
{p.model}
{p.model}
{/* D3: 승리 예측 팀의 승리 확률 + 승리팀 국기 */}
{wp.team && ( )} {wpLabel} {wp.pct}%
{p.scoreA} - {p.scoreB}
{/* 메인 리즌 강조 (D3: 확률은 텍스트, 바 제거) */}

“{p.reasonShort}”

); })}
{/* 당신의 선택 */}
{t.yourPickLabel} {t.yourChoice}
{( [ ["TEAM_A_WIN", `${aShort} ${t.win}`], ["DRAW", t.drawLabel], ["TEAM_B_WIN", `${bShort} ${t.win}`], ] as [Outcome, string][] ).map(([val, label]) => ( ))}
:
{/* ===== 종료된 경기: 결과 보기 ===== */} {finished && match.result && (
{t.finalResult}
{aShort} {match.result.scoreA}-{match.result.scoreB} {bShort} {outcomeLabel(match, match.result.outcome, lang)}
{predictions.map((p) => { const hit = p.outcome === match.result!.outcome; return (
{p.model} {hit ? t.hit : t.miss}
); })}
)} {/* ===== 투표 마감/오픈 전: 상태 버튼만 ===== */} {!finished && disabled && ( )} {/* ===== 투표 제출 (D5): 이메일 + 단일 CTA를 한 카드로 통합 ===== */} {!finished && !disabled && step === "form" && (
{t.emailGate}
onEmailChange(e.target.value)} placeholder={t.emailPh} className="w-full rounded-2xl border border-[var(--share)] bg-[#0f1217] px-4 py-3 text-[16px] text-white shadow-[0_0_0_2px_rgba(166,94,255,0.12)] outline-none focus:shadow-[0_0_0_2px_rgba(166,94,255,0.3)]" />
)} {/* ===== 제출 완료: 같은 AI + 경기별 공유 (D8) ===== */} {!finished && step === "done" && outcome && (
{t.myPick}
{aShort} {scoreA}-{scoreB} {bShort} {outcomeLabel(match, outcome, lang)}

{matched.length > 0 ? t.sameAI( matched.map((m) => m.model).join("·"), matched.some((m) => m.exact), ) : t.soloPick}

)} {/* ===== 내 지난 예측 (이메일 기준, 로그인 없음) ===== */} {myPreds.length > 0 && (

{t.myVotesTitle}

{t.myVotesSub}
    {(votesExpanded ? myPreds : myPreds.slice(0, 1)).map((p) => { const isThis = p.matchId === match.matchId; const pickTxt = p.outcome === "TEAM_A_WIN" ? `${p.teamAShort} ${t.win}` : p.outcome === "TEAM_B_WIN" ? `${p.teamBShort} ${t.win}` : t.drawLabel; return (
  • {p.teamAShort} {p.scoreA}-{p.scoreB} {p.teamBShort}
    {pickTxt} {isThis && ( · {t.thisMatch} )}
    {p.result ? ( {p.result.hitOutcome ? t.hit : t.miss} ) : ( {fmtSubmitted(p.submittedAt)} )}
  • ); })}
{myPreds.length > 1 && ( )}
)} {/* ===== 골드 상금 (003) ===== */}
₩1,000,000 Final Challenge

{t.goldDesc}

{/* ===== Crowd Pick (003) — 보팅 비율(≠ AI 승리 확률) ===== */}
Crowd Pick{" "} {t.crowdSub}
{t.joined(crowd.total.toLocaleString())}
); } function Stepper({ label, value, onChange, disabled, }: { label: string; value: number; onChange: (n: number) => void; disabled?: boolean; }) { return (
onChange(Math.max(0, value - 1))}> −
{value} {label}
= 9} onClick={() => onChange(Math.min(9, value + 1))}> +
); } function StepBtn({ children, onClick, disabled, }: { children: React.ReactNode; onClick: () => void; disabled?: boolean; }) { return ( ); } function CrowdSeg({ team, value, tone, }: { team?: Team; value: number; tone: "a" | "draw" | "b"; }) { const bg = tone === "draw" ? "#2b313c" : tone === "a" ? "rgba(74,255,160,0.22)" : "#222831"; return (
{team && } {value}%
); }