- 백엔드: FastAPI + SQLAlchemy(async) + PostgreSQL, 별도 워커(APScheduler) - 프론트: React 19 + Vite + TS + Tailwind v4 (기존 UI 이식, API 연동) - docker-compose: db · api · worker · frontend 일괄 실행 - 경기 일정: openfootball 매일 09:00 크롤링 → DB upsert (신규 경기 자동 삽입+예측 생성) - AI 예측: GPT·Claude·Gemini 실 API 매일 생성 → DB 저장 → 조회 (목데이터 제거) - 투표창: 오픈 킥오프 D-2 / 마감 킥오프 1시간 전, lockAt 백엔드 전달→프론트 카운트다운 - 채점/리더보드: docs/SCORING.md 기준 누적 포인트 - crowd: 0 시작, 실제 제출로만 증분 - 구 Next.js/Firebase 파일 제거, README/AGENTS 갱신 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
65 lines
2.6 KiB
TypeScript
65 lines
2.6 KiB
TypeScript
import type { Match } from "@/lib/types";
|
|
import { kickoffDisplay } from "@/lib/format";
|
|
import { type Lang, dict } from "@/lib/i18n";
|
|
import BallIcon from "./BallIcon";
|
|
import TeamFlag from "./TeamFlag";
|
|
|
|
export default function MatchupHUD({ match, lang = "ko" }: { match: Match; lang?: Lang }) {
|
|
const t = dict(lang);
|
|
const { group } = match;
|
|
const finished = !!match.result;
|
|
return (
|
|
<section className="mt-6">
|
|
{/* 대결 카드 (녹색 글로우 보더) */}
|
|
<div
|
|
className="rounded-3xl border-2 border-[var(--green)] bg-[#171b21] p-5"
|
|
style={{ boxShadow: "0 0 28px rgba(74,255,160,0.35), inset 0 0 24px rgba(74,255,160,0.06)" }}
|
|
>
|
|
{/* 헤더: 브랜드 아이콘 + 팀명 + 일시 */}
|
|
<div className="flex items-center gap-3">
|
|
<BallIcon className="h-14 w-14 shrink-0" />
|
|
<div className="min-w-0">
|
|
<div className="text-[22px] font-extrabold leading-tight">
|
|
{match.teamA.name} <span className="text-white/55">vs</span>{" "}
|
|
{match.teamB.name}
|
|
</div>
|
|
<div className="mt-1 text-[15px] font-bold text-[var(--green)]">
|
|
{kickoffDisplay(match.kickoffKst, lang)} · {t.group(group)}
|
|
</div>
|
|
<div className="mt-0.5 text-[12px] text-white/65">{match.venue}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 국기 + 라이트닝 VS (종료 시 최종 스코어) */}
|
|
<div className="mt-5 grid grid-cols-[1fr_auto_1fr] items-center gap-3">
|
|
<TeamFlag team={match.teamA} className="mx-auto h-[68px] w-[104px]" />
|
|
<div className="relative grid place-items-center">
|
|
<div className="vs-glow absolute h-28 w-28" />
|
|
{finished && match.result ? (
|
|
<span className="relative whitespace-nowrap font-mono text-[34px] font-extrabold tabular-nums text-white">
|
|
{match.result.scoreA}
|
|
<span className="px-1 text-white/55">-</span>
|
|
{match.result.scoreB}
|
|
</span>
|
|
) : (
|
|
<span
|
|
className="relative font-impact text-[46px] italic leading-none text-[var(--green)]"
|
|
style={{ textShadow: "0 0 18px rgba(74,255,160,0.8)" }}
|
|
>
|
|
VS
|
|
</span>
|
|
)}
|
|
</div>
|
|
<TeamFlag team={match.teamB} className="mx-auto h-[68px] w-[104px]" />
|
|
</div>
|
|
|
|
{finished && (
|
|
<div className="mt-3 text-center text-[12px] font-bold text-[var(--green)]">
|
|
{t.matchEnded}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|