From 2e8cf0d1c72678fef1d62cbb37931e8a02811c28 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 8 Aug 2026 15:56:29 +0900 Subject: [PATCH] =?UTF-8?q?fix(chain):=20=EC=9E=90=EB=8F=99=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EC=B2=B4=EC=9D=B8=EC=97=90=20=EB=B0=B0=EC=88=98?= =?UTF-8?q?=EC=9C=A0=EC=97=AD=20=EB=B6=84=EC=84=9D=C2=B7=EA=B4=80=20?= =?UTF-8?q?=EC=A7=80=EC=A0=90=20=ED=99=95=EC=A0=95=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 계획선 1차 로직(design_pipe_anchored_profile)은 저장된 관 지점을 변화점으로 쓰는데, 자동 설계 체인이 그 파일을 만들어 주지 않았다. 그래서 초기 계획선이 2차 폴백(지반 추종 직선 분할)으로 산출돼 관 자리 틸팅·R이 통째로 빠지고, B05/B06에 들어와도 배수유역도가 비어 있었다. - _prepare_drainage_pipes_and_reprofile() 신설: 배수유역 분석 → 기본 관 지점 확정 저장 → 배관 정착 계획선으로 종횡단 재생성. 실패해도 체인을 멈추지 않는다. - run_auto_design_chain: B05 경로 확정 다음, B06 확정 전에 호출. - run_redesign_chain: 노선이 바뀌었으므로 refresh=True로 호출 — 저장된 배수유역 분석 응답은 노선 변경을 스스로 알지 못한다. 검증(테스트 프로젝트 fc86f247, 노선 350m): 관 4개(계류 1 + 간격 보충 3) 확정 후 종횡단 재생성 → PVI 6개(BP + 관 4자리 + EP)가 관 측점과 일치하고 각 자리에 R(1108.57/180.79/450.0/580.97m)이 붙는 것을 확인. 반영 전에는 관과 무관한 지반 추종 변화점이었다. Co-Authored-By: Claude Opus 5 (1M context) --- B03_FileInput/B03_FileInput_Service_Chain.py | 83 ++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index d4cb4639..ddc509a6 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -52,6 +52,81 @@ def _planned_route_points_in_project_crs(project_root: Path) -> list[dict[str, f return [{"x": x, "y": y} for x, y in points] +async def _prepare_drainage_pipes_and_reprofile( + project_id: UUID, route_id: int, *, refresh: bool = False +) -> bool: + """배수유역 분석 → 기본 관 지점 확정 저장 → 배관 정착 계획선으로 종횡단 재생성. + + 계획선 1차 로직(`design_pipe_anchored_profile`)은 **저장된 관 지점**을 변화점으로 쓴다. + 자동 체인이 이 파일을 만들어 주지 않으면 초기 계획선이 2차 폴백(지반 추종 직선 분할)으로 + 산출돼 관 자리 틸팅·R이 통째로 빠진다(2026-08-08 사용자 보고). 같은 이유로 B05·B06에 + 들어왔을 때 배수유역도도 비어 있게 된다 — 여기서 미리 계산해 영구저장소에 남긴다. + + 실패해도 예외를 던지지 않는다 — 관 없이 만든 계획선이라도 남는 편이 낫다. + """ + from B04_PreProcess.B04_PreProcess_Router_Basins import put_pipe_points + from B04_PreProcess.B04_PreProcess_Router_Watershed import get_primary_region + from B06_Section.B06_Section_Repository import get_latest_section_options + from B06_Section.B06_Section_Router import regenerate_sections + from B06_Section.B06_Section_Schema import SectionRegenerateRequest + from config.config_db import get_db_pool + from config.config_system import SECTION_CROSS_HALF_WIDTH_M + + try: + # 1) 배수유역 분석(30초 내외). 저장분이 있으면 그대로 쓴다. + # 저장분은 노선이 바뀌었는지 보지 않으므로, 노선을 다시 푼 뒤에는 refresh로 부른다. + region = await get_primary_region(project_id, refresh=refresh) + if isinstance(region, JSONResponse): + logger.error( + "자동 설계 체인 배수유역 분석 실패(계획선 폴백 유지): project_id=%s status=%s", + project_id, + region.status_code, + ) + return False + + # 2) 기본 관(도로 × 상류 세류선) + 최대 간격 자동 보충을 정본으로 저장한다. + pipes = await put_pipe_points(project_id, None) + if isinstance(pipes, JSONResponse): + logger.error( + "자동 설계 체인 관 지점 확정 실패: project_id=%s status=%s", + project_id, + pipes.status_code, + ) + return False + pipe_count = int(pipes.get("saved_count") or 0) + + # 3) 저장된 관 자리를 물려 계획선을 다시 산출한다. 반폭은 이미 쓰던 값을 유지한다. + pool = get_db_pool() + async with pool.acquire() as connection: + stored_options = await get_latest_section_options(connection, project_id) + half_width = float( + (stored_options or {}).get("cross_half_width_m") or SECTION_CROSS_HALF_WIDTH_M + ) + regenerated = await regenerate_sections( + project_id, route_id, SectionRegenerateRequest(cross_half_width_m=half_width) + ) + if isinstance(regenerated, JSONResponse): + logger.error( + "자동 설계 체인 배관 정착 계획선 재산출 실패: project_id=%s status=%s", + project_id, + regenerated.status_code, + ) + return False + logger.info( + "자동 설계 체인 배수유역·배관 정착 계획선 완료: project_id=%s route_id=%s 관=%d개", + project_id, + route_id, + pipe_count, + ) + return True + except Exception: + logger.exception( + "자동 설계 체인 배수유역·관 지점 단계 실패(계획선은 폴백으로 유지): project_id=%s", + project_id, + ) + return False + + async def run_auto_design_chain( project_id: UUID, surface_model_id: int | None = None ) -> dict[str, Any] | None: @@ -134,6 +209,10 @@ async def run_auto_design_chain( ) return None + # 4.5) 배수유역 분석 → 관 지점 확정 → 배관 정착 계획선 재산출. + # B06 확정 전에 끝내야 확정 스냅샷에 배관 정착 계획선이 담긴다. + await _prepare_drainage_pipes_and_reprofile(project_id, route_id) + # 5) B06 횡단 설계 확정 — 미지정 측점을 기본값으로 채워 저장. stage 3은 # IN_PROGRESS(스텝바 노란 표시)로 남겨 사용자 검토·확정을 기다린다. sections_result = await confirm_sections( @@ -315,6 +394,10 @@ async def run_redesign_chain( confirm_result.status_code, ) return + # 노선이 바뀌었으므로 배수유역·관 지점도 새 노선 기준으로 다시 만든다 — 옛 관 자리로 + # 계획선을 앉히면 전부 어긋나고, 저장분은 노선 변경을 스스로 알지 못한다. + await _prepare_drainage_pipes_and_reprofile(project_id, new_route_id, refresh=True) + old_options = ((old_longitudinal or {}).get("data") or {}).get("options") or {} section_request = ( SectionConfirmRequest(standard_cross_section=old_options["standard_cross_section"])