"use client"; import { useMemo, useState } from "react"; import { DEMO_FORCE_OPEN } from "@/lib/mockData"; import { outcomeLabel, pct, kickoffDisplay, winProb } from "@/lib/format"; import type { Outcome, CrowdStats, ModelName, Match, AIPrediction, } from "@/lib/types"; import Countdown from "./Countdown"; import TeamFlag from "./TeamFlag"; type Step = "pick" | "form" | "done"; const MODEL_ICON: Record = { GPT: "/icons/gpt.png", Claude: "/icons/claude.jpg", Gemini: "/icons/gemini.jpeg", }; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; export default function Arena({ match, predictions, crowd: initialCrowd, shareUrl, }: { match: Match; predictions: AIPrediction[]; crowd: CrowdStats; shareUrl: string; }) { const [outcome, setOutcome] = useState(null); const [scoreA, setScoreA] = useState(2); const [scoreB, setScoreB] = useState(1); const [step, setStep] = useState("pick"); const [email, setEmail] = useState(""); const [notify, setNotify] = useState(true); const [crowd, setCrowd] = useState(initialCrowd); const [copied, setCopied] = useState(false); // 투표 창: 오픈(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 ? "결과 보기" : notOpen ? `투표 오픈 ${kickoffDisplay(match.opensAt)}` : locked ? "예측 마감 (경기 시작)" : "AI보다 잘 맞히기"; 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 submitPick = () => { if (!outcome) return; setStep("form"); }; const confirmSubmit = () => { if (!EMAIL_RE.test(email.trim())) return; 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), })); setStep("done"); }; const shareText = useMemo(() => { if (!outcome) return ""; const me = `${match.teamA.shortName} ${scoreA}-${scoreB} ${match.teamB.shortName}`; const sameAI = matched.map((m) => m.model).join("·"); return sameAI ? `나는 ${me}. ${sameAI}와 같은 선택! 너는? — TriplePick` : `나는 ${me}. AI 셋과 다 다른 선택! 너는? — TriplePick`; }, [outcome, scoreA, scoreB, matched, match]); 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: `${match.teamA.shortName} vs ${match.teamB.shortName} — TriplePick`, text: shareText, url: shareUrl, }); } catch { /* cancelled */ } } else copyLink(); }; return ( <> {/* ===== 카운트다운 (D7) — 투표 진행 중일 때만 ===== */} {!finished && !notOpen && (
)} {/* ===== 레이어드 화이트 시트 (002) ===== */}

AI의 예측 대결

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

“{p.reasonShort}”

); })}
{/* 당신의 선택 */}
Your Pick · 내 예측 당신의 선택
{( [ ["TEAM_A_WIN", `${match.teamA.shortName} 승`], ["DRAW", "무승부"], ["TEAM_B_WIN", `${match.teamB.shortName} 승`], ] as [Outcome, string][] ).map(([val, label]) => ( ))}
:
{/* ===== 종료된 경기: 결과 보기 ===== */} {finished && match.result && (
최종 결과
{match.teamA.shortName} {match.result.scoreA}-{match.result.scoreB}{" "} {match.teamB.shortName} {outcomeLabel(match, match.result.outcome)}
{predictions.map((p) => { const hit = p.outcome === match.result!.outcome; return (
{p.model} {hit ? "적중 ✓" : "빗나감"}
); })}
)} {/* ===== CTA 민트 #85FCA8 (진행 중일 때) ===== */} {!finished && step === "pick" && ( )} {/* ===== 폼: 이메일만 (D5, 닉네임 제거) ===== */} {!finished && step === "form" && (
setEmail(e.target.value)} placeholder="이메일 (결과·다음 경기 알림)" className="w-full rounded-lg border border-[var(--line-d)] bg-[#0f1217] px-3 py-3 text-[15px] text-white outline-none focus:border-[var(--green)]" />
)} {/* ===== 제출 완료: 같은 AI + 경기별 공유 (D8) ===== */} {!finished && step === "done" && outcome && (
내 예측
{match.teamA.shortName} {scoreA}-{scoreB} {match.teamB.shortName} {outcomeLabel(match, outcome)}

{matched.length > 0 ? ( <> 당신은 {matched.map((m) => m.model).join("·")}와 같은 선택입니다. {matched.some((m) => m.exact) ? " 스코어까지 일치! " : " "} 나머지 AI는 생각이 다릅니다. ) : ( <>당신은 AI 셋 모두와 다른 선택! 소신 픽 🔥 )}

)} {/* ===== 골드 상금 (003) ===== */}
₩1,000,000 Final Challenge

경기마다 승패·스코어를 맞혀 포인트를 쌓고, 누적 1위에게 최종 100만원

{/* ===== Crowd Pick (003) — 보팅 비율(≠ AI 승리 확률) ===== */}
Crowd Pick{" "} | 참여자들의 선택
참여자 투표 비율입니다 (AI 승리 확률과 다릅니다)
{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?: import("@/lib/types").Team; value: number; tone: "a" | "draw" | "b"; }) { const bg = tone === "draw" ? "#2b313c" : tone === "a" ? "rgba(74,255,160,0.22)" : "#222831"; return (
{team && } {value}%
); }