From e1ae0f75018e133de650eebd39e8685088f90a4a Mon Sep 17 00:00:00 2001 From: gahusb Date: Wed, 15 Apr 2026 08:18:20 +0900 Subject: [PATCH] =?UTF-8?q?feat(lotto):=20lotto=5Fbriefings=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20+=20CRUD=20=ED=95=A8=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- backend/app/db.py | 121 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/backend/app/db.py b/backend/app/db.py index b6e7774..1a26bb9 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -277,6 +277,26 @@ def init_db() -> None: conn.execute("CREATE INDEX IF NOT EXISTS idx_purchase_strategy ON purchase_history(source_strategy)") conn.execute("CREATE INDEX IF NOT EXISTS idx_purchase_checked ON purchase_history(draw_no, checked)") + # ── lotto_briefings 테이블 ───────────────────────────────────────────── + conn.execute(""" + CREATE TABLE IF NOT EXISTS lotto_briefings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + draw_no INTEGER UNIQUE NOT NULL, + picks TEXT NOT NULL, + narrative TEXT NOT NULL, + confidence INTEGER NOT NULL, + model TEXT NOT NULL, + tokens_input INTEGER NOT NULL DEFAULT 0, + tokens_output INTEGER NOT NULL DEFAULT 0, + cache_read INTEGER NOT NULL DEFAULT 0, + cache_write INTEGER NOT NULL DEFAULT 0, + latency_ms INTEGER NOT NULL DEFAULT 0, + source TEXT NOT NULL DEFAULT 'auto', + generated_at TEXT NOT NULL DEFAULT (datetime('now','localtime')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_briefings_draw ON lotto_briefings(draw_no DESC)") + # ── todos CRUD ─────────────────────────────────────────────────────────────── @@ -1096,3 +1116,104 @@ def update_purchase_results(purchase_id: int, results: list, total_prize: int) - (json.dumps(results, ensure_ascii=False), total_prize, purchase_id), ) + +# --- Lotto Briefings --- + +def save_briefing(data: Dict[str, Any]) -> int: + with _conn() as conn: + cur = conn.execute(""" + INSERT INTO lotto_briefings + (draw_no, picks, narrative, confidence, model, + tokens_input, tokens_output, cache_read, cache_write, + latency_ms, source) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(draw_no) DO UPDATE SET + picks=excluded.picks, narrative=excluded.narrative, + confidence=excluded.confidence, model=excluded.model, + tokens_input=excluded.tokens_input, + tokens_output=excluded.tokens_output, + cache_read=excluded.cache_read, + cache_write=excluded.cache_write, + latency_ms=excluded.latency_ms, + source=excluded.source, + generated_at=datetime('now','localtime') + """, ( + data["draw_no"], + json.dumps(data["picks"], ensure_ascii=False), + json.dumps(data["narrative"], ensure_ascii=False), + int(data["confidence"]), + data["model"], + int(data.get("tokens_input", 0)), + int(data.get("tokens_output", 0)), + int(data.get("cache_read", 0)), + int(data.get("cache_write", 0)), + int(data.get("latency_ms", 0)), + data.get("source", "auto"), + )) + return cur.lastrowid + + +def _briefing_row(r) -> Dict[str, Any]: + return { + "id": r["id"], + "draw_no": r["draw_no"], + "picks": json.loads(r["picks"]), + "narrative": json.loads(r["narrative"]), + "confidence": r["confidence"], + "model": r["model"], + "tokens_input": r["tokens_input"], + "tokens_output": r["tokens_output"], + "cache_read": r["cache_read"], + "cache_write": r["cache_write"], + "latency_ms": r["latency_ms"], + "source": r["source"], + "generated_at": r["generated_at"], + } + + +def get_latest_briefing() -> Optional[Dict[str, Any]]: + with _conn() as conn: + r = conn.execute("SELECT * FROM lotto_briefings ORDER BY draw_no DESC LIMIT 1").fetchone() + return _briefing_row(r) if r else None + + +def get_briefing(draw_no: int) -> Optional[Dict[str, Any]]: + with _conn() as conn: + r = conn.execute("SELECT * FROM lotto_briefings WHERE draw_no=?", (draw_no,)).fetchone() + return _briefing_row(r) if r else None + + +def list_briefings(limit: int = 10) -> List[Dict[str, Any]]: + with _conn() as conn: + rows = conn.execute( + "SELECT * FROM lotto_briefings ORDER BY draw_no DESC LIMIT ?", + (limit,), + ).fetchall() + return [_briefing_row(r) for r in rows] + + +def get_curator_usage(days: int = 30) -> Dict[str, Any]: + with _conn() as conn: + r = conn.execute(""" + SELECT COUNT(*) AS calls, + SUM(tokens_input) AS in_tokens, + SUM(tokens_output) AS out_tokens, + SUM(cache_read) AS cache_read, + SUM(cache_write) AS cache_write, + AVG(latency_ms) AS avg_latency + FROM lotto_briefings + WHERE generated_at >= datetime('now', ?, 'localtime') + """, (f"-{int(days)} days",)).fetchone() + cr = int(r["cache_read"] or 0) + cw = int(r["cache_write"] or 0) + return { + "days": days, + "calls": int(r["calls"] or 0), + "tokens_input": int(r["in_tokens"] or 0), + "tokens_output": int(r["out_tokens"] or 0), + "cache_read": cr, + "cache_write": cw, + "cache_hit_rate": round(cr / (cr + cw), 3) if (cr + cw) > 0 else 0.0, + "avg_latency_ms": round(float(r["avg_latency"] or 0), 1), + } +