feat(lotto): POST /api/lotto/purchase/bulk — 결정카드 원클릭 기록
This commit is contained in:
@@ -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 ---
|
# --- Lotto Briefings ---
|
||||||
|
|
||||||
def save_briefing(data: Dict[str, Any]) -> int:
|
def save_briefing(data: Dict[str, Any]) -> int:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from .db import (
|
|||||||
get_recommendation_performance,
|
get_recommendation_performance,
|
||||||
# Phase 2: 구매 이력
|
# Phase 2: 구매 이력
|
||||||
add_purchase, get_purchases, update_purchase, delete_purchase, get_purchase_stats,
|
add_purchase, get_purchases, update_purchase, delete_purchase, get_purchase_stats,
|
||||||
|
bulk_insert_purchases_from_briefing,
|
||||||
# Phase 2: 주간 리포트 캐시
|
# Phase 2: 주간 리포트 캐시
|
||||||
save_weekly_report, get_weekly_report_list, get_weekly_report,
|
save_weekly_report, get_weekly_report_list, get_weekly_report,
|
||||||
# Phase 2: 개인 패턴 분석
|
# Phase 2: 개인 패턴 분석
|
||||||
@@ -343,6 +344,22 @@ def api_purchase_delete(purchase_id: int):
|
|||||||
return {"ok": True}
|
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 ──────────────────────────────────────────────────────────
|
# ── 전략 진화 API ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/api/lotto/strategy/weights")
|
@app.get("/api/lotto/strategy/weights")
|
||||||
|
|||||||
53
lotto/tests/test_bulk_purchase.py
Normal file
53
lotto/tests/test_bulk_purchase.py
Normal file
@@ -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"]
|
||||||
Reference in New Issue
Block a user