diff --git a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py index 6534d92d..2fadf6dd 100644 --- a/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py +++ b/B04_PreProcess/B04_PreProcess_Engine_Watershed_Analyze.py @@ -166,8 +166,12 @@ def preview_stages( """ if len(vertices) < 2: return None + from B03_FileInput.B03_FileInput_Service_Chain import _log_steps + + marks = [("시작", time.perf_counter())] route_line = LineString([(vertex.x, vertex.y) for vertex in vertices]) region = resolve_primary_region(vertices, route_line, contour_features, stream_features) + marks.append(("1차 영역(resolve_primary_region)", time.perf_counter())) if region is None: return None @@ -185,6 +189,7 @@ def preview_stages( logger.warning("배수유역: 등고선 하강 방향을 세우지 못해 흐름 판정을 건너뜁니다.") return StagePreview(region=region) + marks.append(("격자 확장·흐름 판정(expand_by_red_boundary)", time.perf_counter())) analysis = expansion.analysis spec = analysis.spec red = analysis.flow.reaches_road & analysis.flow.analyzed @@ -194,6 +199,7 @@ def preview_stages( # 최종적으로 어느 도로 셀로 들어가는지를 봐야 하므로 도로만 흡수점으로 두고 다시 따라간다. routing = trace_flow(analysis.terrain, analysis.road) if analysis.road.count else None strength_curve = _preview_strength(analysis, routing, red, route_line.length) + marks.append(("흐름 강도(trace_flow)", time.perf_counter())) # ⑦ 2차 전체 배수유역 외곽선 = 적색 셀 전체의 외곽. boundary = outer_boundary(spec, red.reshape(spec.n_rows, spec.n_cols)) @@ -209,6 +215,8 @@ def preview_stages( # B05에 얹을 평균 흐름 화살표 — 셀 화살표는 도면 배율에서 안 보인다. flow_arrows = build_flow_arrows(analysis, analysis.flow) + marks.append(("외곽선·기본 관·화살표", time.perf_counter())) + _log_steps("배수유역 preview_stages", marks) logger.info( "배수유역: 단계 분석 %.1fs (확장 %d회, 최종 셀 %d개) — " diff --git a/B04_PreProcess/B04_PreProcess_Router_Watershed.py b/B04_PreProcess/B04_PreProcess_Router_Watershed.py index aec83a12..9beacc38 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Watershed.py +++ b/B04_PreProcess/B04_PreProcess_Router_Watershed.py @@ -11,6 +11,7 @@ import asyncio import json import logging import math +import time from pathlib import Path from typing import Any from uuid import UUID @@ -389,9 +390,13 @@ async def get_primary_region( logger.info("배수유역: 저장된 분석 결과를 그대로 돌려줍니다 (%s).", stored_path) return {**saved, "from_cache": True} + from B03_FileInput.B03_FileInput_Service_Chain import _log_steps + + marks = [("시작", time.perf_counter())] prepared = await _prepare(project_id) if isinstance(prepared, JSONResponse): return prepared + marks.append(("_prepare(도엽 읽기·좌표변환)", time.perf_counter())) preview = await asyncio.to_thread( preview_stages, prepared["vertices"], @@ -403,6 +408,7 @@ async def get_primary_region( status_code=400, content={"status": "error", "message": "1차 배수유역을 정할 등고선이 없습니다."}, ) + marks.append(("preview_stages(유역 산정)", time.perf_counter())) region = preview.region to_lonlat = prepared["to_lonlat"] # 확장을 거치면 격자·해석 영역이 1차 영역보다 커진다. 최종본을 써야 화면과 어긋나지 않는다. @@ -516,5 +522,7 @@ async def get_primary_region( _write_stage_arrays(prepared["stored_path"], preview, domain, spec) _write_road_routing(prepared["stored_path"], preview, spec, prepared["route_line"], to_lonlat) # 응답 자체를 캐시로 남긴다 — 다음 조회는 배열을 재조립하지 않고 이 파일을 그대로 준다. + marks.append(("응답 만들기·저장", time.perf_counter())) + _log_steps("배수유역 분석 내부", marks) _save_response(prepared["stored_path"], payload) return payload diff --git a/B06_Section/B06_Section_Router_Confirm.py b/B06_Section/B06_Section_Router_Confirm.py index c88978d8..8b5455f6 100644 --- a/B06_Section/B06_Section_Router_Confirm.py +++ b/B06_Section/B06_Section_Router_Confirm.py @@ -11,6 +11,7 @@ import asyncio import json import logging +import time from pathlib import Path from typing import Any from uuid import UUID @@ -20,6 +21,7 @@ from fastapi import APIRouter, Body from fastapi.responses import JSONResponse from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path +from B03_FileInput.B03_FileInput_Service_Chain import _log_steps from B05_Profile.B05_Profile_Repository import confirm_route as confirm_route_status from B05_Profile.B05_Profile_Router_Confirm import _merge_uphill_overrides_into_longitudinal from B06_Section.B06_Section_Repository import ( @@ -212,6 +214,7 @@ async def save_sections( 통째로 버려져 조작값이 사라진다. 그래서 여기서 정본 행을 만든다. """ pool = get_db_pool() + marks = [("시작", time.perf_counter())] try: async with pool.acquire() as connection: existing = await get_longitudinal_section(connection, project_id, route_id) @@ -240,6 +243,8 @@ async def save_sections( request.standard_cross_section if request else None, ) + marks.append(("기본설계 계산(미지정 측점)", time.perf_counter())) + async with pool.acquire() as connection: await connection.begin() try: @@ -294,6 +299,7 @@ async def confirm_sections( missing = await get_cross_sections_missing_design_chainages(connection, route_id) known = await get_cross_section_chainages(connection, route_id) + marks.append(("조회(종단·경로·미지정 측점)", time.perf_counter())) project_root = Path(resolve_stored_project_path(stored_path)) # 행 자체가 없는 측점(구조물 등 비정규)도 확정 대상에 넣는다 — 정본이 없으면 # 조회 때마다 프리뷰가 다시 계산돼 3D·수량이 확정 결과가 아니게 된다(2026-08-24). @@ -336,8 +342,11 @@ async def confirm_sections( await connection.rollback() raise + marks.append(("확정 저장(트랜잭션)", time.perf_counter())) + # 편집이 들어간 **뒤** 서버가 정본을 다시 낸다(임시저장과 같은 자리). await _recompute_stored_designs(project_id, route_id) + marks.append(("서버 재계산", time.perf_counter())) # 측구 방향(design.ditch_side) 변경을 B05 종단 정본 stations.uphill_side에 역반영한다(E-7). # 파일 기반·비치명적: 실패해도 확정은 유지한다. @@ -363,6 +372,8 @@ async def confirm_sections( project_id, route_id, ) + marks.append(("측구 방향 B05 역반영", time.perf_counter())) + _log_steps("B06 확정", marks) return SectionConfirmResponse(project_id=str(project_id), route_id=route_id) except Exception: logger.exception("B06 종횡단 확정 실패: project_id=%s", project_id) diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 78645c13..fb9d7fb4 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -25,11 +25,13 @@ from __future__ import annotations import asyncio import json import logging +import time from pathlib import Path from typing import Any from uuid import UUID from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path +from B03_FileInput.B03_FileInput_Service_Chain import _log_steps from B06_Section.B06_Section_Repository import ( get_longitudinal_section, merge_longitudinal_section_data, @@ -99,6 +101,7 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: from B06_Section.B06_Section_Router import get_section_detail project_uuid = UUID(str(project_id)) + marks = [("시작", time.perf_counter())] # 상세 만들기(파일 읽기 위주)와 DB 두 건은 서로 기다릴 이유가 없다 — 같이 보낸다. # 원격 DB 라 순차로 내면 왕복이 그대로 더해진다(질의 하나 약 12ms, 2026-09-06 실측). pool = get_db_pool() @@ -107,6 +110,7 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: run_with_connection(get_project_storage_relative_path, project_uuid), run_with_connection(get_longitudinal_section, project_uuid, route_id), ) + marks.append(("종횡단 상세+DB 조회(병렬)", time.perf_counter())) payload = getattr(response, "model_dump", None) if payload is None: # JSONResponse = 실패 logger.warning("서버 재계산: 종횡단 상세를 못 받음 (route_id=%s)", route_id) @@ -128,12 +132,15 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: if json.dumps(item.get("design"), sort_keys=True, default=str) != before[index] ] + marks.append(("포장·세월교 보정", time.perf_counter())) + output = await asyncio.to_thread( run_bundle_json, BUNDLE, _NPM_SCRIPT, {"detail": detail, "context": _mass_haul_context()}, ) + marks.append(("Node 번들(면적·유토곡선)", time.perf_counter())) if not isinstance(output, dict): output = {} rows = output.get("areas") @@ -182,6 +189,8 @@ async def _recompute(project_id: UUID | str, route_id: int) -> int: except Exception: await connection.rollback() raise + marks.append(("정본 저장", time.perf_counter())) + _log_steps("서버 재계산 내부", marks) logger.info( "서버 재계산: route_id=%s 설계 보정 %s곳, 구조물 면적 %s곳, 유토곡선 %s", route_id,