196 lines
8.6 KiB
Python
196 lines
8.6 KiB
Python
"""B06 종횡단 생성 FastAPI 라우터."""
|
|
|
|
import asyncio
|
|
import json
|
|
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_Sections_Core import SectionGenerationOptions
|
|
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
|
|
confirm_sections_for_route,
|
|
count_cross_sections,
|
|
get_confirmed_route_context,
|
|
get_longitudinal_section,
|
|
)
|
|
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Schema import (
|
|
SectionConfirmResponse,
|
|
SectionContextResponse,
|
|
SectionDetailResponse,
|
|
SectionOptionDefaults,
|
|
SectionSummaryResponse,
|
|
)
|
|
from common_util.common_util_storage import resolve_stored_project_path
|
|
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
|
from common_util.common_util_workflow_state import complete_stage
|
|
from config.config_db import get_db_pool
|
|
from config.config_system import SECTION_VERTICAL_EXAGGERATION
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(prefix="/api/projects", tags=["B06 Profile Cross"])
|
|
|
|
|
|
@router.get("/{project_id}/sections/context", response_model=SectionContextResponse)
|
|
async def get_section_context(project_id: UUID) -> SectionContextResponse | JSONResponse:
|
|
"""최신 확정 경로와 B04 확정값, config 기반 생성 기본값을 반환한다."""
|
|
pool = get_db_pool()
|
|
try:
|
|
async with pool.acquire() as connection:
|
|
route_context = await get_confirmed_route_context(connection, project_id)
|
|
surface_params = await get_surface_confirmation_params(connection, str(project_id))
|
|
|
|
defaults = SectionGenerationOptions()
|
|
return SectionContextResponse(
|
|
project_id=str(project_id),
|
|
route_id=route_context["route_id"] if route_context else None,
|
|
filter_key=surface_params["source_filter"] if route_context else None,
|
|
method=surface_params["method"] if route_context else None,
|
|
smooth=bool(surface_params["smooth"]) if route_context else None,
|
|
crs_epsg=route_context["crs_epsg"] if route_context else None,
|
|
defaults=SectionOptionDefaults(
|
|
station_interval_m=defaults.station_interval_m,
|
|
cross_half_width_m=defaults.cross_half_width_m,
|
|
cross_sample_interval_m=defaults.cross_sample_interval_m,
|
|
long_sample_interval_m=defaults.long_sample_interval_m,
|
|
vertical_exaggeration=SECTION_VERTICAL_EXAGGERATION,
|
|
),
|
|
)
|
|
except Exception:
|
|
logger.exception("B06 종횡단 컨텍스트 조회 실패: project_id=%s", project_id)
|
|
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)
|
|
cross_section_count = await count_cross_sections(connection, route_id)
|
|
data = longitudinal.get("data") if longitudinal else None
|
|
length_m = data.get("length_m") if isinstance(data, dict) else None
|
|
return SectionSummaryResponse(
|
|
project_id=str(project_id),
|
|
route_id=route_id,
|
|
longitudinal=longitudinal,
|
|
length_m=length_m,
|
|
cross_section_count=cross_section_count,
|
|
)
|
|
except Exception:
|
|
logger.exception("B06 종횡단 조회 실패: project_id=%s", project_id)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": "종횡단 조회 중 오류가 발생했습니다."},
|
|
)
|
|
|
|
|
|
def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dict:
|
|
"""검증된 프로젝트 루트 안의 종단 및 횡단 JSON을 읽는다."""
|
|
root = project_root.resolve()
|
|
longitudinal_path = (root / longitudinal_file_path).resolve()
|
|
if root not in longitudinal_path.parents:
|
|
raise ValueError("종단면 파일 경로가 프로젝트 저장소를 벗어났습니다.")
|
|
if not longitudinal_path.is_file():
|
|
raise FileNotFoundError("종단면 상세 파일을 찾을 수 없습니다.")
|
|
|
|
stage_root = longitudinal_path.parent.parent
|
|
cross_dir = stage_root / "cross_sections"
|
|
if not cross_dir.is_dir():
|
|
raise FileNotFoundError("횡단면 상세 파일을 찾을 수 없습니다.")
|
|
|
|
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
|
|
cross_sections = [
|
|
json.loads(path.read_text(encoding="utf-8"))
|
|
for path in sorted(cross_dir.glob("cross_*.json"))
|
|
]
|
|
if not isinstance(longitudinal, dict) or not all(
|
|
isinstance(section, dict) for section in cross_sections
|
|
):
|
|
raise ValueError("종횡단 상세 파일 형식이 올바르지 않습니다.")
|
|
return {"longitudinal": longitudinal, "cross_sections": cross_sections}
|
|
|
|
|
|
@router.get("/{project_id}/sections/{route_id}/detail", response_model=SectionDetailResponse)
|
|
async def get_section_detail(
|
|
project_id: UUID, route_id: int
|
|
) -> SectionDetailResponse | JSONResponse:
|
|
"""경로의 SVG 렌더링용 종단·횡단 원시 샘플을 반환한다."""
|
|
pool = get_db_pool()
|
|
try:
|
|
async with pool.acquire() as connection:
|
|
longitudinal = await get_longitudinal_section(connection, project_id, route_id)
|
|
if not longitudinal:
|
|
return JSONResponse(
|
|
status_code=404,
|
|
content={"status": "error", "message": "종횡단 상세 결과가 없습니다."},
|
|
)
|
|
stored_path = await get_project_storage_relative_path(connection, project_id)
|
|
project_root = Path(resolve_stored_project_path(stored_path))
|
|
detail = await asyncio.to_thread(
|
|
_read_section_detail,
|
|
project_root,
|
|
str(longitudinal["longitudinal_file_path"]),
|
|
)
|
|
return SectionDetailResponse(**detail)
|
|
except FileNotFoundError as exc:
|
|
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
logger.warning(
|
|
"B06 종횡단 상세 파일 조회 실패: project_id=%s route_id=%s error=%s",
|
|
project_id,
|
|
route_id,
|
|
exc,
|
|
)
|
|
return JSONResponse(
|
|
status_code=500,
|
|
content={"status": "error", "message": "종횡단 상세 파일을 읽지 못했습니다."},
|
|
)
|
|
except Exception:
|
|
logger.exception(
|
|
"B06 종횡단 상세 조회 실패: project_id=%s route_id=%s", project_id, route_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": "종횡단 확정 처리 중 오류가 발생했습니다."},
|
|
)
|