# Lotto Curator Evolution — Design Spec - 일자: 2026-05-11 - 범위: `web-ui` (브리핑 탭 재구성), `web-backend/lotto` (스키마·잡), `web-backend/agent-office` (큐레이터·텔레그램) - 컨셉 한 줄: **매주 같은 시간에 큐레이터가 한 번 더 똑똑해진다** ## 1. 동기와 문제 현재 `/lotto`는 3탭(브리핑·분석·구매)으로 구성되어 정보가 풍부하지만, 사용자가 5천~1만원 어치를 즐기며 구매하기에 다음 페인이 있다. - 분석·통계·브리핑이 모두 *결정용 화면*처럼 노출되어 정보 과다. - 큐레이터가 매주 5세트를 추천하지만, 5세트의 *역할*과 *왜 이 분배인지*가 와닿지 않는다. - 큐레이터·시스템에 시간축이 없다. 매주 동일 알고리즘을 새로 도는 느낌. - 1만원어치 구매 시 5세트로는 부족하다. 추가 게임에 대한 설계가 없다. ## 2. 컨셉 다음 두 축으로 강화한다. - **서사적 진화**: 큐레이터가 매주 *지난 주를 회고*하고 이번 주 전략으로 이어간다. 자기 추천 결과 + 사용자 실제 구매 결과를 둘 다 회고 데이터로 사용한다. - **포트폴리오 명료성**: 5게임이 단순 5장이 아니라 안정/균형/공격 분배가 그 주 데이터에 따라 동적으로 바뀌고, 그 이유가 한 줄로 와닿는다. 5~20세트로 위계적으로 확장된다. ## 3. 주간 사이클 ``` 토 20:35 추첨 │ 일 03:00 추첨결과 sync (기존) ↓ 채점 잡 (신규) → weekly_review INSERT lotto_purchase auto_graded UPDATE │ 월 09:00 큐레이션 트리거 (lotto_agent.on_schedule) ├─ build_retrospective(target_draw) ├─ collect_candidates(N=30) ├─ build_context (+retrospective) ├─ Claude 호출 (회고+계층 규칙) └─ briefings INSERT (4계층 picks) │ 월 09:05 텔레그램 헤드라인 푸시 │ 월~토 사용자: 사이트 결정 카드 → 모드 선택(5/10/15/20) → 1탭 구매 기록 │ 토 20:35 추첨 → 다음 사이클 ``` cron 시간(일 03:00 / 월 09:00)은 운영하며 조정 가능한 기본값. ## 4. 결정 카드 (브리핑 탭 메인) 브리핑 탭을 단일 `DecisionCard`로 재구성한다. 정보 위계는 위→아래로: 1. **헤더** — 회차 + 한 줄 헤드라인 + 신뢰도(0~100, 큐레이터 자기 평가) 2. **회고 박스** (▸ 보라색 라벨) — 지난 주 너 + 큐레이터 한 줄 회고. *시간축*의 핵심. 3. **헤드라인 + 3줄** — 이번 주 전망 + 근거 3줄(기존 narrative 유지). 4. **분배 칩** — 선택 모드까지의 안정/균형/공격 합산 + "왜 이 분배인지" 한 줄. 5. **모드 토글** — 4단계 칩(코어 5 / +보너스 5 / +확장 5 / +풀 5). 6. **계층 섹션 × 4** — 각 계층마다 타이틀 + 사유 한 줄 + 5장 PickCard. 코어는 항상 펼침, 그 외는 모드에 따라. 7. **하단 액션** — "이대로 N세트 구매했음" 한 클릭 → 자동 기록. ### 4계층 위계 | 계층 | 누적 게임 | 비용 | 큐레이터의 의도 | |---|---|---|---| | 코어(필수) | 5 | 5천 | 안정 2 / 균형 2 / 공격 1, 그 주 주축 | | + 보너스 | 10 | 1만 | 코어 분배의 공백 보완 | | + 확장 | 15 | 1.5만 | 코어·보너스에 없던 시각(합계 극단·콜드 누적·4주 미등장) | | + 풀 | 20 | 2만 | 한 번도 누르지 않은 패턴(연속·동끝·5수 균등) | 각 5세트는 *큐레이터가 의도한 한 묶음*이며, 늘어날수록 *서사가 더해지는 구조*. 마지막 모드 선택은 브라우저 `localStorage` 에 `lotto.tier_mode` 키로 저장하여 다음 주 진입 시 디폴트로 사용한다(서버 저장 X — 사용자 디바이스 단위 기억). ### 분석 탭은 "Deep Dive" 자료실로 강등 - 라벨 변경: `📊 분석·통계` → `📚 자료실 / Deep Dive` - 첫 진입 시 모든 패널 접힘 - 기존 패널 모두 보존 (CombinedRecommendPanel, ReportPanel, 시뮬레이션, 통계, 빈도, PersonalAnalysisPanel, 수동 추천, 히스토리) - PerformanceBanner는 결정 카드 헤더와 역할 중복 없도록 자료실에만 둠 ## 5. 데이터 모델 ### 신규 테이블 — `weekly_review` ```sql CREATE TABLE weekly_review ( id INTEGER PRIMARY KEY AUTOINCREMENT, draw_no INTEGER NOT NULL UNIQUE, -- 큐레이터 자기 평가 (briefings.picks vs 추첨) curator_avg_match REAL, curator_best_tier TEXT, -- 안정 | 균형 | 공격 curator_best_match INTEGER, curator_5plus_prizes INTEGER, -- 3개↑ 일치 카운트(5등 이상) -- 사용자 구매 평가 (lotto_purchase vs 추첨) user_avg_match REAL, user_best_match INTEGER, user_5plus_prizes INTEGER, -- 패턴 갭 (서사 재료) user_pattern_summary TEXT, draw_pattern_summary TEXT, pattern_delta TEXT, -- "너 저번호 편향 +1.2 / 합계 -18" created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); ``` ### `lotto_purchase` 컬럼 추가 ```sql ALTER TABLE lotto_purchase ADD COLUMN numbers TEXT; -- JSON [3,11,17,25,33,41] ALTER TABLE lotto_purchase ADD COLUMN match_count INTEGER; ALTER TABLE lotto_purchase ADD COLUMN auto_graded INTEGER DEFAULT 0; ALTER TABLE lotto_purchase ADD COLUMN curator_tier TEXT; -- core | bonus | extended | pool ALTER TABLE lotto_purchase ADD COLUMN curator_role TEXT; -- 안정 | 균형 | 공격 ``` ### `briefings.picks` 구조 변경 JSON 컬럼을 4계층 구조로 마이그레이션: ```json { "core": [/* 5세트 */], "bonus": [/* 5세트 */], "extended": [/* 5세트 */], "pool": [/* 5세트 */] } ``` 기존 단일 배열 데이터는 `core` 키에만 매핑하고 나머지 키는 빈 배열로 채우는 1회 마이그레이션 스크립트. ## 6. 큐레이터 변경 ### 출력 스키마 (`agent-office/curator/schema.py`) ```python class CuratorOutput(BaseModel): core_picks: List[Pick] = Field(min_length=5, max_length=5) bonus_picks: List[Pick] = Field(min_length=5, max_length=5) extended_picks: List[Pick] = Field(min_length=5, max_length=5) pool_picks: List[Pick] = Field(min_length=5, max_length=5) tier_rationale: TierRationale # bonus / extended / pool 각 30자 이내 narrative: Narrative # retrospective(60자 이내) 필드 추가 confidence: int # 0~100 ``` ### SYSTEM_PROMPT 추가 규칙 ``` 회고 규칙: - context.retrospective 가 있으면 narrative.retrospective 에 한 줄(60자 이내). - 큐레이터 자기 결과(curator_avg, best_tier) + 사용자 결과(user_avg, pattern_delta) 둘 다 짚을 것. - 이번 주 코어 분배는 회고에 근거해 조정. 사유는 narrative.headline 에 한 줄로. 계층별 큐레이션 규칙: - core_picks (5): 안정 2 / 균형 2 / 공격 1. 그 주 주축. - bonus_picks (5): 코어 분배의 공백을 메움. 코어와 상보적. - extended_picks (5): 코어·보너스에 없는 시각(합계 극단 / 콜드 누적 / 4주 미등장). - pool_picks (5): 이번 주 한 번도 누르지 않은 패턴(연속·동끝·5수 균등). - tier_rationale 의 3개 키(bonus·extended·pool)에 각각 30자 이내 사유. - 후보에 없는 번호 조합은 절대 사용 금지(기존 규칙 유지). ``` ### 회고 컨텍스트 — `agent-office/curator/retrospective.py` (신규) ```python def build_retrospective(target_draw_no: int) -> dict | None: last = lotto_get_review(target_draw_no - 1) prev3 = lotto_get_reviews(target_draw_no - 4, target_draw_no - 2) if not last: return None return { "last_draw": { "draw_no": last["draw_no"], "curator_avg": last["curator_avg_match"], "curator_best_tier": last["curator_best_tier"], "user_avg": last["user_avg_match"], "user_5plus": last["user_5plus_prizes"], "pattern_delta": last["pattern_delta"], }, "trend_4w": { "curator_avg_4w": mean(curator_avg_match for r in [last, *prev3]), "user_avg_4w": mean(user_avg_match for r in [last, *prev3] if user_avg_match is not None), "user_persistent_bias": _detect_bias([last, *prev3]), # 3주↑ 유지된 패턴 편향(예: "저번호 편향") } } ``` ### 후보 풀 N=30 `collect_candidates(n=30)` — 20세트 선별 + 다양성 여유. 기존 4개 소스(simulation/heatmap/statistics/meta) 추출량을 비례 확대. ## 7. 자동 채점 잡 — `lotto/app/jobs/grade_weekly_review.py` ``` 실행: 매주 일요일 03:00 KST (cron) 입력: 가장 최근 sync된 추첨 회차 처리: 1) briefings 에서 해당 회차의 4계층 picks 로드 (없으면 curator_* NULL) 2) lotto_purchase 에서 해당 회차의 사용자 구매 로드 (없으면 user_* NULL) 3) 각 세트별 일치 수 계산 → 큐레이터/사용자 집계 4) 패턴 요약(저번호·홀짝·합계 평균) → user/draw_pattern_summary 5) 패턴 갭 한 줄(가장 큰 격차 1~2개) → pattern_delta 6) weekly_review UPSERT (draw_no 유니크) 7) lotto_purchase 채점: - 일치 3개 → prize=5000, auto_graded=1 - 일치 4개 → prize=NULL, note 에 "4등 가능성 — 동행복권 확인" 플래그 - 일치 5+ → prize=NULL, note 에 "🚨 큰 당첨 가능성 — 즉시 확인" 플래그 + agent-office HTTP webhook(`POST /api/agent-office/notify/lotto-prize`) 호출하여 텔레그램 별도 알림 트리거 - numbers NULL 인 행은 스킵 ``` ## 8. 텔레그램 알림 — `agent-office/notifiers/telegram_lotto.py` (신규) 큐레이션 성공 후 `lotto_agent` 가 호출. 발송 실패는 try/except 로 흡수(briefing 저장과 분리). 4등 이상 당첨 알림은 lotto-backend 채점 잡이 `POST /api/agent-office/notify/lotto-prize` webhook 으로 트리거(agent-office 측 라우터 신규 추가). ``` 🎟 1154회 · 큐레이션 떴음 "이번 주는 안정 +1, 콜드 누적 보강." 신뢰도 72 · 분배 안정 3·균형 1·공격 1 ▸ 회고: 너 2.0 / 나 1.8 너 저번호 편향 → 보너스 고번호 보강 👉 결정 카드 보러가기 (https://gahusb.synology.me/lotto) ``` 회고 단락은 retrospective 가 있을 때만(첫 주 생략). ## 9. 프론트 변경 ### 파일 변경 맵 | 파일 | 종류 | 내용 | |------|------|------| | `pages/lotto/Functions.jsx` | 수정 | 분석탭 라벨 변경 | | `pages/lotto/tabs/BriefingTab.jsx` | 수정 | DecisionCard 단일로 재구성 | | `pages/lotto/components/decision/DecisionCard.jsx` | 신규 | 결정 카드 메인 | | `pages/lotto/components/decision/RetrospectiveBox.jsx` | 신규 | 회고 박스 | | `pages/lotto/components/decision/TierModeToggle.jsx` | 신규 | 4단계 칩 토글 | | `pages/lotto/components/decision/TierSection.jsx` | 신규 | 한 계층 영역(타이틀+사유+5장) | | `pages/lotto/components/decision/PickCard.jsx` | 신규 | 한 세트 카드(역할+번호+사유) | | `pages/lotto/components/decision/BulkPurchaseButton.jsx` | 신규 | 원클릭 구매 | | `pages/lotto/components/briefing/*` | 삭제·이동 | DecisionCard 하위로 흡수, CuratorUsageFooter 는 자료실 이동 | | `pages/lotto/components/PurchasePanel.jsx` | 수정 | auto_graded 표시 + 4등 이상 플래그 | | `pages/lotto/components/PurchaseTrendChart.jsx` | 신규 | 4주 추세 라인(너 vs 큐레이터 평균 일치) | | `pages/lotto/hooks/useBriefing.js` | 수정 | 4계층 + retrospective 수용 | | `pages/lotto/hooks/useReview.js` | 신규 | weekly_review 로드 | | `pages/lotto/hooks/usePurchases.js` | 수정 | bulkPurchase 추가 | | `api.js` | 수정 | getLatestReview, getReviewHistory, bulkPurchase 헬퍼 | ### 컴포넌트 격리 원칙 - `DecisionCard` 는 `briefing` + `review` 두 객체만 props 로 받음(내부 hook 호출 X). - `TierSection` 은 `tier`, `picks`, `rationale` 만 받아 4번 재사용. - `BulkPurchaseButton` 은 `draw_no`, `tier_mode`, `sets`, `amount` 4개로 작동. ## 10. 백엔드 변경 ### `web-backend/lotto/` | 파일 | 종류 | 내용 | |------|------|------| | `app/db/migrations/00X_weekly_review.sql` | 신규 | 테이블 생성 | | `app/db/migrations/00X_purchase_grading.sql` | 신규 | lotto_purchase 컬럼 추가 | | `app/db/migrations/00X_briefings_tiers.sql` | 신규 | briefings.picks 4계층 마이그레이션 | | `app/jobs/grade_weekly_review.py` | 신규 | 채점 잡 | | `app/curator_helpers.py` | 수정 | collect_candidates(N=30) 기본값, build_context 에 retrospective 합치기 | | `app/routers/briefing.py` | 수정 | BriefingRequest 4계층 + narrative.retrospective 수용 | | `app/routers/review.py` | 신규 | GET /api/lotto/review/latest, GET /api/lotto/review/history?limit=N | | `app/routers/purchase.py` | 수정 | POST /api/lotto/purchase/bulk | | `app/cron.py` (또는 compose 스케줄러) | 수정 | 채점 잡 일 03:00 등록 | ### `web-backend/agent-office/` | 파일 | 종류 | 내용 | |------|------|------| | `app/curator/retrospective.py` | 신규 | build_retrospective | | `app/curator/schema.py` | 수정 | 4계층 + tier_rationale + narrative.retrospective | | `app/curator/prompt.py` | 수정 | 회고·계층 규칙 추가 | | `app/curator/pipeline.py` | 수정 | retrospective 빌드 호출, 4계층 직렬화 | | `app/agents/lotto.py` | 수정 | on_schedule 월 09:00, 성공 시 텔레그램 호출 | | `app/notifiers/telegram_lotto.py` | 신규 | 알림 포맷·발송(큐레이션 완료, 4등 이상 당첨 알림 둘 다) | | `app/routers/notify.py` | 신규 | `POST /api/agent-office/notify/lotto-prize` — lotto-backend 채점 잡이 호출 | | `app/service_proxy.py` | 수정 | review 헬퍼 추가 | ## 11. API 추가·변경 | 메서드 | 경로 | 설명 | |--------|------|------| | GET | `/api/lotto/review/latest` | 최신 weekly_review 1건 | | GET | `/api/lotto/review/history?limit=N` | 최근 N건 (4주 추세 차트용) | | POST | `/api/lotto/purchase/bulk` | 결정 카드 원클릭 — body: `{ draw_no, tier_mode, sets, amount }` | | POST | `/api/agent-office/notify/lotto-prize` | 4등 이상 당첨 시 lotto-backend 가 트리거 — body: `{ draw_no, match_count, numbers, purchase_id }` | 기존 엔드포인트는 그대로 유지(스키마 호환). ## 12. 에러 처리 / 격리 | 단계 | 실패 | 처리 | |------|------|------| | 추첨결과 sync | 동행복권 API down | 기존 정책(재시도). 채점 잡은 자동 지연만. | | 채점 — 큐레이터 picks 없음 | 첫 주, 큐레이션 실패 회차 | curator_* NULL 로 INSERT | | 채점 — 사용자 구매 없음 | 그 주 미구매 | user_* NULL | | 채점 — numbers NULL 행 | 마이그레이션 이전 데이터 | 스킵, auto_graded=0 유지 | | build_retrospective — review 없음 | 첫 주 | None 반환 → 프롬프트 분기 자연 처리 | | Claude 스키마 실패 | 4계층 미준수 등 | 기존 1회 retry, 2회 실패 시 텔레그램 에러 알림 | | 텔레그램 발송 실패 | 봇/네트워크 | try/except, 로그만. briefing 저장은 영향 없음 | | bulk purchase — briefing 없음 | 큐레이션 실패 회차 | 400 + 토스트 | | bulk purchase — 중복 호출 | 더블클릭 | (draw_no, tier_mode) 유니크 → idempotent | | 자동채점 — 4등 이상 | 큰 당첨 | prize NULL + 메모 플래그 + 텔레그램 별도 알림 | ## 13. 테스트 ### 백엔드 (`lotto/`) - `grade_weekly_review`: (a) 정상 (b) user 구매 없음 (c) numbers NULL 스킵 (d) 일치 3개 → prize 5000 (e) 일치 4개 → 메모 플래그 - 마이그레이션: 빈 DB → 더미 → 잡 실행 → 행 정확 - briefings 마이그레이션: 구 단일 picks → core 매핑, 나머지 빈 배열 - `POST /purchase/bulk`: 정상 / 잘못된 tier_mode / briefing 없음 / 중복 호출 - `GET /review/latest`: 데이터 있음 / 빈 DB → 404 ### 큐레이터 (`agent-office/curator/`) - `build_retrospective`: review 1건 / 4건 / 0건 - `validate_response`: 정상 / 계층 누락 / 후보 외 번호 / tier_rationale 누락 - `curate_weekly` (Claude API mock): retrospective 있음·없음 / 1차 실패 → 2차 성공 / 2회 실패 - `telegram_lotto.format`: retrospective 있음·없음 ### 프론트 - `DecisionCard` 수동: retrospective 있음·없음 / 모드 토글 5/10/15/20 / confidence 색 - `TierModeToggle` 단위: onChange 콜백 정확 - `BulkPurchaseButton` 수동 E2E: 클릭 → POST → 토스트 → 구매탭 갱신 - 자료실 탭 수동: 첫 진입 모두 접힘 - 모바일: DecisionCard 좁은 화면에서 깨짐 없음 ## 14. 운영 점검 (배포 후 1주차) 수동으로 확인: 1. 일 03:00 채점 잡 1회 실행(`weekly_review` 1행 추가) 2. 월 09:00 큐레이션 실행(`briefings` 1행, 4계층 5×4=20개) 3. 텔레그램 알림 도착(회고 단락 정확 포함/생략) 4. 결정 카드 렌더링 정상(모바일 + PC) 5. 원클릭 구매 정확 N건 INSERT 6. cron 시간(03:00 / 09:00) 운영 패턴에 맞게 조정 ## 15. Out of Scope - 4등 이상 당첨금 자동 입력(회차별 변동, 사용자 PUT 으로 갱신) - 큐레이터 호출 재무 비용 모니터링 강화(기존 `curator_usage` 그대로) - 분석 탭 패널 자체의 리팩토링(라벨·디폴트 접힘만 변경) - 1만원 외 임의 분량(7세트 등) 토글(4계층 5단위로 고정)