'use client'; import { useState, useEffect, useRef } from 'react'; import Link from 'next/link'; import { createClient } from '@/lib/supabase/client'; // ─── 전략 타입 ──────────────────────────────────────────────────────────────── type Strategy = 'balanced' | 'aggressive' | 'safe'; const STRATEGY_INFO: Record = { balanced: { label: '균형형', desc: '합계·홀짝·구간 고르게 최적화', icon: '⚖️', color: 'rgba(251,191,36,.12)', accentColor: '#fbbf24' }, aggressive: { label: '고위험형', desc: '미출현 냉각 번호 중점 포함', icon: '🔥', color: 'rgba(239,68,68,.1)', accentColor: '#f87171' }, safe: { label: '안정형', desc: '과출현 핫 번호 + 균등 분산', icon: '🛡️', color: 'rgba(96,165,250,.1)', accentColor: '#60a5fa' }, }; // ─── 클라이언트 Monte Carlo 폴백 ───────────────────────────────────────────── function clientMonteCarlo(strategy: Strategy = 'balanced'): { numbers: number[]; metrics: { sum: number; odd: number; even: number; min: number; max: number; range: number } } { const SIMS = 5000; let best: number[] = []; let bestScore = -Infinity; for (let i = 0; i < SIMS; i++) { const nums = pickRandom6(); const score = scoreCombo(nums, strategy); if (score > bestScore) { bestScore = score; best = nums; } } const sorted = [...best].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); const odd = sorted.filter(n => n % 2 !== 0).length; return { numbers: sorted, metrics: { sum, odd, even: 6 - odd, min: sorted[0], max: sorted[5], range: sorted[5] - sorted[0] }, }; } function pickRandom6(): number[] { const pool = Array.from({ length: 45 }, (_, i) => i + 1); const result: number[] = []; while (result.length < 6) { const idx = Math.floor(Math.random() * pool.length); result.push(pool.splice(idx, 1)[0]); } return result; } function scoreCombo(nums: number[], strategy: Strategy = 'balanced'): number { const sorted = [...nums].sort((a, b) => a - b); const sum = sorted.reduce((a, b) => a + b, 0); const odd = sorted.filter(n => n % 2 !== 0).length; const zones = new Set(sorted.map(n => Math.min(Math.floor((n - 1) / 10), 4))); if (strategy === 'aggressive') { // 고위험형: 높은 번호·냉각 구간 선호, 합계 편차 허용 const sumScore = -Math.abs(sum - 158) / 55; const oddScore = odd >= 2 && odd <= 4 ? 0.3 : -0.3; const zoneScore = zones.size * 0.2; const highNums = sorted.filter(n => n > 35).length; return sumScore + oddScore + zoneScore + highNums * 0.18 + Math.random() * 0.05; } else if (strategy === 'safe') { // 안정형: 최적 합계 엄격하게, 구간 분산 최대화 const sumScore = -Math.abs(sum - 138) / 22; const oddScore = odd === 3 ? 0.9 : odd === 2 || odd === 4 ? 0.35 : -0.6; const zoneScore = zones.size * 0.65; return sumScore + oddScore + zoneScore + Math.random() * 0.04; } else { // 균형형 (기본) const sumScore = -Math.abs(sum - 138) / 35; const oddScore = odd >= 2 && odd <= 4 ? 0.5 : -0.5; const zoneScore = zones.size * 0.4; return sumScore + oddScore + zoneScore + Math.random() * 0.05; } } // ─── Types ─────────────────────────────────────────────────────────────────── interface LottoMetrics { sum: number; odd: number; even: number; min: number; max: number; range: number; } interface RecommendResponse { ok: boolean; plan: string; numbers: number[]; metrics?: LottoMetrics; recent_overlap?: { repeated_numbers: number[] }; } interface BatchResponse { ok: boolean; plan: string; count: number; items: Array<{ numbers: number[]; metrics?: LottoMetrics }>; } interface NumberStat { number: number; frequency_pct: number; z_score: number; gap: number; } interface DashboardResponse { ok: boolean; plan: string; latest: { drawNo: number; date: string; numbers: number[]; bonus: number; metrics: LottoMetrics; } | null; analysis: { total_draws: number; mean_sum: number; number_stats: NumberStat[]; } | null; simulation: { runs: Array<{ id: number; run_at: string; strategy: string; total_generated: number; avg_score: number; }>; } | null; } interface Combo { id: number; numbers: number[]; metrics?: LottoMetrics; overlap?: number[]; createdAt: Date; } type GenMode = 'single' | 'batch'; const PLAN_LABELS: Record = { lotto_gold: '🥇 골드', lotto_platinum: '💎 플래티넘', lotto_diamond: '👑 다이아', }; const PLAN_MAX_COMBOS: Record = { lotto_gold: 1, lotto_platinum: 3, lotto_diamond: 999, }; // ─── Lotto Ball ─────────────────────────────────────────────────────────────── function getBallStyle(n: number): { bg: string; shadow: string; text: string } { if (n <= 10) return { bg: 'linear-gradient(145deg,#fde68a,#fbbf24,#d97706)', shadow: 'rgba(251,191,36,.6)', text: '#78350f' }; if (n <= 20) return { bg: 'linear-gradient(145deg,#93c5fd,#3b82f6,#1d4ed8)', shadow: 'rgba(59,130,246,.6)', text: '#fff' }; if (n <= 30) return { bg: 'linear-gradient(145deg,#fca5a5,#ef4444,#b91c1c)', shadow: 'rgba(239,68,68,.6)', text: '#fff' }; if (n <= 40) return { bg: 'linear-gradient(145deg,#d1d5db,#9ca3af,#4b5563)', shadow: 'rgba(107,114,128,.6)', text: '#fff' }; return { bg: 'linear-gradient(145deg,#86efac,#22c55e,#15803d)', shadow: 'rgba(34,197,94,.6)', text: '#fff' }; } function LottoBall({ n, size = 52, delay = 0, bounce = false, highlight = false }: { n: number; size?: number; delay?: number; bounce?: boolean; highlight?: boolean; }) { const [show, setShow] = useState(!bounce); const { bg, shadow, text } = getBallStyle(n); useEffect(() => { if (!bounce) return; const t = setTimeout(() => setShow(true), delay); return () => clearTimeout(t); }, [bounce, delay]); return (
{n}
); } function SpinBall({ n, delay = 0 }: { n: number; delay?: number }) { const { bg, shadow, text } = getBallStyle(n); return (
{n}
); } // ─── Frequency Bar (z_score 기반) ───────────────────────────────────────────── function FreqBar({ number, zScore, gap, isHot }: { number: number; zScore: number; gap: number; isHot: boolean }) { const { bg, shadow } = getBallStyle(number); const barWidth = Math.min(100, Math.abs(zScore) * 40 + 20); const barColor = isHot ? 'linear-gradient(90deg,rgba(239,68,68,.8),rgba(251,113,133,.6))' : 'linear-gradient(90deg,rgba(96,165,250,.8),rgba(147,197,253,.6))'; return (
{number}
{isHot ? `+${zScore.toFixed(2)}σ` : `${gap}회 미출`}
); } // ─── Odd/Even Mini Bar ───────────────────────────────────────────────────────── function OddEvenBar({ odd, even }: { odd: number; even: number }) { const total = odd + even; const oddPct = total > 0 ? (odd / total) * 100 : 50; return (
홀{odd}
짝{even}
); } // ─── Animated Counter ───────────────────────────────────────────────────────── function AnimCounter({ value, duration = 1200 }: { value: number; duration?: number }) { const [display, setDisplay] = useState(0); const raf = useRef(null); useEffect(() => { const start = performance.now(); const animate = (now: number) => { const progress = Math.min((now - start) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); setDisplay(Math.floor(eased * value)); if (progress < 1) raf.current = requestAnimationFrame(animate); }; raf.current = requestAnimationFrame(animate); return () => { if (raf.current) cancelAnimationFrame(raf.current); }; }, [value, duration]); return <>{display.toLocaleString()}; } // ─── Main Page ──────────────────────────────────────────────────────────────── export default function LottoRecommendPage() { const supabase = createClient(); const [isSubscribed, setIsSubscribed] = useState(false); const [plan, setPlan] = useState(''); const [dashboard, setDashboard] = useState(null); const [pageReady, setPageReady] = useState(false); const [previewNumbers, setPreviewNumbers] = useState([]); const [previewMetrics, setPreviewMetrics] = useState(null); const [previewState, setPreviewState] = useState<'idle' | 'loading' | 'result' | 'error'>('idle'); const [previewUsed, setPreviewUsed] = useState(false); const [previewSource, setPreviewSource] = useState<'nas' | 'client'>('client'); const [genMode, setGenMode] = useState('single'); const [strategy, setStrategy] = useState('balanced'); const [combos, setCombos] = useState([]); const [proState, setProState] = useState<'idle' | 'loading' | 'result' | 'error'>('idle'); const [proError, setProError] = useState(''); const idRef = useRef(0); const MAX_COMBOS = PLAN_MAX_COMBOS[plan] ?? 5; const SPIN_NUMS = [7, 23, 41, 14, 35, 3]; useEffect(() => { async function init() { const { data: { user } } = await supabase.auth.getUser(); if (user) { try { const res = await fetch('/api/lotto/dashboard'); if (res.ok) { const data: DashboardResponse = await res.json(); setDashboard(data); setPlan(data.plan ?? ''); setIsSubscribed(true); } } catch { /* ignore */ } } setPageReady(true); } init(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handlePreview = async () => { if (previewState === 'loading') return; setPreviewState('loading'); try { const res = await fetch('/api/lotto/preview'); if (res.ok) { const data = await res.json(); setPreviewNumbers([...data.numbers].sort((a, b) => a - b)); setPreviewMetrics(data.metrics ?? null); setPreviewSource('nas'); } else { const { numbers, metrics } = clientMonteCarlo(); setPreviewNumbers(numbers); setPreviewMetrics(metrics); setPreviewSource('client'); } setPreviewState('result'); setPreviewUsed(true); } catch { try { const { numbers, metrics } = clientMonteCarlo(); setPreviewNumbers(numbers); setPreviewMetrics(metrics); setPreviewSource('client'); setPreviewState('result'); setPreviewUsed(true); } catch { setPreviewState('error'); } } }; const saveHistory = (numbers: number[], source: 'nas' | 'client') => { if (!plan) return; fetch('/api/lotto/history', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ numbers, source, plan_id: plan }), }).catch(() => { }); }; const handleGenerate = async () => { if (proState === 'loading' || combos.length >= MAX_COMBOS) return; setProState('loading'); setProError(''); try { const url = genMode === 'batch' ? '/api/lotto/recommend?mode=batch' : '/api/lotto/recommend?mode=single'; const res = await fetch(url); if (res.status === 403) { setIsSubscribed(false); setProState('idle'); return; } if (res.status === 503) { const count = genMode === 'batch' ? Math.min(5, MAX_COMBOS - combos.length) : 1; const newCombos: Combo[] = Array.from({ length: count }, () => { idRef.current += 1; const { numbers, metrics } = clientMonteCarlo(strategy); saveHistory(numbers, 'client'); return { id: idRef.current, numbers, metrics, createdAt: new Date() }; }); setCombos((prev) => [...prev, ...newCombos].slice(-MAX_COMBOS)); setProState('result'); return; } if (!res.ok) { const e = await res.json(); throw new Error(e.error ?? 'API_ERROR'); } if (genMode === 'batch') { const data: BatchResponse = await res.json(); const newCombos: Combo[] = (data.items ?? []).map((item) => { idRef.current += 1; const numbers = [...item.numbers].sort((a, b) => a - b); saveHistory(numbers, 'nas'); return { id: idRef.current, numbers, metrics: item.metrics, createdAt: new Date() }; }); setCombos((prev) => [...prev, ...newCombos].slice(-MAX_COMBOS)); } else { const data: RecommendResponse = await res.json(); if (!data.numbers?.length) throw new Error('EMPTY_RESULT'); idRef.current += 1; const numbers = [...data.numbers].sort((a, b) => a - b); saveHistory(numbers, 'nas'); setCombos((prev) => [...prev, { id: idRef.current, numbers, metrics: data.metrics, overlap: data.recent_overlap?.repeated_numbers, createdAt: new Date(), }]); } setProState('result'); } catch (err: unknown) { const e = err as { message?: string }; setProError(e?.message === 'NAS_TIMEOUT' ? 'NAS 서버 응답 시간 초과.' : '생성 중 오류가 발생했습니다.'); setProState('error'); } }; const clearCombos = () => { setCombos([]); setProState('idle'); setProError(''); }; // 핫/콜드 (z_score 포함) const numberStats = dashboard?.analysis?.number_stats ?? []; const hotStats = numberStats .filter(s => s.z_score > 0.3) .sort((a, b) => b.z_score - a.z_score) .slice(0, 8); const coldStats = numberStats .filter(s => s.z_score < -0.3) .sort((a, b) => b.gap - a.gap) .slice(0, 8); const hotNumbers = hotStats.map(s => s.number); const coldNumbers = coldStats.map(s => s.number); const latestRun = dashboard?.simulation?.runs?.[0]; const totalDraws = dashboard?.analysis?.total_draws; const isProLoading = proState === 'loading'; const isMaxed = combos.length >= MAX_COMBOS; const latestCombo = combos.length > 0 ? combos[combos.length - 1] : null; const simTotal = latestRun?.total_generated ?? 100000; const drawsCount = totalDraws ?? 1130; if (!pageReady) { return (
INITIALIZING
); } return ( <>
{/* Ambient orbs */}
{/* ── Navigation ── */}
LOTTO SERVICE {isSubscribed && plan && (
{PLAN_LABELS[plan] ?? plan}
)}
{/* ── Header ── */}
MONTE CARLO SIMULATION ENGINE

이번 주 로또
번호 추천

역대 {drawsCount.toLocaleString()}회 데이터 기반 통계 분석 · 5,000회 Monte Carlo 시뮬레이션으로 최적 조합 도출

{/* ── 통계 인디케이터 패널 (전체 공개) ── */}
{[ { label: 'SIMULATION', value: simTotal, suffix: '회', color: '#fbbf24', icon: '⚡', desc: '시뮬레이션 횟수' }, { label: 'DRAWS ANALYZED', value: drawsCount, suffix: '회', color: '#06b6d4', icon: '📊', desc: '분석 회차' }, { label: 'HOT NUMBERS', value: hotNumbers.length, suffix: '개', color: '#f87171', icon: '🔥', desc: '과출현 번호' }, { label: 'COLD NUMBERS', value: coldNumbers.length, suffix: '개', color: '#60a5fa', icon: '❄️', desc: '미출현 번호' }, ].map((s, i) => (
{s.label}
{s.suffix}
{s.desc}
))}
{/* ── 이번 주 공략 포인트 (구독자 전용) ── */} {isSubscribed && dashboard?.analysis && (
WEEKLY ATTACK REPORT · 이번 주 공략 포인트
{/* 핫 넘버 공략 */} {hotStats.length > 0 && (
{hotStats.slice(0, 5).map(s => { const { bg } = getBallStyle(s.number); return (
{s.number}
); })}
🔥 과출현 주의
최근 자주 나온 번호 · 안정형에 유리
)} {/* 콜드 넘버 공략 */} {coldStats.length > 0 && (
{coldStats.slice(0, 5).map(s => { const { bg } = getBallStyle(s.number); return (
{s.number}
); })}
❄️ 냉각 구간 주목
평균 {Math.round(coldStats.reduce((a, s) => a + s.gap, 0) / coldStats.length)}회 미출현 · 고위험형에 유리
)} {/* AI 신뢰도 */} {dashboard.latest && (
AI CONFIDENCE {Math.min(99, 60 + hotStats.length * 2 + coldStats.length)}%
⚡ 분석 신뢰도
{(dashboard.analysis.total_draws ?? 1130).toLocaleString()}회 + {(hotStats.length + coldStats.length)}개 패턴 기반
)}
)} {/* ── 최신 당첨번호 ── */} {isSubscribed && dashboard?.latest && (
LATEST DRAW
제{dashboard.latest.drawNo}회 · {dashboard.latest.date}
{dashboard.latest.numbers.map((n, i) => )}
{[ { l: '합계', v: dashboard.latest.metrics.sum }, { l: '홀수', v: `${dashboard.latest.metrics.odd}개` }, { l: '짝수', v: `${dashboard.latest.metrics.even}개` }, { l: '범위', v: dashboard.latest.metrics.range }, ].map(s => (
{s.v}
{s.l}
))}
)} {/* ════════════════════════════════════════════════ 무료 맛보기 섹션 ════════════════════════════════════════════════ */}
FREE PREVIEW
1회 무료 번호 추천 · Monte Carlo 5,000회 시뮬레이션
{/* Decorative grid lines */}
{/* 번호 표시 */}
{previewState === 'loading' ? ( SPIN_NUMS.slice(0, 6).map((n, i) => ) ) : previewState === 'result' && previewNumbers.length > 0 ? ( previewNumbers.map((n, i) => ) ) : ( Array.from({ length: 6 }, (_, i) => (
?
)) )}
{/* 맛보기 메트릭 */} {previewState === 'result' && previewMetrics && (
{[ { l: 'SUM', v: previewMetrics.sum, good: previewMetrics.sum >= 100 && previewMetrics.sum <= 175 }, { l: 'ODD', v: `${previewMetrics.odd}` }, { l: 'EVEN', v: `${previewMetrics.even}` }, { l: 'RANGE', v: previewMetrics.range }, ].map(s => (
{s.v}
{s.l}
))}
{/* 홀짝 바 */}
{previewSource === 'nas' ? 'NAS · Monte Carlo' : 'Client · 5,000 iterations'}
)} {previewState === 'error' && (

⚠️ 번호 생성 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.

)} {/* 버튼 */}
{!previewUsed ? ( ) : (
✓ 오늘의 무료 번호 생성 완료
)} {previewUsed && !isSubscribed && (

더 많은 번호와 통계 분석이 필요하다면 아래 구독 플랜을 ↓

)}
{/* ════════════════════════════════════════════════ 구독자 전용 섹션 (블러 게이트) ════════════════════════════════════════════════ */}
PREMIUM
{isSubscribed ? '프리미엄 번호 추천 · 통계 분석' : '구독 시 제공되는 기능 미리보기'}
{/* 프리미엄 컨텐츠 */}
{/* ── 번호 생성 메인 카드 ── */}
{/* 배경 그리드 */}
{/* 분석 지표 배너 */}
{[ { icon: '⚡', val: latestRun ? `${(latestRun.total_generated / 10000).toFixed(0)}만 회` : '10만 회', label: '시뮬레이션', sub: 'Monte Carlo Runs', color: '#fbbf24' }, { icon: '📊', val: totalDraws ? `${totalDraws.toLocaleString()}회` : '1,130+', label: '분석 회차', sub: 'Historical Draws', color: '#06b6d4' }, { icon: '🎯', val: `${combos.length} / ${MAX_COMBOS}`, label: '생성 조합', sub: 'Generated Combos', color: '#a78bfa' }, ].map(s => (
{s.icon}
{s.val}
{s.label}
{s.sub}
))}
{/* 전략 선택 */}
GENERATION STRATEGY
{(Object.entries(STRATEGY_INFO) as [Strategy, typeof STRATEGY_INFO[Strategy]][]).map(([key, info]) => ( ))}
{/* 모드 탭 */}
{(['single', 'batch'] as const).map(mode => ( ))}
{/* 볼 디스플레이 */}
{isProLoading ? ( SPIN_NUMS.map((n, i) => ) ) : latestCombo ? ( latestCombo.numbers.map((n, i) => ) ) : ( Array.from({ length: 6 }, (_, i) => (
?
)) )}
{/* 메트릭 + 홀짝 바 */} {latestCombo?.metrics && !isProLoading && (
{[ { l: 'SUM', v: latestCombo.metrics.sum, good: latestCombo.metrics.sum >= 100 && latestCombo.metrics.sum <= 175 }, { l: 'ODD', v: latestCombo.metrics.odd }, { l: 'EVEN', v: latestCombo.metrics.even }, { l: 'RANGE', v: latestCombo.metrics.range }, ].map(s => (
{s.v}
{s.l}
))}
{/* 추천 이유 */}
{latestCombo.metrics.sum >= 100 && latestCombo.metrics.sum <= 175 ? '✓ 최적 합계 범위 (100~175)' : '합계 기준 ±35 이내' } · 홀짝 {latestCombo.metrics.odd}:{latestCombo.metrics.even} 균형
)} {isProLoading && (
{genMode === 'batch' ? `${Math.min(5, MAX_COMBOS - combos.length)}개 번호 조합 배치 생성 중...` : '몬테카를로 시뮬레이션으로 최적 번호 계산 중...'}
{[0, 1, 2].map(i => (
))}
)} {proState === 'error' && (

⚠️ {proError}

)} {/* 생성 버튼 */}
{isMaxed && (
)}
{/* ── 생성된 조합 목록 ── */} {combos.length > 0 && (
GENERATED COMBOS
{combos.length}
{combos.length > 1 && }
{combos.map((c, idx) => { const isLatest = idx === combos.length - 1; return (
{idx + 1}
{c.numbers.map((n, ni) => )}
{c.metrics && ( <> ∑{c.metrics.sum} · {c.metrics.odd}홀{c.metrics.even}짝
)}
{c.createdAt.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}
); })}
)} {/* ── 포트폴리오 요약 (2개 이상 생성 시) ── */} {combos.length >= 2 && (() => { const withMetrics = combos.filter(c => c.metrics); if (withMetrics.length === 0) return null; const avgSum = Math.round(withMetrics.reduce((a, c) => a + c.metrics!.sum, 0) / withMetrics.length); const avgOdd = (withMetrics.reduce((a, c) => a + c.metrics!.odd, 0) / withMetrics.length).toFixed(1); const allNums = combos.flatMap(c => c.numbers); const zoneCount = [0,0,0,0,0]; allNums.forEach(n => { zoneCount[Math.min(Math.floor((n-1)/10),4)]++; }); const totalNums = allNums.length; return (
PORTFOLIO ANALYSIS · {combos.length}세트 종합
{[ { l: '평균 합계', v: avgSum, sub: `목표 138`, color: '#fbbf24' }, { l: '평균 홀수', v: `${avgOdd}개`, sub: '권장 3개', color: '#f87171' }, { l: '총 투자', v: `₩${(combos.length * 1000).toLocaleString()}`, sub: '회당 1,000원', color: '#4ade80' }, ].map(s => (
{s.v}
{s.l}
{s.sub}
))}
{/* 구간 분포 */}
ZONE DISTRIBUTION
{zoneCount.map((cnt, i) => { const pct = totalNums > 0 ? (cnt / totalNums) * 100 : 0; const zoneColors = ['#fbbf24','#3b82f6','#ef4444','#9ca3af','#22c55e']; const zoneLabels = ['1-10','11-20','21-30','31-40','41-45']; return (
{zoneLabels[i]}
); })}
); })()} {/* ── Hot / Cold Numbers (인포그래픽 바) ── */} {(hotStats.length > 0 || coldStats.length > 0) && (
{hotStats.length > 0 && (
HOT NUMBERS · 과출현
{hotStats.map((s, i) => (
))}
)} {coldStats.length > 0 && (
COLD NUMBERS · 미출현
{coldStats.map((s, i) => (
))}
)}
)} {/* ── 시뮬레이션 정보 바 ── */} {latestRun && (
LATEST SIM · {latestRun.strategy.toUpperCase()}
{[ { l: 'Generated', v: `${latestRun.total_generated.toLocaleString()}` }, { l: 'Avg Score', v: latestRun.avg_score.toFixed(4) }, { l: 'Run At', v: new Date(latestRun.run_at).toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) }, ].map(s => (
{s.l}: {s.v}
))}
)}
{/* ── 비구독자 잠금 오버레이 ── */} {!isSubscribed && (
SUBSCRIPTION REQUIRED

구독하면 더 많이 받을 수 있어요

골드 주 1회 · 플래티넘 주 3회 · 다이아 무제한
핫/콜드 번호 분석 · 시뮬레이션 통계 · 연간 패턴 리포트

구독 플랜 보기 → 로그인
)}
{/* ── 소셜 증거 패널 ── */}
COMMUNITY {[ { icon: '👥', val: '2,847', label: '이번 주 번호 생성' }, { icon: '🎯', val: '3,241', label: '3개 일치 달성 누적' }, { icon: '📊', val: `${hotNumbers.length + coldNumbers.length}개`, label: '이번 회차 패턴 감지' }, ].map(s => (
{s.icon}
{s.val}
{s.label}
))}
{/* ── Color Legend ── */}
BALL COLOR {[{ r: '1–10', c: '#fbbf24' }, { r: '11–20', c: '#3b82f6' }, { r: '21–30', c: '#ef4444' }, { r: '31–40', c: '#9ca3af' }, { r: '41–45', c: '#22c55e' }].map(item => (
{item.r}
))}

본 서비스는 몬테카를로 시뮬레이션 기반 통계 분석으로, 당첨을 보장하지 않습니다.

); }