diff --git a/.gitignore b/.gitignore index bd5379a..1e9a192 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ yarn-error.log* # secrets (구 firebase 잔재) serviceAccount.json .firebaserc + +# 로컬 전용 댓글 관리 도구 +tp-comment-admin/ diff --git a/frontend/public/audio/Red Wave 2026.mp3 b/frontend/public/audio/Red Wave 2026.mp3 new file mode 100644 index 0000000..288ccf9 Binary files /dev/null and b/frontend/public/audio/Red Wave 2026.mp3 differ diff --git a/frontend/public/thumbnail/Red Wave 2026.webp b/frontend/public/thumbnail/Red Wave 2026.webp new file mode 100644 index 0000000..0ce307d Binary files /dev/null and b/frontend/public/thumbnail/Red Wave 2026.webp differ diff --git a/frontend/src/components/Arena.tsx b/frontend/src/components/Arena.tsx index d657a2e..d276f2b 100644 --- a/frontend/src/components/Arena.tsx +++ b/frontend/src/components/Arena.tsx @@ -13,7 +13,7 @@ import { ApiError, getDeviceId, getRecentEmails, rememberEmail, submitPrediction import Countdown from "./Countdown"; import TeamFlag from "./TeamFlag"; -type Step = "pick" | "form" | "done"; +type Step = "form" | "done"; const MODEL_ICON: Record = { GPT: "/icons/gpt.png", @@ -42,7 +42,7 @@ export default function Arena({ const [outcome, setOutcome] = useState("DRAW"); const [scoreA, setScoreA] = useState(0); const [scoreB, setScoreB] = useState(0); - const [step, setStep] = useState("pick"); + const [step, setStep] = useState("form"); const [recentEmails, setRecentEmails] = useState(getRecentEmails); const [email, setEmail] = useState(""); // 칸은 비우고, 입력/포커스 시 datalist 드롭다운으로만 자동완성 const [notify, setNotify] = useState(true); @@ -79,10 +79,7 @@ export default function Arena({ .map((p) => ({ model: p.model, exact: p.scoreA === scoreA && p.scoreB === scoreB })); }, [outcome, scoreA, scoreB, predictions]); - const submitPick = () => { - if (!outcome) return; - setStep("form"); - }; + const emailValid = EMAIL_RE.test(email.trim()); const confirmSubmit = async () => { if (!EMAIL_RE.test(email.trim()) || !outcome) return; @@ -285,20 +282,22 @@ export default function Arena({ )} - {/* ===== CTA 민트 #85FCA8 (진행 중일 때) ===== */} - {!finished && step === "pick" && ( + {/* ===== 투표 마감/오픈 전: 상태 버튼만 ===== */} + {!finished && disabled && ( )} - {/* ===== 폼: 이메일만 (D5, 닉네임 제거) ===== */} - {!finished && step === "form" && ( -
+ {/* ===== 투표 제출 (D5): 이메일 + 단일 CTA를 한 카드로 통합 ===== */} + {!finished && !disabled && step === "form" && ( +
+
+ {t.emailGate} +
setEmail(e.target.value)} 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)]" + 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)]" /> {recentEmails.length > 0 && ( @@ -316,24 +315,24 @@ export default function Arena({ ))} )} -
)} diff --git a/frontend/src/globals.css b/frontend/src/globals.css index 016247c..72b874a 100644 --- a/frontend/src/globals.css +++ b/frontend/src/globals.css @@ -130,6 +130,15 @@ body { 0 10px 28px rgba(133, 252, 168, 0.28); } +/* 사용자 액션(투표 제출) — 브랜드 포인트 퍼플 */ +.btn-share { + background: var(--share); + color: #fff; + box-shadow: + 0 0 0 1px rgba(166, 94, 255, 0.5), + 0 12px 30px rgba(166, 94, 255, 0.35); +} + .gold-card { background: linear-gradient(180deg, rgba(94, 240, 180, 0.08), rgba(21, 169, 155, 0.03)), diff --git a/frontend/src/lib/i18n.ts b/frontend/src/lib/i18n.ts index 6de8091..bdda0bc 100644 --- a/frontend/src/lib/i18n.ts +++ b/frontend/src/lib/i18n.ts @@ -63,6 +63,7 @@ type Dict = { ctaLocked: string; ctaLive: string; ctaOpens: (d: string) => string; + emailGate: string; emailPh: string; notify: string; submit: string; @@ -150,9 +151,10 @@ export const DICT: Record = { ctaLocked: "투표 종료", ctaLive: "경기 진행 중", ctaOpens: (d) => `투표 오픈 ${d}`, - emailPh: "이메일 (결과·다음 경기 알림)", - notify: "경기 결과가 나오면 알려주세요. 다음 경기·100만원 챌린지 소식도 받겠습니다.", - submit: "제출하고 AI와 겨루기", + emailGate: "투표를 제출하려면 이메일을 입력하세요", + emailPh: "이메일 입력 → 투표 결과·다음 경기 알림", + notify: "결과 알림 + 다음 경기 소식 + ₩100만 챌린지 기회까지 무료로 받기", + submit: "투표 제출하고 AI와 겨루기", myPick: "내 예측", sameAI: (m, exact) => `당신은 ${m}와 같은 선택입니다.${exact ? " 스코어까지 일치!" : ""} 나머지 AI는 생각이 다릅니다.`, @@ -237,9 +239,10 @@ export const DICT: Record = { ctaLocked: "Voting closed", ctaLive: "Match in progress", ctaOpens: (d) => `Opens ${d}`, - emailPh: "Email (result & next-match alerts)", - notify: "Notify me when the result is out. I'll also get next-match & ₩1,000,000 challenge news.", - submit: "Submit & face the AI", + emailGate: "Enter your email to submit your vote", + emailPh: "Your email → result & next-match alerts", + notify: "Result alerts + next-match news + a shot at the ₩1,000,000 challenge — free", + submit: "Submit your vote & face the AI", myPick: "My Pick", sameAI: (m, exact) => `You matched ${m}.${exact ? " Exact score too!" : ""} The other AIs disagree.`, diff --git a/frontend/src/lib/playlist.ts b/frontend/src/lib/playlist.ts index e2649bf..adbe48e 100644 --- a/frontend/src/lib/playlist.ts +++ b/frontend/src/lib/playlist.ts @@ -13,6 +13,7 @@ export type Track = { // VISIBLE_PATHS는 이 맵의 키에서 자동 생성됩니다. export const PATH_PLAYLISTS: Record = { "/match/A_MEX_KOR_20260619": [ + { title: "Red Wave 2026", artist: "aio2o", src: "/audio/Red Wave 2026.mp3", cover: "/thumbnail/Red Wave 2026.webp" }, { title: "붉은 물결", artist: "aio2o", src: "/audio/붉은 물결.mp3", cover: "/thumbnail/붉은 물결.webp" }, { title: "Triple Pick, We Believe", artist: "aio2o", src: "/audio/Triple Pick, We Believe.mp3", cover: "/thumbnail/Triple Pick, We Believe.webp" }, ],