diff --git a/app/globals.css b/app/globals.css index 329ab0e..016247c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -62,7 +62,6 @@ body { .flag-img { object-fit: cover; - border-radius: 8px; border: 1px solid var(--line-d); } diff --git a/app/layout.tsx b/app/layout.tsx index 3e5c100..94b6524 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,7 +2,7 @@ import type { Metadata, Viewport } from "next"; import "./globals.css"; export const metadata: Metadata = { - title: "TriplePick 2026 | AI 글로벌 축구 승부예측 — GPT vs Claude vs Gemini", + title: "TriplePick 2026 | AI 2026 월드컵 승부예측 — GPT vs Claude vs Gemini", description: "GPT·Claude·Gemini가 매 경기를 서로 다르게 예측합니다. 당신의 픽을 찍고 AI와 겨뤄보세요. 끝까지 잘 맞히면 100만 원 챌린지.", applicationName: "TriplePick", diff --git a/app/match/[matchId]/page.tsx b/app/match/[matchId]/page.tsx index 6faa6ab..6ecbc6a 100644 --- a/app/match/[matchId]/page.tsx +++ b/app/match/[matchId]/page.tsx @@ -6,6 +6,7 @@ import Arena from "@/components/Arena"; import Footer from "@/components/Footer"; import { GROUP_A } from "@/lib/schedule"; import { getPredictions, getCrowd, matchUrl } from "@/lib/mockData"; +import { parseLang, dict, teamShort } from "@/lib/i18n"; // L2. 경기 상세(대결) 페이지 — 6경기 정적 생성 export function generateStaticParams() { @@ -32,42 +33,56 @@ export async function generateMetadata({ export default async function MatchPage({ params, + searchParams, }: { params: Promise<{ matchId: string }>; + searchParams: Promise<{ lang?: string }>; }) { const { matchId } = await params; + const { lang: langParam } = await searchParams; + const lang = parseLang(langParam); + const t = dict(lang); + const match = GROUP_A.find((m) => m.matchId === matchId); if (!match) notFound(); - const predictions = getPredictions(match); + const predictions = getPredictions(match, lang); const crowd = getCrowd(match); + const aShort = teamShort(match.teamA, lang); + const bShort = teamShort(match.teamB, lang); + const hook = lang === "en" ? t.hook(aShort, bShort) : match.hookText; return (
{/* 경기별 후킹 카피 (한 줄) */}

- {match.hookText} + {hook}

- + -
); } diff --git a/app/page.tsx b/app/page.tsx index d863a59..9f1b5c5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,30 +1,35 @@ import Hero from "@/components/Hero"; import ScheduleBoard from "@/components/ScheduleBoard"; import Footer from "@/components/Footer"; +import { parseLang, dict } from "@/lib/i18n"; // L1. 전체 일정 대시보드 (랜딩 진입) -export default function Home() { +export default async function Home({ + searchParams, +}: { + searchParams: Promise<{ lang?: string }>; +}) { + const { lang: langParam } = await searchParams; + const lang = parseLang(langParam); + const t = dict(lang); return (
- + {/* 기간 안내 + CTA 영역 */}
-

- GPT · Claude · Gemini가 매 경기를 +

+ {t.introLead} +

+

+ {t.prizeLine1}
- 서로 다르게 예측합니다. -
- - 가장 많이 참여하고, 가장 정확한 최종 우승자에게 -
- 100만원 상금 -
+ {t.prizeLine2}

- -
); } diff --git a/components/Arena.tsx b/components/Arena.tsx index bee773a..a4f355a 100644 --- a/components/Arena.tsx +++ b/components/Arena.tsx @@ -1,14 +1,16 @@ "use client"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { DEMO_FORCE_OPEN } 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"; @@ -28,12 +30,17 @@ export default function Arena({ 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 [outcome, setOutcome] = useState(null); const [scoreA, setScoreA] = useState(2); const [scoreB, setScoreB] = useState(1); @@ -43,6 +50,13 @@ export default function Arena({ const [crowd, setCrowd] = useState(initialCrowd); const [copied, setCopied] = useState(false); + // 점수 선택 시 승/무/패 자동 선택 (스코어가 결과의 소스) + 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; @@ -50,12 +64,12 @@ export default function Arena({ const locked = now >= new Date(match.lockAt).getTime(); const disabled = finished || locked || notOpen; const ctaLabel = finished - ? "결과 보기" + ? t.ctaResult : notOpen - ? `투표 오픈 ${kickoffDisplay(match.opensAt)}` + ? t.ctaOpens(kickoffDisplay(match.opensAt, lang)) : locked - ? "예측 마감 (경기 시작)" - : "AI보다 잘 맞히기"; + ? t.ctaLocked + : t.ctaBeat; const matched = useMemo(() => { if (!outcome) return []; @@ -82,12 +96,17 @@ export default function Arena({ const shareText = useMemo(() => { if (!outcome) return ""; - const me = `${match.teamA.shortName} ${scoreA}-${scoreB} ${match.teamB.shortName}`; + 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, match]); + }, [outcome, scoreA, scoreB, matched, aShort, bShort, lang]); const copyLink = async () => { try { @@ -102,7 +121,7 @@ export default function Arena({ if (navigator.share) { try { await navigator.share({ - title: `${match.teamA.shortName} vs ${match.teamB.shortName} — TriplePick`, + title: `${aShort} vs ${bShort} — TriplePick`, text: shareText, url: shareUrl, }); @@ -117,19 +136,22 @@ export default function Arena({ {/* ===== 카운트다운 (D7) — 투표 진행 중일 때만 ===== */} {!finished && !notOpen && (
- +
)} {/* ===== 레이어드 화이트 시트 (002) ===== */}
-

AI의 예측 대결

+

{t.aiBattle}

{predictions.map((p) => { const wp = winProb(match, p); + const wpLabel = wp.team + ? t.winProb(teamShort(wp.team, lang)) + : t.drawOdds; return (
)} - {wp.label} {wp.pct}% + {wpLabel} {wp.pct}%
@@ -172,17 +194,17 @@ export default function Arena({ {/* 당신의 선택 */}
- Your Pick · 내 예측 + {t.yourPickLabel} - 당신의 선택 + {t.yourChoice}
{( [ - ["TEAM_A_WIN", `${match.teamA.shortName} 승`], - ["DRAW", "무승부"], - ["TEAM_B_WIN", `${match.teamB.shortName} 승`], + ["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}
- {match.teamA.shortName} {match.result.scoreA}-{match.result.scoreB}{" "} - {match.teamB.shortName} + {aShort} {match.result.scoreA}-{match.result.scoreB} {bShort} - {outcomeLabel(match, match.result.outcome)} + {outcomeLabel(match, match.result.outcome, lang)}
@@ -225,7 +246,7 @@ export default function Arena({
{p.model} - {hit ? "적중 ✓" : "빗나감"} + {hit ? t.hit : t.miss}
); @@ -253,7 +274,7 @@ export default function Arena({ inputMode="email" value={email} onChange={(e) => setEmail(e.target.value)} - placeholder="이메일 (결과·다음 경기 알림)" + placeholder={t.emailPh} 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)]" />
)} @@ -278,30 +299,27 @@ export default function Arena({ {/* ===== 제출 완료: 같은 AI + 경기별 공유 (D8) ===== */} {!finished && step === "done" && outcome && (
-
내 예측
+
{t.myPick}
- {match.teamA.shortName} {scoreA}-{scoreB} {match.teamB.shortName} + {aShort} {scoreA}-{scoreB} {bShort} - {outcomeLabel(match, outcome)} + {outcomeLabel(match, outcome, lang)}

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

@@ -316,25 +334,20 @@ export default function Arena({

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

-
{/* ===== Crowd Pick (003) — 보팅 비율(≠ AI 승리 확률) ===== */}
-
+
Crowd Pick{" "} - | 참여자들의 선택 -
-
- 참여자 투표 비율입니다 (AI 승리 확률과 다릅니다) + {t.crowdSub}
@@ -342,7 +355,7 @@ export default function Arena({
- {crowd.total.toLocaleString()}명 참여 + {t.joined(crowd.total.toLocaleString())}
@@ -403,7 +416,7 @@ function CrowdSeg({ value, tone, }: { - team?: import("@/lib/types").Team; + team?: Team; value: number; tone: "a" | "draw" | "b"; }) { diff --git a/components/Countdown.tsx b/components/Countdown.tsx index de1feae..041bb5c 100644 --- a/components/Countdown.tsx +++ b/components/Countdown.tsx @@ -1,16 +1,13 @@ "use client"; import { useEffect, useState } from "react"; +import { type Lang, dict } from "@/lib/i18n"; // "투표 종료까지" 카운트다운 (D7). 마감 = 킥오프 정각(lockAt). // 서버/클라 hydration mismatch 방지: 시간 계산은 마운트 후 useEffect에서만. -export default function Countdown({ - to, - label = "투표 종료까지", -}: { - to: string; - label?: string; -}) { +export default function Countdown({ to, lang = "ko" }: { to: string; lang?: Lang }) { + const t = dict(lang); + const label = t.cdUntil; const [left, setLeft] = useState(null); useEffect(() => { @@ -26,7 +23,7 @@ export default function Countdown({ return (
- {ended ? "투표가 마감되었습니다" : label} + {ended ? t.cdClosed : label} {!ended && ( - {left === null ? "--:--:--" : fmt(left)} + {left === null ? "--:--:--" : fmt(left, lang)} )}
); } -function fmt(ms: number): string { +function fmt(ms: number, lang: Lang): string { const s = Math.floor(ms / 1000); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; const pad = (n: number) => String(n).padStart(2, "0"); - if (d > 0) return `${d}일 ${pad(h)}:${pad(m)}:${pad(sec)}`; + const dayUnit = lang === "en" ? "d" : "일"; + if (d > 0) return `${d}${dayUnit} ${pad(h)}:${pad(m)}:${pad(sec)}`; return `${pad(h)}:${pad(m)}:${pad(sec)}`; } diff --git a/components/Footer.tsx b/components/Footer.tsx index 2c8923f..1a85c4c 100644 --- a/components/Footer.tsx +++ b/components/Footer.tsx @@ -1,23 +1,19 @@ -export default function Footer() { +import { type Lang, dict } from "@/lib/i18n"; + +export default function Footer({ lang = "ko" }: { lang?: Lang }) { + const t = dict(lang); return (