feat(saju-lab): db.py — saju_records 3 컬럼 추가 (fortune_scores/lucky/monthly_flow) + 4 마이그레이션 테스트

This commit is contained in:
2026-05-26 08:05:41 +09:00
parent 030367da6c
commit f57c790437
2 changed files with 98 additions and 6 deletions

View File

@@ -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,
}

View File

@@ -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