diff --git a/src/pages/lotto/evolver/BaseDiff.jsx b/src/pages/lotto/evolver/BaseDiff.jsx
new file mode 100644
index 0000000..bdd7589
--- /dev/null
+++ b/src/pages/lotto/evolver/BaseDiff.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+
+const METRIC_NAMES = ['freq', 'finger', 'gap', 'cooccur', 'divers'];
+
+function diffMarker(diff) {
+ if (Math.abs(diff) < 0.005) return { mark: '=', cls: 'eq' };
+ if (diff > 0) return diff < 0.05 ? { mark: '↑', cls: 'up' } : { mark: '↑↑', cls: 'up-big' };
+ return diff > -0.05 ? { mark: '↓', cls: 'down' } : { mark: '↓↓', cls: 'down-big' };
+}
+
+export default function BaseDiff({ previousBase, newBase, updateReason }) {
+ if (!previousBase || !newBase) {
+ return (
+
+
다음주 base 변경
+
아직 base 변경 이력 없음.
+
+ );
+ }
+ return (
+
+
다음주 base 변경 {updateReason && {updateReason}}
+
+ {METRIC_NAMES.map((name, i) => {
+ const prev = previousBase[i] || 0;
+ const next = newBase[i] || 0;
+ const diff = next - prev;
+ const { mark, cls } = diffMarker(diff);
+ return (
+
+
{name}
+
+ {prev.toFixed(2)} → {next.toFixed(2)}
+
+
+ {mark} {diff >= 0 ? '+' : ''}{(diff * 100).toFixed(0)}%p
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/src/pages/lotto/evolver/BaseHistory.jsx b/src/pages/lotto/evolver/BaseHistory.jsx
new file mode 100644
index 0000000..281cb6d
--- /dev/null
+++ b/src/pages/lotto/evolver/BaseHistory.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import {
+ LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
+} from 'recharts';
+
+const METRIC_NAMES = ['freq', 'finger', 'gap', 'cooccur', 'divers'];
+const COLORS = ['#34d399', '#60a5fa', '#fbbf24', '#f43f5e', '#c084fc'];
+
+export default function BaseHistory({ history }) {
+ if (!history || history.length === 0) {
+ return (
+
+
12주 Base 변화
+
학습 이력이 부족합니다.
+
+ );
+ }
+
+ const data = history
+ .slice()
+ .reverse()
+ .map(h => {
+ const w = h.weight || [0, 0, 0, 0, 0];
+ return {
+ date: (h.effective_from || '').slice(5),
+ freq: w[0], finger: w[1], gap: w[2], cooccur: w[3], divers: w[4],
+ reason: h.update_reason,
+ };
+ });
+
+ return (
+
+
Base 변화 (최근 {history.length}주)
+
+
+
+
+
+
+
+ {METRIC_NAMES.map((name, i) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/pages/lotto/evolver/TrialsGrid.jsx b/src/pages/lotto/evolver/TrialsGrid.jsx
new file mode 100644
index 0000000..5b7280e
--- /dev/null
+++ b/src/pages/lotto/evolver/TrialsGrid.jsx
@@ -0,0 +1,56 @@
+import React, { useState } from 'react';
+
+const DAY_NAMES = ['월', '화', '수', '목', '금', '토'];
+
+export default function TrialsGrid({ trials, perDay, winnerTrialId }) {
+ const [expanded, setExpanded] = useState(null);
+
+ const byDow = {};
+ for (const t of trials || []) byDow[t.day_of_week] = t;
+
+ const perDayByDow = {};
+ for (const d of perDay || []) perDayByDow[d.day_of_week] = d;
+
+ const maxScore = Math.max(...(perDay || []).map(d => d.avg_score || 0), 0.001);
+
+ return (
+
+
이번주 6일 Trials
+
+ {DAY_NAMES.map((name, dow) => {
+ const trial = byDow[dow];
+ const day = perDayByDow[dow];
+ const isWinner = trial && trial.id === winnerTrialId;
+ const heightPct = day ? (day.avg_score / maxScore) * 100 : 0;
+ return (
+
+ );
+ })}
+
+ {expanded !== null && byDow[expanded] && (
+
+
{DAY_NAMES[expanded]}요일 상세
+
W = [{(byDow[expanded].weight || []).map(w => w.toFixed(2)).join(', ')}]
+
+ {(byDow[expanded].picks || []).map(p => (
+ -
+ {(p.numbers || []).join(', ')} —
+ score {(p.meta_score || 0).toFixed(3)}
+ {p.correct != null && ` · 적중 ${p.correct}개`}
+
+ ))}
+
+
+ )}
+
+ );
+}