로또 종합 추론 번호 추천 기능 추가
5가지 통계 기법(빈도Z-score·조합지문·갭분석·공동출현·다양성)을 기법별 가중치(30/25/20/15/10%)로 투표 집계하여 최적 6개 번호 도출. - 기법별 추천 번호 시각화 (최종 번호 하이라이트) - 투표 참여 기법 수 점 표시 (최대 5개) - 조합 품질 점수 5차원 바 차트 - 추천 이력 히스토리 누적 저장 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
getPerformanceStats, getLatestReport, getReportHistory,
|
||||
getPersonalAnalysis, getPurchases, getPurchaseStats,
|
||||
addPurchase, updatePurchase, deletePurchase,
|
||||
getCombinedRecommend, getCombinedHistory,
|
||||
} from '../../api';
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
@@ -286,6 +287,179 @@ const ConfidenceRing = ({ 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 (
|
||||
<section className="lotto-panel lotto-panel--wide lotto-combined">
|
||||
<div className="lotto-panel__head">
|
||||
<div>
|
||||
<p className="lotto-panel__eyebrow">AI · 종합 추론</p>
|
||||
<h3>종합 추론 번호 추천</h3>
|
||||
<p className="lotto-panel__sub">
|
||||
5가지 통계 기법(빈도·지문·갭·공동출현·다양성)을 가중 투표로 합산해
|
||||
최적 6개 번호를 도출합니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="lotto-panel__actions">
|
||||
{loading && <span className="lotto-chip">분석 중…</span>}
|
||||
<button className="button primary small" onClick={onRun} disabled={loading}>
|
||||
{loading ? '추론 중…' : '🔮 종합 추론 실행'}
|
||||
</button>
|
||||
{history.length > 0 && (
|
||||
<button className="button ghost small" onClick={() => setHistExpand(p => !p)}>
|
||||
이력 {history.length}건 {histExpand ? '▲' : '▼'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!combined && !loading && (
|
||||
<p className="lotto-empty">버튼을 눌러 종합 추론을 실행하세요.</p>
|
||||
)}
|
||||
|
||||
{combined && (
|
||||
<>
|
||||
{/* 기법별 추천 번호 */}
|
||||
<div className="lotto-combined__methods">
|
||||
{METHOD_ORDER.map((key) => {
|
||||
const meta = METHOD_META[key];
|
||||
const m = combined.methods?.[key];
|
||||
if (!m) return null;
|
||||
return (
|
||||
<div key={key} className="lotto-combined__method">
|
||||
<div className="lotto-combined__method-head">
|
||||
<span className="lotto-combined__method-icon">{meta.icon}</span>
|
||||
<div>
|
||||
<p className="lotto-combined__method-name" style={{ color: meta.color }}>
|
||||
{meta.label}
|
||||
<span className="lotto-combined__method-weight"> ({m.weight_pct}%)</span>
|
||||
</p>
|
||||
<p className="lotto-combined__method-desc">{meta.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lotto-combined__method-nums">
|
||||
{m.numbers.map((n) => {
|
||||
const inFinal = combined.final_numbers.includes(n);
|
||||
return (
|
||||
<span
|
||||
key={n}
|
||||
className={`lotto-ball ${ballClass(n).replace('lotto-ball ', '')} ${inFinal ? 'is-final' : 'is-dim'}`}
|
||||
>
|
||||
{n}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* 최종 추론 결과 */}
|
||||
<div className="lotto-combined__final">
|
||||
<div className="lotto-combined__final-head">
|
||||
<span className="lotto-combined__final-badge">종합 추론 결과</span>
|
||||
{combined.deduped && (
|
||||
<span className="lotto-chip lotto-chip--muted">중복 (이미 저장됨)</span>
|
||||
)}
|
||||
<button className="button ghost small" onClick={() => onCopy(combined.final_numbers)}>
|
||||
복사
|
||||
</button>
|
||||
</div>
|
||||
<div className="lotto-combined__final-balls">
|
||||
{combined.final_numbers.map((n) => {
|
||||
const votes = combined.vote_counts?.[String(n)] ?? 0;
|
||||
return (
|
||||
<div key={n} className="lotto-combined__final-ball-wrap">
|
||||
<span className={ballClass(n)}>{n}</span>
|
||||
<span className="lotto-combined__vote-dots">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<span key={i} className={`lotto-combined__vote-dot ${i < votes ? 'is-on' : ''}`} />
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="lotto-combined__final-sub">
|
||||
● 점은 해당 번호가 채택된 기법 수 (최대 5개)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 점수 바 */}
|
||||
<div className="lotto-combined__scores">
|
||||
<p className="lotto-combined__scores-title">조합 품질 점수</p>
|
||||
{SCORE_META.map(({ key, label, color, weight }) => {
|
||||
const val = combined.scores?.[key] ?? 0;
|
||||
const pct = Math.round(val * 100);
|
||||
return (
|
||||
<div key={key} className="lotto-combined__score-row">
|
||||
<span className="lotto-combined__score-label">{label}</span>
|
||||
<span className="lotto-combined__score-weight">{weight}%</span>
|
||||
<div className="lotto-combined__score-bar-wrap">
|
||||
<div
|
||||
className="lotto-combined__score-bar"
|
||||
style={{ width: `${pct}%`, background: color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="lotto-combined__score-val">{pct}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="lotto-combined__score-total">
|
||||
종합 점수 <strong>{Math.round((combined.scores?.score_total ?? 0) * 100)}</strong> / 100
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="lotto-combined__disclaimer">
|
||||
※ 이 추천은 역대 통계 패턴 기반 참고 자료이며, 당첨을 보장하지 않습니다.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 추천 이력 */}
|
||||
{histExpand && (
|
||||
<div className="lotto-combined__history">
|
||||
<p className="lotto-combined__history-title">종합 추론 이력</p>
|
||||
{histLoading && <p className="lotto-empty">로딩 중…</p>}
|
||||
{history.map((item) => (
|
||||
<div key={item.id} className="lotto-combined__history-item">
|
||||
<div className="lotto-combined__history-meta">
|
||||
<span>#{item.id}</span>
|
||||
<span>{fmtKST(item.created_at)}</span>
|
||||
<span>기준 {item.based_on_draw ?? '-'}회</span>
|
||||
</div>
|
||||
<NumberRow nums={item.numbers} />
|
||||
<button className="button ghost small" onClick={() => onCopy(item.numbers)}>복사</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/* 공략 리포트 패널 */
|
||||
const ReportPanel = ({ report, history, loading, onRefresh, onSelectDrw }) => {
|
||||
const [histExpand, setHistExpand] = useState(false);
|
||||
@@ -710,6 +884,11 @@ export default function Functions() {
|
||||
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([]);
|
||||
@@ -811,6 +990,27 @@ export default function Functions() {
|
||||
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()); }
|
||||
@@ -943,6 +1143,7 @@ export default function Functions() {
|
||||
refreshReport();
|
||||
refreshPersonalAnalysis();
|
||||
refreshPurchases();
|
||||
loadCombinedHistory();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -970,6 +1171,16 @@ export default function Functions() {
|
||||
{/* ── 신뢰도 배너 ── */}
|
||||
<PerformanceBanner perf={perfStats} />
|
||||
|
||||
{/* ── 종합 추론 번호 추천 ── */}
|
||||
<CombinedRecommendPanel
|
||||
combined={combined}
|
||||
history={combinedHistory}
|
||||
loading={combinedLoading}
|
||||
histLoading={combinedHistLoading}
|
||||
onRun={runCombinedRecommend}
|
||||
onCopy={copyNumbers}
|
||||
/>
|
||||
|
||||
{/* ── 최신 회차 + 시뮬레이션 추천 ── */}
|
||||
<div className="lotto-grid">
|
||||
{/* Latest Draw */}
|
||||
|
||||
Reference in New Issue
Block a user