- 주간 AI 큐레이터: 월요일 07:00 자동 생성, Claude Sonnet 4.5 - lotto-backend = 엔진·저장소, agent-office = AI 판단 분리 - 브리핑 중심 프론트 재배치(3탭), 토큰·비용 노출 - 최종 미사용 DB/코드 정리 패스 포함
14 KiB
14 KiB
Lotto AI 큐레이터 — 설계 문서
작성일: 2026-04-15 목표: 난잡한 lotto 랩을 주간 AI 브리핑을 축으로 재정리. 매주 월요일 아침 자동으로 "이번 주 5세트 + 내러티브 리포트"를 생성해 구매 의사결정 참고.
1. 배경
- 현재 lotto 랩은 분석(5가지)·추천(통계/히트맵/메타)·시뮬레이션·전략진화 등 기능이 풍부하지만 출력이 분산되어 "결국 뭘 사야 하지"가 한눈에 들어오지 않음.
docs/lotto-premium-roadmap.mdPhase 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 usagebackend/app/routers/curator.py— candidates / contextbackend/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 파이프라인 의사코드
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/latestregenerate():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. 구현 순서
- Backend: curator 엔드포인트 + briefing CRUD + 라우터 분리
- Agent-office: lotto 에이전트 + curator pipeline + 월요일 스케줄러
- Frontend: BriefingTab + 컴포넌트 + 훅 + 탭 재배치
- 미사용 정리 패스: 아래 "10. 정리 대상" 후보를 실제 참조 grep → 제거
10. 정리 대상 (최종 패스에서 검증 후 제거)
Frontend
components/CombinedRecommendPanel.jsxcomponents/ConfidenceRing.jsxFunctions.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일 누적 사용량이 표시된다.
- 기존 난잡한 패널이 분석/구매 탭으로 정돈되고 브리핑 탭이 기본 진입점이다.
- 미사용 테이블·컴포넌트가 최종 정리 패스에서 제거된다.