/* ─── Sections: Hero, Analysis cards, Carousel, Leaderboard, Guide, CTA, Footer, MobileTabBar ─── */
const { useState, useMemo, useEffect, useRef } = React;
const D = window.KB_DATA;
/* ─── Tiny inline icons ─────────────────────────────────────────────── */
const I = {
search: (p) => ,
moon: (p) => ,
star: (p) => ,
bell: (p) => ,
arrow: (p) => ,
home: (p) => ,
user: (p) => ,
chart: (p) => ,
pin: (p) => ,
menu: (p) => ,
vs: (p) => ,
};
/* ─── Sparkline SVG ─────────────────────────────────────────────────── */
function Sparkline({ data, color = "var(--mlb)", h = 36 }) {
const w = 220;
const min = Math.min(...data), max = Math.max(...data);
const span = max - min || 1;
const pts = data.map((v, i) => {
const x = (i / (data.length - 1)) * w;
const y = h - 4 - ((v - min) / span) * (h - 8);
return `${x.toFixed(1)},${y.toFixed(1)}`;
});
const path = `M ${pts.join(" L ")}`;
const fill = `M 0,${h} L ${pts.join(" L ")} L ${w},${h} Z`;
const id = useMemo(() => "g" + Math.random().toString(36).slice(2, 7), []);
return (
);
}
/* ─── Strike-zone heatmap (decorative) ──────────────────────────────── */
function ZoneHeat({ accent = "var(--mlb)" }) {
const cells = [
[0.10, 0.18, 0.22, 0.18, 0.10],
[0.18, 0.42, 0.68, 0.45, 0.20],
[0.24, 0.72, 0.96, 0.74, 0.26],
[0.20, 0.55, 0.78, 0.52, 0.22],
[0.10, 0.20, 0.28, 0.20, 0.10],
];
return (
{cells.flat().map((v,i)=>(
))}
);
}
/* ─── Header ────────────────────────────────────────────────────────── */
function Header() {
const [active, setActive] = useState("home");
const items = [
{ id: "home", label: "홈" },
{ id: "matchup", label: "매치업" },
{ id: "mlb", label: "MLB" },
{ id: "kbo", label: "KBO" },
{ id: "players", label: "선수" },
{ id: "rank", label: "랭킹" },
{ id: "guide", label: "가이드" },
];
return (
);
}
/* ─── Hero / live matchup ───────────────────────────────────────────── */
function Hero() {
const h = D.hero;
const sp = h.spotlight;
return (
{/* top meta row */}
LIVE
MLB
{h.status}
{h.venue} · {h.date}
{/* Left: scoreboard + spotlight */}
{window.KB_TEAM_LOGOS[h.away.code] ? (

) : h.away.code}
{h.away.short}
{h.away.score}
:
{h.home.score}
SCORE · 6회초
{window.KB_TEAM_LOGOS[h.home.code] ? (

) : h.home.code}
{h.home.short}
{/* spotlight player block */}
{sp.stats.map((s, i) => (
))}
{/* Right: at-bat tracker + sidecar */}
{sp.pitches.map((p, i) => (
{p.type}
{p.h ? `H ${p.h}` : "·"}
))}
다른 한국인 선수
{D.hero.sidecar.map((s, i) => (
{s.team}
{s.player}
{s.line}
{s.tag==="kbo"?"KBO":"휴식"}
))}
);
}
/* ─── Today's matchups grid ─────────────────────────────────────────── */
function MatchupCard({ m }) {
const total = m.away.win + m.home.win;
const aPct = (m.away.win / total) * 100;
const favorAway = m.away.win >= m.home.win;
return (
{e.currentTarget.style.borderColor="var(--hairline-strong)"; e.currentTarget.style.transform="translateY(-2px)";}}
onMouseLeave={(e)=>{e.currentTarget.style.borderColor="var(--hairline)"; e.currentTarget.style.transform="";}}>
{/* meta */}
{/* matchup row */}
{window.KB_TEAM_LOGOS[m.away.code] ? (

) : m.away.code}
{m.away.code}
{m.away.short}
VS
{m.home.code}
{m.home.short}
{window.KB_TEAM_LOGOS[m.home.code] ? (

) : m.home.code}
{/* starting pitchers */}
선발 (어웨이)
{m.away.sp}
{m.away.spLine}
선발 (홈)
{m.home.sp}
{m.home.spLine}
{/* predicted win % bar — data analysis tone */}
모델 예측 승률
데이터 분석 · {m.tag}
{m.away.win}%
{m.home.win}%
);
}
function MatchupGrid() {
return (
오늘의 매치업 분석
KBO 5경기 · 데이터 기반 예측
전체 일정
{D.matchups.map((m,i)=> )}
※ 예측 승률은 최근 10경기 성적·선발 매치업·구장 효과 기반의 통계 모델 결과입니다. 참고용 데이터 분석 자료입니다.
);
}
/* ─── Analysis card ─────────────────────────────────────────────────── */
function AnalysisCard({ a }) {
return (
{e.currentTarget.style.borderColor="var(--hairline-strong)"; e.currentTarget.style.transform="translateY(-2px)";}}
onMouseLeave={(e)=>{e.currentTarget.style.borderColor="var(--hairline)"; e.currentTarget.style.transform="";}}>
{a.image && (
)}
{a.ribbon} · {a.tag}
{a.read}
{/* visual — varies per kind */}
{a.kind === "spark-up" &&
}
{a.kind === "bars" && (
{[0.55,0.72,0.41,0.86,0.62,0.93,0.48,0.78,0.34,0.66,0.55,0.81].map((h,i)=>(
))}
)}
{a.kind === "zone" && (
102.1mph
발사각 26° · 비거리 134m
)}
{a.kind === "compare" && (
{[
{l:"2017 한화", c:"var(--text-3)", h:.62},
{l:"2026 한화", c:a.color, h:.86},
].map((b,i)=>(
))}
)}
{a.title}
{a.excerpt}
);
}
function AnalysisGrid() {
return (
{D.analysis.map((a,i)=>
)}
);
}
/* ─── Player carousel ───────────────────────────────────────────────── */
function PlayerCarousel() {
return (
{D.players.map((p,i)=>(
= 3 ? 14 : 18, overflow:"hidden"}}>
{window.KB_TEAM_LOGOS[p.team] ? (

) : p.team}
{p.live &&
}
{p.ko}
{p.team} · {p.pos}
{p.league}
))}
);
}
/* ─── Leaderboard ───────────────────────────────────────────────────── */
function Leaderboard() {
const [tab, setTab] = useState("kbo");
const lb = D.leaderboards[tab];
const max = Math.max(...lb.rows.map(r=>r.ops));
return (
2026 시즌 리더보드
OPS 기준 탑 플레이어
| # |
선수 |
OPS |
HR |
AVG |
WAR |
{lb.rows.map((r,i)=>(
| {i+1} |
{window.KB_TEAM_LOGOS[r.team] ? (

) : r.team}
|
{r.ops.toFixed(3).replace(/^0/,"")}
|
{r.hr} |
{r.avg.toFixed(3).replace(/^0/,"")} |
{r.war.toFixed(1)} |
))}
);
}
/* ─── Guide boxes ───────────────────────────────────────────────────── */
function Guides() {
return (
입문자 가이드
처음 보는 야구 데이터, 5분이면 이해
가이드 전체
{D.guides.map((g,i)=>(
{g.image && (
)}
{g.n}
{g.eyebrow}
{g.title}
{g.body}
읽으러 가기
{g.time}
))}
);
}
/* ─── Notify CTA ────────────────────────────────────────────────────── */
function NotifyCTA() {
const [val, setVal] = useState("");
const [done, setDone] = useState(false);
return (
푸시 알림
내 픽 선수가 홈런을 치면 즉시 알림
이정후·김혜성·김하성·류현진 — 한 번의 등록으로 4명 모두 추적합니다. 광고는 보내지 않습니다.
);
}
/* ─── Footer ────────────────────────────────────────────────────────── */
function Footer() {
return (
);
}
/* ─── Mobile bottom tab bar ─────────────────────────────────────────── */
function MobileTabBar() {
const [act, setAct] = useState("home");
const tabs = [
{ id:"home", label:"홈", icon: I.home },
{ id:"matchup",label:"매치업", icon: I.vs },
{ id:"player", label:"선수", icon: I.user },
{ id:"pin", label:"내 픽", icon: I.pin },
{ id:"menu", label:"메뉴", icon: I.menu },
];
return (
);
}
/* expose to window */
Object.assign(window, { Header, Hero, MatchupGrid, AnalysisGrid, PlayerCarousel, Leaderboard, Guides, NotifyCTA, Footer, MobileTabBar, KBIcons: I });