"""B06 종횡단 생성 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_Repository import get_latest_route from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine import run_section_generation from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Engine_Section import SectionGenerationOptions from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import ( confirm_sections_for_route, create_longitudinal_section, delete_sections_for_route, get_longitudinal_section, insert_cross_sections, ) from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import ( SectionConfirmResponse, SectionGenerateRequest, SectionGenerateResponse, SectionSummaryResponse, ) 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=["B06 Profile Cross"]) def _build_options(request: SectionGenerateRequest) -> SectionGenerationOptions: """요청의 옵션(미지정은 config 기본값)으로 SectionGenerationOptions를 만든다.""" defaults = SectionGenerationOptions() return SectionGenerationOptions( station_interval_m=request.station_interval_m or defaults.station_interval_m, cross_half_width_m=request.cross_half_width_m or defaults.cross_half_width_m, cross_sample_interval_m=request.cross_sample_interval_m or defaults.cross_sample_interval_m, long_sample_interval_m=request.long_sample_interval_m or defaults.long_sample_interval_m, include_endpoint=defaults.include_endpoint, ) @router.post("/{project_id}/sections/generate", response_model=SectionGenerateResponse) async def generate_sections( project_id: UUID, request: SectionGenerateRequest ) -> SectionGenerateResponse | JSONResponse: """확정 경로에서 종횡단을 생성·저장하고 DB에 기록한다.""" pool = get_db_pool() try: params = { "route_id": request.route_id, "filter_key": request.filter_key, "method": request.method, "smooth": request.smooth, "station_interval_m": request.station_interval_m, "cross_half_width_m": request.cross_half_width_m, "cross_sample_interval_m": request.cross_sample_interval_m, "long_sample_interval_m": request.long_sample_interval_m, "crs": request.crs, } async with pool.acquire() as connection: async with connection.cursor() as cursor: await start_stage(cursor, str(project_id), 3, params) await connection.commit() stored_path = await get_project_storage_relative_path(connection, project_id) project_root = Path(resolve_stored_project_path(stored_path)) latest = await get_latest_route(connection, project_id) if not latest or latest["id"] != request.route_id: return JSONResponse( status_code=404, content={"status": "error", "message": "대상 경로를 찾을 수 없습니다."}, ) route_data_path = latest["route_data_path"] options = _build_options(request) design = await asyncio.to_thread( run_section_generation, project_root, route_data_path, request.filter_key, request.method, request.smooth, options=options, crs=request.crs, ) await connection.begin() try: await delete_sections_for_route(connection, request.route_id) longitudinal_id = await create_longitudinal_section( connection, project_id=project_id, route_id=request.route_id, data=design["longitudinal"]["data"], longitudinal_file_path=design["longitudinal"]["file_path"], ) await insert_cross_sections( connection, project_id=project_id, route_id=request.route_id, sections=design["cross_sections"], ) async with connection.cursor() as cursor: await complete_stage(cursor, str(project_id), 3) await connection.commit() except Exception: await connection.rollback() raise return SectionGenerateResponse( project_id=str(project_id), route_id=request.route_id, longitudinal_id=longitudinal_id, cross_section_count=len(design["cross_sections"]), length_m=design["longitudinal"]["data"]["length_m"], longitudinal_file_path=design["longitudinal"]["file_path"], ) except LookupError as exc: async with pool.acquire() as connection, connection.cursor() as cursor: await fail_stage(cursor, str(project_id), 3, 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), 3, 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), 3, str(exc)) await connection.commit() return JSONResponse(status_code=400, content={"status": "error", "message": str(exc)}) except Exception as exc: logger.exception("B06 종횡단 생성 실패: project_id=%s", project_id) async with pool.acquire() as connection, connection.cursor() as cursor: await fail_stage(cursor, str(project_id), 3, str(exc)) await connection.commit() return JSONResponse( status_code=500, content={"status": "error", "message": "종횡단 생성 처리 중 오류가 발생했습니다."}, ) @router.get("/{project_id}/sections/{route_id}", response_model=SectionSummaryResponse) async def get_sections(project_id: UUID, route_id: int) -> SectionSummaryResponse | JSONResponse: """경로의 종단면 요약을 조회한다.""" pool = get_db_pool() try: async with pool.acquire() as connection: longitudinal = await get_longitudinal_section(connection, project_id, route_id) return SectionSummaryResponse( project_id=str(project_id), route_id=route_id, longitudinal=longitudinal ) except Exception: logger.exception("B06 종횡단 조회 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "종횡단 조회 중 오류가 발생했습니다."}, ) @router.post("/{project_id}/sections/{route_id}/confirm", response_model=SectionConfirmResponse) async def confirm_sections( project_id: UUID, route_id: int ) -> SectionConfirmResponse | JSONResponse: """경로의 종횡단면을 확정(CONFIRMED)한다.""" pool = get_db_pool() try: async with pool.acquire() as connection: existing = await get_longitudinal_section(connection, project_id, route_id) if not existing: return JSONResponse( status_code=404, content={"status": "error", "message": "확정할 종횡단이 없습니다."}, ) await connection.begin() try: await confirm_sections_for_route(connection, route_id) async with connection.cursor() as cursor: await complete_stage(cursor, str(project_id), 3) await connection.commit() except Exception: await connection.rollback() raise return SectionConfirmResponse(project_id=str(project_id), route_id=route_id) except Exception: logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id) return JSONResponse( status_code=500, content={"status": "error", "message": "종횡단 확정 처리 중 오류가 발생했습니다."}, )