/* ─── 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 (
DualbatMLB · KBO ANALYTICS
선수, 팀, 지표 검색 ⌘ K
); } /* ─── 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.code}
{h.away.short}
{h.away.score} : {h.home.score}
SCORE · 6회초
{window.KB_TEAM_LOGOS[h.home.code] ? ( {h.home.code} ) : h.home.code}
{h.home.short}
{/* spotlight player block */}
LJH
{sp.player}
진행 중
{sp.en} · {sp.role}
{sp.vsLine}
{sp.stats.map((s, i) => (
{s.k}
{s.v}
{s.d}
))}
{/* Right: at-bat tracker + sidecar */}
오늘의 타석
4 PA
{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 */}
KBO
{m.time} · {m.venue}
{/* matchup row */}
{window.KB_TEAM_LOGOS[m.away.code] ? ( {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} ) : 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)=>(
{b.l}
))}
)}

{a.title}

{a.excerpt}

{a.author} · {a.time}
); } function AnalysisGrid() { return (
오늘의 분석

한국 야구를 보는 6가지 시선

전체 보기
{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.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 기준 탑 플레이어

{lb.rows.map((r,i)=>( ))}
# 선수 OPS HR AVG WAR
{i+1}
{window.KB_TEAM_LOGOS[r.team] ? ( {r.team} ) : r.team}
{r.name}
{r.sub}
{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명 모두 추적합니다. 광고는 보내지 않습니다.

{e.preventDefault(); setDone(true);}}> setVal(e.target.value)} placeholder="이메일 또는 휴대폰 번호" type="text"/>
); } /* ─── 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 });