This commit is contained in:
2026-07-19 00:19:59 +09:00
parent eed809748b
commit 14ba5f96b7
6 changed files with 95 additions and 42 deletions
+2 -2
View File
@@ -60,8 +60,8 @@ export interface RouteSolveResponse {
metrics: Record<string, unknown>;
required_points_ok: boolean;
route_data_path: string;
longitudinal_length_m: number;
cross_section_count: number;
longitudinal_length_m: number | null;
cross_section_count: number | null;
}
/** 경로 확정 결과 (RouteConfirmResponse) */
+26
View File
@@ -206,3 +206,29 @@ async def confirm_route(connection: aiomysql.Connection, route_id: int) -> None:
"UPDATE routes SET status = 'CONFIRMED' WHERE id = %s",
(route_id,),
)
async def get_surface_crs_epsg(
connection: aiomysql.Connection, project_id: UUID, surface_model_id: int
) -> int | None:
"""종횡단 메타데이터용 좌표계를 조회한다.
지표면 모델 crs_epsg가 NULL이면 같은 프로젝트 input_files의 감지된
좌표계로 폴백한다 (B06 get_confirmed_route_context와 동일 규칙).
"""
async with connection.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""
SELECT COALESCE(
(SELECT sm.crs_epsg FROM surface_models sm WHERE sm.id = %s),
(SELECT f.crs_epsg
FROM input_files f
WHERE f.project_id = %s AND f.crs_epsg IS NOT NULL
ORDER BY f.id DESC
LIMIT 1)
) AS crs_epsg
""",
(surface_model_id, str(project_id)),
)
row = await cursor.fetchone()
return int(row["crs_epsg"]) if row and row["crs_epsg"] is not None else None
+44 -27
View File
@@ -20,6 +20,7 @@ from B05_wf2_Route.B05_wf2_Route_Repository import (
create_route_statistics,
get_latest_route,
get_route_points,
get_surface_crs_epsg,
insert_route_points,
)
from B05_wf2_Route.B05_wf2_Route_Schema import (
@@ -198,35 +199,51 @@ async def solve_route(
)
raise
sections = await asyncio.to_thread(
run_section_generation,
project_root,
design["route_data_path"],
request.filter_key,
request.method,
request.smooth,
options=_section_options(request),
)
await connection.begin()
# 종횡단 생성 실패는 저장된 경로를 무효화하지 않으므로 비치명적으로 처리한다.
longitudinal_length_m: float | None = None
cross_section_count: int | None = None
try:
await delete_sections_for_route(connection, route_id)
await create_longitudinal_section(
connection,
project_id=project_id,
route_id=route_id,
data=sections["longitudinal"]["data"],
longitudinal_file_path=sections["longitudinal"]["file_path"],
crs_epsg = await get_surface_crs_epsg(
connection, project_id, request.surface_model_id
)
await insert_cross_sections(
connection,
project_id=project_id,
route_id=route_id,
sections=sections["cross_sections"],
sections = await asyncio.to_thread(
run_section_generation,
project_root,
design["route_data_path"],
request.filter_key,
request.method,
request.smooth,
options=_section_options(request),
crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None,
)
await connection.commit()
await connection.begin()
try:
await delete_sections_for_route(connection, route_id)
await create_longitudinal_section(
connection,
project_id=project_id,
route_id=route_id,
data=sections["longitudinal"]["data"],
longitudinal_file_path=sections["longitudinal"]["file_path"],
)
await insert_cross_sections(
connection,
project_id=project_id,
route_id=route_id,
sections=sections["cross_sections"],
)
await connection.commit()
except Exception:
await connection.rollback()
raise
longitudinal_length_m = sections["longitudinal"]["data"]["length_m"]
cross_section_count = len(sections["cross_sections"])
except Exception:
await connection.rollback()
raise
logger.exception(
"B05 종횡단 생성 실패 (경로는 저장됨): project_id=%s route_id=%s",
project_id,
route_id,
)
return RouteSolveResponse(
project_id=str(project_id),
@@ -235,8 +252,8 @@ async def solve_route(
metrics=metrics,
required_points_ok=solver["required_points_ok"],
route_data_path=design["route_data_path"],
longitudinal_length_m=sections["longitudinal"]["data"]["length_m"],
cross_section_count=len(sections["cross_sections"]),
longitudinal_length_m=longitudinal_length_m,
cross_section_count=cross_section_count,
)
except LookupError as exc:
async with pool.acquire() as connection, connection.cursor() as cursor:
+3 -2
View File
@@ -100,8 +100,9 @@ class RouteSolveResponse(BaseModel):
metrics: dict[str, Any]
required_points_ok: bool
route_data_path: str
longitudinal_length_m: float
cross_section_count: int
# 종횡단 생성 실패 시 None (경로 자체는 저장됨)
longitudinal_length_m: float | None = None
cross_section_count: int | None = None
class RouteConfirmResponse(BaseModel):
+12 -3
View File
@@ -97,6 +97,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
const profilePanel = createRouteProfilePanel();
let confirmedSurface: SurfaceModelSummary | null = null;
let latest: RouteLatestResponse | null = null;
let defaultCrossHalfWidth = 0;
let routeReady = false;
let stale = false;
let restoring = true;
@@ -154,7 +155,10 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
function renderSections(detail: SectionDetailResponse): void {
profilePanel.render(detail);
viewer.renderStationLines(detail.longitudinal.stations, panel.values().crossHalfWidth);
viewer.renderStationLines(
detail.longitudinal.stations,
panel.values().crossHalfWidth ?? defaultCrossHalfWidth,
);
}
async function restoreSections(routeId: number): Promise<void> {
@@ -239,8 +243,12 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
long_sample_interval_m: values.longSampleInterval,
});
renderLatest(await fetchLatestRoute(activeProjectId));
renderSections(await fetchSectionDetail(activeProjectId, solved.route_id));
showToast("최적 경로 계산이 완료되었습니다.", "success");
await restoreSections(solved.route_id);
if (solved.cross_section_count === null) {
showToast("경로는 저장되었지만 종횡단 생성에 실패했습니다.", "error");
} else {
showToast("최적 경로 계산이 완료되었습니다.", "success");
}
} catch (error) {
showToast(error instanceof Error ? error.message : "경로 계산에 실패했습니다.", "error");
} finally {
@@ -270,6 +278,7 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
fetchSectionContext(activeProjectId),
]);
confirmedSurface = models.models.find((model) => model.status === "CONFIRMED") ?? null;
defaultCrossHalfWidth = sectionContext.defaults.cross_half_width_m;
if (!confirmedSurface) {
showToast("확정된 지표면 모델이 없습니다.", "error");
} else {
+8 -8
View File
@@ -12,10 +12,10 @@ export interface RoutePanelValues {
minUphillGrade: number | null;
minDownhillGrade: number | null;
allowAvoidPassThrough: boolean;
stationInterval: number;
crossHalfWidth: number;
crossSampleInterval: number;
longSampleInterval: number;
stationInterval: number | null;
crossHalfWidth: number | null;
crossSampleInterval: number | null;
longSampleInterval: number | null;
}
interface PanelCallbacks {
@@ -275,10 +275,10 @@ export function createRoutePanel(callbacks: PanelCallbacks) {
minUphillGrade: parseOptional(minUphillGrade),
minDownhillGrade: parseOptional(minDownhillGrade),
allowAvoidPassThrough: avoidPass.checked,
stationInterval: Number(stationInterval.value),
crossHalfWidth: Number(crossHalfWidth.value),
crossSampleInterval: Number(crossSampleInterval.value),
longSampleInterval: Number(longSampleInterval.value),
stationInterval: parseOptional(stationInterval),
crossHalfWidth: parseOptional(crossHalfWidth),
crossSampleInterval: parseOptional(crossSampleInterval),
longSampleInterval: parseOptional(longSampleInterval),
};
},
restore(values: Partial<RoutePanelValues>) {