"""B05 경로 설계 결과의 aiomysql Raw SQL 접근. routes(경로), route_points(렌더링 샘플), route_statistics(통계) 테이블에 메타데이터와 상대 경로를 기록한다. 전체 폴리라인은 route_data_path의 GeoJSON 파일에 저장하고 DB에는 경로만 기록한다. """ import json from pathlib import PurePosixPath from typing import Any from uuid import UUID import aiomysql _STAGE_ROOT = "B05_wf2_Route" def _validate_stage_path(relative_path: str) -> str: normalized = PurePosixPath(relative_path.replace("\\", "/")) if normalized.is_absolute() or ".." in normalized.parts: raise ValueError("DB에는 프로젝트 루트 기준 상대 경로만 저장할 수 있습니다.") if not normalized.parts or normalized.parts[0] != _STAGE_ROOT: raise ValueError(f"B05 산출물 경로는 {_STAGE_ROOT} 아래여야 합니다.") return normalized.as_posix() async def create_route( connection: aiomysql.Connection, *, project_id: UUID, surface_model_id: int | None, total_length_m: float | None, start_chainage_m: float | None, end_chainage_m: float | None, grade_percent: list[float] | None, constraints: dict[str, Any] | None, algorithm_params: dict[str, Any] | None, route_data_path: str, status: str = "DRAFT", ) -> int: """경로 메타데이터를 저장하고 생성된 ID를 반환한다.""" route_rel = _validate_stage_path(route_data_path) async with connection.cursor() as cursor: await cursor.execute( """ INSERT INTO routes ( project_id, surface_model_id, status, start_chainage_m, end_chainage_m, total_length_m, grade_percent, constraints, algorithm_params, route_data_path, computed_at ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP) """, ( str(project_id), surface_model_id, status, start_chainage_m, end_chainage_m, total_length_m, json.dumps(grade_percent) if grade_percent is not None else None, json.dumps(constraints, ensure_ascii=False) if constraints is not None else None, json.dumps(algorithm_params, ensure_ascii=False) if algorithm_params is not None else None, route_rel, ), ) route_id = cursor.lastrowid if not route_id: raise RuntimeError("routes 레코드 생성 결과에 ID가 없습니다.") return int(route_id) async def insert_route_points( connection: aiomysql.Connection, route_id: int, points: list[dict[str, Any]], ) -> int: """경로 렌더링 샘플 포인트를 일괄 저장하고 저장 건수를 반환한다. 각 point dict: {chainage_m, elevation_m, slope_percent, sequence_num} """ if not points: return 0 rows = [ ( route_id, point.get("chainage_m"), point.get("elevation_m"), point.get("slope_percent"), point.get("sequence_num"), ) for point in points ] async with connection.cursor() as cursor: await cursor.executemany( """ INSERT INTO route_points ( route_id, chainage_m, elevation_m, slope_percent, sequence_num ) VALUES (%s, %s, %s, %s, %s) """, rows, ) return len(rows) async def create_route_statistics( connection: aiomysql.Connection, *, route_id: int, min_slope: float | None, max_slope: float | None, mean_slope: float | None, cut_volume_m3: float | None = None, fill_volume_m3: float | None = None, tree_cutting_volume: float | None = None, cost_score: float | None = None, ) -> int: """경로 통계를 저장하고 생성된 ID를 반환한다.""" async with connection.cursor() as cursor: await cursor.execute( """ INSERT INTO route_statistics ( route_id, min_slope, max_slope, mean_slope, cut_volume_m3, fill_volume_m3, tree_cutting_volume, cost_score ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """, ( route_id, min_slope, max_slope, mean_slope, cut_volume_m3, fill_volume_m3, tree_cutting_volume, cost_score, ), ) stat_id = cursor.lastrowid if not stat_id: raise RuntimeError("route_statistics 레코드 생성 결과에 ID가 없습니다.") return int(stat_id) async def get_latest_route( connection: aiomysql.Connection, project_id: UUID ) -> dict[str, Any] | None: """프로젝트의 최신 경로를 조회한다 (없으면 None).""" async with connection.cursor() as cursor: await cursor.execute( """ SELECT id, status, total_length_m, route_data_path, computed_at FROM routes WHERE project_id = %s ORDER BY computed_at DESC, id DESC LIMIT 1 """, (str(project_id),), ) row = await cursor.fetchone() if not row: return None return { "id": int(row[0]), "status": row[1], "total_length_m": row[2], "route_data_path": row[3], "computed_at": row[4].isoformat() if row[4] else None, } async def confirm_route(connection: aiomysql.Connection, route_id: int) -> None: """경로 상태를 CONFIRMED로 변경한다.""" async with connection.cursor() as cursor: await cursor.execute( "UPDATE routes SET status = 'CONFIRMED' WHERE id = %s", (route_id,), )