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 ? ( -
-
-

오류

-

{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 ? ( +
+
+

오류

+

{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 ( + + ); +}