diff --git a/src/pages/lotto/Functions.jsx b/src/pages/lotto/Functions.jsx
index 9a0946a..640c51c 100644
--- a/src/pages/lotto/Functions.jsx
+++ b/src/pages/lotto/Functions.jsx
@@ -1,460 +1,32 @@
-import React, { useMemo } from 'react';
-import {
- fmtKST, Ball, NumberRow, copyNumbers,
- buildMetricsFromFrequency, BEST_PICKS_DEFAULT_SHOW,
-} from './lottoUtils';
+import { useState } from 'react';
+import BriefingTab from './tabs/BriefingTab';
+import AnalysisTab from './tabs/AnalysisTab';
+import PurchaseTab from './tabs/PurchaseTab';
-/* ── hooks ──────────────────────────────────────────────────────── */
-import useLottoData from './hooks/useLottoData';
-import usePurchases from './hooks/usePurchases';
-import useManualRecommend from './hooks/useManualRecommend';
+const TABS = [
+ { id: 'briefing', label: '🗓 이번 주 브리핑' },
+ { id: 'analysis', label: '📊 분석·통계' },
+ { id: 'purchase', label: '💰 구매·성과' },
+];
-/* ── components ─────────────────────────────────────────────────── */
-import MetricBlock from './components/MetricBlock';
-import FrequencyChart from './components/FrequencyChart';
-import PerformanceBanner from './components/PerformanceBanner';
-import CombinedRecommendPanel from './components/CombinedRecommendPanel';
-import ReportPanel from './components/ReportPanel';
-import PersonalAnalysisPanel from './components/PersonalAnalysisPanel';
-import PurchasePanel from './components/PurchasePanel';
-
-/* ── component ──────────────────────────────────────────────────── */
export default function Functions() {
- const ld = useLottoData();
- const pur = usePurchases();
- const mr = useManualRecommend();
-
- /* ── derived ────────────────────────────────────────────────── */
- const overallMetrics = useMemo(() => buildMetricsFromFrequency(ld.stats?.frequency), [ld.stats]);
- const visibleBestPicks = ld.bestPicksExpanded ? ld.bestPicks : ld.bestPicks.slice(0, BEST_PICKS_DEFAULT_SHOW);
-
- /* ── merged error ───────────────────────────────────────────── */
- const error = ld.error || mr.error;
- const clearError = () => { ld.setError(''); mr.setError(''); };
-
- /* ── render ──────────────────────────────────────────────────── */
+ const [tab, setTab] = useState('briefing');
return (
- {error ? (
-
- ) : null}
-
- {/* ── 신뢰도 배너 ── */}
-
-
- {/* ── 종합 추론 번호 추천 ── */}
-
-
- {/* ── 최신 회차 + 시뮬레이션 추천 ── */}
-
- {/* Latest Draw */}
-
-
-
-
Latest Draw
-
최신 회차
-
최신 회차와 번호를 빠르게 확인할 수 있습니다.
-
-
- {ld.loading.latest ? 로딩 중 : null}
-
-
-
- {ld.latest ? (
- <>
-
-
-
{ld.latest.drawNo}회
-
{ld.latest.date}
-
-
-
-
- 보너스 {ld.latest.bonus}
- {overallMetrics && (
-
- )}
- >
- ) : (
- 최신 회차 데이터가 없습니다.
- )}
-
-
- {/* Simulation Picks */}
-
-
-
-
Simulation Picks
-
시뮬레이션 추천
-
- 하루 6회 몬테카를로 시뮬레이션으로 선별된 최적 번호입니다.
-
-
-
- {ld.loading.bestPicks ? 로딩 중 : null}
- {ld.simulating ? 분석 중 : null}
-
-
-
-
-
- {ld.simResult && (
-
-
완료: {ld.simResult.total_generated?.toLocaleString()}개 후보 → 상위 {ld.simResult.best_n_saved}개 저장
-
최고 점수 {((ld.simResult.best_score ?? 0) * 100).toFixed(1)}% / 평균 {((ld.simResult.avg_score ?? 0) * 100).toFixed(1)}%
-
- )}
-
- {ld.bestPicks.length === 0 ? (
-
- {ld.loading.bestPicks ? '불러오는 중...' : "시뮬레이션 결과가 없습니다. '지금 실행'으로 시작하세요."}
-
- ) : (
- <>
-
- {visibleBestPicks.map((pick) => (
-
-
#{pick.rank}
-
-
-
-
- {((pick.score_total ?? 0) * 100).toFixed(1)}%
-
-
-
-
-
-
-
-
- ))}
-
- {ld.bestPicks.length > BEST_PICKS_DEFAULT_SHOW && (
-
- )}
-
- 갱신: {fmtKST(ld.bestPicks[0]?.created_at) || '-'}
- {ld.bestPicks[0]?.based_on_draw ? ` · ${ld.bestPicks[0].based_on_draw}회 기준` : ''}
-
- >
- )}
-
+
+
+ {tab === 'briefing' &&
}
+ {tab === 'analysis' &&
}
+ {tab === 'purchase' &&
}
-
- {/* ── 이번 주 공략 리포트 ── */}
-
-
- {/* ── 통계 분석 ── */}
-
-
-
-
Analysis
-
통계 분석
-
빈도, Z-score, 갭 분석으로 번호를 분류합니다.
-
-
- {ld.loading.analysis ? 로딩 중 : null}
-
-
-
- {ld.analysis ? (
-
-
-
-
🔥 핫 번호 출현 빈도 상위 10
-
- {(ld.analysis.hot_numbers ?? []).map((n) => )}
-
-
-
-
🧊 콜드 번호 출현 빈도 하위 10
-
- {(ld.analysis.cold_numbers ?? []).map((n) => )}
-
-
-
-
⏰ 오버듀 번호 오래 안 나온 번호 (회차 수)
-
- {(ld.analysis.overdue_numbers ?? []).map((n) => {
- const stat = (ld.analysis.number_stats ?? []).find((s) => s.number === n);
- return (
-
-
- {stat?.gap ?? '-'}회
-
- );
- })}
-
-
-
-
- 역대 합계 평균 {ld.analysis.mean_sum}
- 표준편차 ±{ld.analysis.std_sum}
- 분석 회차 {ld.analysis.total_draws?.toLocaleString()}
-
- 홀수 3:짝수 3 확률{' '}
-
- {ld.analysis.odd_distribution?.['3'] ? `${ld.analysis.odd_distribution['3']}%` : '-'}
-
-
-
-
- ) : (
-
- {ld.loading.analysis ? '불러오는 중...' : '통계 분석 데이터가 없습니다.'}
-
- )}
-
-
- {/* ── 전체 번호 분포 ── */}
-
-
-
-
Distribution
-
전체 회차 번호 분포
-
1~45번 번호가 등장한 횟수를 기준으로 분포를 표시합니다.
-
-
- {ld.statsLoading ? 로딩 중 : null}
- {ld.stats?.total_draws ? (
- {ld.stats.total_draws}회차
- ) : null}
-
-
-
- {ld.statsError ? {ld.statsError}
: null}
- {ld.stats ? (
-
- ) : (
- 통계 데이터를 불러오지 못했습니다.
- )}
-
-
- {/* ── 내 번호 패턴 ── */}
-
-
- {/* ── 구매 기록 ── */}
-
-
- {/* ── 수동 추천 ── */}
-
-
-
-
Manual Recommendation
-
수동 추천
-
파라미터를 직접 조정해 번호를 추천받을 수 있습니다.
-
-
- {mr.loading.recommend ? 계산 중 : null}
-
-
-
-
- {mr.presets.map((preset) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
- {mr.result ? (
-
-
-
-
추천 ID #{mr.result.id}
-
기준 회차 {mr.result.based_on_latest_draw ?? '-'}
-
-
-
- {mr.result.numbers &&
}
- {mr.historyMetrics && (
-
-
-
- )}
- {Array.isArray(mr.result.items) && mr.result.items.length ? (
-
- 추천 후보 보기
-
- {mr.result.items.map((item, idx) => (
-
-
-
-
후보 #{item.id ?? idx + 1}
-
기준 회차 {item.based_on_draw ?? '-'}
-
-
-
-
- {item.metrics &&
}
-
- ))}
-
-
- ) : null}
- {mr.result.explain && (
-
- 설명 보기
- {JSON.stringify(mr.result.explain, null, 2)}
-
- )}
-
- ) : (
- 아직 추천 결과가 없습니다.
- )}
-
-
- {/* ── 추천 히스토리 ── */}
-
-
-
-
History
-
추천 히스토리
-
수동 추천 결과를 모아서 확인할 수 있습니다.
-
-
- {mr.history.length}건
- {mr.history.length > 5 && (
-
- )}
-
-
-
-
- {mr.loading.history ? 불러오는 중...
: null}
- {mr.history.length === 0 ? (
- 저장된 히스토리가 없습니다.
- ) : (
-
- {mr.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}
-
-
-
-
-
-
-
- ))}
-
-
- )}
-
-
-
);
}
diff --git a/src/pages/lotto/Lotto.css b/src/pages/lotto/Lotto.css
index 4ac59dc..a4d957f 100644
--- a/src/pages/lotto/Lotto.css
+++ b/src/pages/lotto/Lotto.css
@@ -1509,3 +1509,13 @@
.briefing-tokens { width: 100%; }
.pick-card-balls { justify-content: center; }
}
+
+/* ── Tab navigation ───────────────────────────────────────────────────────── */
+.lotto-tabs { display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid rgba(255,255,255,0.1); }
+.lotto-tabs button { padding: 8px 16px; background: transparent; border: none; color: #94a3b8; cursor: pointer; border-bottom: 2px solid transparent; }
+.lotto-tabs button.active { color: #e2e8f0; border-bottom-color: #818cf8; }
+.lotto-tab-body { padding-top: 8px; }
+@media (max-width: 768px) {
+ .lotto-tabs { overflow-x: auto; }
+ .lotto-tabs button { white-space: nowrap; }
+}
diff --git a/src/pages/lotto/tabs/AnalysisTab.jsx b/src/pages/lotto/tabs/AnalysisTab.jsx
new file mode 100644
index 0000000..ed98fde
--- /dev/null
+++ b/src/pages/lotto/tabs/AnalysisTab.jsx
@@ -0,0 +1,428 @@
+import React, { useMemo } from 'react';
+import {
+ fmtKST, Ball, NumberRow, copyNumbers,
+ buildMetricsFromFrequency, BEST_PICKS_DEFAULT_SHOW,
+} from '../lottoUtils';
+
+import useLottoData from '../hooks/useLottoData';
+import useManualRecommend from '../hooks/useManualRecommend';
+
+import MetricBlock from '../components/MetricBlock';
+import FrequencyChart from '../components/FrequencyChart';
+import PerformanceBanner from '../components/PerformanceBanner';
+import CombinedRecommendPanel from '../components/CombinedRecommendPanel';
+import ReportPanel from '../components/ReportPanel';
+import PersonalAnalysisPanel from '../components/PersonalAnalysisPanel';
+
+export default function AnalysisTab() {
+ const ld = useLottoData();
+ const mr = useManualRecommend();
+
+ const overallMetrics = useMemo(() => buildMetricsFromFrequency(ld.stats?.frequency), [ld.stats]);
+ const visibleBestPicks = ld.bestPicksExpanded ? ld.bestPicks : ld.bestPicks.slice(0, BEST_PICKS_DEFAULT_SHOW);
+
+ const error = ld.error || mr.error;
+ const clearError = () => { ld.setError(''); mr.setError(''); };
+
+ return (
+ <>
+ {error ? (
+
+ ) : null}
+
+ {/* 신뢰도 배너 */}
+
+
+ {/* 종합 추론 번호 추천 */}
+
+
+ {/* 최신 회차 + 시뮬레이션 추천 */}
+
+ {/* Latest Draw */}
+
+
+
+
Latest Draw
+
최신 회차
+
최신 회차와 번호를 빠르게 확인할 수 있습니다.
+
+
+ {ld.loading.latest ? 로딩 중 : null}
+
+
+
+ {ld.latest ? (
+ <>
+
+
+
{ld.latest.drawNo}회
+
{ld.latest.date}
+
+
+
+
+ 보너스 {ld.latest.bonus}
+ {overallMetrics && (
+
+ )}
+ >
+ ) : (
+ 최신 회차 데이터가 없습니다.
+ )}
+
+
+ {/* Simulation Picks */}
+
+
+
+
Simulation Picks
+
시뮬레이션 추천
+
+ 하루 6회 몬테카를로 시뮬레이션으로 선별된 최적 번호입니다.
+
+
+
+ {ld.loading.bestPicks ? 로딩 중 : null}
+ {ld.simulating ? 분석 중 : null}
+
+
+
+
+
+ {ld.simResult && (
+
+
완료: {ld.simResult.total_generated?.toLocaleString()}개 후보 → 상위 {ld.simResult.best_n_saved}개 저장
+
최고 점수 {((ld.simResult.best_score ?? 0) * 100).toFixed(1)}% / 평균 {((ld.simResult.avg_score ?? 0) * 100).toFixed(1)}%
+
+ )}
+
+ {ld.bestPicks.length === 0 ? (
+
+ {ld.loading.bestPicks ? '불러오는 중...' : "시뮬레이션 결과가 없습니다. '지금 실행'으로 시작하세요."}
+
+ ) : (
+ <>
+
+ {visibleBestPicks.map((pick) => (
+
+
#{pick.rank}
+
+
+
+
+ {((pick.score_total ?? 0) * 100).toFixed(1)}%
+
+
+
+
+
+
+
+
+ ))}
+
+ {ld.bestPicks.length > BEST_PICKS_DEFAULT_SHOW && (
+
+ )}
+
+ 갱신: {fmtKST(ld.bestPicks[0]?.created_at) || '-'}
+ {ld.bestPicks[0]?.based_on_draw ? ` · ${ld.bestPicks[0].based_on_draw}회 기준` : ''}
+
+ >
+ )}
+
+
+
+ {/* 이번 주 공략 리포트 */}
+
+
+ {/* 통계 분석 */}
+
+
+
+
Analysis
+
통계 분석
+
빈도, Z-score, 갭 분석으로 번호를 분류합니다.
+
+
+ {ld.loading.analysis ? 로딩 중 : null}
+
+
+
+ {ld.analysis ? (
+
+
+
+
🔥 핫 번호 출현 빈도 상위 10
+
+ {(ld.analysis.hot_numbers ?? []).map((n) => )}
+
+
+
+
🧊 콜드 번호 출현 빈도 하위 10
+
+ {(ld.analysis.cold_numbers ?? []).map((n) => )}
+
+
+
+
⏰ 오버듀 번호 오래 안 나온 번호 (회차 수)
+
+ {(ld.analysis.overdue_numbers ?? []).map((n) => {
+ const stat = (ld.analysis.number_stats ?? []).find((s) => s.number === n);
+ return (
+
+
+ {stat?.gap ?? '-'}회
+
+ );
+ })}
+
+
+
+
+ 역대 합계 평균 {ld.analysis.mean_sum}
+ 표준편차 ±{ld.analysis.std_sum}
+ 분석 회차 {ld.analysis.total_draws?.toLocaleString()}
+
+ 홀수 3:짝수 3 확률{' '}
+
+ {ld.analysis.odd_distribution?.['3'] ? `${ld.analysis.odd_distribution['3']}%` : '-'}
+
+
+
+
+ ) : (
+
+ {ld.loading.analysis ? '불러오는 중...' : '통계 분석 데이터가 없습니다.'}
+
+ )}
+
+
+ {/* 전체 번호 분포 */}
+
+
+
+
Distribution
+
전체 회차 번호 분포
+
1~45번 번호가 등장한 횟수를 기준으로 분포를 표시합니다.
+
+
+ {ld.statsLoading ? 로딩 중 : null}
+ {ld.stats?.total_draws ? (
+ {ld.stats.total_draws}회차
+ ) : null}
+
+
+
+ {ld.statsError ? {ld.statsError}
: null}
+ {ld.stats ? (
+
+ ) : (
+ 통계 데이터를 불러오지 못했습니다.
+ )}
+
+
+ {/* 내 번호 패턴 */}
+
+
+ {/* 수동 추천 */}
+
+
+
+
Manual Recommendation
+
수동 추천
+
파라미터를 직접 조정해 번호를 추천받을 수 있습니다.
+
+
+ {mr.loading.recommend ? 계산 중 : null}
+
+
+
+
+ {mr.presets.map((preset) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {mr.result ? (
+
+
+
+
추천 ID #{mr.result.id}
+
기준 회차 {mr.result.based_on_latest_draw ?? '-'}
+
+
+
+ {mr.result.numbers &&
}
+ {mr.historyMetrics && (
+
+
+
+ )}
+ {Array.isArray(mr.result.items) && mr.result.items.length ? (
+
+ 추천 후보 보기
+
+ {mr.result.items.map((item, idx) => (
+
+
+
+
후보 #{item.id ?? idx + 1}
+
기준 회차 {item.based_on_draw ?? '-'}
+
+
+
+
+ {item.metrics &&
}
+
+ ))}
+
+
+ ) : null}
+ {mr.result.explain && (
+
+ 설명 보기
+ {JSON.stringify(mr.result.explain, null, 2)}
+
+ )}
+
+ ) : (
+ 아직 추천 결과가 없습니다.
+ )}
+
+
+ {/* 추천 히스토리 */}
+
+
+
+
History
+
추천 히스토리
+
수동 추천 결과를 모아서 확인할 수 있습니다.
+
+
+ {mr.history.length}건
+ {mr.history.length > 5 && (
+
+ )}
+
+
+
+
+ {mr.loading.history ? 불러오는 중...
: null}
+ {mr.history.length === 0 ? (
+ 저장된 히스토리가 없습니다.
+ ) : (
+
+ {mr.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}
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ >
+ );
+}
diff --git a/src/pages/lotto/tabs/BriefingTab.jsx b/src/pages/lotto/tabs/BriefingTab.jsx
new file mode 100644
index 0000000..8e2210a
--- /dev/null
+++ b/src/pages/lotto/tabs/BriefingTab.jsx
@@ -0,0 +1,25 @@
+import useBriefing from '../hooks/useBriefing';
+import BriefingHeader from '../components/briefing/BriefingHeader';
+import BriefingSummary from '../components/briefing/BriefingSummary';
+import PickSetCard from '../components/briefing/PickSetCard';
+import BriefingEmpty from '../components/briefing/BriefingEmpty';
+import CuratorUsageFooter from '../components/briefing/CuratorUsageFooter';
+
+export default function BriefingTab() {
+ const { briefing, loading, error, regenerating, regenerate } = useBriefing();
+
+ if (loading) return
;
+ if (!briefing) return
;
+
+ return (
+
+
+
+
+
이번 주 5세트
+ {briefing.picks.map((p, i) =>
)}
+
+
+
+ );
+}
diff --git a/src/pages/lotto/tabs/PurchaseTab.jsx b/src/pages/lotto/tabs/PurchaseTab.jsx
new file mode 100644
index 0000000..c967414
--- /dev/null
+++ b/src/pages/lotto/tabs/PurchaseTab.jsx
@@ -0,0 +1,25 @@
+import usePurchases from '../hooks/usePurchases';
+import PurchasePanel from '../components/PurchasePanel';
+
+export default function PurchaseTab() {
+ const pur = usePurchases();
+
+ return (
+
+ );
+}