"""B04 지표면 분석 FastAPI 라우터.""" import asyncio import logging from pathlib import Path from uuid import UUID from fastapi import APIRouter from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B04_wf1_Surface.B04_wf1_Surface_Engine import run_surface_analysis from B04_wf1_Surface.B04_wf1_Surface_Repository import ( create_processed_point_cloud, create_surface_model, create_terrain_layer, get_input_file, list_surface_models, ) from B04_wf1_Surface.B04_wf1_Surface_Schema import ( SurfaceAnalyzeRequest, SurfaceAnalyzeResponse, SurfaceModelListResponse, SurfaceModelSummary, ) from common_util.common_util_storage import resolve_stored_project_path from config.config_db import get_db_pool logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B04 Surface Analysis"]) @router.post("/{project_id}/surface/analyze", response_model=SurfaceAnalyzeResponse) async def analyze_surface( project_id: UUID, request: SurfaceAnalyzeRequest ) -> SurfaceAnalyzeResponse | JSONResponse: """LAS 구조화·지면 필터·지표면 모델 생성을 실행하고 DB에 기록한다.""" try: source_filters = request.resolved_filters() methods = request.resolved_methods() except ValueError as exc: return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) pool = get_db_pool() try: async with pool.acquire() as connection: stored_path = await get_project_storage_relative_path(connection, project_id) project_root = Path(resolve_stored_project_path(stored_path)) input_file = await get_input_file(connection, project_id, request.input_file_id) las_path = project_root / Path(input_file["raw_file_path"]) if not las_path.is_file(): return JSONResponse( status_code=404, content={"status": "error", "message": "원본 LAS 파일을 찾을 수 없습니다."}, ) # 무거운 지형 연산은 이벤트 루프를 막지 않도록 별도 스레드에서 실행. result = await asyncio.to_thread( run_surface_analysis, project_root, las_path, source_filters=source_filters, methods=methods, force=request.force, ) # DB 기록 (트랜잭션) await connection.begin() try: processed = result["processed"] processed_cloud_id = await create_processed_point_cloud( connection, input_file_id=request.input_file_id, project_id=project_id, process_type="structured", processed_file_path=processed["processed_file_path"], converted_format=None, converted_file_path=processed["converted_file_path"], point_count=processed["point_count"], bounds=processed["bounds"], statistics=processed["statistics"], classification_summary=None, processing_params={"filters": source_filters}, ) surface_model_ids: list[int] = [] for model in result["models"]: model_id = await create_surface_model( connection, project_id=project_id, model_type=model["model_type"], source_file_id=request.input_file_id, processed_cloud_id=processed_cloud_id, crs_epsg=input_file["crs_epsg"], resolution_m=model["resolution_m"], model_file_path=model["model_file_path"], generation_params=model["generation_params"], ) surface_model_ids.append(model_id) for layer in model["layers"]: await create_terrain_layer( connection, surface_model_id=model_id, layer_name=layer["layer_name"], geometry_type=layer["geometry_type"], layer_file_path=layer["file_path"], file_format=layer["file_format"], file_size_mb=None, statistics=None, ) await connection.commit() except Exception: await connection.rollback() raise return SurfaceAnalyzeResponse( project_id=str(project_id), ground_summary=result["ground_summary"], manifest_status=result["manifest"].get("status", "unknown"), surface_model_ids=surface_model_ids, ) except LookupError as exc: return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) except (OSError, ValueError) as exc: return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) except Exception: logger.exception("B04 지표면 분석 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "지표면 분석 처리 중 오류가 발생했습니다."}, ) @router.get("/{project_id}/surface/models", response_model=SurfaceModelListResponse) async def get_surface_models(project_id: UUID) -> SurfaceModelListResponse | JSONResponse: """프로젝트의 지표면 모델 목록을 조회한다.""" pool = get_db_pool() try: async with pool.acquire() as connection: models = await list_surface_models(connection, project_id) return SurfaceModelListResponse( project_id=str(project_id), models=[SurfaceModelSummary(**model) for model in models], ) except Exception: logger.exception("B04 지표면 모델 목록 조회 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "모델 목록 조회 중 오류가 발생했습니다."}, )