Phase 2 백엔드 마지막 task — FastAPI app · 11 endpoint · 10 route tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""compat API — /api/saju/compat/* 5 endpoints."""
|
|
from fastapi import APIRouter, HTTPException
|
|
from typing import Optional
|
|
|
|
from ..models import (
|
|
CompatInterpretRequest,
|
|
CompatInterpretResponse,
|
|
CompatPatchRequest,
|
|
)
|
|
from ..interpret import pipeline
|
|
from ..calculator.core import calculate_saju
|
|
from ..calculator.analysis import perform_full_analysis
|
|
from ..calculator.compatibility import calculate_compatibility
|
|
from ..calculator.lunar import lunar_to_solar
|
|
from .. import db as db_module
|
|
|
|
|
|
router = APIRouter(prefix="/api/saju/compat")
|
|
|
|
|
|
def _calc_one(p) -> tuple[dict, dict]:
|
|
"""한 사람의 입력 → (saju, analysis)."""
|
|
if p.calendar_type == "lunar":
|
|
sy, sm, sd = lunar_to_solar(p.year, p.month, p.day, p.is_leap_month)
|
|
else:
|
|
sy, sm, sd = p.year, p.month, p.day
|
|
saju = calculate_saju(sy, sm, sd, p.hour, p.gender)
|
|
analysis = perform_full_analysis(saju, 2026)
|
|
return saju, analysis
|
|
|
|
|
|
@router.post("/interpret", response_model=CompatInterpretResponse)
|
|
async def interpret_compat_endpoint(req: CompatInterpretRequest):
|
|
try:
|
|
saju_a, analysis_a = _calc_one(req.person_a)
|
|
saju_b, analysis_b = _calc_one(req.person_b)
|
|
compat = calculate_compatibility(saju_a, saju_b)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=f"계산 실패: {e}")
|
|
|
|
try:
|
|
interp_result = await pipeline.interpret_compat(
|
|
saju_a, saju_b, analysis_a, analysis_b,
|
|
compat["score"], compat["breakdown"],
|
|
)
|
|
except pipeline.SajuError as e:
|
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
|
|
|
rid = db_module.save_compat_record({
|
|
"person_a": req.person_a.model_dump(),
|
|
"person_b": req.person_b.model_dump(),
|
|
"saju_a": saju_a, "saju_b": saju_b,
|
|
"score": compat["score"],
|
|
"breakdown": compat["breakdown"],
|
|
"interpretation_json": interp_result["interpretation_json"],
|
|
"model": interp_result["model"],
|
|
"tokens_in": interp_result["tokens_in"],
|
|
"tokens_out": interp_result["tokens_out"],
|
|
"cost_usd": interp_result["cost_usd"],
|
|
"latency_ms": interp_result["latency_ms"],
|
|
"reroll_count": interp_result["reroll_count"],
|
|
})
|
|
|
|
return {
|
|
"saju_a": saju_a, "saju_b": saju_b,
|
|
"score": compat["score"],
|
|
"breakdown": compat["breakdown"],
|
|
"interpretation_json": interp_result["interpretation_json"],
|
|
"reading_id": rid,
|
|
"model": interp_result["model"],
|
|
"tokens_in": interp_result["tokens_in"],
|
|
"tokens_out": interp_result["tokens_out"],
|
|
"cost_usd": interp_result["cost_usd"],
|
|
"latency_ms": interp_result["latency_ms"],
|
|
"reroll_count": interp_result["reroll_count"],
|
|
}
|
|
|
|
|
|
@router.get("/readings")
|
|
async def list_readings(page: int = 1, size: int = 20, favorite: Optional[bool] = None):
|
|
return db_module.list_compat_records(page=page, size=size, favorite=favorite)
|
|
|
|
|
|
@router.get("/readings/{reading_id}")
|
|
async def get_reading(reading_id: int):
|
|
row = db_module.get_compat_record(reading_id)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="reading not found")
|
|
return row
|
|
|
|
|
|
@router.patch("/readings/{reading_id}")
|
|
async def patch_reading(reading_id: int, req: CompatPatchRequest):
|
|
row = db_module.get_compat_record(reading_id)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="reading not found")
|
|
db_module.update_compat_record(reading_id, **req.model_dump(exclude_none=True))
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/readings/{reading_id}")
|
|
async def delete_reading(reading_id: int):
|
|
row = db_module.get_compat_record(reading_id)
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail="reading not found")
|
|
db_module.delete_compat_record(reading_id)
|
|
return {"ok": True}
|