import React, { useEffect, useMemo, useRef, useState } from 'react'; import { deleteHistory, getHistory, getLatest, getStats, recommend, getBestPicks, getAnalysis, triggerSimulate, getPerformanceStats, getLatestReport, getReportHistory, getPersonalAnalysis, getPurchases, getPurchaseStats, addPurchase, updatePurchase, deletePurchase, getCombinedRecommend, getCombinedHistory, } from '../../api'; /* ───────────────────────────────────────────── 공통 유틸 ───────────────────────────────────────────── */ const fmtKST = (value) => value?.replace('T', ' ') ?? ''; const fmtWon = (n) => { if (n == null || isNaN(Number(n))) return '-'; return new Intl.NumberFormat('ko-KR').format(Math.round(Number(n))) + '원'; }; const ballClass = (n) => { if (n <= 10) return 'lotto-ball range-a'; if (n <= 20) return 'lotto-ball range-b'; if (n <= 30) return 'lotto-ball range-c'; if (n <= 40) return 'lotto-ball range-d'; return 'lotto-ball range-e'; }; const Ball = ({ n }) => {n}; const NumberRow = ({ nums }) => (
{nums.map((n) => )}
); /* ───────────────────────────────────────────── 기존 통계 헬퍼 ───────────────────────────────────────────── */ const bucketOrder = ['1-10', '11-20', '21-30', '31-40', '41-45']; const STATS_CACHE_KEY = 'lotto_stats_v1'; const BEST_PICKS_DEFAULT_SHOW = 5; const readStatsCache = () => { if (typeof window === 'undefined') return null; try { const raw = localStorage.getItem(STATS_CACHE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || !Array.isArray(parsed.frequency)) return null; return parsed; } catch { return null; } }; const writeStatsCache = (data) => { if (typeof window === 'undefined') return; try { localStorage.setItem(STATS_CACHE_KEY, JSON.stringify(data)); } catch {} }; const buildFrequencySeries = (frequency) => { const map = new Map(); (frequency ?? []).forEach((item) => { const number = Number(item?.number); const count = Number(item?.count) || 0; if (Number.isFinite(number) && number >= 1 && number <= 45) map.set(number, count); }); const series = Array.from({ length: 45 }, (_, idx) => ({ number: idx + 1, count: map.get(idx + 1) ?? 0, })); const max = Math.max(1, ...series.map((item) => item.count)); return { series, max }; }; const buildMetricsFromCounts = (counts) => { if (!counts?.length) return null; const total = counts.reduce((acc, v) => acc + v, 0); if (!total) return null; const min = Math.min(...counts), max = Math.max(...counts); const odd = counts.reduce((acc, v, idx) => (idx % 2 === 0 ? acc + v : acc), 0); const even = total - odd; const buckets = { '1-10': counts.slice(0, 10).reduce((a, b) => a + b, 0), '11-20': counts.slice(10, 20).reduce((a, b) => a + b, 0), '21-30': counts.slice(20, 30).reduce((a, b) => a + b, 0), '31-40': counts.slice(30, 40).reduce((a, b) => a + b, 0), '41-45': counts.slice(40, 45).reduce((a, b) => a + b, 0), }; return { sum: total, min, max, range: max - min, odd, even, buckets }; }; const buildMetricsFromFrequency = (frequency) => { if (!frequency?.length) return null; const counts = Array.from({ length: 45 }, () => 0); frequency.forEach((item) => { const number = Number(item?.number), count = Number(item?.count) || 0; if (number >= 1 && number <= 45) counts[number - 1] = count; }); return buildMetricsFromCounts(counts); }; const buildMetricsFromHistory = (items) => { if (!items?.length) return null; const counts = Array.from({ length: 45 }, () => 0); items.forEach((item) => { (item?.numbers ?? []).forEach((value) => { const number = Number(value); if (number >= 1 && number <= 45) counts[number - 1] += 1; }); }); return buildMetricsFromCounts(counts); }; const toBucketEntries = (metrics) => { if (!metrics?.buckets) return []; const ordered = bucketOrder .filter((key) => Object.prototype.hasOwnProperty.call(metrics.buckets, key)) .map((key) => [key, metrics.buckets[key]]); const rest = Object.entries(metrics.buckets) .filter(([key]) => !bucketOrder.includes(key)) .sort((a, b) => Number(a[0].split('-')[0]) - Number(b[0].split('-')[0])); return [...ordered, ...rest]; }; /* ───────────────────────────────────────────── SubComponents — 기존 ───────────────────────────────────────────── */ const MetricBlock = ({ title, metrics }) => { if (!metrics) return null; const buckets = toBucketEntries(metrics); const maxBucket = buckets.length ? Math.max(...buckets.map(([, v]) => Number(v) || 0), 1) : 1; const odd = Number(metrics.odd) || 0; const even = Number(metrics.even) || 0; const totalOE = odd + even || 1; const oddPct = (odd / totalOE) * 100; return (

{title}

총 출현 횟수 {metrics.sum ?? '-'}

최소 출현

{metrics.min ?? '-'}

최대 출현

{metrics.max ?? '-'}

출현 편차

{metrics.range ?? '-'}

홀 {odd}짝 {even}
{buckets.length ? (
{buckets.map(([label, value]) => (
{label}
{value}
))}
) : null}
); }; const FrequencyChart = ({ stats }) => { const { series, max } = useMemo(() => buildFrequencySeries(stats?.frequency), [stats]); const ticks = useMemo(() => [max, Math.round(max * 0.5), 0], [max]); if (!stats) return null; return (
횟수
{ticks.map((value) => {value})}
{series.map((item) => { const showLabel = item.number === 1 || item.number % 5 === 0; return (
{showLabel ? item.number : ''}
); })}
); }; /* ───────────────────────────────────────────── SubComponents — 신규 ───────────────────────────────────────────── */ /* 신뢰도 배너 */ const PerformanceBanner = ({ perf }) => { if (!perf || perf.total_checked === 0) return null; const imp = perf.vs_random?.improvement_pct ?? 0; const prizeHits = (perf.by_rank?.rank_3 ?? 0) + (perf.by_rank?.rank_4 ?? 0) + (perf.by_rank?.rank_5 ?? 0); return (
신뢰도 지표
{perf.total_checked} 검증 회차
{(perf.avg_correct ?? 0).toFixed(1)} 평균 일치수
0 ? 'is-pos' : ''}`}> {imp > 0 ? '+' : ''}{imp.toFixed(1)}% 무작위 대비
{((perf.rate_3plus ?? 0) * 100).toFixed(1)}% 3개↑ 일치율
{prizeHits > 0 && ( <>
{prizeHits}건 3~5등 당첨
)}
); }; /* 신뢰도 링 SVG */ const ConfidenceRing = ({ score }) => { const r = 28, c = 2 * Math.PI * r; const fill = (score / 100) * c; const color = score >= 80 ? '#97c9aa' : score >= 60 ? '#fdd4b1' : '#f7a8a5'; return ( {score} ); }; /* ───────────────────────────────────────────── 종합 추론 추천 패널 ───────────────────────────────────────────── */ const METHOD_META = { frequency: { label: '빈도 Z-score', desc: '역대 출현 빈도가 기댓값보다 높은 번호', color: '#818cf8', icon: '📊' }, fingerprint: { label: '조합 지문', desc: '역대 당첨 조합의 합계·홀짝·구간 분포에 맞는 번호', color: '#fbbf24', icon: '🔏' }, gap: { label: '갭 분석', desc: '가장 오래 등장하지 않은 오버듀 번호', color: '#34d399', icon: '⏳' }, cooccur: { label: '공동 출현', desc: '역대에 함께 출현한 빈도가 높은 번호', color: '#f472b6', icon: '🔗' }, diversity: { label: '다양성', desc: '구간 커버리지와 번호 범위를 극대화한 번호', color: '#fb923c', icon: '🌈' }, }; const METHOD_ORDER = ['fingerprint', 'frequency', 'gap', 'cooccur', 'diversity']; const SCORE_META = [ { key: 'score_fingerprint', label: '조합 지문', color: '#fbbf24', weight: 30 }, { key: 'score_frequency', label: '빈도 Z', color: '#818cf8', weight: 25 }, { key: 'score_gap', label: '갭 분석', color: '#34d399', weight: 20 }, { key: 'score_cooccur', label: '공동 출현', color: '#f472b6', weight: 15 }, { key: 'score_diversity', label: '다양성', color: '#fb923c', weight: 10 }, ]; const CombinedRecommendPanel = ({ combined, history, loading, histLoading, onRun, onCopy }) => { const [histExpand, setHistExpand] = useState(false); return (

AI · 종합 추론

종합 추론 번호 추천

5가지 통계 기법(빈도·지문·갭·공동출현·다양성)을 가중 투표로 합산해 최적 6개 번호를 도출합니다.

{loading && 분석 중…} {history.length > 0 && ( )}
{!combined && !loading && (

버튼을 눌러 종합 추론을 실행하세요.

)} {combined && ( <> {/* 기법별 추천 번호 */}
{METHOD_ORDER.map((key) => { const meta = METHOD_META[key]; const m = combined.methods?.[key]; if (!m) return null; return (
{meta.icon}

{meta.label} ({m.weight_pct}%)

{meta.desc}

{m.numbers.map((n) => { const inFinal = combined.final_numbers.includes(n); return ( {n} ); })}
); })}
{/* 최종 추론 결과 */}
종합 추론 결과 {combined.deduped && ( 중복 (이미 저장됨) )}
{combined.final_numbers.map((n) => { const votes = combined.vote_counts?.[String(n)] ?? 0; return (
{n} {Array.from({ length: 5 }).map((_, i) => ( ))}
); })}

● 점은 해당 번호가 채택된 기법 수 (최대 5개)

{/* 점수 바 */}

조합 품질 점수

{SCORE_META.map(({ key, label, color, weight }) => { const val = combined.scores?.[key] ?? 0; const pct = Math.round(val * 100); return (
{label} {weight}%
{pct}
); })}
종합 점수 {Math.round((combined.scores?.score_total ?? 0) * 100)} / 100

※ 이 추천은 역대 통계 패턴 기반 참고 자료이며, 당첨을 보장하지 않습니다.

)} {/* 추천 이력 */} {histExpand && (

종합 추론 이력

{histLoading &&

로딩 중…

} {history.map((item) => (
#{item.id} {fmtKST(item.created_at)} 기준 {item.based_on_draw ?? '-'}회
))}
)}
); }; /* 공략 리포트 패널 */ const ReportPanel = ({ report, history, loading, onRefresh, onSelectDrw }) => { const [histExpand, setHistExpand] = useState(false); return (

Weekly Report

이번 주 공략 리포트

{report && (

{report.target_drw_no}회 대상 · {report.based_on_draw}회 기준

)}
{loading && 로딩 중} {history?.length > 0 && ( )}
{/* 지난 리포트 목록 */} {histExpand && history?.length > 0 && (
{history.map((h) => ( ))}
)} {!report && !loading && (

리포트 데이터가 없습니다.

)} {loading && !report && (

불러오는 중...

)} {report && ( <> {/* 신뢰도 + 패턴 요약 */}

신뢰도 점수

{Object.entries(report.confidence_factors ?? {}).map(([k, v]) => (
{k === 'data_volume' ? '데이터 충분도' : k === 'pattern_consistency' ? '패턴 안정성' : k === 'recent_trend' ? '최근 트렌드' : k}
{v}
))}

최근 패턴

합계 평균 {report.recent_pattern?.recent_sum_avg?.toFixed(1) ?? '-'}
홀수 평균 {report.recent_pattern?.recent_odd_avg?.toFixed(1) ?? '-'}
{(report.recent_pattern?.triple_appear ?? []).length > 0 && (
3회 연속 출현
)}
{/* 핫 / 콜드 / 오버듀 */}

🔥 핫 번호 최근 10회 과출현

🧊 콜드 번호 역대 저빈도 10개

⏰ 오버듀 가장 오래 미출현

{/* 전략 추천 세트 */} {(report.recommended_sets ?? []).length > 0 && (
{report.recommended_sets.map((set, i) => (

{set.strategy}

{set.description}

))}
)} )}
); }; /* 개인 패턴 분석 */ const PersonalAnalysisPanel = ({ data, loading }) => { const zones = Object.entries(data?.pattern?.zone_avg ?? {}); const maxZone = zones.length ? Math.max(...zones.map(([, v]) => Number(v) || 0), 1) : 1; return (

My Pattern

내 번호 패턴

{data && data.total_analyzed > 0 && (

총 {data.total_analyzed}회 추천 기반 분석

)}
{(loading || !data || data.total_analyzed === 0) ? (

{loading ? '불러오는 중...' : '추천 이력이 없습니다.'}

) : (

내가 자주 선택한 번호 TOP 10

선택 성향

{data.vs_draw_avg?.odd_tendency && ( {data.vs_draw_avg.odd_tendency} )} {data.vs_draw_avg?.sum_tendency && ( {data.vs_draw_avg.sum_tendency} )}
홀수 평균 {data.pattern?.avg_odd_count?.toFixed(1)} 합계 평균 {data.pattern?.avg_sum?.toFixed(1)} 연속번호 포함률{' '} {((data.pattern?.consecutive_rate ?? 0) * 100).toFixed(0)}%
{zones.length > 0 && (

구간별 선택 비율

{zones.map(([zone, avg]) => (
{zone}
{Number(avg).toFixed(1)}
))}
)}
)}
); }; /* 구매 기록 패널 */ const emptyPurchaseForm = () => ({ draw_no: '', amount: 5000, sets: 5, prize: 0, note: '' }); const PurchasePanel = ({ records, stats, loading, formOpen, form, formSaving, formError, editId, onFormOpen, onFormClose, onFormChange, onFormSubmit, onEditStart, onDelete, }) => { const winRate = stats?.total_records > 0 ? ((stats.prize_count / stats.total_records) * 100).toFixed(1) : '0.0'; const netColor = (stats?.net ?? 0) >= 0 ? 'is-pos' : 'is-neg'; return (

Purchase Tracker

구매 기록

구매 내역 기록 및 수익률 추적

{loading && 로딩 중}
{/* 통계 바 */} {stats && stats.total_records > 0 && (
{fmtWon(stats.total_invested)} 총 투자
{fmtWon(stats.total_prize)} 총 당첨금
{(stats.net ?? 0) >= 0 ? '+' : ''}{fmtWon(stats.net)} 순손익
{stats.return_rate?.toFixed(1)}% 회수율
{winRate}% 당첨률
{stats.max_prize > 0 && (
{fmtWon(stats.max_prize)} 최대 당첨금
)}
)} {/* 입력 폼 */} {formOpen && (

{editId != null ? '기록 수정' : '구매 기록 추가'}

{formError && (

{formError}

)}
)} {/* 기록 목록 */} {records.length === 0 ? (

구매 기록이 없습니다.

) : (
회차 투자금 당첨금 손익 메모
{records.map((rec) => { const net = (rec.prize ?? 0) - (rec.amount ?? 0); return (
{rec.draw_no}회 {fmtWon(rec.amount)} 0 ? 'is-prize' : ''}> {fmtWon(rec.prize)} = 0 ? 'is-pos' : 'is-neg'}> {net >= 0 ? '+' : ''}{fmtWon(net)} {rec.note || '-'}
); })}
)}
); }; /* ───────────────────────────────────────────── Main Functions Component ───────────────────────────────────────────── */ export default function Functions() { // ── 기존 상태 ────────────────────────────────────────────────────────────── const [latest, setLatest] = useState(null); const [params, setParams] = useState({ recent_window: 200, recent_weight: 2.0, avoid_recent_k: 5, }); const presets = useMemo(() => [ { name: '기본', recent_window: 200, recent_weight: 2.0, avoid_recent_k: 5 }, { name: '최근 가중치↑', recent_window: 100, recent_weight: 3.0, avoid_recent_k: 10 }, { name: '안전(분산)', recent_window: 300, recent_weight: 1.6, avoid_recent_k: 8 }, { name: '공격(최근)', recent_window: 80, recent_weight: 3.5, avoid_recent_k: 12 }, ], []); const [result, setResult] = useState(null); const [history, setHistory] = useState([]); const [historyExpanded, setHistoryExpanded] = useState(false); const historyEndRef = useRef(null); const prevHistoryExpandedRef = useRef(false); const [stats, setStats] = useState(() => readStatsCache()); const [statsLoading, setStatsLoading] = useState(false); const [statsError, setStatsError] = useState(''); const [loading, setLoading] = useState({ latest: false, recommend: false, history: false, bestPicks: false, analysis: false, }); const [error, setError] = useState(''); const [bestPicks, setBestPicks] = useState([]); const [bestPicksExpanded, setBestPicksExpanded] = useState(false); const [analysis, setAnalysis] = useState(null); const [simulating, setSimulating] = useState(false); const [simResult, setSimResult] = useState(null); // ── 신규 상태 ────────────────────────────────────────────────────────────── const [combined, setCombined] = useState(null); const [combinedLoading, setCombinedLoading] = useState(false); const [combinedHistory, setCombinedHistory] = useState([]); const [combinedHistLoading, setCombinedHistLoading] = useState(false); const [perfStats, setPerfStats] = useState(null); const [report, setReport] = useState(null); const [reportHistory, setReportHistory] = useState([]); const [reportLoading, setReportLoading] = useState(false); const [personalAnalysis, setPersonalAnalysis] = useState(null); const [personalLoading, setPersonalLoading] = useState(false); const [purchases, setPurchases] = useState([]); const [purchaseStats, setPurchaseStats] = useState(null); const [purchaseLoading, setPurchaseLoading] = useState(false); // 구매 폼 상태 const [purchaseFormOpen, setPurchaseFormOpen] = useState(false); const [purchaseForm, setPurchaseForm] = useState(emptyPurchaseForm); const [purchaseFormSaving, setPurchaseFormSaving] = useState(false); const [purchaseFormError, setPurchaseFormError] = useState(''); const [purchaseEditId, setPurchaseEditId] = useState(null); // ── 파생 값 ──────────────────────────────────────────────────────────────── const overallMetrics = useMemo(() => buildMetricsFromFrequency(stats?.frequency), [stats]); const historyMetrics = useMemo(() => buildMetricsFromHistory(history), [history]); const visibleHistory = historyExpanded ? history : history.slice(0, 5); const visibleBestPicks = bestPicksExpanded ? bestPicks : bestPicks.slice(0, BEST_PICKS_DEFAULT_SHOW); // ── 기존 로드 함수 ───────────────────────────────────────────────────────── const refreshLatest = async () => { setLoading((s) => ({ ...s, latest: true })); setError(''); try { setLatest(await getLatest()); } catch (e) { setError(e?.message ?? String(e)); } finally { setLoading((s) => ({ ...s, latest: false })); } }; const refreshHistory = async () => { setLoading((s) => ({ ...s, history: true })); setError(''); try { const limit = 100; let offset = 0; const allItems = []; while (true) { const data = await getHistory(limit, offset); const items = data.items ?? []; allItems.push(...items); if (items.length < limit) break; offset += limit; } setHistory(allItems); } catch (e) { setError(e?.message ?? String(e)); } finally { setLoading((s) => ({ ...s, history: false })); } }; const refreshStats = async () => { setStatsLoading(true); setStatsError(''); try { const cached = readStatsCache(); if (cached && !stats) setStats(cached); const data = await getStats(); if (!cached || cached.total_draws !== data?.total_draws) { setStats(data); writeStatsCache(data); } } catch (e) { setStatsError(e?.message ?? String(e)); } finally { setStatsLoading(false); } }; const refreshBestPicks = async () => { setLoading((s) => ({ ...s, bestPicks: true })); try { setBestPicks((await getBestPicks(20)).items ?? []); } catch {} finally { setLoading((s) => ({ ...s, bestPicks: false })); } }; const refreshAnalysis = async () => { setLoading((s) => ({ ...s, analysis: true })); try { setAnalysis(await getAnalysis()); } catch {} finally { setLoading((s) => ({ ...s, analysis: false })); } }; // ── 신규 로드 함수 ───────────────────────────────────────────────────────── const refreshPerfStats = async () => { try { setPerfStats(await getPerformanceStats()); } catch {} }; const refreshReport = async () => { setReportLoading(true); try { const [rep, hist] = await Promise.all([ getLatestReport(), getReportHistory(10), ]); setReport(rep); setReportHistory(hist?.reports ?? []); } catch {} finally { setReportLoading(false); } }; const loadSpecificReport = async (drwNo) => { setReportLoading(true); try { setReport(await getReport(drwNo)); } catch {} finally { setReportLoading(false); } }; const runCombinedRecommend = async () => { setCombinedLoading(true); try { const data = await getCombinedRecommend(); setCombined(data); // 이력도 새로고침 const hist = await getCombinedHistory(30); setCombinedHistory(hist?.items ?? []); } catch (e) { setError(e?.message ?? String(e)); } finally { setCombinedLoading(false); } }; const loadCombinedHistory = async () => { setCombinedHistLoading(true); try { const hist = await getCombinedHistory(30); setCombinedHistory(hist?.items ?? []); } catch {} finally { setCombinedHistLoading(false); } }; const refreshPersonalAnalysis = async () => { setPersonalLoading(true); try { setPersonalAnalysis(await getPersonalAnalysis()); } catch {} finally { setPersonalLoading(false); } }; const refreshPurchases = async () => { setPurchaseLoading(true); try { const [recs, st] = await Promise.all([getPurchases(), getPurchaseStats()]); setPurchases(recs?.records ?? []); setPurchaseStats(st); } catch {} finally { setPurchaseLoading(false); } }; // ── 시뮬레이션 ───────────────────────────────────────────────────────────── const onSimulate = async () => { const ok = confirm('시뮬레이션을 즉시 실행할까요?\n20,000개 후보를 분석합니다. (약 1~3분 소요)'); if (!ok) return; setSimulating(true); setSimResult(null); setError(''); try { const data = await triggerSimulate(); setSimResult(data); await refreshBestPicks(); } catch (e) { setError(e?.message ?? String(e)); } finally { setSimulating(false); } }; // ── 수동 추천 ────────────────────────────────────────────────────────────── const onRecommend = async () => { setLoading((s) => ({ ...s, recommend: true })); setError(''); try { const data = await recommend(params); setResult(data); await refreshHistory(); } catch (e) { setError(e?.message ?? String(e)); } finally { setLoading((s) => ({ ...s, recommend: false })); } }; const onDelete = async (id) => { if (!confirm(`히스토리 #${id}를 삭제할까요?`)) return; setError(''); try { await deleteHistory(id); setHistory((prev) => prev.filter((item) => item.id !== id)); } catch (e) { setError(e?.message ?? String(e)); } }; const copyNumbers = async (nums) => { const text = nums.join(', '); try { await navigator.clipboard.writeText(text); alert(`복사 완료: ${text}`); } catch { prompt('복사해서 사용하세요:', text); } }; // ── 구매 기록 CRUD ───────────────────────────────────────────────────────── const handlePurchaseFormOpen = () => { setPurchaseEditId(null); setPurchaseForm(emptyPurchaseForm()); setPurchaseFormError(''); setPurchaseFormOpen(true); }; const handlePurchaseFormClose = () => { setPurchaseFormOpen(false); setPurchaseEditId(null); setPurchaseFormError(''); }; const handlePurchaseFormChange = (field, value) => { setPurchaseForm((prev) => ({ ...prev, [field]: value })); }; const handlePurchaseEditStart = (rec) => { setPurchaseEditId(rec.id); setPurchaseForm({ draw_no: String(rec.draw_no ?? ''), amount: rec.amount ?? 5000, sets: rec.sets ?? 5, prize: rec.prize ?? 0, note: rec.note ?? '', }); setPurchaseFormError(''); setPurchaseFormOpen(true); }; const handlePurchaseFormSubmit = async (e) => { e.preventDefault(); setPurchaseFormSaving(true); setPurchaseFormError(''); const payload = { draw_no: Number(purchaseForm.draw_no), amount: Number(purchaseForm.amount), sets: Number(purchaseForm.sets), prize: Number(purchaseForm.prize), note: purchaseForm.note.trim(), }; try { if (purchaseEditId != null) { const updated = await updatePurchase(purchaseEditId, payload); setPurchases((prev) => prev.map((r) => r.id === purchaseEditId ? (updated ?? { ...payload, id: purchaseEditId }) : r) ); } else { const saved = await addPurchase(payload); setPurchases((prev) => [saved ?? { ...payload, id: Date.now() }, ...prev]); } // 통계 재로드 try { setPurchaseStats(await getPurchaseStats()); } catch {} handlePurchaseFormClose(); } catch (err) { setPurchaseFormError(err?.message ?? String(err)); } finally { setPurchaseFormSaving(false); } }; const handlePurchaseDelete = async (id) => { if (!confirm('이 구매 기록을 삭제할까요?')) return; setPurchases((prev) => prev.filter((r) => r.id !== id)); try { await deletePurchase(id); try { setPurchaseStats(await getPurchaseStats()); } catch {} } catch { refreshPurchases(); } }; // ── 초기 로드 ────────────────────────────────────────────────────────────── useEffect(() => { refreshLatest(); refreshHistory(); refreshStats(); refreshBestPicks(); refreshAnalysis(); refreshPerfStats(); refreshReport(); refreshPersonalAnalysis(); refreshPurchases(); loadCombinedHistory(); }, []); useEffect(() => { if (historyExpanded && !prevHistoryExpandedRef.current) { requestAnimationFrame(() => { historyEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }); } prevHistoryExpandedRef.current = historyExpanded; }, [historyExpanded, visibleHistory.length]); // ── 렌더 ─────────────────────────────────────────────────────────────────── return (
{error ? (

오류

{error}

) : null} {/* ── 신뢰도 배너 ── */} {/* ── 종합 추론 번호 추천 ── */} {/* ── 최신 회차 + 시뮬레이션 추천 ── */}
{/* Latest Draw */}

Latest Draw

최신 회차

최신 회차와 번호를 빠르게 확인할 수 있습니다.

{loading.latest ? 로딩 중 : null}
{latest ? ( <>

{latest.drawNo}회

{latest.date}

보너스 {latest.bonus}

{overallMetrics && ( )} ) : (

최신 회차 데이터가 없습니다.

)}
{/* Simulation Picks */}

Simulation Picks

시뮬레이션 추천

하루 6회 몬테카를로 시뮬레이션으로 선별된 최적 번호입니다.

{loading.bestPicks ? 로딩 중 : null} {simulating ? 분석 중 : null}
{simResult && (

완료: {simResult.total_generated?.toLocaleString()}개 후보 → 상위 {simResult.best_n_saved}개 저장

최고 점수 {((simResult.best_score ?? 0) * 100).toFixed(1)}% / 평균 {((simResult.avg_score ?? 0) * 100).toFixed(1)}%

)} {bestPicks.length === 0 ? (

{loading.bestPicks ? '불러오는 중...' : "시뮬레이션 결과가 없습니다. '지금 실행'으로 시작하세요."}

) : ( <>
{visibleBestPicks.map((pick) => (
#{pick.rank}
{((pick.score_total ?? 0) * 100).toFixed(1)}%
))}
{bestPicks.length > BEST_PICKS_DEFAULT_SHOW && ( )}

갱신: {fmtKST(bestPicks[0]?.created_at) || '-'} {bestPicks[0]?.based_on_draw ? ` · ${bestPicks[0].based_on_draw}회 기준` : ''}

)}
{/* ── 이번 주 공략 리포트 ── */} {/* ── 통계 분석 ── */}

Analysis

통계 분석

빈도, Z-score, 갭 분석으로 번호를 분류합니다.

{loading.analysis ? 로딩 중 : null}
{analysis ? (

🔥 핫 번호 출현 빈도 상위 10

{(analysis.hot_numbers ?? []).map((n) => )}

🧊 콜드 번호 출현 빈도 하위 10

{(analysis.cold_numbers ?? []).map((n) => )}

⏰ 오버듀 번호 오래 안 나온 번호 (회차 수)

{(analysis.overdue_numbers ?? []).map((n) => { const stat = (analysis.number_stats ?? []).find((s) => s.number === n); return (
{stat?.gap ?? '-'}회
); })}
역대 합계 평균 {analysis.mean_sum} 표준편차 ±{analysis.std_sum} 분석 회차 {analysis.total_draws?.toLocaleString()} 홀수 3:짝수 3 확률{' '} {analysis.odd_distribution?.['3'] ? `${analysis.odd_distribution['3']}%` : '-'}
) : (

{loading.analysis ? '불러오는 중...' : '통계 분석 데이터가 없습니다.'}

)}
{/* ── 전체 번호 분포 ── */}

Distribution

전체 회차 번호 분포

1~45번 번호가 등장한 횟수를 기준으로 분포를 표시합니다.

{statsLoading ? 로딩 중 : null} {stats?.total_draws ? ( {stats.total_draws}회차 ) : null}
{statsError ?

{statsError}

: null} {stats ? ( ) : (

통계 데이터를 불러오지 못했습니다.

)}
{/* ── 내 번호 패턴 ── */} {/* ── 구매 기록 ── */} {/* ── 수동 추천 ── */}

Manual Recommendation

수동 추천

파라미터를 직접 조정해 번호를 추천받을 수 있습니다.

{loading.recommend ? 계산 중 : null}
{presets.map((preset) => ( ))}
{result ? (

추천 ID #{result.id}

기준 회차 {result.based_on_latest_draw ?? '-'}

{result.numbers && } {historyMetrics && (
)} {Array.isArray(result.items) && result.items.length ? (
추천 후보 보기
{result.items.map((item, idx) => (

후보 #{item.id ?? idx + 1}

기준 회차 {item.based_on_draw ?? '-'}

{item.metrics && }
))}
) : null} {result.explain && (
설명 보기
{JSON.stringify(result.explain, null, 2)}
)}
) : (

아직 추천 결과가 없습니다.

)}
{/* ── 추천 히스토리 ── */}

History

추천 히스토리

수동 추천 결과를 모아서 확인할 수 있습니다.

{history.length}건 {history.length > 5 && ( )}
{loading.history ?

불러오는 중...

: null} {history.length === 0 ? (

저장된 히스토리가 없습니다.

) : (
{visibleHistory.map((item) => (

#{item.id}

{fmtKST(item.created_at)}

기준 회차 {item.based_on_draw ?? '-'}

window={item.params?.recent_window}, weight={item.params?.recent_weight}, avoid_k={item.params?.avoid_recent_k}

))}
)}
); }