From cb86e4b40fd825939c3f141c8292c6278042764c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 18 Jul 2026 18:31:21 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B0=B1=EC=97=94=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- B05_wf2_Route/B05_wf2_Route_Debug.py | 18 ++++ B05_wf2_Route/B05_wf2_Route_Engine.py | 46 ++++++++- B05_wf2_Route/B05_wf2_Route_Router.py | 124 +++++++++++++++++++---- B05_wf2_Route/B05_wf2_Route_UI_Page.ts | 1 + B05_wf2_Route/B05_wf2_Route_UI_Style.css | 10 +- 5 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 B05_wf2_Route/B05_wf2_Route_Debug.py diff --git a/B05_wf2_Route/B05_wf2_Route_Debug.py b/B05_wf2_Route/B05_wf2_Route_Debug.py new file mode 100644 index 00000000..db96c7bb --- /dev/null +++ b/B05_wf2_Route/B05_wf2_Route_Debug.py @@ -0,0 +1,18 @@ +"""B05 DEBUG 모드 전용 구조화 로그.""" + +import json +import logging +from typing import Any + +from config.config_system import DEBUG + + +def log_b05_debug(logger: logging.Logger, event: str, **payload: Any) -> None: + """DEBUG=True일 때만 B05 계산·DB 저장 정보를 출력한다.""" + if not DEBUG: + return + logger.info( + "[B05 DEBUG] %s | %s", + event, + json.dumps(payload, ensure_ascii=False, default=str), + ) diff --git a/B05_wf2_Route/B05_wf2_Route_Engine.py b/B05_wf2_Route/B05_wf2_Route_Engine.py index 0b751b09..01c73135 100644 --- a/B05_wf2_Route/B05_wf2_Route_Engine.py +++ b/B05_wf2_Route/B05_wf2_Route_Engine.py @@ -5,9 +5,11 @@ GeoJSON으로 저장하며 DB 기록용 데이터(메타·렌더링 샘플·통 라우터에서 asyncio.to_thread로 호출한다. """ +import logging from pathlib import Path from typing import Any +from B05_wf2_Route.B05_wf2_Route_Debug import log_b05_debug from B05_wf2_Route.B05_wf2_Route_Engine_RidgeValley import solve_ridge_valley_route from B05_wf2_Route.B05_wf2_Route_Engine_Solver import solve_optimal_route from common_util.common_util_json import atomic_write_json @@ -15,6 +17,7 @@ from common_util.common_util_json import atomic_write_json _ROUTE_SUBDIR = Path("B05_wf2_Route") / "route" # route_points 테이블에 저장할 렌더링 샘플 최대 개수 _MAX_RENDER_POINTS = 500 +logger = logging.getLogger(__name__) def _route_geojson(polyline: list[list[float]]) -> dict[str, Any]: @@ -87,6 +90,22 @@ def run_route_design( - render_points: route_points 테이블 저장용 샘플 - statistics: route_statistics 저장용 요약 """ + log_b05_debug( + logger, + "calculation.start", + algorithm=algorithm, + filter_key=filter_key, + method=method, + smooth=smooth, + point_counts={ + "bp": 1 if points_data.get("bp") else 0, + "ep": 1 if points_data.get("ep") else 0, + "cp": len(points_data.get("cp", [])), + "ap": len(points_data.get("ap", [])), + "fp": len(points_data.get("fp", [])), + }, + options=options, + ) if algorithm == "ridge_valley": result = solve_ridge_valley_route( project_root, filter_key, smooth, points_data, options, method=method @@ -97,11 +116,27 @@ def run_route_design( ) polyline = result["polyline"] chainage_m = result["chainage_m"] + log_b05_debug( + logger, + "calculation.solver_complete", + algorithm=algorithm, + polyline_count=len(polyline), + segment_count=len(result.get("segments", [])), + metrics=result.get("metrics", {}), + required_points_ok=result.get("required_points_ok"), + warning_count=len(result.get("curve_warning_segments", [])), + ) route_dir = project_root / _ROUTE_SUBDIR route_dir.mkdir(parents=True, exist_ok=True) geojson_path = route_dir / "route_main.geojson" atomic_write_json(geojson_path, _route_geojson(polyline)) + log_b05_debug( + logger, + "calculation.geojson_saved", + path=geojson_path.relative_to(project_root).as_posix(), + coordinate_count=len(polyline), + ) # 통계 요약 (solver 메트릭에서 파생) metrics = result["metrics"] @@ -112,10 +147,19 @@ def run_route_design( "cost_score": None, } + render_points = _sample_render_points(polyline, chainage_m, _MAX_RENDER_POINTS) + log_b05_debug( + logger, + "calculation.output_prepared", + render_point_count=len(render_points), + first_render_point=render_points[0] if render_points else None, + last_render_point=render_points[-1] if render_points else None, + statistics=statistics, + ) return { "route_data_path": geojson_path.relative_to(project_root).as_posix(), "solver_result": result, - "render_points": _sample_render_points(polyline, chainage_m, _MAX_RENDER_POINTS), + "render_points": render_points, "statistics": statistics, "grade_percent": [seg.get("max_grade_pct") for seg in result.get("segments", [])], "constraints": result.get("conditions_snapshot", {}), diff --git a/B05_wf2_Route/B05_wf2_Route_Router.py b/B05_wf2_Route/B05_wf2_Route_Router.py index 2b3aa4f7..755ff91a 100644 --- a/B05_wf2_Route/B05_wf2_Route_Router.py +++ b/B05_wf2_Route/B05_wf2_Route_Router.py @@ -10,6 +10,7 @@ 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_Debug import log_b05_debug from B05_wf2_Route.B05_wf2_Route_Engine import run_route_design from B05_wf2_Route.B05_wf2_Route_Repository import ( confirm_route, @@ -55,10 +56,23 @@ async def solve_route( "algorithm": request.algorithm, "surface_model_id": request.surface_model_id, } + log_b05_debug( + logger, + "request.solve", + project_id=str(project_id), + workflow_stage_params=params, + ) async with pool.acquire() as connection: async with connection.cursor() as cursor: await start_stage(cursor, str(project_id), 2, params) await connection.commit() + log_b05_debug( + logger, + "db.project_workflow_stages.start_stage_committed", + project_id=str(project_id), + stage_no=2, + params=params, + ) stored_path = await get_project_storage_relative_path(connection, project_id) project_root = Path(resolve_stored_project_path(stored_path)) @@ -76,38 +90,88 @@ async def solve_route( ) solver = design["solver_result"] metrics = solver["metrics"] + log_b05_debug( + logger, + "calculation.complete", + project_id=str(project_id), + metrics=metrics, + required_points_ok=solver.get("required_points_ok"), + route_data_path=design["route_data_path"], + ) await connection.begin() try: - route_id = await create_route( - connection, - project_id=project_id, - surface_model_id=request.surface_model_id, - total_length_m=metrics.get("length_m"), - start_chainage_m=0.0, - end_chainage_m=metrics.get("length_m"), - grade_percent=design["grade_percent"], - constraints=design["constraints"], - algorithm_params={ + route_record = { + "project_id": str(project_id), + "surface_model_id": request.surface_model_id, + "status": "DRAFT", + "start_chainage_m": 0.0, + "end_chainage_m": metrics.get("length_m"), + "total_length_m": metrics.get("length_m"), + "grade_percent": design["grade_percent"], + "constraints": design["constraints"], + "algorithm_params": { **design["algorithm_params"], "metrics": metrics, "curve_warning_segments": solver.get("curve_warning_segments", []), }, - route_data_path=design["route_data_path"], + "route_data_path": design["route_data_path"], + } + log_b05_debug(logger, "db.routes.insert", record=route_record) + route_id = await create_route( + connection, + project_id=project_id, + surface_model_id=route_record["surface_model_id"], + total_length_m=route_record["total_length_m"], + start_chainage_m=route_record["start_chainage_m"], + end_chainage_m=route_record["end_chainage_m"], + grade_percent=route_record["grade_percent"], + constraints=route_record["constraints"], + algorithm_params=route_record["algorithm_params"], + route_data_path=route_record["route_data_path"], ) - await insert_route_points(connection, route_id, design["render_points"]) + render_points = design["render_points"] + log_b05_debug( + logger, + "db.route_points.insert_many", + route_id=route_id, + row_count=len(render_points), + first_row=render_points[0] if render_points else None, + last_row=render_points[-1] if render_points else None, + ) + await insert_route_points(connection, route_id, render_points) stats = design["statistics"] + statistics_record = { + "route_id": route_id, + "min_slope": stats["min_slope"], + "max_slope": stats["max_slope"], + "mean_slope": stats["mean_slope"], + "cost_score": stats["cost_score"], + } + log_b05_debug( + logger, + "db.route_statistics.insert", + record=statistics_record, + ) await create_route_statistics( connection, - route_id=route_id, - min_slope=stats["min_slope"], - max_slope=stats["max_slope"], - mean_slope=stats["mean_slope"], - cost_score=stats["cost_score"], + **statistics_record, ) await connection.commit() - except Exception: + log_b05_debug( + logger, + "db.route_transaction.committed", + project_id=str(project_id), + route_id=route_id, + ) + except Exception as exc: await connection.rollback() + log_b05_debug( + logger, + "db.route_transaction.rolled_back", + project_id=str(project_id), + reason=str(exc), + ) raise return RouteSolveResponse( @@ -188,12 +252,34 @@ async def confirm_latest_route(project_id: UUID) -> RouteConfirmResponse | JSONR ) await connection.begin() try: + log_b05_debug( + logger, + "db.routes.confirm", + project_id=str(project_id), + route_id=latest["id"], + previous_status=latest["status"], + next_status="CONFIRMED", + ) await confirm_route(connection, latest["id"]) async with connection.cursor() as cursor: await complete_stage(cursor, str(project_id), 2) await connection.commit() - except Exception: + log_b05_debug( + logger, + "db.route_confirmation.committed", + project_id=str(project_id), + route_id=latest["id"], + completed_stage=2, + ) + except Exception as exc: await connection.rollback() + log_b05_debug( + logger, + "db.route_confirmation.rolled_back", + project_id=str(project_id), + route_id=latest["id"], + reason=str(exc), + ) raise return RouteConfirmResponse(project_id=str(project_id), route_id=latest["id"]) except Exception: diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts index 2f4eb8a3..e079d58f 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts @@ -224,6 +224,7 @@ export async function renderB05Route(root: HTMLElement): Promise { await confirmRoute(activeProjectId); renderLatest(await fetchLatestRoute(activeProjectId)); showToast("경로를 확정했습니다.", "success"); + goToWorkflowStage(activeProjectId, WORKFLOW_STEP_ROUTES[3]); } catch (error) { showToast(error instanceof Error ? error.message : "경로 확정에 실패했습니다.", "error"); } finally { diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Style.css b/B05_wf2_Route/B05_wf2_Route_UI_Style.css index eafd1d92..8146a94c 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Style.css +++ b/B05_wf2_Route/B05_wf2_Route_UI_Style.css @@ -29,16 +29,19 @@ .b05-route__panel { display: flex; flex-direction: column; + width: 100%; + min-width: 0; gap: var(--spacing-16); - min-width: 300px; padding-bottom: var(--spacing-24); } .b05-route__panel-section { display: flex; flex-direction: column; + width: 100%; + min-width: 0; gap: var(--spacing-8); - padding: var(--spacing-16); + padding: calc(var(--spacing-8) + var(--spacing-4)); border: 1px solid var(--color-border); border-radius: var(--radius-cards); background: var(--color-surface-raised); @@ -55,6 +58,7 @@ .b05-route__metrics { display: flex; flex-direction: column; + min-width: 0; gap: var(--spacing-8); } @@ -69,6 +73,7 @@ .b05-route__field select, .b05-route__panel-section > select { width: 100%; + min-width: 0; padding: var(--spacing-8); border: 1px solid var(--color-border); border-radius: var(--radius-inputs); @@ -86,6 +91,7 @@ .b05-route__palette { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 0; gap: var(--spacing-8); }