From 53525a1d91047f9ec20a33d6857933a1e7f7e7be Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 00:44:29 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05):=20=EB=85=B8=EC=84=A0=20=EC=9E=AC?= =?UTF-8?q?=EA=B3=84=EC=82=B0=EC=9D=B4=20=EB=81=8A=EA=B2=BC=EC=9D=84=20?= =?UTF-8?q?=EB=95=8C=20=EB=85=B8=EC=84=A0=EB=A7=8C=20=EA=B0=88=EC=95=84=20?= =?UTF-8?q?=EB=81=BC=EC=9B=8C=EC=A7=84=20=EC=B1=84=20=EA=B5=B3=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보조 창 실측 — [확인] 이 체인 중간에 400 으로 끊겨도 route/replan 은 success 를 돌려주고 계획노선 CSV 는 이미 새 노선으로 바뀐 뒤였음. 그 결과 배수유역·관은 새 노선, 종횡단은 옛 노선으로 어긋난 채 굳고(배수관 측점 9 -> 0), 다시 누를수록 어긋남이 쌓였음 (1,097.4 -> 1,079.4 -> 1,064.7m). - run_redesign_chain 이 중단 사유를 문자열로 돌려줌(성공은 None). 중단 지점 넷 모두. - replan 이 쓰기 전에 이전 수정본을 쥐고 있다가 실패하면 되돌림. 처음 고치는 경우였으면 파일을 지워 초기 폴리라인으로 돌아감. - 실패는 409 + 사유로 응답 — 화면이 성공으로 보이던 것을 막음. 원인 자체(자동탐색이 사용자 노선을 다시 풀다 실패)는 별건으로 남음 — 로그 사유 「세그먼트 1 (BP -> CP1) 경로 탐색 실패: 종단경사 한계(26%)·최소곡선반지름(12m)·회피지역 제약으로 통과 경로가 없습니다」. 고친 노선을 그대로 받아 쓰는 입구가 필요함(PLAN 0-2 항목 9). 시험 409 통과·17 건너뜀. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_Service_Chain.py | 14 ++++++----- B05_Profile/B05_Profile_Router_Replan.py | 25 ++++++++++++++++---- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index cd3052e9..fb91884f 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -331,7 +331,7 @@ async def run_redesign_chain( project_id: UUID, surface_model_id: int, selection: dict[str, Any], -) -> None: +) -> str | None: """B04 재확정 후 — **사용자 입력을 유지한 채** 새 지표면 기준으로 B05·B06 재계산·저장. 관리자가 B04에서 다른 지표면 모델로 재확정하면(2026-08-04 사용자 확정) 그 값을 @@ -405,7 +405,7 @@ async def run_redesign_chain( logger.warning( "재확정 체인 중단(stage 2 params에 제어점 없음): project_id=%s", project_id ) - return + return "노선 제어점(BP·EP)이 없어 재계산할 수 없습니다." # 2) B05 재계산 — 지표면 관련 값만 새 확정 선택으로 교체, 나머지는 사용자 저장분. request = RouteSolveRequest( @@ -455,7 +455,7 @@ async def run_redesign_chain( solve_result.status_code, reason, ) - return + return f"노선 재계산 실패({solve_result.status_code}): {reason}" new_route_id = int(solve_result.route_id) logger.info( "재확정 체인 B05 재계산 완료: project_id=%s %s→%s", @@ -501,7 +501,7 @@ async def run_redesign_chain( project_id, confirm_result.status_code, ) - return + return f"노선 확정 실패({confirm_result.status_code})" # 노선이 바뀌었으므로 배수유역·관 지점도 새 노선 기준으로 다시 만든다 — 옛 관 자리로 # 계획선을 앉히면 전부 어긋나고, 저장분은 노선 변경을 스스로 알지 못한다. await _prepare_drainage_pipes_and_reprofile(project_id, new_route_id, refresh=True) @@ -521,7 +521,7 @@ async def run_redesign_chain( project_id, sections_result.status_code, ) - return + return f"종횡단 확정 실패({sections_result.status_code})" logger.info( "재확정 체인 완료: project_id=%s route %s→%s (설계 %d건 이월)", project_id, @@ -529,8 +529,10 @@ async def run_redesign_chain( new_route_id, carried, ) - except Exception: + return None + except Exception as exc: logger.exception("재확정 체인 실패: project_id=%s", project_id) + return f"재계산 중 오류: {exc}" finally: if project_root is not None: clear_designing(project_root) diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 783ba8fe..0af08e06 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -287,7 +287,12 @@ async def _recompute(project_id: UUID, project_root: Path, stored_path: str) -> await start_stage(cursor, str(project_id), 2, params) await connection.commit() - await run_redesign_chain(project_id, int(surface_model_id), selection) + failure = await run_redesign_chain(project_id, int(surface_model_id), selection) + if failure: + # 체인이 끊기면 **노선만 새것으로 갈아 끼워진 채** 종횡단은 옛 노선에 남아 어긋난다 + # (2026-09-06 실측: 배수유역·관은 새 노선, 종횡단은 옛 노선 → 배수관 측점 9 → 0). + # 부르는 쪽이 계획노선을 되돌리도록 사유를 올려 보낸다. + return {"error": failure} pool = get_db_pool() async with pool.acquire() as connection: @@ -363,9 +368,11 @@ async def replan_route( # 화면이 보낸 것은 **노드(꺾임점)** 다 — 같은 R 규칙으로 다시 폴리라인을 만든다. # 노드만 옮기면 선이 저절로 규칙을 지키는 것이 이 구조의 목적이다(2026-09-06 사용자). nodes = [(vertex.x, vertex.y) for vertex in request.vertices] - summary = await asyncio.to_thread( - _write_planned_polyline, planned_route_working_path(project_root), nodes, radius_m - ) + # 체인이 끊기면 되돌릴 수 있게 이전 수정본을 손에 쥔다 — 실패했는데 노선만 바뀌면 + # 종횡단과 어긋난 채 굳고, 다시 누를수록 어긋남이 쌓인다(2026-09-06 실측). + working_path = planned_route_working_path(project_root) + previous = working_path.read_bytes() if working_path.is_file() else None + summary = await asyncio.to_thread(_write_planned_polyline, working_path, nodes, radius_m) written = summary["vertices"] logger.info( "계획노선 갈아 끼움: project_id=%s 노드 %d → 정점 %d (곡선 %d · 위반 %d · R %.1fm)", @@ -378,6 +385,16 @@ async def replan_route( ) result = await _recompute(project_id, project_root, stored_path) if "error" in result: + # 노선을 되돌린다 — 실패했는데 새 노선만 남으면 화면·정본이 어긋난 채 굳는다. + if previous is None: + working_path.unlink(missing_ok=True) + else: + working_path.write_bytes(previous) + logger.warning( + "계획노선 갈아 끼우기 실패 — 노선을 되돌렸습니다: project_id=%s 사유=%s", + project_id, + result["error"], + ) return JSONResponse( status_code=409, content={"status": "error", "message": result["error"]} )