From ea3485cde69444d3c3518684d5c157953f7b158d Mon Sep 17 00:00:00 2001 From: gahusb Date: Sat, 23 May 2026 01:31:56 +0900 Subject: [PATCH] =?UTF-8?q?docs(spec):=20Lotto=20Evolver=20UI=20+=20?= =?UTF-8?q?=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8=20=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EA=B0=80=EC=8B=9C=ED=99=94=20(v2.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: v2 텔레그램 메시지의 /lotto/evolver 링크가 404 → 페이지 신설. + LottoAgent 활동(signal/digest/evolution/curate)이 agent_tasks에 누락된 거 보강. 모든 활동을 한 timeline에서 추적 가능. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-23-lotto-evolver-ui-design.md | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-23-lotto-evolver-ui-design.md diff --git a/docs/superpowers/specs/2026-05-23-lotto-evolver-ui-design.md b/docs/superpowers/specs/2026-05-23-lotto-evolver-ui-design.md new file mode 100644 index 0000000..b974d5b --- /dev/null +++ b/docs/superpowers/specs/2026-05-23-lotto-evolver-ui-design.md @@ -0,0 +1,368 @@ +# Lotto Evolver UI + 에이전트 활동 가시화 설계 (v2.1) + +- **상태**: Draft (사용자 리뷰 대기) +- **작성일**: 2026-05-23 +- **대상 저장소**: + - `web-ui` (프론트엔드) — `/lotto/evolver` 페이지 신설 + 공용 활동 컴포넌트 + - `web-backend` agent-office — LottoAgent task_id 도입 + sync_evolver_activity cron +- **선행 작업**: v2 Lotto Weight Evolver (2026-05-22 배포, 운영 중) +- **목표**: 토요일 22:15 텔레그램 리포트의 "[웹에서 차트 보기]" 링크가 가리키는 페이지 구축 + 로또 에이전트의 모든 활동(시그널·digest·큐레이션·evolver)을 한 곳에서 추적 가능하게. + +--- + +## 1. 문제 정의 + +v2 텔레그램 메시지가 `https://gahusb.synology.me/lotto/evolver` 링크를 포함하지만 web-ui repo에 해당 라우트가 없음 → React Router catch-all 404. spec section 13에서 "프론트 UI는 별도 PR"로 명시했지만 링크는 미리 박혀있음 → UX 깨짐. + +또한 LottoAgent의 활동(signals / digest / weekly_evolution_report / curate)이 agent_office.db의 `agent_logs`에는 기록되지만 `agent_tasks` 테이블에는 **`curate_weekly`만** 들어감 → agent-office UI에서 "Tasks" 섹션 봤을 때 활동 이력이 누락. lotto-lab의 weight_evolver cron(매일 apply / 월 generate / 토 evaluate)은 lotto.db에만 기록 → agent_office에서 완전히 안 보임. + +사용자 의도: "로또 에이전트가 무엇을 했는지" 한 곳에서 확인 가능하게. + +## 2. 의사결정 요약 + +| 결정 사항 | 선택 | 비고 | +|---|---|---| +| 라우트 위치 | 별도 `/lotto/evolver` (텔레그램 링크와 일치) | `/stock/trade`, `/stock/screener` 패턴 따름 | +| 사용 시나리오 | 토 22:15 텔레그램 직후 주간 요약 대시보드 | 평일 운영·장기 분석은 부차 | +| 페이지 구조 | 단일 스크롤, 5개 카드 (Header / Winner / TrialsGrid / BaseDiff / BaseHistory / Actions) | sub-tab 불필요 | +| 차트 | Recharts (이미 dep) — Radar / Bar / Line + 인라인 metric-card | small multiples 대신 텍스트 강조 | +| 활동 노출 위치 | `/lotto/evolver` + `/agent-office` 양쪽 (공용 컴포넌트) | DRY | +| 백엔드 보강 | 기존 add_log만 있던 LottoAgent 메서드에 task_id 도입 + 신규 sync_evolver_activity cron | 멱등 guard 포함 | + +## 3. 아키텍처 + +### 3.1 컴포넌트 다이어그램 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ web-ui (신규 컴포넌트) │ +│ │ +│ src/pages/lotto/ │ +│ Evolver.jsx ← /lotto/evolver 진입점 │ +│ Evolver.css │ +│ evolver/ │ +│ WinnerCard.jsx ← Radar (5축) + 메타 │ +│ TrialsGrid.jsx ← 6일 Bar 비교 + 펼치기 │ +│ BaseDiff.jsx ← 5 metric-card (텍스트+arrow)│ +│ BaseHistory.jsx ← LineChart 12주 시계열 │ +│ EvolverActions.jsx ← 수동 트리거 (dev) │ +│ useEvolverApi.js ← status+history+activity hook│ +│ │ +│ src/components/lotto/ │ +│ LottoActivityTimeline.jsx ← 공용 활동 timeline │ +│ /lotto/evolver + /agent-office│ +└─────────────────────────────────────────────────────────────┘ + ↓ (HTTP) +┌─────────────────────────────────────────────────────────────┐ +│ web-backend (보강) │ +│ │ +│ agent-office/app/agents/lotto.py │ +│ • run_signal_check → task_id 도입 (신규) │ +│ • run_daily_digest → task_id 도입 (신규) │ +│ • run_weekly_evolution_report → task_id 도입 (신규) │ +│ • sync_evolver_activity → 신규 메서드 │ +│ │ +│ agent-office/app/scheduler.py │ +│ • lotto_evolver_activity_sync — 매일 09:30 cron 신규 │ +│ │ +│ agent-office/app/db.py │ +│ • get_tasks_by_agent_date_kind — 멱등 guard helper 신규 │ +│ │ +│ agent-office/app/main.py │ +│ • GET /agents/{id}/tasks에 task_type 필터 추가 (확장) │ +│ │ +│ lotto-lab: 변경 없음 (web-ui가 evolver API 직접 소비) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 3.2 책임 경계 + +- **web-ui Evolver 페이지**: 데이터 시각화 전담. 비즈니스 로직 없음. fetch는 useEvolverApi에 집중. +- **LottoActivityTimeline**: 시간순 timeline 표현만. logs/tasks/evolverEvents 3종 입력 받아 merge sort + 렌더. +- **LottoAgent**: 모든 자율 작업 시 task row 생성 (다른 에이전트와 동일 패턴). +- **sync_evolver_activity**: lotto-lab의 결과를 agent_office.db에 거울 비추기. 백엔드 polling 패턴. 멱등. +- **lotto-lab**: 변경 없음. 모든 evolver API는 web-ui가 직접 호출. + +## 4. 페이지 정보 layout + +``` +┌─────────────────────────────────────────────────────────────┐ +│ HEADER │ +│ Lotto · Weight Evolver │ +│ "스스로 가중치를 조절하는 자율 학습 루프" │ +│ 마지막 회고: 1225회 (2026-05-21 22:00) │ +├─────────────────────────────────────────────────────────────┤ +│ ① WinnerCard (대형, 메인) │ +│ 🏆 목요일 · W_4 · max=4개 일치 │ +│ ┌─ Radar Chart (5축) ──┐ │ +│ │ freq, finger, gap, │ │ +│ │ cooccur, divers │ │ +│ └──────────────────────┘ │ +│ avg_score · n_picks graded · update reason │ +├─────────────────────────────────────────────────────────────┤ +│ ② TrialsGrid │ +│ 월 화 수 목⭐ 금 토 (가로 6개 Bar) │ +│ ░░ ▓▓ ░░ ██ ▒▒ ░░ │ +│ max=2 1 3 4 2 1 │ +│ 클릭 → 그날 5세트 numbers + scores 펼침 │ +├─────────────────────────────────────────────────────────────┤ +│ ③ BaseDiff │ +│ 5개 metric-card 가로 정렬 │ +│ freq 0.20 → 0.18 ↓ -10% │ +│ finger 0.20 → 0.32 ↑↑ +60% │ +│ gap 0.20 → 0.20 = (변화 없음) │ +│ cooccur 0.20 → 0.22 ↑ +10% │ +│ divers 0.20 → 0.08 ↓↓ -60% │ +│ → reason: winner_4plus │ +├─────────────────────────────────────────────────────────────┤ +│ ④ BaseHistory (12주) │ +│ LineChart 5 라인 (freq/finger/gap/cooccur/divers) │ +│ X축: effective_from, Y축: weight 0~1 │ +│ dot click → reason tooltip + 회차 표시 │ +├─────────────────────────────────────────────────────────────┤ +│ ⑤ LottoActivityTimeline (compact=false) │ +│ 최근 7일 — task + log + lotto-lab evolver 이벤트 merge │ +│ 2026-05-23 22:15 🧬 weekly_evolution_report succeeded │ +│ 2026-05-23 22:00 ⚖️ weight_evolver_eval (lotto-lab) │ +│ 2026-05-23 21:15 🔍 deep_check succeeded │ +│ ... │ +├─────────────────────────────────────────────────────────────┤ +│ ⑥ EvolverActions (개발자 모드) │ +│ [수동 generate-now] [수동 evaluate-now] │ +│ 응답 JSON 콘솔에 표시 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 4.1 모바일 반응형 + +- ≤640px: 1 컬럼, 차트는 가로폭 100% +- 641-1024px: WinnerCard·TrialsGrid 가로 분할 (50/50) +- ≥1025px: 위 layout 그대로 + +## 5. 데이터 흐름 + +### 5.1 useEvolverApi hook + +```js +function useEvolverApi({ days = 7, weeks = 12 } = {}) { + // 4개 fetch 동시 — Promise.all + // 1. GET /api/lotto/evolver/status → status + // 2. GET /api/lotto/evolver/history?weeks=12 → history + // 3. GET /api/agent-office/agents/lotto/logs?days=7 → logs + // 4. GET /api/agent-office/agents/lotto/tasks?days=7 → tasks + // + // activity = merge(logs, tasks, evolverEventsFromHistory) sorted by timestamp DESC + return { status, history, activity, loading, error, refetch }; +} +``` + +`activity` 합성 규칙: +- agent_logs의 created_at + level + message + task_id +- agent_tasks의 created_at + task_type + status + result_data +- history.items의 created_at + update_reason + weight (evolver eval 자체 이벤트로 별도 표시) +- 클라이언트에서 timestamp DESC sort → React에서 렌더링 + +### 5.2 Recharts 매핑 + +| 컴포넌트 | 차트 | data prop | +|---|---|---| +| WinnerCard | `RadarChart` | `[{metric, value, previous}]` 5점 (overlay: previous_base) | +| TrialsGrid | `BarChart` 수평 6개 | `[{day_name, avg_score, max_correct, is_winner}]` | +| BaseHistory | `LineChart` | `[{effective_from, freq, finger, gap, cooccur, divers}, ...]` | + +### 5.3 LottoActivityTimeline + +```jsx + +``` + +merge & sort: +```js +const stream = [ + ...logs.map(l => ({ ts: l.created_at, kind: 'log', payload: l })), + ...tasks.map(t => ({ ts: t.created_at, kind: 'task', payload: t })), + ...evolverEvents.map(e => ({ ts: e.created_at, kind: 'evolver', payload: e })), +].sort((a, b) => b.ts.localeCompare(a.ts)); +``` + +각 stream item: +- kind='task': 아이콘 + task_type label + status badge + (completed_at - created_at) 소요시간 +- kind='log': 아이콘(level) + message +- kind='evolver': ⚖️ + update_reason + winner_score + +icon · color mapping (task_type 기준): +``` +curate_weekly 📋 blue +signal_check 🔍 green / fired면 amber +daily_digest 📊 cyan +weekly_evolution_report 🧬 purple +evolver_generate 🌱 teal +evolver_apply 🎲 gray +``` + +### 5.4 cold start / empty state + +- `weight_base_history` empty → 큰 빈 카드: "아직 학습 시작 전. 다음 월요일 09:00 자동 시작" + `[수동 generate-now 트리거]` 버튼 +- `trials` empty (월 09:00 전) → 안내 카드 +- `activity` empty → 회색 "최근 활동 없음" + +## 6. 백엔드 보강 + +### 6.1 LottoAgent 메서드 — task_id 도입 + +3개 메서드에 `_run` 패턴(`create_task` + try/except + `update_task_status` + `add_log(..., task_id=...)`) 적용: + +| 메서드 | 새 task_type | result_data 핵심 | +|---|---|---| +| `run_signal_check(source)` | `signal_check` | source, overall_fire, n_results, fired_metrics | +| `run_daily_digest()` | `daily_digest` | evaluated, fired, signals_count | +| `run_weekly_evolution_report()` | `weekly_evolution_report` | draw_no, update_reason, winner_day | + +기존 `_run`(`curate_weekly`)은 그대로. + +### 6.2 sync_evolver_activity — 신규 메서드 + +매일 09:30 cron. lotto-lab의 today_trial 가져와 agent_office.db에 task+log 기록. 멱등 guard. + +```python +async def sync_evolver_activity(self): + """lotto-lab evolver 상태 polling → agent_office.db에 거울. 멱등.""" + today_iso = _today_kst_iso() + dow = _today_dow() + + status = await service_proxy.lotto_evolver_status() + + # 오늘 trial + picks → evolver_apply task + today_trial = next((t for t in status["trials"] if t["day_of_week"] == dow), None) + if today_trial and today_trial.get("picks") and not db.get_tasks_by_agent_date_kind("lotto", today_iso, "evolver_apply"): + tid = db.create_task("lotto", "evolver_apply", { + "date": today_iso, "trial_id": today_trial["id"], + "day_of_week": dow, "weight": today_trial["weight"], + }) + db.update_task_status(tid, "succeeded", result_data={ + "n_picks": len(today_trial["picks"]), + "meta_scores": [p["meta_score"] for p in today_trial["picks"]], + }) + db.add_log("lotto", f"evolver_apply: 오늘 W로 {len(today_trial['picks'])}세트 추출", task_id=tid) + + # 월요일 + 6 trials 완성 → evolver_generate task + if dow == 0 and len(status["trials"]) == 6 and not db.get_tasks_by_agent_date_kind("lotto", today_iso, "evolver_generate"): + tid = db.create_task("lotto", "evolver_generate", {"week_start": status["week_start"]}) + db.update_task_status(tid, "succeeded", result_data={"trials_count": 6}) + db.add_log("lotto", f"evolver_generate: {status['week_start']} 주의 6 trials 생성", task_id=tid) +``` + +토요일 22:15 evaluate는 `run_weekly_evolution_report`가 이미 task 기록 → sync 불필요. + +### 6.3 db.py — 신규 helper + +```python +def get_tasks_by_agent_date_kind(agent_id: str, date_iso: str, task_type: str) -> List[Dict[str, Any]]: + """같은 (agent, date, task_type)으로 이미 생성된 task 조회 — 멱등 guard.""" + with _conn() as conn: + rows = conn.execute( + """ + SELECT * FROM agent_tasks + WHERE agent_id = ? AND task_type = ? + AND substr(created_at, 1, 10) = ? + ORDER BY created_at DESC + """, + (agent_id, task_type, date_iso), + ).fetchall() + return [dict(r) for r in rows] +``` + +### 6.4 scheduler.py — cron 추가 + +```python +async def _run_lotto_sync_evolver_activity(): + agent = AGENT_REGISTRY.get("lotto") + if agent: + await agent.sync_evolver_activity() + +scheduler.add_job( + _run_lotto_sync_evolver_activity, + "cron", hour=9, minute=30, + id="lotto_evolver_activity_sync", +) +``` + +### 6.5 main.py — API 확장 + +`GET /api/agent-office/agents/{id}/tasks`에 query param 추가: +```python +@app.get("/api/agent-office/agents/{agent_id}/tasks") +async def get_agent_tasks(agent_id: str, days: int = 7, task_type: Optional[str] = None): + return {"items": db.get_agent_tasks(agent_id, days=days, task_type=task_type)} +``` + +`db.get_agent_tasks`도 task_type 필터 추가 (기존 함수 보강). + +### 6.6 task_type 명세 (참조) + +| task_type | 트리거 | 어디서 생성 | +|---|---|---| +| `curate_weekly` | 월 09:05 또는 deep_check | LottoAgent._run (기존) | +| `signal_check` | light / sim / deep cron | LottoAgent.run_signal_check (신규 wrap) | +| `daily_digest` | 매일 09:25 | LottoAgent.run_daily_digest (신규 wrap) | +| `weekly_evolution_report` | 토 22:15 | LottoAgent.run_weekly_evolution_report (신규 wrap) | +| `evolver_generate` | 월 09:30 sync | LottoAgent.sync_evolver_activity (신규) | +| `evolver_apply` | 매일 09:30 sync | LottoAgent.sync_evolver_activity (신규) | + +## 7. 라우터 등록 + +`web-ui/src/routes.jsx`에 추가: + +```jsx +const Evolver = lazy(() => import('./pages/lotto/Evolver')); + +// appRoutes 배열에 추가: +{ + path: 'lotto/evolver', + element: , +}, +``` + +## 8. 구현 Phase + +| Phase | 범위 | 검증 | +|---|---|---| +| 1 | agent-office 백엔드 보강 (LottoAgent task_id wrap + sync cron + db helper) + 단위 테스트 | task row 생성 확인, 멱등 가드 동작 | +| 2 | agent-office API 확장 (task_type 필터) | curl로 필터링 동작 확인 | +| 3 | web-ui Evolver 페이지 — useEvolverApi + WinnerCard + TrialsGrid + BaseDiff + BaseHistory + EvolverActions | 로컬 dev 브라우저에서 모든 카드 정상 렌더, 모바일 반응형 | +| 4 | LottoActivityTimeline 공용 컴포넌트 — /lotto/evolver에 통합 + /agent-office LottoAgent 카드에 compact 모드 통합 | 두 페이지에서 동일 데이터 보임 | +| 5 | 라우터 등록 + 텔레그램 링크 404 해결 확인 | `release:nas` → 텔레그램 [차트 보기] 클릭 → 정상 페이지 | + +Phase 1-2: web-backend repo, Phase 3-5: web-ui repo. 각 repo는 별도 git, 별도 배포 (web-backend git push → Gitea webhook auto, web-ui `npm run release:nas`). + +## 9. 비기능 요구 + +- **백워드 호환**: 기존 LottoAgent 호출자 (cron 등) 시그니처 변경 없음. 내부 task_id wrap만 추가. +- **장애 격리**: sync_evolver_activity 실패해도 lotto-lab 영향 없음. task_id wrap 실패 시 try/except로 메서드 자체는 계속 동작. +- **멱등성**: sync_evolver_activity는 멱등 guard로 cron 재실행·재시작 안전. +- **테스트**: + - LottoAgent task_id wrap — mock task_id 받아 update 호출 확인 + - sync_evolver_activity 멱등 — 같은 날 2번 호출 시 1 row만 + - LottoActivityTimeline merge sort — unit test로 stream 순서·아이콘 매핑 +- **관측**: 모든 LottoAgent 메서드의 result_data 표준화 (Section 6.1 표 참조) + +## 10. 비목표 (Out of scope) + +- TrialsGrid에서 과거 주 deep dive 조회 (`GET /trials/{week_start}` 사용) — v2.2 후속, 별도 UI +- 차트 export / CSV 다운로드 +- 가중치 수동 편집 UI — v3에서 사용자 개입 모드 도입 검토 +- 다른 에이전트(stock / music / realestate)의 활동 통합 timeline — 현재 spec은 lotto만 +- 실시간 WebSocket 푸시 (agent-office에 ws 있지만 evolver 활동은 polling으로 충분) + +## 11. v3 후속 검토 + +- 다른 에이전트 활동도 같은 패턴(LottoActivityTimeline 제너릭화 → AgentActivityTimeline)으로 노출 +- /lotto/evolver 페이지에 사용자 의견 입력 (이번 winner가 마음에 듦/싫음) → 학습 시그널로 활용 +- BaseHistory에 brush 도입 (긴 history 시계열 zoom) +- TrialsGrid에 picks 채점 결과 통계 (몇 개 trial에서 4개 일치 났는지 등)