From 6b2fcda2af4b44515f852d8601ef2807b5734e6c Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 11 May 2026 03:19:58 +0900 Subject: [PATCH] =?UTF-8?q?docs(spec):=20Lotto=20Curator=20Evolution=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매주 같은 시간에 큐레이터가 한 번 더 똑똑해지는 컨셉으로 - 회고 컨텍스트(weekly_review + 자동 채점 잡) - 4계층 위계(코어/보너스/확장/풀, 5~20세트) - 결정 카드 단일 화면(브리핑 탭 재구성) - 분석 탭은 자료실로 강등 - 월요일 09:00 큐레이션 + 텔레그램 푸시 --- ...26-05-11-lotto-curator-evolution-design.md | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-11-lotto-curator-evolution-design.md diff --git a/docs/superpowers/specs/2026-05-11-lotto-curator-evolution-design.md b/docs/superpowers/specs/2026-05-11-lotto-curator-evolution-design.md new file mode 100644 index 0000000..467abb1 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-lotto-curator-evolution-design.md @@ -0,0 +1,358 @@ +# 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단위로 고정)