"""B05 경로 설계 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 B05_wf2_Route.B05_wf2_Route_Engine import run_route_design from B05_wf2_Route.B05_wf2_Route_Repository import ( confirm_route, create_route, create_route_statistics, get_latest_route, insert_route_points, ) from B05_wf2_Route.B05_wf2_Route_Schema import ( RouteConfirmResponse, RouteSolveRequest, RouteSolveResponse, ) from common_util.common_util_storage import resolve_stored_project_path from common_util.common_util_workflow_state import complete_stage, fail_stage, start_stage from config.config_db import get_db_pool logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["B05 Route Design"]) @router.post("/{project_id}/route/solve", response_model=RouteSolveResponse) async def solve_route( project_id: UUID, request: RouteSolveRequest ) -> RouteSolveResponse | JSONResponse: """경로 탐색을 실행하고 결과를 GeoJSON 저장 + DB 기록한다.""" pool = get_db_pool() try: params = { "filter_key": request.filter_key, "method": request.method, "smooth": request.smooth, "points": request.points, "options": request.options(), "algorithm": request.algorithm, "surface_model_id": request.surface_model_id, } async with pool.acquire() as connection: async with connection.cursor() as cursor: await start_stage(cursor, str(project_id), 2, params) await connection.commit() stored_path = await get_project_storage_relative_path(connection, project_id) project_root = Path(resolve_stored_project_path(stored_path)) # 무거운 경로 탐색은 이벤트 루프를 막지 않도록 별도 스레드에서 실행. design = await asyncio.to_thread( run_route_design, project_root, request.filter_key, request.method, request.smooth, request.points_data(), request.options(), request.algorithm, ) solver = design["solver_result"] metrics = solver["metrics"] await connection.begin() try: route_id = await create_route( connection, project_id=project_id, surface_model_id=request.surface_model_id, total_length_m=metrics.get("length_m"), start_chainage_m=0.0, end_chainage_m=metrics.get("length_m"), grade_percent=design["grade_percent"], constraints=design["constraints"], algorithm_params=design["algorithm_params"], route_data_path=design["route_data_path"], ) await insert_route_points(connection, route_id, design["render_points"]) stats = design["statistics"] await create_route_statistics( connection, route_id=route_id, min_slope=stats["min_slope"], max_slope=stats["max_slope"], mean_slope=stats["mean_slope"], cost_score=stats["cost_score"], ) async with connection.cursor() as cursor: await complete_stage(cursor, str(project_id), 2) await connection.commit() except Exception: await connection.rollback() raise return RouteSolveResponse( project_id=str(project_id), route_id=route_id, total_length_m=metrics.get("length_m", 0.0), metrics=metrics, required_points_ok=solver["required_points_ok"], route_data_path=design["route_data_path"], ) except LookupError as exc: async with pool.acquire() as connection, connection.cursor() as cursor: await fail_stage(cursor, str(project_id), 2, str(exc)) await connection.commit() return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) except FileNotFoundError as exc: async with pool.acquire() as connection, connection.cursor() as cursor: await fail_stage(cursor, str(project_id), 2, str(exc)) await connection.commit() return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) except (OSError, ValueError) as exc: async with pool.acquire() as connection, connection.cursor() as cursor: await fail_stage(cursor, str(project_id), 2, str(exc)) await connection.commit() return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) except Exception as exc: logger.exception("B05 경로 탐색 실패: project_id=%s", project_id) async with pool.acquire() as connection, connection.cursor() as cursor: await fail_stage(cursor, str(project_id), 2, str(exc)) await connection.commit() return JSONResponse( status_code=500, content={"status": "error", "message": "경로 탐색 처리 중 오류가 발생했습니다."}, ) @router.post("/{project_id}/route/confirm", response_model=RouteConfirmResponse) async def confirm_latest_route(project_id: UUID) -> RouteConfirmResponse | JSONResponse: """프로젝트의 최신 경로를 확정(CONFIRMED)한다.""" pool = get_db_pool() try: async with pool.acquire() as connection: latest = await get_latest_route(connection, project_id) if not latest: return JSONResponse( status_code=404, content={"status": "error", "message": "확정할 경로가 없습니다."}, ) await connection.begin() try: await confirm_route(connection, latest["id"]) async with connection.cursor() as cursor: await complete_stage(cursor, str(project_id), 2) await connection.commit() except Exception: await connection.rollback() raise return RouteConfirmResponse(project_id=str(project_id), route_id=latest["id"]) except Exception: logger.exception("B05 경로 확정 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "경로 확정 처리 중 오류가 발생했습니다."}, )