diff --git a/src/api.js b/src/api.js index 3c79907..ab25655 100644 --- a/src/api.js +++ b/src/api.js @@ -82,6 +82,25 @@ export function deleteHistory(id) { return apiDelete(`/api/history/${id}`); } +// ── 시뮬레이션 관련 API ────────────────────────────────────────────────────── + +export function getBestPicks(limit = 20) { + return apiGet(`/api/lotto/best?limit=${limit}`); +} + +export function getAnalysis() { + return apiGet('/api/lotto/analysis'); +} + +export function triggerSimulate(nCandidates = 20000, topK = 100, bestN = 20) { + const qs = new URLSearchParams({ + n_candidates: String(nCandidates), + top_k: String(topK), + best_n: String(bestN), + }); + return apiPost(`/api/admin/simulate?${qs.toString()}`); +} + export function getStockNews(limit = 20, category) { const qs = new URLSearchParams({ limit: String(limit) }); if (category) { diff --git a/src/pages/lotto/Functions.jsx b/src/pages/lotto/Functions.jsx index 84bbb86..7178d4f 100644 --- a/src/pages/lotto/Functions.jsx +++ b/src/pages/lotto/Functions.jsx @@ -1,5 +1,8 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { deleteHistory, getHistory, getLatest, getStats, recommend } from '../../api'; +import { + deleteHistory, getHistory, getLatest, getStats, recommend, + getBestPicks, getAnalysis, triggerSimulate, +} from '../../api'; const fmtKST = (value) => value?.replace('T', ' ') ?? ''; @@ -23,6 +26,7 @@ const NumberRow = ({ nums }) => ( const bucketOrder = ['1-10', '11-20', '21-30', '31-40', '41-45']; const STATS_CACHE_KEY = 'lotto_stats_v1'; +const BEST_PICKS_DEFAULT_SHOW = 5; const readStatsCache = () => { if (typeof window === 'undefined') return null; @@ -243,6 +247,7 @@ const FrequencyChart = ({ stats }) => { }; export default function Functions() { + // ── 기존 상태 ────────────────────────────────────────────────────────────── const [latest, setLatest] = useState(null); const [params, setParams] = useState({ recent_window: 200, @@ -258,7 +263,6 @@ export default function Functions() { ], [] ); - const [result, setResult] = useState(null); const [history, setHistory] = useState([]); const [historyExpanded, setHistoryExpanded] = useState(false); @@ -271,8 +275,19 @@ export default function Functions() { latest: false, recommend: false, history: false, + bestPicks: false, + analysis: false, }); const [error, setError] = useState(''); + + // ── 시뮬레이션 관련 상태 ─────────────────────────────────────────────────── + const [bestPicks, setBestPicks] = useState([]); + const [bestPicksExpanded, setBestPicksExpanded] = useState(false); + const [analysis, setAnalysis] = useState(null); + const [simulating, setSimulating] = useState(false); + const [simResult, setSimResult] = useState(null); + + // ── 파생 값 ──────────────────────────────────────────────────────────────── const overallMetrics = useMemo( () => buildMetricsFromFrequency(stats?.frequency), [stats] @@ -281,7 +296,12 @@ export default function Functions() { () => buildMetricsFromHistory(history), [history] ); + const visibleHistory = historyExpanded ? history : history.slice(0, 5); + const visibleBestPicks = bestPicksExpanded + ? bestPicks + : bestPicks.slice(0, BEST_PICKS_DEFAULT_SHOW); + // ── 기존 데이터 로드 함수 ────────────────────────────────────────────────── const refreshLatest = async () => { setLoading((s) => ({ ...s, latest: true })); setError(''); @@ -339,6 +359,49 @@ export default function Functions() { } }; + // ── 시뮬레이션 관련 함수 ─────────────────────────────────────────────────── + const refreshBestPicks = async () => { + setLoading((s) => ({ ...s, bestPicks: true })); + try { + const data = await getBestPicks(20); + setBestPicks(data.items ?? []); + } catch (e) { + // best picks 없을 수 있음 (첫 실행 전) - 전역 에러 표시 안 함 + } finally { + setLoading((s) => ({ ...s, bestPicks: false })); + } + }; + + const refreshAnalysis = async () => { + setLoading((s) => ({ ...s, analysis: true })); + try { + const data = await getAnalysis(); + setAnalysis(data); + } catch { + // 분석 데이터는 보조 정보이므로 에러 무시 + } finally { + setLoading((s) => ({ ...s, analysis: false })); + } + }; + + const onSimulate = async () => { + const ok = confirm('시뮬레이션을 즉시 실행할까요?\n20,000개 후보를 분석합니다. (약 1~3분 소요)'); + if (!ok) return; + setSimulating(true); + setSimResult(null); + setError(''); + try { + const data = await triggerSimulate(); + setSimResult(data); + await refreshBestPicks(); + } catch (e) { + setError(e?.message ?? String(e)); + } finally { + setSimulating(false); + } + }; + + // ── 수동 추천 ────────────────────────────────────────────────────────────── const onRecommend = async () => { setLoading((s) => ({ ...s, recommend: true })); setError(''); @@ -356,7 +419,6 @@ export default function Functions() { const onDelete = async (id) => { const ok = confirm(`히스토리 #${id}를 삭제할까요?`); if (!ok) return; - setError(''); try { await deleteHistory(id); @@ -376,14 +438,15 @@ export default function Functions() { } }; + // ── 초기 로드 ────────────────────────────────────────────────────────────── useEffect(() => { refreshLatest(); refreshHistory(); refreshStats(); + refreshBestPicks(); + refreshAnalysis(); }, []); - const visibleHistory = historyExpanded ? history : history.slice(0, 5); - useEffect(() => { if (historyExpanded && !prevHistoryExpandedRef.current) { requestAnimationFrame(() => { @@ -396,6 +459,7 @@ export default function Functions() { prevHistoryExpandedRef.current = historyExpanded; }, [historyExpanded, visibleHistory.length]); + // ── 렌더 ─────────────────────────────────────────────────────────────────── return (
Recommendation
-Simulation Picks
+- 파라미터를 조정해 다른 추천 전략을 만들 수 있습니다. + 하루 6회 몬테카를로 시뮬레이션으로 선별된 최적 번호입니다.
추천 ID #{result.id}
-- 기준 회차 {result.based_on_latest_draw ?? '-'} -
-- 후보 #{item.id ?? idx + 1} -
-- 기준 회차 {item.based_on_draw ?? '-'} -
-{JSON.stringify(result.explain, null, 2)}
- + 완료: {simResult.total_generated?.toLocaleString()}개 후보 →{' '} + 상위 {simResult.best_n_saved}개 저장 +
++ 최고 점수 {((simResult.best_score ?? 0) * 100).toFixed(1)}% /{' '} + 평균 {((simResult.avg_score ?? 0) * 100).toFixed(1)}% +
++ {loading.bestPicks + ? '불러오는 중...' + : "시뮬레이션 결과가 없습니다. '지금 실행'으로 시작하세요."} +
) : ( -아직 추천 결과가 없습니다.
+ <> ++ 갱신: {fmtKST(bestPicks[0]?.created_at) || '-'} + {bestPicks[0]?.based_on_draw + ? ` · ${bestPicks[0].based_on_draw}회 기준` + : ''} +
+ > )} -Analysis
++ 빈도, Z-score, 갭 분석으로 번호를 분류합니다. +
++ 🔥 핫 번호 + 출현 빈도 상위 10 +
++ 🧊 콜드 번호 + 출현 빈도 하위 10 +
++ ⏰ 오버듀 번호 + 오래 안 나온 번호 (회차 수) +
++ {loading.analysis ? '불러오는 중...' : '통계 분석 데이터가 없습니다.'} +
+ )} +- 통계 데이터를 불러오지 못했습니다. -
+통계 데이터를 불러오지 못했습니다.
)} + {/* ── 수동 추천 ── */} +Manual Recommendation
++ 파라미터를 직접 조정해 번호를 추천받을 수 있습니다. +
+추천 ID #{result.id}
++ 기준 회차 {result.based_on_latest_draw ?? '-'} +
++ 후보 #{item.id ?? idx + 1} +
++ 기준 회차 {item.based_on_draw ?? '-'} +
+{JSON.stringify(result.explain, null, 2)}
+ 아직 추천 결과가 없습니다.
+ )} +History
- 최근 추천 결과를 모아서 확인할 수 있습니다. + 수동 추천 결과를 모아서 확인할 수 있습니다.
다음 업데이트 아이디어
+시뮬레이션 추천 시스템