Files
Aislo/B05_wf2_Route/B05_wf2_Route_Router.py
2026-07-10 19:01:33 +09:00

164 lines
6.8 KiB
Python

"""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": "경로 확정 처리 중 오류가 발생했습니다."},
)