diff --git a/docs/superpowers/specs/2026-04-05-lotto-purchase-strategy-evolution-design.md b/docs/superpowers/specs/2026-04-05-lotto-purchase-strategy-evolution-design.md new file mode 100644 index 0000000..4888c48 --- /dev/null +++ b/docs/superpowers/specs/2026-04-05-lotto-purchase-strategy-evolution-design.md @@ -0,0 +1,402 @@ +# Lotto 구매 연동 + 전략 진화 시스템 설계 + +> 작성일: 2026-04-05 +> 상태: 승인 대기 + +--- + +## 1. 목표 + +로또 번호 추천 기능을 고도화하여: +1. **동행복권 실 구매 연동** — 추천 번호를 클립보드 복사 + 동행복권 바로가기로 실제 구매 지원 +2. **가상 구매 모드** — 돈을 쓰지 않고 "이 번호로 구매한다"를 등록, 결과 발표 후 자동 가상 수익률 계산 +3. **전략 진화 시스템** — 구매 이력 기반으로 각 추천 전략(combined, simulation, heatmap, manual, custom)의 성과를 추적하고, EMA + Softmax로 가중치를 자동 조정하는 메타 전략 +4. **통합 구매 이력** — 실제/가상 구매를 하나의 리스트에서 관리하되, 실 구매는 시각적으로 강조 + +--- + +## 2. 접근 방식 + +**방식 1 (단일 확장) 채택**: 기존 `lotto-backend`(backend/) 서비스 내부에 모듈 추가. +- NAS Celeron J4025 환경에서 새 컨테이너 추가는 리소스 부담 +- 기존 checker/recommender/DB와 자연스러운 연동 가능 +- 파일 수준 모듈 분리로 유지보수성 확보 + +--- + +## 3. 데이터 모델 + +### 3.1 기존 `purchase_history` 테이블 마이그레이션 + +현재 스키마: +```sql +CREATE TABLE purchase_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + draw_no INTEGER NOT NULL, + amount INTEGER NOT NULL, + sets INTEGER NOT NULL DEFAULT 1, + prize INTEGER NOT NULL DEFAULT 0, + note TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) +); +``` + +마이그레이션 전략: **ALTER TABLE로 컬럼 추가** (기존 데이터 보존) + +```sql +ALTER TABLE purchase_history ADD COLUMN numbers TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE purchase_history ADD COLUMN is_real INTEGER NOT NULL DEFAULT 1; +ALTER TABLE purchase_history ADD COLUMN source_strategy TEXT NOT NULL DEFAULT 'manual'; +ALTER TABLE purchase_history ADD COLUMN source_detail TEXT NOT NULL DEFAULT '{}'; +ALTER TABLE purchase_history ADD COLUMN checked INTEGER NOT NULL DEFAULT 0; +ALTER TABLE purchase_history ADD COLUMN results TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE purchase_history ADD COLUMN total_prize INTEGER NOT NULL DEFAULT 0; +``` + +- 기존 레코드: `is_real=1`, `source_strategy='manual'`, `checked=0` (기본값) +- 기존 `prize` 컬럼은 하위호환용으로 유지. 신규 로직은 `total_prize` + `results` 사용 +- 기존 `sets` 컬럼은 하위호환용으로 유지. 신규 로직은 `numbers` JSON 배열 길이로 세트 수 산출 + +### 3.2 신규 `strategy_performance` 테이블 + +```sql +CREATE TABLE IF NOT EXISTS strategy_performance ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy TEXT NOT NULL, + draw_no INTEGER NOT NULL, + sets_count INTEGER NOT NULL DEFAULT 0, + total_correct INTEGER NOT NULL DEFAULT 0, + max_correct INTEGER NOT NULL DEFAULT 0, + prize_total INTEGER NOT NULL DEFAULT 0, + avg_score REAL NOT NULL DEFAULT 0.0, + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), + UNIQUE(strategy, draw_no) +); +``` + +### 3.3 신규 `strategy_weights` 테이블 + +```sql +CREATE TABLE IF NOT EXISTS strategy_weights ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy TEXT NOT NULL UNIQUE, + weight REAL NOT NULL DEFAULT 0.2, + ema_score REAL NOT NULL DEFAULT 0.15, + total_sets INTEGER NOT NULL DEFAULT 0, + total_hits_3plus INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')) +); +``` + +초기 가중치 (첫 실행 시 seed): + +| strategy | weight | ema_score | +|-----------|--------|-----------| +| combined | 0.30 | 0.15 | +| simulation | 0.25 | 0.15 | +| heatmap | 0.20 | 0.15 | +| manual | 0.15 | 0.15 | +| custom | 0.10 | 0.15 | + +--- + +## 4. API 설계 + +### 4.1 구매 API (기존 경로 확장) + +| 메서드 | 경로 | 변경 사항 | +|--------|------|----------| +| `POST` | `/api/lotto/purchase` | 요청 바디 확장 (numbers, is_real, source_strategy, source_detail 추가) | +| `GET` | `/api/lotto/purchase` | 필터 추가: `is_real`, `strategy`, `checked` | +| `GET` | `/api/lotto/purchase/stats` | 응답 확장: total/real/virtual + by_strategy 섹션 | +| `PUT` | `/api/lotto/purchase/{id}` | 기존 그대로 (allowed 필드 확장) | +| `DELETE` | `/api/lotto/purchase/{id}` | 기존 그대로 | + +**POST 요청 바디:** +```json +{ + "draw_no": 1125, + "numbers": [[3, 12, 23, 34, 38, 45], [7, 14, 21, 29, 36, 42]], + "is_real": true, + "amount": 2000, + "source_strategy": "combined", + "source_detail": {"recommendation_ids": [451, 452]}, + "note": "" +} +``` + +하위호환: `numbers`가 빈 배열이면 기존 방식(sets + amount만)으로 동작. `is_real` 미지정 시 기본값 `true`. + +**GET /purchase/stats 응답:** +```json +{ + "total": {"sets": 48, "invested": 48000, "prize": 15000, "roi": -68.75, "win_rate": 12.5}, + "real": {"sets": 20, "invested": 20000, "prize": 10000, "roi": -50.0, "win_rate": 15.0}, + "virtual": {"sets": 28, "invested": 28000, "prize": 5000, "roi": -82.14, "win_rate": 10.7}, + "by_strategy": { + "combined": {"sets": 15, "avg_correct": 1.8, "hits_3plus": 3, "roi": -45.0}, + "simulation": {"sets": 12, "avg_correct": 2.1, "hits_3plus": 4, "roi": -30.0} + } +} +``` + +### 4.2 전략 진화 API (신규) + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| `GET` | `/api/lotto/strategy/weights` | 현재 전략별 가중치 + 성과 요약 + trend | +| `GET` | `/api/lotto/strategy/performance` | 전략별 회차 성과 이력 (차트용, `days` 파라미터) | +| `POST` | `/api/lotto/strategy/evolve` | 수동 가중치 재계산 트리거 | + +**GET /strategy/weights 응답:** +```json +{ + "weights": [ + {"strategy": "combined", "weight": 0.32, "ema_score": 0.285, "total_sets": 15, "hits_3plus": 3, "trend": "up"}, + {"strategy": "simulation", "weight": 0.28, "ema_score": 0.312, "total_sets": 12, "hits_3plus": 4, "trend": "up"}, + {"strategy": "heatmap", "weight": 0.18, "ema_score": 0.195, "total_sets": 10, "hits_3plus": 1, "trend": "down"}, + {"strategy": "manual", "weight": 0.14, "ema_score": 0.160, "total_sets": 8, "hits_3plus": 1, "trend": "stable"}, + {"strategy": "custom", "weight": 0.08, "ema_score": 0.105, "total_sets": 3, "hits_3plus": 0, "trend": "stable"} + ], + "last_evolved": "2026-04-05T09:10:00", + "min_data_draws": 10, + "current_data_draws": 32, + "status": "active" +} +``` + +### 4.3 스마트 추천 API (신규) + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| `GET` | `/api/lotto/recommend/smart` | 전략 가중치 기반 메타 전략 추천. `sets` 파라미터 (기본 5) | + +**응답:** +```json +{ + "sets": [ + { + "numbers": [3, 12, 23, 34, 38, 45], + "meta_score": 0.847, + "source_strategy": "simulation", + "contribution": {"simulation": 0.42, "combined": 0.31, "heatmap": 0.27}, + "individual_scores": {"frequency": 0.82, "fingerprint": 0.91, "gap": 0.78, "cooccur": 0.85, "diversity": 0.73} + } + ], + "strategy_weights_used": {"combined": 0.32, "simulation": 0.28, "heatmap": 0.18, "manual": 0.14, "custom": 0.08}, + "learning_status": {"draws_learned": 32, "status": "active", "message": ""} +} +``` + +--- + +## 5. 전략 진화 알고리즘 + +### 5.1 성과 점수 산출 (회차별, 세트별) + +```python +set_score = correct_count / 6.0 + +# 당첨 등수별 보너스 +RANK_BONUS = {5: 0.1, 4: 0.3, 3: 0.6, 2: 0.8, 1: 1.0} +set_score += RANK_BONUS.get(rank, 0) + +# 한 구매 건의 draw_score = avg(set_scores) +``` + +### 5.2 EMA 갱신 + +```python +ALPHA = 0.3 # 최근 3~4회차가 EMA의 ~65% 차지 +new_ema = ALPHA * draw_score + (1 - ALPHA) * old_ema +``` + +### 5.3 가중치 변환 (Softmax) + +```python +TEMPERATURE = 2.0 +MIN_WEIGHT = 0.05 + +raw = {s: exp(ema / TEMPERATURE) for s, ema in ema_scores.items()} +total = sum(raw.values()) +weights = {s: max(v / total, MIN_WEIGHT) for s, v in raw.items()} +# 재정규화하여 합 = 1.0 +remainder = 1.0 - sum(weights.values()) +# ... 비례 배분으로 조정 +``` + +### 5.4 재계산 타이밍 + +- **자동**: `check_results_for_draw()` → purchases 체크 → strategy_performance 갱신 → weights 재계산 +- **수동**: `POST /api/lotto/strategy/evolve` + +### 5.5 스마트 추천 흐름 + +1. `strategy_weights` 로드 +2. 각 전략에서 후보 10세트 생성: + - `combined`: `generate_combined_recommendation()` x 10 + - `simulation`: `get_best_picks()` 상위 10개 + - `heatmap`: `recommend_with_heatmap()` x 10 + - `manual`: `recommend_numbers()` x 10 + - `custom`: 데이터 없으면 skip +3. `meta_score = original_score x strategy_weight` +4. 전체 풀에서 중복 제거 후 상위 N세트 선출 +5. 각 세트에 출처 전략 + 기여도 breakdown 첨부 + +### 5.6 콜드 스타트 + +- 구매 이력 0건: 초기 가중치 그대로 사용 +- 특정 전략 구매 0건: 해당 전략 EMA 초기값(0.15) 유지 +- 10회차 미만: 스마트 추천 응답에 `status: "learning"` + 기존 combined 추천 병행 + +### 5.7 Trend 판정 + +```python +recent_delta = current_ema - ema_5_draws_ago +if recent_delta > 0.02: trend = "up" +elif recent_delta < -0.02: trend = "down" +else: trend = "stable" +``` + +--- + +## 6. 체커 연동 (자동 파이프라인) + +기존 흐름에 purchase 체크를 연결: + +``` +Scheduler (09:10 / 21:10) + → sync_latest() + → 새 회차 감지 시: + → check_results_for_draw() # 기존: recommendations 체크 + → check_purchases_for_draw() # 신규: purchases 체크 + → 각 세트별 rank/correct/bonus 계산 (checker._calc_rank 재사용) + → purchases.results, total_prize, checked=1 갱신 + → strategy_performance upsert + → strategy_evolver.recalculate_weights() +``` + +--- + +## 7. 백엔드 모듈 구조 + +### 7.1 신규 파일 + +| 파일 | 역할 | +|------|------| +| `purchase_manager.py` | 구매 이력 관리 + 결과 체크 | +| `strategy_evolver.py` | EMA 계산 + 가중치 진화 + 스마트 추천 | + +### 7.2 수정 파일 + +| 파일 | 변경 내용 | +|------|----------| +| `db.py` | purchase_history ALTER + 신규 테이블 2개 + CRUD 함수 추가 | +| `main.py` | 신규 엔드포인트 9개 + Pydantic 모델 + import | +| `checker.py` | `check_results_for_draw()` 끝에 purchase 체크 호출 추가 | + +### 7.3 기존 유지 파일 (변경 없음) + +`recommender.py`, `generator.py`, `analyzer.py`, `collector.py`, `utils.py` + +--- + +## 8. 프론트엔드 변경 + +### 8.1 신규 컴포넌트 + +| 컴포넌트 | 역할 | +|----------|------| +| `SmartRecommendPanel.jsx` | 전략 진화 기반 메타 추천 + 구매 버튼 | +| `PurchaseHub.jsx` | 통합 구매 이력 (기존 PurchasePanel 대체) | +| `StrategyDashboard.jsx` | 전략 가중치 시각화 + 성과 추이 차트 | +| `PurchaseButton.jsx` | 공통 구매 버튼 (실구매/가상구매) | + +### 8.2 수정 컴포넌트 + +| 컴포넌트 | 변경 내용 | +|----------|----------| +| `CombinedRecommendPanel.jsx` | 구매 버튼(PurchaseButton) 추가 | +| `Functions.jsx` | 신규 패널 3개 추가 + import | + +### 8.3 신규 훅 + +| 훅 | 역할 | +|----|------| +| `useStrategyWeights.js` | 전략 가중치/성과 데이터 fetch | + +### 8.4 수정 훅 + +| 훅 | 변경 내용 | +|----|----------| +| `usePurchases.js` | 새 API 스키마 연동 (numbers, is_real, source_strategy 등) | + +### 8.5 API 헬퍼 추가 (`api.js`) + +```javascript +// 전략 +getStrategyWeights() // GET /api/lotto/strategy/weights +getStrategyPerformance(days) // GET /api/lotto/strategy/performance +triggerStrategyEvolve() // POST /api/lotto/strategy/evolve + +// 스마트 추천 +getSmartRecommend(sets) // GET /api/lotto/recommend/smart +``` + +### 8.6 동행복권 바로가기 + +별도 API 없음. 프론트엔드 PurchaseButton에서: +1. 번호를 클립보드에 복사 +2. `window.open('https://dhlottery.co.kr/gameResult.do?method=byWin')` — 새 탭 +3. 확인 다이얼로그 "구매 완료했나요?" → 예 → `POST /api/lotto/purchase (is_real=1)` + +### 8.7 UI 시각 구분 + +- 실 구매: 금색/강조 배경 + 지갑 아이콘 +- 가상 구매: 기본 배경 + 게임패드 아이콘 +- 미확인: 시계 아이콘 +- 당첨: 초록 하이라이트 + 체크 아이콘 + +--- + +## 9. 전체 데이터 흐름 + +``` +추천(기존) ──[구매 버튼]──→ POST /purchase + │ +스마트 추천(신규) ──[구매 버튼]──┘ + ↓ + purchase_history 테이블 + │ +매주 토요일 추첨 결과 ──→ sync_latest() + ↓ + check_results_for_draw() + ├── recommendations 체크 (기존) + └── check_purchases_for_draw() (신규) + ↓ + strategy_performance 갱신 + ↓ + recalculate_weights() + ↓ + strategy_weights 갱신 + ↓ + 다음 스마트 추천에 반영 ──→ 순환 +``` + +--- + +## 10. 비기능 요구사항 + +- **하위호환**: 기존 purchase API 사용자(프론트 PurchasePanel)는 마이그레이션 중에도 동작해야 함 +- **성능**: 스마트 추천은 각 전략 10세트 생성 → 총 50세트 중 상위 N개 선출. 1-2초 내 응답 목표 +- **데이터 안전**: ALTER TABLE은 SQLite 트랜잭션으로 안전하게 실행. 기존 데이터 유실 없음 +- **콜드 스타트**: 구매 데이터 없어도 스마트 추천 동작 (초기 가중치 사용) + +--- + +## 11. 범위 외 (추후 고려) + +- 동행복권 자동 로그인/자동 구매 (CAPTCHA + 보안 정책으로 불가) +- 번호 자동 입력 브라우저 확장 프로그램 +- 푸시 알림 (당첨 결과 통보) +- 다중 사용자 지원