# 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` 응답 구조 ```json { "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` 응답 구조 ```json { "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` ```sql 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.py`에 `lotto_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 파이프라인 의사코드 ```python 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`에 추가: ```python 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.js** — `GET /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일 누적 사용량이 표시된다. - 기존 난잡한 패널이 분석/구매 탭으로 정돈되고 브리핑 탭이 기본 진입점이다. - 미사용 테이블·컴포넌트가 최종 정리 패스에서 제거된다.