diff --git a/saju-lab/app/db.py b/saju-lab/app/db.py index 078018e..5bdad29 100644 --- a/saju-lab/app/db.py +++ b/saju-lab/app/db.py @@ -46,6 +46,17 @@ def init_db() -> None: CREATE INDEX IF NOT EXISTS idx_saju_created ON saju_records(created_at DESC) """) + # 신규 컬럼 ALTER (idempotent — 이미 있으면 OperationalError로 skip) + for col in ( + "fortune_scores_json TEXT", + "lucky_json TEXT", + "monthly_flow_json TEXT", + ): + try: + conn.execute(f"ALTER TABLE saju_records ADD COLUMN {col}") + except sqlite3.OperationalError: + pass + conn.execute(""" CREATE TABLE IF NOT EXISTS compat_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -76,8 +87,9 @@ def save_saju_record(data: Dict[str, Any]) -> int: """INSERT INTO saju_records (birth_year, birth_month, birth_day, birth_hour, gender, calendar_type, saju_data, analysis_data, daeun_data, interpretation_json, - model, tokens_in, tokens_out, cost_usd, latency_ms, reroll_count) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + model, tokens_in, tokens_out, cost_usd, latency_ms, reroll_count, + fortune_scores_json, lucky_json, monthly_flow_json) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", ( data["birth_year"], data["birth_month"], data["birth_day"], data.get("birth_hour"), data["gender"], data.get("calendar_type", "solar"), @@ -89,6 +101,9 @@ def save_saju_record(data: Dict[str, Any]) -> int: data.get("tokens_in", 0), data.get("tokens_out", 0), data.get("cost_usd", 0.0), data.get("latency_ms", 0), data.get("reroll_count", 0), + json.dumps(data["fortune_scores_json"], ensure_ascii=False) if data.get("fortune_scores_json") else None, + json.dumps(data["lucky_json"], ensure_ascii=False) if data.get("lucky_json") else None, + json.dumps(data["monthly_flow_json"], ensure_ascii=False) if data.get("monthly_flow_json") else None, ), ) return int(cur.lastrowid) @@ -138,18 +153,29 @@ def delete_saju_record(record_id: int) -> None: def _saju_row_to_dict(r) -> Dict[str, Any]: + def _safe_json(val): + if val is None: + return None + try: + return json.loads(val) + except (ValueError, TypeError): + return None + return { "id": r["id"], "created_at": r["created_at"], "birth_year": r["birth_year"], "birth_month": r["birth_month"], "birth_day": r["birth_day"], "birth_hour": r["birth_hour"], "gender": r["gender"], "calendar_type": r["calendar_type"], - "saju_data": json.loads(r["saju_data"]) if r["saju_data"] else None, - "analysis_data": json.loads(r["analysis_data"]) if r["analysis_data"] else None, - "daeun_data": json.loads(r["daeun_data"]) if r["daeun_data"] else None, - "interpretation_json": json.loads(r["interpretation_json"]) if r["interpretation_json"] else None, + "saju_data": _safe_json(r["saju_data"]), + "analysis_data": _safe_json(r["analysis_data"]), + "daeun_data": _safe_json(r["daeun_data"]), + "interpretation_json": _safe_json(r["interpretation_json"]), "model": r["model"], "tokens_in": r["tokens_in"], "tokens_out": r["tokens_out"], "cost_usd": r["cost_usd"], "latency_ms": r["latency_ms"], "reroll_count": r["reroll_count"], "favorite": int(r["favorite"]), "memo": r["memo"], + "fortune_scores": _safe_json(r["fortune_scores_json"]) if "fortune_scores_json" in r.keys() else None, + "lucky": _safe_json(r["lucky_json"]) if "lucky_json" in r.keys() else None, + "monthly_flow": _safe_json(r["monthly_flow_json"]) if "monthly_flow_json" in r.keys() else None, } diff --git a/saju-lab/tests/test_db_migration.py b/saju-lab/tests/test_db_migration.py new file mode 100644 index 0000000..76db7b7 --- /dev/null +++ b/saju-lab/tests/test_db_migration.py @@ -0,0 +1,66 @@ +import sqlite3 +import pytest + +from app import db as db_module + + +@pytest.fixture(autouse=True) +def fresh_db(monkeypatch, tmp_path): + db_file = tmp_path / "test_saju.db" + monkeypatch.setattr(db_module, "DB_PATH", str(db_file)) + yield + try: + if db_file.exists(): + db_file.unlink() + except PermissionError: + pass + + +def test_new_columns_exist(): + db_module.init_db() + conn = sqlite3.connect(db_module.DB_PATH) + cols = [row[1] for row in conn.execute("PRAGMA table_info(saju_records)").fetchall()] + conn.close() + assert "fortune_scores_json" in cols + assert "lucky_json" in cols + assert "monthly_flow_json" in cols + + +def test_idempotent_init(): + db_module.init_db() + db_module.init_db() # 두 번째 호출 — ALTER TABLE이 OperationalError 캐치 + + +def test_save_and_get_with_new_fields(): + db_module.init_db() + rid = db_module.save_saju_record({ + "birth_year": 1990, "birth_month": 5, "birth_day": 15, "birth_hour": 14, + "gender": "male", "calendar_type": "solar", + "saju_data": {"day_stem": "辛"}, + "analysis_data": {"element_balance": {"金": 3.0}}, + "daeun_data": [{"age": 10}], + "interpretation_json": {"items": []}, + "model": "claude-sonnet-4-6", + "tokens_in": 100, "tokens_out": 200, "cost_usd": 0.005, + "fortune_scores_json": {"wealth": 80, "romance": 60, "social": 70, "career": 75, "overall": 73}, + "lucky_json": {"color": ["청록"], "number": 5, "direction": "동쪽", "good_signs": ["S1"], "warnings": []}, + "monthly_flow_json": [{"month": 1, "stem": "壬", "branch": "寅", "score": 65, "label": "변동"}], + }) + row = db_module.get_saju_record(rid) + assert row["fortune_scores"]["wealth"] == 80 + assert row["lucky"]["number"] == 5 + assert row["monthly_flow"][0]["month"] == 1 + + +def test_save_without_new_fields_backwards_compat(): + db_module.init_db() + rid = db_module.save_saju_record({ + "birth_year": 1990, "birth_month": 5, "birth_day": 15, "birth_hour": None, + "gender": "male", + "saju_data": {}, "analysis_data": {}, "daeun_data": [], + "model": "x", + }) + row = db_module.get_saju_record(rid) + assert row["fortune_scores"] is None + assert row["lucky"] is None + assert row["monthly_flow"] is None