diff --git a/src/api.js b/src/api.js index ec0b38c..d3f857d 100644 --- a/src/api.js +++ b/src/api.js @@ -304,6 +304,17 @@ export function getPersonalAnalysis() { return apiGet('/api/lotto/analysis/personal'); } +// ── 종합 추론 추천 ────────────────────────────────────────────────────────── +// GET /api/lotto/recommend/combined +export function getCombinedRecommend() { + return apiGet('/api/lotto/recommend/combined'); +} + +// GET /api/lotto/recommend/combined/history +export function getCombinedHistory(limit = 30) { + return apiGet(`/api/lotto/recommend/combined/history?limit=${limit}`); +} + // GET /api/lotto/purchase?draw_no=N&days=N export function getPurchases({ draw_no, days } = {}) { const qs = new URLSearchParams(); diff --git a/src/pages/lotto/Functions.jsx b/src/pages/lotto/Functions.jsx index f1d38fa..6d5bebe 100644 --- a/src/pages/lotto/Functions.jsx +++ b/src/pages/lotto/Functions.jsx @@ -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 ( +
+
+
+

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); @@ -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() { {/* ── 신뢰도 배너 ── */} + {/* ── 종합 추론 번호 추천 ── */} + + {/* ── 최신 회차 + 시뮬레이션 추천 ── */}
{/* Latest Draw */} diff --git a/src/pages/lotto/Lotto.css b/src/pages/lotto/Lotto.css index 084f13c..0b70d6a 100644 --- a/src/pages/lotto/Lotto.css +++ b/src/pages/lotto/Lotto.css @@ -1195,3 +1195,283 @@ gap: 12px; } } + +/* ═══════════════════════════════════════════════════════ + 종합 추론 패널 +═══════════════════════════════════════════════════════ */ + +.lotto-combined { + display: flex; + flex-direction: column; + gap: 20px; +} + +/* 기법별 추천 행 */ +.lotto-combined__methods { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--line, rgba(255,255,255,0.08)); + border-radius: 14px; +} + +.lotto-combined__method { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.lotto-combined__method-head { + display: flex; + align-items: flex-start; + gap: 10px; + min-width: 200px; +} + +.lotto-combined__method-icon { + font-size: 18px; + flex-shrink: 0; + margin-top: 2px; +} + +.lotto-combined__method-name { + margin: 0; + font-size: 13px; + font-weight: 600; +} + +.lotto-combined__method-weight { + font-size: 11px; + opacity: 0.6; + font-weight: 400; +} + +.lotto-combined__method-desc { + margin: 2px 0 0; + font-size: 11px; + color: var(--text-muted, rgba(255,255,255,0.45)); +} + +.lotto-combined__method-nums { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +/* 최종 결과 */ +.lotto-combined__final { + padding: 20px; + background: rgba(129, 140, 248, 0.06); + border: 1px solid rgba(129, 140, 248, 0.25); + border-radius: 14px; + display: flex; + flex-direction: column; + gap: 14px; +} + +.lotto-combined__final-head { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.lotto-combined__final-badge { + font-size: 11px; + font-weight: 700; + letter-spacing: .04em; + text-transform: uppercase; + color: #818cf8; + background: rgba(129, 140, 248, 0.15); + border: 1px solid rgba(129, 140, 248, 0.3); + border-radius: 20px; + padding: 3px 10px; +} + +.lotto-combined__final-balls { + display: flex; + gap: 14px; + flex-wrap: wrap; +} + +.lotto-combined__final-ball-wrap { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; +} + +.lotto-combined__final-ball-wrap .lotto-ball { + width: 52px; + height: 52px; + font-size: 18px; +} + +.lotto-combined__vote-dots { + display: flex; + gap: 3px; +} + +.lotto-combined__vote-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); + transition: background 0.2s; +} + +.lotto-combined__vote-dot.is-on { + background: #818cf8; +} + +.lotto-combined__final-sub { + margin: 0; + font-size: 11px; + color: var(--text-muted, rgba(255,255,255,0.4)); +} + +/* 볼 상태 */ +.lotto-ball.is-dim { + opacity: 0.35; + transform: scale(0.92); +} + +.lotto-ball.is-final { + opacity: 1; + box-shadow: 0 0 0 2px rgba(129, 140, 248, 0.5); +} + +/* 점수 바 */ +.lotto-combined__scores { + display: flex; + flex-direction: column; + gap: 8px; +} + +.lotto-combined__scores-title { + margin: 0 0 4px; + font-size: 12px; + font-weight: 600; + color: var(--text-muted, rgba(255,255,255,0.5)); + text-transform: uppercase; + letter-spacing: .05em; +} + +.lotto-combined__score-row { + display: flex; + align-items: center; + gap: 8px; +} + +.lotto-combined__score-label { + font-size: 12px; + color: var(--text-muted, rgba(255,255,255,0.5)); + width: 72px; + flex-shrink: 0; +} + +.lotto-combined__score-weight { + font-size: 11px; + color: var(--text-muted, rgba(255,255,255,0.35)); + width: 28px; + flex-shrink: 0; + text-align: right; +} + +.lotto-combined__score-bar-wrap { + flex: 1; + height: 6px; + background: rgba(255,255,255,0.06); + border-radius: 3px; + overflow: hidden; +} + +.lotto-combined__score-bar { + height: 100%; + border-radius: 3px; + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.lotto-combined__score-val { + font-size: 12px; + font-weight: 600; + color: var(--text-bright, #fff); + width: 28px; + text-align: right; + flex-shrink: 0; +} + +.lotto-combined__score-total { + margin-top: 6px; + font-size: 13px; + color: var(--text-muted, rgba(255,255,255,0.5)); + text-align: right; +} + +.lotto-combined__score-total strong { + color: #818cf8; + font-size: 16px; +} + +.lotto-combined__disclaimer { + margin: 0; + font-size: 11px; + color: var(--text-muted, rgba(255,255,255,0.35)); +} + +/* 이력 */ +.lotto-combined__history { + border-top: 1px solid var(--line, rgba(255,255,255,0.08)); + padding-top: 16px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.lotto-combined__history-title { + margin: 0 0 4px; + font-size: 12px; + font-weight: 600; + color: var(--text-muted, rgba(255,255,255,0.45)); + text-transform: uppercase; + letter-spacing: .05em; +} + +.lotto-combined__history-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: rgba(255,255,255,0.02); + border: 1px solid var(--line, rgba(255,255,255,0.06)); + border-radius: 10px; + flex-wrap: wrap; +} + +.lotto-combined__history-meta { + display: flex; + gap: 8px; + font-size: 11px; + color: var(--text-muted, rgba(255,255,255,0.4)); + flex-shrink: 0; +} + +@media (max-width: 640px) { + .lotto-combined__method { + flex-direction: column; + align-items: flex-start; + } + + .lotto-combined__final-ball-wrap .lotto-ball { + width: 42px; + height: 42px; + font-size: 15px; + } + + .lotto-combined__final-balls { + gap: 10px; + } +}