From a57ac230647071323bc1d344c79cbab33d16aa18 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 31 May 2026 18:07:03 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EC=9E=90?= =?UTF-8?q?=EC=9C=A8=ED=95=99=EC=8A=B5=20=ED=83=AD=20=E2=80=94=20=EC=84=B1?= =?UTF-8?q?=EC=A0=81=ED=91=9C=C2=B7=EC=BA=98=EB=A6=AC=EB=B8=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=C2=B7=EB=8B=B9=EC=B2=A8=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/api.js | 5 ++ src/pages/lotto/Evolver.css | 41 ++++++++++++++ src/pages/lotto/evolver/CalibrationChart.jsx | 53 +++++++++++++++++++ src/pages/lotto/evolver/TrackRecordCard.jsx | 47 ++++++++++++++++ .../lotto/evolver/WinnerAnalysisCard.jsx | 49 +++++++++++++++++ src/pages/lotto/tabs/EvolverTab.jsx | 42 ++++++++++++++- 6 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 src/pages/lotto/evolver/CalibrationChart.jsx create mode 100644 src/pages/lotto/evolver/TrackRecordCard.jsx create mode 100644 src/pages/lotto/evolver/WinnerAnalysisCard.jsx diff --git a/src/api.js b/src/api.js index a85affd..02ba9bf 100644 --- a/src/api.js +++ b/src/api.js @@ -740,6 +740,11 @@ export async function triggerEvolverEvaluate() { return r.json(); } +// --- Lotto Backtest --- +export const lottoBacktestTrackRecord = () => apiGet('/api/lotto/backtest/track-record'); +export const lottoBacktestCalibration = (weeks=52) => apiGet(`/api/lotto/backtest/calibration?weeks=${weeks}`); +export const lottoBacktestReview = (drawNo) => apiGet(`/api/lotto/backtest/review/${drawNo}`); + // --- Tarot Lab --- export function tarotInterpret(body) { diff --git a/src/pages/lotto/Evolver.css b/src/pages/lotto/Evolver.css index fed3069..77bd977 100644 --- a/src/pages/lotto/Evolver.css +++ b/src/pages/lotto/Evolver.css @@ -186,6 +186,47 @@ font-size: 0.75rem; } +/* Backtest — TrackRecordCard */ +.backtest-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; + margin-bottom: 10px; +} +.backtest-table th { + text-align: left; + color: #94a3b8; + font-weight: 500; + padding: 6px 8px; + border-bottom: 1px solid rgba(255,255,255,0.08); +} +.backtest-table td { + padding: 6px 8px; + color: #cbd5e1; + border-bottom: 1px solid rgba(255,255,255,0.04); +} +.backtest-table tr:last-child td { border-bottom: none; } + +/* Backtest — shared note */ +.backtest-note { + margin: 8px 0 0; + color: #64748b; + font-size: 0.8rem; + line-height: 1.4; +} +.backtest-note strong { color: #cbd5e1; } + +/* Backtest — section divider */ +.backtest-section-header { + margin: 8px 0 4px; + color: #94a3b8; + font-size: 0.85rem; + font-weight: 600; + letter-spacing: 0.03em; + border-top: 1px solid rgba(255,255,255,0.06); + padding-top: 14px; +} + @media (max-width: 640px) { .trials-grid .grid { grid-template-columns: repeat(3, 1fr); height: auto; } .base-diff .diff-grid { grid-template-columns: repeat(3, 1fr); } diff --git a/src/pages/lotto/evolver/CalibrationChart.jsx b/src/pages/lotto/evolver/CalibrationChart.jsx new file mode 100644 index 0000000..52d79f2 --- /dev/null +++ b/src/pages/lotto/evolver/CalibrationChart.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, +} from 'recharts'; + +export default function CalibrationChart({ history }) { + if (!history || history.length === 0) { + return ( +
+

당첨조합 캘리브레이션 추세

+

캘리브레이션 데이터가 없습니다.

+
+ ); + } + + // history는 DESC 순서로 오므로 역순해서 오름차순 x축 + const data = [...history].reverse().map((h) => ({ + draw: h.draw_no, + score: h.score_total != null ? +h.score_total.toFixed(3) : null, + pct: h.percentile != null ? +h.percentile.toFixed(3) : null, + })); + + return ( +
+

당첨조합 캘리브레이션 추세 (최근 {history.length}회차)

+ + + + + + + + + + + +
+ ); +} diff --git a/src/pages/lotto/evolver/TrackRecordCard.jsx b/src/pages/lotto/evolver/TrackRecordCard.jsx new file mode 100644 index 0000000..82db1cc --- /dev/null +++ b/src/pages/lotto/evolver/TrackRecordCard.jsx @@ -0,0 +1,47 @@ +import React from 'react'; + +const STRATEGY_ORDER = ['engine_w', 'random_null', 'coverage']; +const STRATEGY_LABEL = { engine_w: '엔진', random_null: '무작위', coverage: '커버리지' }; + +export default function TrackRecordCard({ byStrategy }) { + if (!byStrategy) return null; + + const rows = STRATEGY_ORDER.filter((s) => byStrategy[s]); + if (rows.length === 0) return null; + + return ( +
+

누적 성적표

+ + + + + + + + + + + + + {rows.map((s) => { + const a = byStrategy[s]; + return ( + + + + + + + + + ); + })} + +
전략누적 장수회차수3등4등5등
{STRATEGY_LABEL[s] || s}{(a.n_tickets || 0).toLocaleString()}{a.draws || 0}{a['3rd'] || 0}{a['4th'] || 0}{a['5th'] || 0}
+

+ 엔진이 무작위를 넘지 못하면 분석에 통계적 우위가 없다는 정직한 증거입니다. +

+
+ ); +} diff --git a/src/pages/lotto/evolver/WinnerAnalysisCard.jsx b/src/pages/lotto/evolver/WinnerAnalysisCard.jsx new file mode 100644 index 0000000..c780f1a --- /dev/null +++ b/src/pages/lotto/evolver/WinnerAnalysisCard.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { + RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, + Radar, ResponsiveContainer, +} from 'recharts'; + +export default function WinnerAnalysisCard({ analysis }) { + if (!analysis) return null; + + const data = [ + { k: '빈도', v: analysis.score_frequency ?? 0 }, + { k: '지문', v: analysis.score_fingerprint ?? 0 }, + { k: '갭', v: analysis.score_gap ?? 0 }, + { k: '공동출현', v: analysis.score_cooccur ?? 0 }, + { k: '다양성', v: analysis.score_diversity ?? 0 }, + ]; + + const pct = analysis.percentile != null + ? `${(analysis.percentile * 100).toFixed(0)}%` + : '—'; + + return ( +
+

+ 이번 당첨조합 분석치 + 무작위 상위 {pct} +

+
+ + + + + + + + +
+

+ 종합 점수: {(analysis.score_total ?? 0).toFixed(3)} +

+
+ ); +} diff --git a/src/pages/lotto/tabs/EvolverTab.jsx b/src/pages/lotto/tabs/EvolverTab.jsx index 4699268..6d54418 100644 --- a/src/pages/lotto/tabs/EvolverTab.jsx +++ b/src/pages/lotto/tabs/EvolverTab.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import '../Evolver.css'; import { useEvolverApi } from '../evolver/useEvolverApi'; import WinnerCard from '../evolver/WinnerCard'; @@ -7,10 +7,40 @@ import BaseDiff from '../evolver/BaseDiff'; import BaseHistory from '../evolver/BaseHistory'; import LottoActivityTimeline from '../evolver/LottoActivityTimeline'; import EvolverActions from '../evolver/EvolverActions'; +import TrackRecordCard from '../evolver/TrackRecordCard'; +import CalibrationChart from '../evolver/CalibrationChart'; +import WinnerAnalysisCard from '../evolver/WinnerAnalysisCard'; +import { getLatest, lottoBacktestTrackRecord, lottoBacktestCalibration, lottoBacktestReview } from '../../../api'; export default function EvolverTab() { const { status, history, activity, loading, error, refetch } = useEvolverApi({ days: 7, weeks: 12 }); + const [trackRecord, setTrackRecord] = useState(null); + const [calibHistory, setCalibHistory] = useState([]); + const [winnerAnalysis, setWinnerAnalysis] = useState(null); + + useEffect(() => { + (async () => { + try { + const [tr, cal] = await Promise.all([ + lottoBacktestTrackRecord(), + lottoBacktestCalibration(52), + ]); + setTrackRecord(tr); + setCalibHistory(cal.history || []); + } catch (_) { /* 백엔드 미준비 시 graceful skip */ } + + try { + const latest = await getLatest(); + const drawNo = latest?.drawNo || latest?.drw_no || latest?.draw_no; + if (drawNo) { + const review = await lottoBacktestReview(drawNo); + setWinnerAnalysis(review.winner_analysis || null); + } + } catch (_) { /* 아직 데이터 없으면 null 유지 */ } + })(); + }, []); + if (loading) return

로딩 중...

; if (error) return

에러: {String(error)}

; @@ -73,6 +103,16 @@ export default function EvolverTab() { )} + + {/* 백테스트 성적표 · 캘리브레이션 · 당첨조합 분석 */} + {(winnerAnalysis || trackRecord || calibHistory.length > 0) && ( + <> +

백테스트 & 캘리브레이션

+ + + + + )} ); } From dacd01e6b98a202054ecb46787bcf875b23aef93 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 31 May 2026 18:13:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EB=A1=9C=EB=98=90=20=EB=B0=B1?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=83=AD=20UI=20=ED=8F=B4?= =?UTF-8?q?=EB=A6=AC=EC=8B=9C=20(1=C2=B72=EB=93=B1=20=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=C2=B7=EB=B9=88=20=EC=83=81=ED=83=9C=C2=B7=EC=B0=A8=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=95=EC=8A=A4=20CSS)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/pages/lotto/Evolver.css | 3 + src/pages/lotto/evolver/TrackRecordCard.jsx | 69 +++++++++++-------- .../lotto/evolver/WinnerAnalysisCard.jsx | 2 +- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/pages/lotto/Evolver.css b/src/pages/lotto/Evolver.css index 77bd977..2fec4ff 100644 --- a/src/pages/lotto/Evolver.css +++ b/src/pages/lotto/Evolver.css @@ -58,6 +58,9 @@ .winner-card .winner-meta strong { color: #f1f5f9; font-weight: 600; } .winner-card .winner-chart { background: rgba(0,0,0,0.15); border-radius: 8px; padding: 8px; } +/* Backtest — WinnerAnalysisCard chart wrapper (standalone, not inside .winner-card) */ +.backtest-winner-chart { background: rgba(0,0,0,0.15); border-radius: 8px; padding: 8px; } + /* TrialsGrid */ .trials-grid .grid { display: grid; grid-template-columns: repeat(6, 1fr); diff --git a/src/pages/lotto/evolver/TrackRecordCard.jsx b/src/pages/lotto/evolver/TrackRecordCard.jsx index 82db1cc..72600c7 100644 --- a/src/pages/lotto/evolver/TrackRecordCard.jsx +++ b/src/pages/lotto/evolver/TrackRecordCard.jsx @@ -7,41 +7,50 @@ export default function TrackRecordCard({ byStrategy }) { if (!byStrategy) return null; const rows = STRATEGY_ORDER.filter((s) => byStrategy[s]); - if (rows.length === 0) return null; return (

누적 성적표

- - - - - - - - - - - - - {rows.map((s) => { - const a = byStrategy[s]; - return ( - - - - - - - + {rows.length === 0 ? ( +

아직 백테스트 데이터가 없습니다.

+ ) : ( + <> +
전략누적 장수회차수3등4등5등
{STRATEGY_LABEL[s] || s}{(a.n_tickets || 0).toLocaleString()}{a.draws || 0}{a['3rd'] || 0}{a['4th'] || 0}{a['5th'] || 0}
+ + + + + + + + + + - ); - })} - -
전략누적 장수회차수1등2등3등4등5등
-

- 엔진이 무작위를 넘지 못하면 분석에 통계적 우위가 없다는 정직한 증거입니다. -

+ + + {rows.map((s) => { + const a = byStrategy[s]; + return ( + + {STRATEGY_LABEL[s] || s} + {(a.n_tickets || 0).toLocaleString()} + {a.draws || 0} + {a['1st'] || 0} + {a['2nd'] || 0} + {a['3rd'] || 0} + {a['4th'] || 0} + {a['5th'] || 0} + + ); + })} + + +

+ 엔진이 무작위를 넘지 못하면 분석에 통계적 우위가 없다는 정직한 증거입니다. +

+ + )}
); } diff --git a/src/pages/lotto/evolver/WinnerAnalysisCard.jsx b/src/pages/lotto/evolver/WinnerAnalysisCard.jsx index c780f1a..af860b3 100644 --- a/src/pages/lotto/evolver/WinnerAnalysisCard.jsx +++ b/src/pages/lotto/evolver/WinnerAnalysisCard.jsx @@ -25,7 +25,7 @@ export default function WinnerAnalysisCard({ analysis }) { 이번 당첨조합 분석치 무작위 상위 {pct} -
+