Loading api/routers/analysis.py +46 −3 Changes for api/routers/analysis.py: 46 added lines, 3 removed lines. Original line number Diff line number Diff line """분석 실행 API 엔드포인트""" import math import traceback import numpy as np import pandas as pd from fastapi import APIRouter, HTTPException from api.schemas.analysis import AnalysisRunRequest, AnalysisRunResponse from fastapi.responses import JSONResponse from api.schemas.analysis import AnalysisRunRequest from api.services import analysis_service router = APIRouter() service = analysis_service @router.post("/run", response_model=AnalysisRunResponse) def make_json_safe(value): if hasattr(value, "model_dump"): return make_json_safe(value.model_dump()) if isinstance(value, pd.DataFrame): return value.replace({np.nan: None}).to_dict(orient="records") if isinstance(value, pd.Series): return value.replace({np.nan: None}).to_dict() if isinstance(value, dict): return {key: make_json_safe(item) for key, item in value.items()} if isinstance(value, list): return [make_json_safe(item) for item in value] if isinstance(value, tuple): return [make_json_safe(item) for item in value] if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): number = float(value) if math.isnan(number): return None return number if isinstance(value, np.ndarray): return make_json_safe(value.tolist()) return value @router.post("/run") async def run_analysis(request: AnalysisRunRequest): """ 품질 분석 실행 Loading Loading @@ -40,6 +82,7 @@ async def run_analysis(request: AnalysisRunRequest): """ try: result = await service.execute(request) return result return JSONResponse(content=make_json_safe(result.model_dump())) except Exception as e: traceback.print_exc() raise HTTPException(status_code=500, detail=str(e)) api/routers/results.py +51 −5 Changes for api/routers/results.py: 51 added lines, 5 removed lines. Original line number Diff line number Diff line """결과 조회 API 엔드포인트""" import math import numpy as np import pandas as pd from fastapi import APIRouter, HTTPException from api.schemas.analysis import AnalysisSummaryResponse, MetricDetailResponse from fastapi.responses import JSONResponse from api.schemas.analysis import AnalysisSummaryResponse from api.services import analysis_service router = APIRouter() service = analysis_service def make_json_safe(value): if hasattr(value, "model_dump"): return make_json_safe(value.model_dump()) if isinstance(value, pd.DataFrame): return value.replace({np.nan: None}).to_dict(orient="records") if isinstance(value, pd.Series): return value.replace({np.nan: None}).to_dict() if isinstance(value, dict): return {key: make_json_safe(item) for key, item in value.items()} if isinstance(value, list): return [make_json_safe(item) for item in value] if isinstance(value, tuple): return [make_json_safe(item) for item in value] if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): number = float(value) if math.isnan(number): return None return number if isinstance(value, np.ndarray): return make_json_safe(value.tolist()) try: if pd.isna(value): return None except (TypeError, ValueError): pass return value @router.get("/{analysis_id}/summary", response_model=AnalysisSummaryResponse) async def get_analysis_summary(analysis_id: str): """ Loading @@ -22,7 +68,7 @@ async def get_analysis_summary(analysis_id: str): return result @router.get("/{analysis_id}/details/{metric}", response_model=MetricDetailResponse) @router.get("/{analysis_id}/details/{metric}") async def get_metric_detail(analysis_id: str, metric: str): """ 개별 지표 상세 결과 조회 Loading @@ -32,7 +78,7 @@ async def get_metric_detail(analysis_id: str, metric: str): result = service.get_metric_detail(analysis_id, metric) if not result: raise HTTPException(status_code=404, detail="Result not found") return result return JSONResponse(content=make_json_safe(result)) @router.get("/{analysis_id}/rules/{metric}") Loading @@ -43,7 +89,7 @@ async def get_metric_rules(analysis_id: str, metric: str): result = service.get_rule_results(analysis_id, metric) if not result: raise HTTPException(status_code=404, detail="Rule results not found") return result return JSONResponse(content=make_json_safe(result)) @router.get("/{analysis_id}/samples/{metric}") Loading @@ -56,4 +102,4 @@ async def get_metric_samples(analysis_id: str, metric: str, limit: int = 50): result = service.get_samples(analysis_id, metric, limit) if not result: raise HTTPException(status_code=404, detail="Samples not found") return result return JSONResponse(content=make_json_safe(result)) api/services/analysis_service.py +47 −3 Changes for api/services/analysis_service.py: 47 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,12 @@ 분석 실행 서비스 - 기존 pipeline.py의 run_analysis()를 감싸는 계층 """ import math import uuid from typing import Optional, Any, Callable from fastapi.encoders import jsonable_encoder import numpy as np import pandas as pd from api.database import SessionLocal from api.model.analysis_history import AnalysisHistory Loading Loading @@ -33,6 +35,7 @@ NAME_TO_LEGACY = { } LEGACY_TO_NAME = {v: k for k, v in NAME_TO_LEGACY.items()} # 지표별 detail 키 prefix METRIC_PREFIX = { "completeness": "comp_", Loading @@ -44,6 +47,47 @@ METRIC_PREFIX = { } def make_json_safe(value): """Convert pandas/numpy objects into JSON-serializable Python objects.""" if isinstance(value, pd.DataFrame): return value.replace({np.nan: None}).to_dict(orient="records") if isinstance(value, pd.Series): return value.replace({np.nan: None}).to_dict() if isinstance(value, dict): return {key: make_json_safe(item) for key, item in value.items()} if isinstance(value, list): return [make_json_safe(item) for item in value] if isinstance(value, tuple): return [make_json_safe(item) for item in value] if isinstance(value, set): return [make_json_safe(item) for item in value] if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): number = float(value) if math.isnan(number): return None return number if isinstance(value, np.ndarray): return make_json_safe(value.tolist()) try: if pd.isna(value): return None except (TypeError, ValueError): pass return value class AnalysisService: """분석 실행 및 결과 관리 서비스""" Loading Loading @@ -113,8 +157,8 @@ class AnalysisService: existing.dataset_id = stored.get("dataset_id") existing.dataset_filename = stored.get("dataset_filename") existing.results_json = jsonable_encoder(stored.get("results", {}) or {}) existing.config_json = jsonable_encoder(config_for_history) existing.results_json = make_json_safe(stored.get("results", {}) or {}) existing.config_json = make_json_safe(config_for_history) overall_score = weighted_summary.get("overall_score") existing.overall_score = str(overall_score) if overall_score is not None else None existing.weighting_method = weighted_summary.get("weighting_method") Loading Loading
api/routers/analysis.py +46 −3 Changes for api/routers/analysis.py: 46 added lines, 3 removed lines. Original line number Diff line number Diff line """분석 실행 API 엔드포인트""" import math import traceback import numpy as np import pandas as pd from fastapi import APIRouter, HTTPException from api.schemas.analysis import AnalysisRunRequest, AnalysisRunResponse from fastapi.responses import JSONResponse from api.schemas.analysis import AnalysisRunRequest from api.services import analysis_service router = APIRouter() service = analysis_service @router.post("/run", response_model=AnalysisRunResponse) def make_json_safe(value): if hasattr(value, "model_dump"): return make_json_safe(value.model_dump()) if isinstance(value, pd.DataFrame): return value.replace({np.nan: None}).to_dict(orient="records") if isinstance(value, pd.Series): return value.replace({np.nan: None}).to_dict() if isinstance(value, dict): return {key: make_json_safe(item) for key, item in value.items()} if isinstance(value, list): return [make_json_safe(item) for item in value] if isinstance(value, tuple): return [make_json_safe(item) for item in value] if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): number = float(value) if math.isnan(number): return None return number if isinstance(value, np.ndarray): return make_json_safe(value.tolist()) return value @router.post("/run") async def run_analysis(request: AnalysisRunRequest): """ 품질 분석 실행 Loading Loading @@ -40,6 +82,7 @@ async def run_analysis(request: AnalysisRunRequest): """ try: result = await service.execute(request) return result return JSONResponse(content=make_json_safe(result.model_dump())) except Exception as e: traceback.print_exc() raise HTTPException(status_code=500, detail=str(e))
api/routers/results.py +51 −5 Changes for api/routers/results.py: 51 added lines, 5 removed lines. Original line number Diff line number Diff line """결과 조회 API 엔드포인트""" import math import numpy as np import pandas as pd from fastapi import APIRouter, HTTPException from api.schemas.analysis import AnalysisSummaryResponse, MetricDetailResponse from fastapi.responses import JSONResponse from api.schemas.analysis import AnalysisSummaryResponse from api.services import analysis_service router = APIRouter() service = analysis_service def make_json_safe(value): if hasattr(value, "model_dump"): return make_json_safe(value.model_dump()) if isinstance(value, pd.DataFrame): return value.replace({np.nan: None}).to_dict(orient="records") if isinstance(value, pd.Series): return value.replace({np.nan: None}).to_dict() if isinstance(value, dict): return {key: make_json_safe(item) for key, item in value.items()} if isinstance(value, list): return [make_json_safe(item) for item in value] if isinstance(value, tuple): return [make_json_safe(item) for item in value] if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): number = float(value) if math.isnan(number): return None return number if isinstance(value, np.ndarray): return make_json_safe(value.tolist()) try: if pd.isna(value): return None except (TypeError, ValueError): pass return value @router.get("/{analysis_id}/summary", response_model=AnalysisSummaryResponse) async def get_analysis_summary(analysis_id: str): """ Loading @@ -22,7 +68,7 @@ async def get_analysis_summary(analysis_id: str): return result @router.get("/{analysis_id}/details/{metric}", response_model=MetricDetailResponse) @router.get("/{analysis_id}/details/{metric}") async def get_metric_detail(analysis_id: str, metric: str): """ 개별 지표 상세 결과 조회 Loading @@ -32,7 +78,7 @@ async def get_metric_detail(analysis_id: str, metric: str): result = service.get_metric_detail(analysis_id, metric) if not result: raise HTTPException(status_code=404, detail="Result not found") return result return JSONResponse(content=make_json_safe(result)) @router.get("/{analysis_id}/rules/{metric}") Loading @@ -43,7 +89,7 @@ async def get_metric_rules(analysis_id: str, metric: str): result = service.get_rule_results(analysis_id, metric) if not result: raise HTTPException(status_code=404, detail="Rule results not found") return result return JSONResponse(content=make_json_safe(result)) @router.get("/{analysis_id}/samples/{metric}") Loading @@ -56,4 +102,4 @@ async def get_metric_samples(analysis_id: str, metric: str, limit: int = 50): result = service.get_samples(analysis_id, metric, limit) if not result: raise HTTPException(status_code=404, detail="Samples not found") return result return JSONResponse(content=make_json_safe(result))
api/services/analysis_service.py +47 −3 Changes for api/services/analysis_service.py: 47 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -2,10 +2,12 @@ 분석 실행 서비스 - 기존 pipeline.py의 run_analysis()를 감싸는 계층 """ import math import uuid from typing import Optional, Any, Callable from fastapi.encoders import jsonable_encoder import numpy as np import pandas as pd from api.database import SessionLocal from api.model.analysis_history import AnalysisHistory Loading Loading @@ -33,6 +35,7 @@ NAME_TO_LEGACY = { } LEGACY_TO_NAME = {v: k for k, v in NAME_TO_LEGACY.items()} # 지표별 detail 키 prefix METRIC_PREFIX = { "completeness": "comp_", Loading @@ -44,6 +47,47 @@ METRIC_PREFIX = { } def make_json_safe(value): """Convert pandas/numpy objects into JSON-serializable Python objects.""" if isinstance(value, pd.DataFrame): return value.replace({np.nan: None}).to_dict(orient="records") if isinstance(value, pd.Series): return value.replace({np.nan: None}).to_dict() if isinstance(value, dict): return {key: make_json_safe(item) for key, item in value.items()} if isinstance(value, list): return [make_json_safe(item) for item in value] if isinstance(value, tuple): return [make_json_safe(item) for item in value] if isinstance(value, set): return [make_json_safe(item) for item in value] if isinstance(value, np.integer): return int(value) if isinstance(value, np.floating): number = float(value) if math.isnan(number): return None return number if isinstance(value, np.ndarray): return make_json_safe(value.tolist()) try: if pd.isna(value): return None except (TypeError, ValueError): pass return value class AnalysisService: """분석 실행 및 결과 관리 서비스""" Loading Loading @@ -113,8 +157,8 @@ class AnalysisService: existing.dataset_id = stored.get("dataset_id") existing.dataset_filename = stored.get("dataset_filename") existing.results_json = jsonable_encoder(stored.get("results", {}) or {}) existing.config_json = jsonable_encoder(config_for_history) existing.results_json = make_json_safe(stored.get("results", {}) or {}) existing.config_json = make_json_safe(config_for_history) overall_score = weighted_summary.get("overall_score") existing.overall_score = str(overall_score) if overall_score is not None else None existing.weighting_method = weighted_summary.get("weighting_method") Loading