# 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 + 보안 정책으로 불가) - 번호 자동 입력 브라우저 확장 프로그램 - 푸시 알림 (당첨 결과 통보) - 다중 사용자 지원