Files
web-page-backend/docs/superpowers/specs/2026-04-15-lotto-ai-curator-design.md
gahusb e691ed9a7d docs(lotto): AI 큐레이터 설계 스펙 추가
- 주간 AI 큐레이터: 월요일 07:00 자동 생성, Claude Sonnet 4.5
- lotto-backend = 엔진·저장소, agent-office = AI 판단 분리
- 브리핑 중심 프론트 재배치(3탭), 토큰·비용 노출
- 최종 미사용 DB/코드 정리 패스 포함
2026-04-15 03:35:29 +09:00

14 KiB

Lotto AI 큐레이터 — 설계 문서

작성일: 2026-04-15 목표: 난잡한 lotto 랩을 주간 AI 브리핑을 축으로 재정리. 매주 월요일 아침 자동으로 "이번 주 5세트 + 내러티브 리포트"를 생성해 구매 의사결정 참고.


1. 배경

  • 현재 lotto 랩은 분석(5가지)·추천(통계/히트맵/메타)·시뮬레이션·전략진화 등 기능이 풍부하지만 출력이 분산되어 "결국 뭘 사야 하지"가 한눈에 들어오지 않음.
  • docs/lotto-premium-roadmap.md Phase 1 방향(신뢰 기반 + 주간 리포트)을 AI 활용으로 압축 실행.

2. 핵심 결정사항

항목 결정
AI 역할 큐레이터(Curator) — 숫자 생성 X, 기존 엔진 후보 중 5세트 선별 + 내러티브 작성
브리핑 형식 A+B 조합 — 리포트형 내러티브 + 최종 5세트 카드
트리거 매주 월요일 07:00 자동 생성 (웹 UI 전용, 텔레그램 미전송)
로직 위치 agent-office lotto 에이전트 (lotto-backend는 엔진·저장소 역할만)
모델 claude-sonnet-4-5 (주 1회 호출, 품질 우선) — 환경변수 LOTTO_CURATOR_MODEL
사용량 노출 브리핑 카드 + 큐레이터 사용량 API(월간 집계)

3. 아키텍처

┌──────────────────────────────────────────────────────────────┐
│ 월요일 07:00 APScheduler (agent-office)                      │
│   → lotto 에이전트 curate_weekly 태스크                       │
│                                                              │
│   ┌─────────────────────────────────────────────────────┐    │
│   │ 1. GET /api/lotto/curator/candidates?n=20           │    │
│   │ 2. GET /api/lotto/curator/context                   │    │
│   │ 3. Claude Sonnet 4.5 호출 (strict JSON out)         │    │
│   │ 4. 스키마·번호 검증 + 1회 재시도                      │    │
│   │ 5. POST /api/lotto/briefing (저장)                   │    │
│   └─────────────────────────────────────────────────────┘    │
│                                                              │
│ 사용자는 웹에서:                                              │
│   GET /api/lotto/briefing/latest (최신 표시)                  │
│   POST /api/agent-office/command {agent:"lotto", …} (수동)    │
└──────────────────────────────────────────────────────────────┘

서비스 경계: lotto-backend = 데이터·엔진 / agent-office = AI 판단.


4. Backend (lotto-backend)

4.1 신규 API

메서드 경로 설명
GET /api/lotto/curator/candidates 큐레이터용 후보 N세트 + 세트별 피처
GET /api/lotto/curator/context 주간 맥락(핫/콜드·직전 회차 분석·내 최근 성과)
POST /api/lotto/briefing 큐레이터 결과 저장
GET /api/lotto/briefing/latest 최신 브리핑
GET /api/lotto/briefing/{draw_no} 특정 회차 브리핑
GET /api/lotto/briefing?limit=10 브리핑 이력
GET /api/lotto/curator/usage?days=30 큐레이터 토큰·비용 집계

4.2 GET /curator/candidates 응답 구조

{
  "draw_no": 1180,
  "generated_at": "2026-04-13T07:00:00Z",
  "candidates": [
    {
      "numbers": [3, 14, 22, 29, 35, 41],
      "source": "simulation" | "meta" | "heatmap" | "statistics",
      "features": {
        "odd_count": 3,
        "even_count": 3,
        "low_count": 3,        // 1~22
        "high_count": 3,       // 23~45
        "range_distribution": [1,1,1,1,1,1],  // 1-10,11-20,...,41-45
        "has_consecutive": true,
        "hot_number_count": 1,   // context.hot_numbers 교집합
        "cold_number_count": 2,  // context.cold_numbers 교집합
        "sum": 144,
        "historical_match_avg": 2.3  // 이 세트가 과거 실제 회차와 평균 몇 개 일치
      }
    }
  ]
}

중복 제거: 6숫자 정렬 튜플 기준 set 해시. 각 세트의 source는 가장 먼저 포함시킨 엔진.

4.3 GET /curator/context 응답 구조

{
  "draw_no": 1180,
  "hot_numbers": [3, 17, 28],      // 최근 10회 과출현 top
  "cold_numbers": [7, 22, 41],     // 최근 30회 미출현 top
  "last_draw_summary": "1179회: 7, 12, 18, 24, 31, 40 (홀4짝2, 저4고2)",
  "recent_analysis": {
    "avg_sum": 138,
    "avg_odd_count": 2.8
  },
  "my_recent_performance": [
    { "draw_no": 1177, "purchased_sets": 5, "best_match": 3 },
    { "draw_no": 1178, "purchased_sets": 5, "best_match": 2 },
    { "draw_no": 1179, "purchased_sets": 5, "best_match": 4 }
  ]
}

4.4 신규 테이블 lotto_briefings

CREATE TABLE lotto_briefings (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  draw_no INTEGER UNIQUE NOT NULL,
  picks TEXT NOT NULL,         -- JSON: 5세트 + reason + risk_tag
  narrative TEXT NOT NULL,     -- JSON: headline/summary_3lines/hot_cold/warnings
  confidence INTEGER NOT NULL, -- 0~100
  model TEXT NOT NULL,
  tokens_input INTEGER DEFAULT 0,
  tokens_output INTEGER DEFAULT 0,
  cache_read INTEGER DEFAULT 0,
  cache_write INTEGER DEFAULT 0,
  latency_ms INTEGER DEFAULT 0,
  source TEXT NOT NULL DEFAULT 'auto',  -- 'auto' | 'manual'
  generated_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE INDEX idx_briefings_draw ON lotto_briefings(draw_no DESC);

4.5 파일 구조 정리

backend/app/main.py 933줄 → 라우터 분리:

  • backend/app/routers/briefing.py — briefing CRUD + curator usage
  • backend/app/routers/curator.py — candidates / context
  • backend/app/curator_helpers.py — 후보 중복 제거, 피처 계산, 맥락 추출

기존 main.py는 라우터 등록과 앱 조립만 담당(목표 ~300줄).


5. agent-office lotto 에이전트

5.1 파일 구조

agent-office/app/
  agents/lotto.py        # LottoAgent (BaseAgent 상속)
  curator/
    __init__.py
    pipeline.py          # curate_weekly() 메인 플로우
    prompt.py            # system prompt + 출력 스키마 정의
    schema.py            # pydantic 응답 모델 + 검증
    service.py           # lotto-backend 호출 래퍼 (httpx)

service_proxy.pylotto_candidates(), lotto_context(), lotto_save_briefing() 메서드 추가.

5.2 태스크 타입

  • curate_weekly — 자동/수동 공통. 파라미터 없음(draw_no 자동 계산).

5.3 큐레이터 규칙 (system prompt 요지)

당신은 로또 번호 큐레이터입니다. 후보 20세트 중 5세트를 다음 규칙으로 선별합니다.

선별 규칙:
- 5세트의 리스크 분포: 안정 2 · 균형 2 · 공격 1 (유연 ±1)
- 홀짝 비율, 저/고 구간, 연속번호 포함 여부가 세트끼리 겹치지 않도록 다양성 확보
- hot_number_count와 cold_number_count 모두 0인 세트는 최소 1개
- 후보 외 번호 사용 절대 금지
- 각 세트 reason은 40자 이내 한 줄 (해당 세트 피처와 context 값만 근거)

출력은 반드시 아래 JSON 스키마로만:
{
  "picks": [
    {"numbers":[...], "risk_tag":"안정"|"균형"|"공격", "reason":"..."}
  ],
  "narrative": {
    "headline": "...",
    "summary_3lines": ["...","...","..."],
    "hot_cold_comment": "...",
    "warnings": "..."   // 없으면 빈 문자열
  },
  "confidence": 0-100
}

5.4 파이프라인 의사코드

async def curate_weekly(draw_no: int) -> dict:
    candidates = await service.lotto_candidates(n=20)
    context = await service.lotto_context()
    prompt = build_prompt(candidates, context, draw_no)

    result, usage = await call_claude(prompt, model=LOTTO_CURATOR_MODEL)
    parsed = validate(result)  # 실패 시 1회 재시도
    if parsed is None:
        raise CuratorError("schema validation failed after retry")

    await service.lotto_save_briefing({
        "draw_no": draw_no,
        "picks": parsed.picks,
        "narrative": parsed.narrative,
        "confidence": parsed.confidence,
        "model": LOTTO_CURATOR_MODEL,
        "tokens_input": usage.input,
        "tokens_output": usage.output,
        "cache_read": usage.cache_read,
        "cache_write": usage.cache_write,
        "latency_ms": usage.latency_ms,
        "source": "auto" | "manual",
    })
    return {"ok": True, "draw_no": draw_no, ...}

5.5 검증 로직 (schema.py)

  • pydantic 모델로 형식 검증
  • 번호 제약: 각 세트 정확히 6개 · 중복 없음 · 1~45 범위
  • 세트 수: 정확히 5
  • 번호가 candidates 내에 존재하는 조합인지 대조 (환각 차단)
  • risk_tag 분포가 규칙에서 ±1 이상 벗어나면 경고 로그(차단은 안 함)
  • 실패 시 errors 리스트 담아 1회 재시도(프롬프트에 에러 피드백 포함)

5.6 스케줄러

scheduler.py에 추가:

scheduler.add_job(_run_lotto_curate, "cron", day_of_week="mon", hour=7, minute=0, id="lotto_curate")

5.7 상태 표시

agent-office 메인 UI에 lotto 에이전트 카드가 추가되어 idle / working / error 상태 실시간 표시(기존 BaseAgent 패턴).


6. Frontend (web-ui)

6.1 새 탭 구조

Lotto
├─ 🗓 이번 주 브리핑   (기본)
├─ 📊 분석·통계
└─ 💰 구매·성과

Functions.jsx 460줄 → 탭 라우터 ~80줄로 축소. 각 탭은 pages/lotto/tabs/BriefingTab.jsx, AnalysisTab.jsx, PurchaseTab.jsx.

6.2 신규 컴포넌트 (components/briefing/)

  • BriefingHeader.jsx — 회차 번호, 생성 시각, 신뢰도 바, 재생성 버튼, 사용 토큰 칩(42K in · 1.2K out · $0.18)
  • BriefingSummary.jsx — 3줄 요약 + 핫/콜드 블록 + 주의사항
  • PickSetCard.jsx — 6볼 + risk 뱃지(🟢안정/🟡균형/🔴공격) + reason + "구매 기록" CTA
  • BriefingEmpty.jsx — 브리핑 없을 때 placeholder + "지금 생성" 버튼
  • CuratorUsageFooter.jsx — 페이지 하단 mini 카드. 최근 30일 호출 수·토큰·추정 비용·캐시 히트율

6.3 훅

  • useBriefing.js
    • GET /api/lotto/briefing/latest
    • regenerate(): POST /api/agent-office/command {agent:"lotto", action:"curate_now"} → 3초 간격 최대 40회(=2분) 폴링으로 신규 briefing 확인
    • 로딩/에러 상태 분리, 월요일 07:00 이후인데 브리핑 없으면 빈 상태 CTA
  • useCuratorUsage.jsGET /api/lotto/curator/usage?days=30

6.4 기존 컴포넌트 처리

컴포넌트 조치
FrequencyChart, MetricBlock, PersonalAnalysisPanel, ReportPanel 분석 탭으로 이동
PurchasePanel, PerformanceBanner 구매 탭으로 이동
CombinedRecommendPanel, ConfidenceRing 제거 후보 — 정리 패스에서 실제 참조 없으면 삭제

6.5 토큰·비용 노출 정책

  • 브리핑 카드 헤더: 이번 브리핑 1건의 in/out 토큰 + 추정 비용 (Sonnet 4.5 단가 기준 계산 — 상수로 프론트에 보유, $3/$15 per 1M tokens)
  • 페이지 하단 푸터: 최근 30일 누적 — 호출 수, 총 토큰, 추정 비용, 캐시 히트율
  • Agent Office 사이드: 기존 GET /api/agent-office/agents/lotto/token-usage 자동 상속

6.6 모바일

브리핑 탭 세로 스택 기본. PickSetCard는 한 행 1카드 + 6볼 flex-wrap. 헤더 토큰 칩은 768px 이하에서 축약 표시($0.18만).


7. 환경변수

변수 기본값 위치
ANTHROPIC_API_KEY (없음) agent-office (이미 존재)
LOTTO_CURATOR_MODEL claude-sonnet-4-5 agent-office
LOTTO_BACKEND_URL http://lotto-backend:8000 agent-office (service_proxy)

8. 에러·폴백

상황 처리
lotto-backend 후보 API 실패 에이전트 상태 error + 로그 + 슬랙/알림 없음(주 1회라 로그 충분)
Claude 호출 실패 1회 재시도 후 실패 시 error 저장, 기존 최신 브리핑 유지
JSON 스키마 검증 실패 피드백 포함 1회 재시도 → 실패 시 error
월요일 생성 자체가 누락 사용자가 웹에서 수동 재생성 버튼으로 보완 가능

9. 구현 순서

  1. Backend: curator 엔드포인트 + briefing CRUD + 라우터 분리
  2. Agent-office: lotto 에이전트 + curator pipeline + 월요일 스케줄러
  3. Frontend: BriefingTab + 컴포넌트 + 훅 + 탭 재배치
  4. 미사용 정리 패스: 아래 "10. 정리 대상" 후보를 실제 참조 grep → 제거

10. 정리 대상 (최종 패스에서 검증 후 제거)

Frontend

  • components/CombinedRecommendPanel.jsx
  • components/ConfidenceRing.jsx
  • Functions.jsx 내 인라인 레이아웃 로직 (탭 분리 후 잔재)

Backend

  • strategy_evolver.py 중 실제 사용되지 않는 EMA 서브 함수
  • 주간 리포트 관련 weekly_reports 테이블 — 브리핑이 대체하므로 드롭 후보
  • best_picks 교체 로직 중 큐레이터 전환 후 사용 안 되는 경로

DB 드롭 후보

  • weekly_reports (브리핑이 대체)
  • simulation_candidates (best_picks만 있으면 충분한지 사용처 grep 후 결정)

정리 패스는 실제 import/참조 grep → 없으면 제거 → 테스트 → 커밋 순서로 별도 커밋 분리.


11. 성공 기준

  • 월요일 07:00 브리핑이 자동 생성되고, 웹 페이지 진입 1초 안에 5세트 + 3줄 요약이 보인다.
  • 큐레이터는 candidates 내 세트만 선택한다(환각 0건).
  • 브리핑 카드에 이번 건 토큰/비용, 페이지 하단에 30일 누적 사용량이 표시된다.
  • 기존 난잡한 패널이 분석/구매 탭으로 정돈되고 브리핑 탭이 기본 진입점이다.
  • 미사용 테이블·컴포넌트가 최종 정리 패스에서 제거된다.