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 ?? '-'}
{buckets.length ? (
{buckets.map(([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 (
);
};
/* ─────────────────────────────────────────────
종합 추론 추천 패널
───────────────────────────────────────────── */
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 (
);
})}
종합 점수 {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회 연속 출현
)}
{/* 핫 / 콜드 / 오버듀 */}
{/* 전략 추천 세트 */}
{(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 ? '불러오는 중...' : '추천 이력이 없습니다.'}
) : (
선택 성향
{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 && (
)}
{/* 기록 목록 */}
{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 ? (
) : 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}
))}
)}
);
}