From 41852da3b36327f3bdbbd0ce1c2673f6f32c2595 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 02:56:45 +0900 Subject: [PATCH] =?UTF-8?q?feat(B03):=20=EC=B4=88=EA=B8=B0=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=EB=B6=80=ED=84=B0=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EB=85=B8=EC=84=A0=20=ED=8F=B4=EB=A6=AC=EB=9D=BC=EC=9D=B8?= =?UTF-8?q?=EC=9D=84=20=EB=85=B8=EC=84=A0=20=EC=A0=95=EB=B3=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 자동 체인 2.7 단계 추가 — 초기 폴리라인을 세운 뒤 노선을 다시 읽어 그것을 BP·경유점·EP 로 세우고 algorithm=as_planned 로 넘김. 전에는 폴리라인 파일만 만들고 노선은 원본 점군으로 풀었음. 그래서 종횡단 측점·유토곡선·3D 코리도가 곡선 없는 점군 위에 섰음. 실측(용화 3건): DB 노선 정점 331·323 vs 초기 폴리라인 145. 노선을 한 번이라도 편집한 프로젝트만 145 로 맞아 있었음. 자체검증 — 2.7 재판독이 145 정점을 돌려주고 BP·EP 좌표가 expected_route.csv 첫·끝 줄과 같음. pytest 409 passed. 계측 로그(_log_steps) 동봉 — 재확정 체인 단계별 경과시간용. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_Service_Chain.py | 43 ++++++++++++++++++++ B05_Profile/B05_Profile_Router_Replan.py | 8 ++++ 2 files changed, 51 insertions(+) diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index ec0b0431..6e8dfec7 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -16,6 +16,7 @@ WF1(지표면 분석·자동 확정)이 끝나면 사용자가 화면에 없어 """ import logging +import time from pathlib import Path from typing import Any from uuid import UUID @@ -25,6 +26,18 @@ from fastapi.responses import JSONResponse logger = logging.getLogger(__name__) +def _log_steps(title: str, marks: list[tuple[str, float]]) -> None: + """단계별 경과시간을 한 줄로 남긴다 — 어디서 시간을 쓰는지 보려는 계측용.""" + total = (marks[-1][1] - marks[0][1]) or 1e-9 + parts = [ + f"{name} {marks[i][1] - marks[i - 1][1]:.1f}s" + f"({(marks[i][1] - marks[i - 1][1]) / total * 100:.0f}%)" + for i, (name, _) in enumerate(marks) + if i + ] + logger.info("[계측] %s 총 %.1fs = %s", title, total, " | ".join(parts)) + + def _planned_route_points_in_project_crs( project_root: Path, surface: dict[str, Any] | None = None, @@ -70,6 +83,7 @@ async def _prepare_drainage_pipes_and_reprofile( from config.config_db import get_db_pool from config.config_system import SECTION_CROSS_HALF_WIDTH_M + marks = [("시작", time.perf_counter())] try: # 1) 배수유역 분석(30초 내외). 저장분이 있으면 그대로 쓴다. # 저장분은 노선이 바뀌었는지 보지 않으므로, 노선을 다시 푼 뒤에는 refresh로 부른다. @@ -82,6 +96,8 @@ async def _prepare_drainage_pipes_and_reprofile( ) return False + marks.append(("배수유역 분석", time.perf_counter())) + # 2) 기본 관(도로 × 상류 세류선) + 최대 간격 자동 보충을 정본으로 저장한다. pipes = await put_pipe_points(project_id, None) if isinstance(pipes, JSONResponse): @@ -93,6 +109,8 @@ async def _prepare_drainage_pipes_and_reprofile( return False pipe_count = int(pipes.get("saved_count") or 0) + marks.append(("관 지점 확정", time.perf_counter())) + # 3) 저장된 관 자리를 물려 계획선을 다시 산출한다. 반폭은 이미 쓰던 값을 유지한다. pool = get_db_pool() async with pool.acquire() as connection: @@ -110,6 +128,8 @@ async def _prepare_drainage_pipes_and_reprofile( regenerated.status_code, ) return False + marks.append(("종횡단 측점 재생성", time.perf_counter())) + _log_steps("배수·관·측점", marks) logger.info( "자동 설계 체인 배수유역·배관 정착 계획선 완료: project_id=%s route_id=%s 관=%d개", project_id, @@ -211,12 +231,24 @@ async def run_auto_design_chain( # 「화면을 안 열면 값이 없다」가 되므로 초기값은 서버가 낸다(0-4 원칙). await _ensure_initial_polyline(project_id, project_root, points) + # 2.7) 노선의 기준을 **초기 폴리라인**으로 갈아 끼운다(2026-09-06 사용자 지시). + # 다시 읽으면 `load_design_route` 가 방금 만든 초기 폴리라인을 집는다 + # (읽는 순서: 수정본 → 초기 폴리라인 → 예상노선). 종횡단 측점·유토곡선· + # 3D 코리도가 전부 이 노선에서 나오므로, 여기서 갈아 끼우지 않으면 화면에는 + # 곡선 없는 원본 점군(용화: 3.3m 간격 331점)이 그대로 선다. + polyline_points = _planned_route_points_in_project_crs(project_root, defaults, route_range) + if polyline_points and len(polyline_points) >= 2: + points = polyline_points + # 3) B05 경로 계산 request = RouteSolveRequest( filter_key=str(defaults["source_filter"]), method=str(defaults["method"]), smooth=bool(defaults["smooth"]), surface_model_id=surface_model_id, + # 계획노선을 **그대로** 쓴다 — 격자 재탐색은 이 선을 자기 제약으로 다시 풀어 + # 곡선을 뭉개거나 통째로 막는다(재확정 체인과 같은 이유, 0-10). + algorithm="as_planned", bp=RoutePoint(**points[0]), ep=RoutePoint(**points[-1]), cp=[ @@ -397,6 +429,7 @@ async def run_redesign_chain( pool = get_db_pool() project_root: Path | None = None + marks = [("시작", time.perf_counter())] try: async with pool.acquire() as connection: latest = await get_latest_route(connection, project_id) @@ -437,6 +470,8 @@ async def run_redesign_chain( ) return "노선 제어점(BP·EP)이 없어 재계산할 수 없습니다." + marks.append(("준비·사용자입력 회수", time.perf_counter())) + # 2) B05 재계산 — 지표면 관련 값만 새 확정 선택으로 교체, 나머지는 사용자 저장분. request = RouteSolveRequest( filter_key=str(selection.get("source_filter") or params.get("filter_key")), @@ -491,6 +526,7 @@ async def run_redesign_chain( ) return f"노선 재계산 실패({solve_result.status_code}): {reason}" new_route_id = int(solve_result.route_id) + marks.append(("노선 재계산(solve)", time.perf_counter())) logger.info( "재확정 체인 B05 재계산 완료: project_id=%s %s→%s", project_id, @@ -514,10 +550,13 @@ async def run_redesign_chain( confirm_result.status_code, ) return f"노선 확정 실패({confirm_result.status_code})" + marks.append(("B05 확정", time.perf_counter())) # 노선이 바뀌었으므로 배수유역·관 지점도 새 노선 기준으로 다시 만든다 — 옛 관 자리로 # 계획선을 앉히면 전부 어긋나고, 저장분은 노선 변경을 스스로 알지 못한다. await _prepare_drainage_pipes_and_reprofile(project_id, new_route_id, refresh=True) + marks.append(("배수·관·측점 재생성", time.perf_counter())) + # 5) 이제 측점이 새 노선 기준으로 다 섰다 — 옛 사용자 설계를 **누가거리로** 얹는다. # 묶어 쓰므로 행 수와 무관하게 왕복 두 번이다(`merge_cross_section_designs`). try: @@ -546,6 +585,8 @@ async def run_redesign_chain( ) logger.info("재확정 체인 설계 이월: project_id=%s %d건", project_id, carried) + marks.append(("설계 이월", time.perf_counter())) + old_options = ((old_longitudinal or {}).get("data") or {}).get("options") or {} section_request = ( SectionConfirmRequest(standard_cross_section=old_options["standard_cross_section"]) @@ -562,6 +603,8 @@ async def run_redesign_chain( sections_result.status_code, ) return f"종횡단 확정 실패({sections_result.status_code})" + marks.append(("B06 확정(서버 재계산 포함)", time.perf_counter())) + _log_steps("재확정 체인", marks) logger.info( "재확정 체인 완료: project_id=%s route %s→%s (설계 %d건 이월)", project_id, diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 8c68c3a4..79cf02fe 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -31,6 +31,7 @@ import asyncio import csv import logging +import time from pathlib import Path from typing import Any from uuid import UUID @@ -225,6 +226,7 @@ async def _recompute(project_id: UUID, project_root: Path, stored_path: str) -> import aiomysql from B03_FileInput.B03_FileInput_Service_Chain import ( + _log_steps, _planned_route_points_in_project_crs, run_redesign_chain, ) @@ -234,6 +236,8 @@ async def _recompute(project_id: UUID, project_root: Path, stored_path: str) -> from common_util.common_util_surface_confirmation import get_surface_confirmation_params from common_util.common_util_workflow_state import get_workflow_state, start_stage + marks = [("시작", time.perf_counter())] + # 횡단배수 지점은 새 노선에서 새로 계산한다 — 저장분을 남기면 옛 자리가 살아난다. await asyncio.to_thread(clear_pipe_points, stored_path) @@ -294,7 +298,11 @@ async def _recompute(project_id: UUID, project_root: Path, stored_path: str) -> await start_stage(cursor, str(project_id), 2, params) await connection.commit() + marks.append(("재계산 준비(관 삭제·노선 읽기·stage 기록)", time.perf_counter())) + failure = await run_redesign_chain(project_id, int(surface_model_id), selection) + marks.append(("재확정 체인", time.perf_counter())) + _log_steps("노선 [확인] 재계산", marks) if failure: # 체인이 끊기면 **노선만 새것으로 갈아 끼워진 채** 종횡단은 옛 노선에 남아 어긋난다 # (2026-09-06 실측: 배수유역·관은 새 노선, 종횡단은 옛 노선 → 배수관 측점 9 → 0).