diff --git a/lotto/app/db.py b/lotto/app/db.py index eacd8d1..e840dde 100644 --- a/lotto/app/db.py +++ b/lotto/app/db.py @@ -991,6 +991,51 @@ def update_purchase_results(purchase_id: int, results: list, total_prize: int) - ) +def bulk_insert_purchases_from_briefing(draw_no: int, tier_mode: str, amount: int) -> Dict[str, Any]: + """tier_mode 에 해당하는 큐레이터 picks 를 purchase_history 에 일괄 INSERT. + + tier_mode: "core" | "core_bonus" | "core_bonus_extended" | "full" + """ + briefing = get_briefing(draw_no) + if not briefing: + return {"ok": False, "reason": "briefing not found"} + + picks = briefing.get("picks") or {} + if isinstance(picks, list): + # 마이그레이션 이전 형태 + picks = {"core": picks, "bonus": [], "extended": [], "pool": []} + + tier_chain = { + "core": ["core"], + "core_bonus": ["core", "bonus"], + "core_bonus_extended": ["core", "bonus", "extended"], + "full": ["core", "bonus", "extended", "pool"], + }.get(tier_mode) + if not tier_chain: + return {"ok": False, "reason": f"unknown tier_mode: {tier_mode}"} + + inserted_ids = [] + with _conn() as conn: + for tier in tier_chain: + for idx, pick in enumerate(picks.get(tier) or []): + source_strategy = f"curator_{tier}" + source_detail = json.dumps({ + "tier": tier, + "role": pick.get("risk_tag"), + "set_index": idx, + "draw_no": draw_no, + }, ensure_ascii=False) + numbers_json = json.dumps([pick.get("numbers")], ensure_ascii=False) + cur = conn.execute( + """INSERT INTO purchase_history + (draw_no, amount, sets, prize, note, numbers, is_real, source_strategy, source_detail) + VALUES (?, ?, 1, 0, '', ?, 1, ?, ?)""", + (draw_no, 1000, numbers_json, source_strategy, source_detail), + ) + inserted_ids.append(cur.lastrowid) + return {"ok": True, "inserted_ids": inserted_ids, "sets": len(inserted_ids)} + + # --- Lotto Briefings --- def save_briefing(data: Dict[str, Any]) -> int: diff --git a/lotto/app/main.py b/lotto/app/main.py index 301f1dc..c8aa3e0 100644 --- a/lotto/app/main.py +++ b/lotto/app/main.py @@ -19,6 +19,7 @@ from .db import ( get_recommendation_performance, # Phase 2: 구매 이력 add_purchase, get_purchases, update_purchase, delete_purchase, get_purchase_stats, + bulk_insert_purchases_from_briefing, # Phase 2: 주간 리포트 캐시 save_weekly_report, get_weekly_report_list, get_weekly_report, # Phase 2: 개인 패턴 분석 @@ -343,6 +344,22 @@ def api_purchase_delete(purchase_id: int): return {"ok": True} +class BulkPurchaseRequest(BaseModel): + draw_no: int + tier_mode: str # core | core_bonus | core_bonus_extended | full + sets: int # 검증용 — 실제 INSERT는 briefing 기준 + amount: int # 검증용 + + +@app.post("/api/lotto/purchase/bulk", status_code=201) +def api_purchase_bulk(body: BulkPurchaseRequest): + """결정카드 원클릭 기록 — 큐레이터 브리핑 picks 를 tier_mode 기준으로 일괄 기록.""" + result = bulk_insert_purchases_from_briefing(body.draw_no, body.tier_mode, body.amount) + if not result["ok"]: + raise HTTPException(status_code=400, detail=result["reason"]) + return result + + # ── 전략 진화 API ────────────────────────────────────────────────────────── @app.get("/api/lotto/strategy/weights") diff --git a/lotto/tests/test_bulk_purchase.py b/lotto/tests/test_bulk_purchase.py new file mode 100644 index 0000000..af044e2 --- /dev/null +++ b/lotto/tests/test_bulk_purchase.py @@ -0,0 +1,53 @@ +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest +from app import db + + +@pytest.fixture(autouse=True) +def setup_db(tmp_path, monkeypatch): + test_db = tmp_path / "test.db" + monkeypatch.setattr(db, "DB_PATH", str(test_db)) + db.init_db() + yield + + +def _seed_briefing(drw=1153): + picks = { + "core": [{"numbers": [1, 2, 3, 4, 5, 6], "risk_tag": "안정", "reason": "x"}] * 5, + "bonus": [{"numbers": [7, 8, 9, 10, 11, 12], "risk_tag": "균형", "reason": "x"}] * 5, + "extended": [{"numbers": [13, 14, 15, 16, 17, 18], "risk_tag": "공격", "reason": "x"}] * 5, + "pool": [{"numbers": [19, 20, 21, 22, 23, 24], "risk_tag": "안정", "reason": "x"}] * 5, + } + db.save_briefing({ + "draw_no": drw, "picks": picks, + "narrative": {"headline": "h", "summary_3lines": ["a", "b", "c"]}, + "confidence": 70, "model": "test", + }) + + +def test_bulk_core_inserts_5(): + _seed_briefing() + r = db.bulk_insert_purchases_from_briefing(1153, "core", 5000) + assert r["ok"] and r["sets"] == 5 + rows = db.get_purchases(draw_no=1153) + assert len(rows) == 5 + assert all(row["source_strategy"] == "curator_core" for row in rows) + + +def test_bulk_full_inserts_20(): + _seed_briefing() + r = db.bulk_insert_purchases_from_briefing(1153, "full", 20000) + assert r["ok"] and r["sets"] == 20 + + +def test_bulk_unknown_tier_mode(): + _seed_briefing() + r = db.bulk_insert_purchases_from_briefing(1153, "garbage", 1000) + assert r["ok"] is False and "garbage" in r["reason"] + + +def test_bulk_no_briefing(): + r = db.bulk_insert_purchases_from_briefing(9999, "core", 5000) + assert r["ok"] is False and "not found" in r["reason"]