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;
+ }
+}