Files
web-page-backend/docs/superpowers/specs/2026-05-23-lotto-evolver-ui-design.md
gahusb ea3485cde6 docs(spec): Lotto Evolver UI + 에이전트 활동 가시화 (v2.1)
Why: v2 텔레그램 메시지의 /lotto/evolver 링크가 404 → 페이지 신설.
+ LottoAgent 활동(signal/digest/evolution/curate)이 agent_tasks에
누락된 거 보강. 모든 활동을 한 timeline에서 추적 가능.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 01:31:56 +09:00

20 KiB

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

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

<LottoActivityTimeline
  logs={agentLogs}
  tasks={agentTasks}
  evolverEvents={evolverEventsFromHistory}
  days={7}
  compact={false}
/>

merge & sort:

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.

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

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 추가

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 추가:

@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에 추가:

const Evolver = lazy(() => import('./pages/lotto/Evolver'));

// appRoutes 배열에 추가:
{
    path: 'lotto/evolver',
    element: <Evolver />,
},

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개 일치 났는지 등)