From ef392f02ed2d6f97d84df34b9a0a3a9e3a87b801 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 23 May 2026 02:38:33 +0900 Subject: [PATCH] =?UTF-8?q?refactor(evolver):=20Lotto=20=ED=83=AD=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9=20+=20=EB=8B=A4=ED=81=AC=20?= =?UTF-8?q?=ED=85=8C=EB=A7=88=20+=20activity=20=EC=8A=A4=ED=81=AC=EB=A1=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EvolverTab.jsx 신규 생성: evolver 컴포넌트를 탭 body로 추출 - Evolver.jsx → Lotto 페이지 thin wrapper로 교체 (/lotto/evolver URL 유지) - Lotto.jsx: useLocation으로 pathname 감지 → initialTab 결정 - Functions.jsx: 4번째 탭 '🧬 자율 학습' 추가 + initialTab prop 수용 - Evolver.css: light → dark 테마 전환 (rgba/slate 팔레트), activity-list max-height+scroll 적용 Co-Authored-By: Claude Sonnet 4.6 --- src/pages/lotto/Evolver.css | 219 +++++++++++++++++++++++----- src/pages/lotto/Evolver.jsx | 81 +--------- src/pages/lotto/Functions.jsx | 20 ++- src/pages/lotto/Lotto.jsx | 6 +- src/pages/lotto/tabs/EvolverTab.jsx | 78 ++++++++++ 5 files changed, 281 insertions(+), 123 deletions(-) create mode 100644 src/pages/lotto/tabs/EvolverTab.jsx diff --git a/src/pages/lotto/Evolver.css b/src/pages/lotto/Evolver.css index f370eee..fed3069 100644 --- a/src/pages/lotto/Evolver.css +++ b/src/pages/lotto/Evolver.css @@ -1,51 +1,194 @@ -.evolver { max-width: 1100px; margin: 0 auto; padding: 24px 16px; } -.evolver-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 24px; gap: 16px; } -.evolver-kicker { letter-spacing: 0.12em; color: #6b7280; font-size: 0.75rem; margin: 0 0 4px; } -.evolver-header h1 { margin: 0 0 8px; font-size: 2rem; } -.evolver-sub { color: #6b7280; margin: 0; } -.refresh-btn { padding: 8px 14px; background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 6px; cursor: pointer; } +/* Evolver tab — dark theme matching Lotto.css patterns */ -.evolver-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px; } -.evolver-card.empty .muted { color: #9ca3af; } -.evolver-card h2 { margin: 0 0 12px; font-size: 1.1rem; display: flex; justify-content: space-between; align-items: center; gap: 8px; } -.evolver-card .badge { background: #ecfdf5; color: #065f46; padding: 2px 8px; border-radius: 4px; font-size: 0.7rem; font-weight: normal; } +.lotto-evolver { display: flex; flex-direction: column; gap: 16px; } +.lotto-evolver-muted { color: #94a3b8; } -.winner-card .winner-meta { display: flex; gap: 16px; flex-wrap: wrap; color: #6b7280; font-size: 0.9rem; margin-bottom: 12px; } -.winner-card .winner-meta strong { color: #111827; } +.lotto-evolver-intro { + display: flex; justify-content: space-between; align-items: center; + gap: 12px; flex-wrap: wrap; +} +.lotto-evolver-sub { margin: 0; color: #94a3b8; font-size: 0.9rem; flex: 1; } +.lotto-evolver-refresh { + padding: 6px 12px; + background: rgba(255,255,255,0.06); + border: 1px solid rgba(255,255,255,0.1); + border-radius: 6px; + color: #cbd5e1; + cursor: pointer; + font-size: 0.85rem; +} +.lotto-evolver-refresh:hover { background: rgba(255,255,255,0.1); } -.trials-grid .grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; height: 140px; align-items: end; } -.trial-cell { border: none; background: #f9fafb; border-radius: 6px; padding: 8px 4px; display: flex; flex-direction: column; align-items: center; justify-content: end; cursor: pointer; height: 100%; } -.trial-cell.winner { background: #ecfdf5; } -.trial-cell .bar { width: 80%; background: #34d399; border-radius: 3px 3px 0 0; min-height: 4px; } -.trial-cell.winner .bar { background: #059669; } -.trial-cell .label { font-size: 0.85rem; margin-top: 6px; } -.trial-cell .max-correct { font-size: 0.7rem; color: #6b7280; } -.trial-detail { margin-top: 16px; padding: 12px; background: #f9fafb; border-radius: 6px; } +/* Generic card */ +.evolver-card { + background: rgba(255,255,255,0.04); + border: 1px solid rgba(255,255,255,0.08); + border-radius: 12px; + padding: 18px 20px; + color: #e2e8f0; +} +.evolver-card h2 { + margin: 0 0 12px; + font-size: 1rem; + font-weight: 600; + color: #f1f5f9; + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; +} +.evolver-card .badge { + background: rgba(52,211,153,0.15); + color: #34d399; + padding: 2px 10px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 500; +} +.evolver-card.empty .muted, .evolver-card .muted { color: #64748b; } + +.lotto-evolver-empty h3 { margin: 0 0 6px; color: #f1f5f9; } +.lotto-evolver-empty p { color: #94a3b8; margin: 0 0 12px; } + +/* WinnerCard */ +.winner-card .winner-meta { + display: flex; gap: 16px; flex-wrap: wrap; + color: #94a3b8; font-size: 0.85rem; margin-bottom: 14px; +} +.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; } + +/* TrialsGrid */ +.trials-grid .grid { + display: grid; grid-template-columns: repeat(6, 1fr); + gap: 8px; height: 140px; align-items: end; +} +.trial-cell { + border: 1px solid rgba(255,255,255,0.06); + background: rgba(255,255,255,0.03); + border-radius: 6px; + padding: 8px 4px; + display: flex; flex-direction: column; + align-items: center; justify-content: end; + cursor: pointer; + height: 100%; + color: #cbd5e1; + transition: background 0.15s; +} +.trial-cell:hover { background: rgba(255,255,255,0.06); } +.trial-cell.winner { background: rgba(52,211,153,0.12); border-color: rgba(52,211,153,0.3); } +.trial-cell .bar { + width: 80%; + background: #475569; + border-radius: 3px 3px 0 0; + min-height: 4px; +} +.trial-cell.winner .bar { background: #34d399; } +.trial-cell .label { font-size: 0.85rem; margin-top: 6px; color: #e2e8f0; } +.trial-cell .max-correct { font-size: 0.7rem; color: #94a3b8; } +.trial-detail { + margin-top: 14px; padding: 12px; + background: rgba(0,0,0,0.15); + border-radius: 6px; + color: #cbd5e1; + font-size: 0.85rem; +} +.trial-detail h3 { margin: 0 0 8px; font-size: 0.9rem; color: #f1f5f9; } .trial-detail ul { margin: 8px 0 0; padding-left: 18px; } +.trial-detail li { margin-bottom: 4px; } -.base-diff .diff-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; } -.metric-card { padding: 12px; background: #f9fafb; border-radius: 8px; text-align: center; } -.metric-card .metric-name { color: #6b7280; font-size: 0.75rem; text-transform: uppercase; } -.metric-card .metric-values { margin: 6px 0; font-size: 0.85rem; } -.metric-card .metric-diff { font-weight: bold; } -.metric-card.up .metric-diff, .metric-card.up-big .metric-diff { color: #059669; } -.metric-card.down .metric-diff, .metric-card.down-big .metric-diff { color: #dc2626; } -.metric-card.eq .metric-diff { color: #9ca3af; } +/* BaseDiff */ +.base-diff .diff-grid { + display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; +} +.metric-card { + padding: 12px 8px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + border-radius: 8px; + text-align: center; + color: #cbd5e1; +} +.metric-card .metric-name { + color: #94a3b8; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.metric-card .metric-values { margin: 6px 0; font-size: 0.8rem; } +.metric-card .metric-values strong { color: #f1f5f9; } +.metric-card .metric-diff { font-weight: 600; font-size: 0.8rem; } +.metric-card.up .metric-diff, .metric-card.up-big .metric-diff { color: #34d399; } +.metric-card.down .metric-diff, .metric-card.down-big .metric-diff { color: #f87171; } +.metric-card.eq .metric-diff { color: #64748b; } -.activity-card .activity-list { list-style: none; padding: 0; margin: 0; } -.activity-item { display: grid; grid-template-columns: 24px 1fr auto; gap: 8px; padding: 8px 0; border-bottom: 1px solid #f3f4f6; } -.activity-item .ts { color: #9ca3af; font-size: 0.75rem; white-space: nowrap; } -.activity-item .status.ok { color: #059669; } -.activity-item .status.err { color: #dc2626; } -.activity-item .detail { color: #6b7280; font-size: 0.85rem; } +/* BaseHistory chart container */ +.base-history { background: rgba(255,255,255,0.04); } +/* ActivityCard — scrollable */ +.activity-card .activity-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 420px; + overflow-y: auto; + overscroll-behavior: contain; +} +.activity-card .activity-list::-webkit-scrollbar { width: 6px; } +.activity-card .activity-list::-webkit-scrollbar-thumb { + background: rgba(255,255,255,0.15); border-radius: 3px; +} +.activity-item { + display: grid; + grid-template-columns: 24px 1fr auto; + gap: 10px; + padding: 10px 4px; + border-bottom: 1px solid rgba(255,255,255,0.05); + color: #cbd5e1; + font-size: 0.85rem; +} +.activity-item:last-child { border-bottom: none; } +.activity-item .icon { font-size: 1rem; text-align: center; } +.activity-item .body .line { color: #e2e8f0; } +.activity-item .body strong { color: #f1f5f9; } +.activity-item .ts { + color: #64748b; + font-size: 0.75rem; + white-space: nowrap; + align-self: center; +} +.activity-item .status.ok { color: #34d399; } +.activity-item .status.err { color: #f87171; } +.activity-item .status.pending { color: #fbbf24; } +.activity-item .detail { color: #94a3b8; font-size: 0.78rem; margin-top: 2px; } + +/* EvolverActions */ .actions-card .action-buttons { display: flex; gap: 8px; flex-wrap: wrap; } -.actions-card button { padding: 8px 14px; background: #1f2937; color: #fff; border: none; border-radius: 6px; cursor: pointer; } +.actions-card button { + padding: 8px 14px; + background: rgba(52,211,153,0.15); + color: #34d399; + border: 1px solid rgba(52,211,153,0.3); + border-radius: 6px; + cursor: pointer; + font-size: 0.85rem; +} +.actions-card button:hover:not(:disabled) { background: rgba(52,211,153,0.25); } .actions-card button:disabled { opacity: 0.5; cursor: wait; } -.action-output { background: #1f2937; color: #d1d5db; padding: 12px; border-radius: 6px; margin-top: 12px; max-height: 200px; overflow: auto; font-size: 0.8rem; } +.action-output { + background: rgba(0,0,0,0.3); + color: #94a3b8; + padding: 12px; + border-radius: 6px; + margin-top: 12px; + max-height: 200px; + overflow: auto; + font-size: 0.75rem; +} @media (max-width: 640px) { .trials-grid .grid { grid-template-columns: repeat(3, 1fr); height: auto; } - .base-diff .diff-grid { grid-template-columns: repeat(2, 1fr); } - .evolver-header { flex-direction: column; } + .base-diff .diff-grid { grid-template-columns: repeat(3, 1fr); } + .lotto-evolver-intro { flex-direction: column; align-items: stretch; } + .activity-card .activity-list { max-height: 360px; } } diff --git a/src/pages/lotto/Evolver.jsx b/src/pages/lotto/Evolver.jsx index 47ca8ba..a3125eb 100644 --- a/src/pages/lotto/Evolver.jsx +++ b/src/pages/lotto/Evolver.jsx @@ -1,82 +1,7 @@ import React from 'react'; -import './Evolver.css'; -import { useEvolverApi } from './evolver/useEvolverApi'; -import WinnerCard from './evolver/WinnerCard'; -import TrialsGrid from './evolver/TrialsGrid'; -import BaseDiff from './evolver/BaseDiff'; -import BaseHistory from './evolver/BaseHistory'; -import LottoActivityTimeline from './evolver/LottoActivityTimeline'; -import EvolverActions from './evolver/EvolverActions'; +import Lotto from './Lotto'; +// /lotto/evolver URL → Lotto 페이지가 useLocation으로 활성 탭 자동 선택 export default function Evolver() { - const { status, history, activity, loading, error, refetch } = useEvolverApi({ days: 7, weeks: 12 }); - - if (loading) return

로딩 중...

; - if (error) return

에러: {String(error)}

; - - const latestBase = (history.items || [])[0]; - const previousBase = (history.items || [])[1]?.weight || status?.current_base || [0.2, 0.2, 0.2, 0.2, 0.2]; - const newBase = latestBase?.weight || status?.current_base; - - const trials = status?.trials || []; - const winnerTrialId = latestBase?.source_trial_id; - const winnerTrial = trials.find(t => t.id === winnerTrialId); - const winnerInfo = winnerTrial ? { - day_of_week: winnerTrial.day_of_week, - weight: winnerTrial.weight, - avg_score: latestBase?.winner_score, - max_correct: latestBase?.winner_max_correct, - n_picks: (winnerTrial.picks || []).length, - } : null; - - const perDay = trials.map(t => ({ - day_of_week: t.day_of_week, - trial_id: t.id, - avg_score: (t.picks || []).reduce((s, p) => s + (p.meta_score || 0), 0) / Math.max(1, (t.picks || []).length), - max_correct: Math.max(0, ...(t.picks || []).map(p => p.correct || 0)), - })); - - const hasBase = (history.items || []).length > 0; - - return ( -
-
-
-

Lotto · Weight Evolver

-

자율 학습 루프

-

- 매주 6가지 가중치를 시도해서 best 조합을 다음주 base로 학습합니다. - {status?.latest_draw && ` 마지막 회차: ${status.latest_draw}회.`} -

-
- -
- - {!hasBase ? ( -
-

아직 학습 시작 전

-

다음 월요일 09:00에 자동 시작 또는 수동 트리거 사용.

- -
- ) : ( - <> - - - - - - - - )} -
- ); + return ; } diff --git a/src/pages/lotto/Functions.jsx b/src/pages/lotto/Functions.jsx index 53560d2..c6d20a4 100644 --- a/src/pages/lotto/Functions.jsx +++ b/src/pages/lotto/Functions.jsx @@ -2,6 +2,7 @@ import { useCallback, useState } from 'react'; import BriefingTab from './tabs/BriefingTab'; import AnalysisTab from './tabs/AnalysisTab'; import PurchaseTab from './tabs/PurchaseTab'; +import EvolverTab from './tabs/EvolverTab'; import { useIsMobile } from '../../hooks/useIsMobile'; import SwipeableView from '../../components/SwipeableView'; @@ -9,10 +10,19 @@ const TABS = [ { id: 'briefing', label: '🗓 이번 주 브리핑' }, { id: 'analysis', label: '📚 자료실 / Deep Dive' }, { id: 'purchase', label: '💰 구매·성과' }, + { id: 'evolver', label: '🧬 자율 학습' }, ]; -export default function Functions() { - const [tab, setTab] = useState('briefing'); +function renderTab(id) { + if (id === 'briefing') return ; + if (id === 'analysis') return ; + if (id === 'purchase') return ; + if (id === 'evolver') return ; + return null; +} + +export default function Functions({ initialTab = 'briefing' }) { + const [tab, setTab] = useState(initialTab); const isMobile = useIsMobile(); const tabIndex = TABS.findIndex(t => t.id === tab); @@ -28,7 +38,7 @@ export default function Functions() { tabs={TABS.map(t => ({ key: t.id, label: t.label, - content: t.id === 'briefing' ? : t.id === 'analysis' ? : , + content: renderTab(t.id), }))} activeIndex={tabIndex} onTabChange={handleTabChange} @@ -45,9 +55,7 @@ export default function Functions() { ))}
- {tab === 'briefing' && } - {tab === 'analysis' && } - {tab === 'purchase' && } + {renderTab(tab)}
)} diff --git a/src/pages/lotto/Lotto.jsx b/src/pages/lotto/Lotto.jsx index 9959c1d..e0c2b40 100644 --- a/src/pages/lotto/Lotto.jsx +++ b/src/pages/lotto/Lotto.jsx @@ -1,8 +1,12 @@ import React from 'react'; +import { useLocation } from 'react-router-dom'; import Functions from './Functions'; import './Lotto.css'; const Lotto = () => { + const location = useLocation(); + const initialTab = location.pathname.endsWith('/evolver') ? 'evolver' : 'briefing'; + return (
@@ -24,7 +28,7 @@ const Lotto = () => {
- + ); }; diff --git a/src/pages/lotto/tabs/EvolverTab.jsx b/src/pages/lotto/tabs/EvolverTab.jsx new file mode 100644 index 0000000..4699268 --- /dev/null +++ b/src/pages/lotto/tabs/EvolverTab.jsx @@ -0,0 +1,78 @@ +import React from 'react'; +import '../Evolver.css'; +import { useEvolverApi } from '../evolver/useEvolverApi'; +import WinnerCard from '../evolver/WinnerCard'; +import TrialsGrid from '../evolver/TrialsGrid'; +import BaseDiff from '../evolver/BaseDiff'; +import BaseHistory from '../evolver/BaseHistory'; +import LottoActivityTimeline from '../evolver/LottoActivityTimeline'; +import EvolverActions from '../evolver/EvolverActions'; + +export default function EvolverTab() { + const { status, history, activity, loading, error, refetch } = useEvolverApi({ days: 7, weeks: 12 }); + + if (loading) return

로딩 중...

; + if (error) return

에러: {String(error)}

; + + const latestBase = (history.items || [])[0]; + const previousBase = (history.items || [])[1]?.weight || status?.current_base || [0.2, 0.2, 0.2, 0.2, 0.2]; + const newBase = latestBase?.weight || status?.current_base; + + const trials = status?.trials || []; + const winnerTrialId = latestBase?.source_trial_id; + const winnerTrial = trials.find(t => t.id === winnerTrialId); + const winnerInfo = winnerTrial ? { + day_of_week: winnerTrial.day_of_week, + weight: winnerTrial.weight, + avg_score: latestBase?.winner_score, + max_correct: latestBase?.winner_max_correct, + n_picks: (winnerTrial.picks || []).length, + } : null; + + const perDay = trials.map(t => ({ + day_of_week: t.day_of_week, + trial_id: t.id, + avg_score: (t.picks || []).reduce((s, p) => s + (p.meta_score || 0), 0) / Math.max(1, (t.picks || []).length), + max_correct: Math.max(0, ...(t.picks || []).map(p => p.correct || 0)), + })); + + const hasBase = (history.items || []).length > 0; + + return ( +
+
+

+ 매주 6가지 가중치를 시도해서 best 조합을 다음주 base로 학습합니다. + {status?.latest_draw && ` 마지막 회차: ${status.latest_draw}회.`} +

+ +
+ + {!hasBase ? ( +
+

아직 학습 시작 전

+

다음 월요일 09:00에 자동 시작 또는 수동 트리거 사용.

+ +
+ ) : ( + <> + + + + + + + + )} +
+ ); +}