From f262007f56006c8c1bb29526ff10cca7c6ecfae1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 20:03:02 +0900 Subject: [PATCH] =?UTF-8?q?fix(profile):=20B05=20[=EC=A0=80=EC=9E=A5]=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=B8=A1=EC=A0=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=3D=20=EC=84=9C=EB=B2=84=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=C2=B7=20=EC=86=8C=EB=8B=A8=20=C2=B7=20?= =?UTF-8?q?=EC=98=B9=EB=B2=BD=20=C2=B7=20=ED=9D=99=EB=A7=89=EC=9D=B4=20?= =?UTF-8?q?=C2=B7=20=EA=B8=B0=EC=8A=AD=EB=A7=89=EC=9D=B4=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EC=A7=80=EC=9B=80=20=EB=A7=89=EC=9D=8C=20(238-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P --- B05_Profile/B05_Profile_Router_Confirm.py | 37 ++-- ...est_238_1_save_keeps_structure_stations.py | 163 ++++++++++++++++++ 2 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 resources/tester/test_238_1_save_keeps_structure_stations.py diff --git a/B05_Profile/B05_Profile_Router_Confirm.py b/B05_Profile/B05_Profile_Router_Confirm.py index 8e5117630..97668b447 100644 --- a/B05_Profile/B05_Profile_Router_Confirm.py +++ b/B05_Profile/B05_Profile_Router_Confirm.py @@ -15,7 +15,12 @@ import aiomysql from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B05_Profile.B05_Profile_Engine_Grade_Alignment import chainage_key from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile -from B05_Profile.B05_Profile_Engine_Sections import generate_irregular_sections +from B05_Profile.B05_Profile_Engine_Sections import ( + _load_pipe_points, + _load_route_polyline, + generate_irregular_sections, + resolve_extra_stations, +) from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions from B05_Profile.B05_Profile_Repository import get_surface_crs_epsg from B05_Profile.B05_Profile_Schema import RouteConfirmRequest @@ -350,17 +355,25 @@ async def _append_irregular_cross_sections( save_sampling_snapshot(project_root, request) stored_options = await get_latest_section_options(connection, project_id) crs_epsg = await get_surface_crs_epsg(connection, project_id, request.surface_model_id) - irregular_stations = await asyncio.to_thread( - generate_irregular_sections, - project_root, - str(route["route_data_path"]), - request.filter_key, - request.method, - request.smooth, - extra_stations=request.extra_stations(), - options=_section_options_from_stored(stored_options), - crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None, - ) + route_data_path = str(route["route_data_path"]) + + def _generate() -> list[dict[str, Any]]: + # 측점 목록 = 서버 정본(관 · 구조물)에서 — 화면 목록(관 · A군)을 받아 적으면 서버가 심은 + # 소단 · 옹벽 · 흙막이 · 독립 기슭막이 시작 · 종료 측점이 통째로 빠짐(238-1). + polyline = _load_route_polyline(project_root, route_data_path) + extras = resolve_extra_stations(project_root, _load_pipe_points(project_root, polyline)) + return generate_irregular_sections( + project_root, + route_data_path, + request.filter_key, + request.method, + request.smooth, + extra_stations=extras, + options=_section_options_from_stored(stored_options), + crs=f"EPSG:{crs_epsg}" if crs_epsg is not None else None, + ) + + irregular_stations = await asyncio.to_thread(_generate) if not irregular_stations: return longitudinal = await get_longitudinal_section(connection, project_id, route["id"]) diff --git a/resources/tester/test_238_1_save_keeps_structure_stations.py b/resources/tester/test_238_1_save_keeps_structure_stations.py new file mode 100644 index 000000000..d2ae972af --- /dev/null +++ b/resources/tester/test_238_1_save_keeps_structure_stations.py @@ -0,0 +1,163 @@ +"""238-1 B05 [저장]이 구조물 측점을 지우지 않음. + +[저장](경로 확정 길 · `_append_irregular_cross_sections`)은 종단 비정규 측점을 통째로 갈아 끼운다. +그 목록을 화면이 보낸 「관 + A군」으로 쓰면 서버가 심어 둔 소단 · 옹벽 · 흙막이 · 독립 기슭막이 +시작 · 종료 측점이 빠진다 → 서버 측점 규칙(`resolve_extra_stations`)으로 만든 목록을 씀. + +- 사본: 임시 폴더 프로젝트(직선 노선 · 관 하나 + 기슭막이 · 소단 · 옹벽 · 흙막이) +- 앞: B06 [측점 만들기]와 같은 길(서버 목록으로 심음) · 뒤: 화면 목록(관만)을 실은 [저장] +- 판정: 앞뒤 구조물 측점 같음 · 소단 측점(깊이 patch 가 붙는 자리) 남음 · 관 옮김은 반영 +""" + +import asyncio +import json +from pathlib import Path + +import numpy as np + +import B05_Profile.B05_Profile_Engine_Sections as sections +import B05_Profile.B05_Profile_Router_Confirm as confirm +from B05_Profile.B05_Profile_Schema import RouteConfirmRequest +from common_util.common_util_drainage_pipes import route_signature +from common_util.common_util_route_geometry import RouteVertex + +LINE = [[0.0, 0.0, 100.0], [350.0, 0.0, 100.0]] +ROUTE = "B05_Profile/route/route.geojson" +LONG = "B06_Section/longitudinal/longitudinal.json" +PIPE = {"chainage_m": 150.0, "source": "user", "options": {"pipe_diameter_mm": 800}} +REVET = { + "chainage_m": 301.0, + "source": "user", + "facility": "revetment", + "start_m": 296.0, + "end_m": 316.0, +} +STRUCTURES = [ + {"type_id": "soil_guard", "chainage_m": 45.0, "start_m": 41.0, "end_m": 51.0}, + {"type_id": "berm", "chainage_m": 201.0, "start_m": 196.0, "end_m": 206.0}, + {"type_id": "retaining_wall", "chainage_m": 251.0, "start_m": 246.0, "end_m": 256.0}, +] + + +class _Flat: + def sample_xy(self, xy): + count = len(np.asarray(xy, dtype=np.float64)) + return np.full(count, 100.0), np.ones(count, dtype=bool) + + +def _write(root: Path, rel: str, data) -> None: + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") + + +def _write_pipes(root: Path, points) -> None: + vertices = [RouteVertex(x=p[0], y=p[1], z=p[2], chainage_m=0.0) for p in LINE] + _write( + root, + "B04_PreProcess/drainage/edits/pipe_points.json", + {"route_signature": route_signature(vertices), "points": points}, + ) + + +def _project(root: Path) -> None: + _write(root, ROUTE, {"geometry": {"type": "LineString", "coordinates": LINE}}) + _write_pipes(root, [PIPE, REVET]) + for item in STRUCTURES: + item["placement"] = "interval" + _write(root, "B05_Profile/route/structures.json", {"revision": 1, "structures": STRUCTURES}) + regular = [{"chainage_m": float(c), "kind": "regular"} for c in range(0, 351, 20)] + _write(root, LONG, {"stations": regular}) + + +def _irregular(root: Path) -> list[tuple[str, str]]: + data = json.loads((root / LONG).read_text(encoding="utf-8")) + return [ + (f"{float(s['chainage_m']):.2f}", s.get("structure")) + for s in data["stations"] + if s.get("kind") == "irregular" + ] + + +def _patch(monkeypatch, root: Path) -> None: + async def _value(value, *_a, **_k): + return value + + monkeypatch.setattr(sections, "build_surface_sampler", lambda *_a, **_k: _Flat()) + monkeypatch.setattr(confirm, "get_project_storage_relative_path", lambda *_a: _value("p")) + monkeypatch.setattr(confirm, "resolve_stored_project_path", lambda _p: str(root)) + monkeypatch.setattr(confirm, "get_latest_section_options", lambda *_a: _value(None)) + monkeypatch.setattr(confirm, "get_surface_crs_epsg", lambda *_a: _value(None)) + monkeypatch.setattr( + confirm, + "get_longitudinal_section", + lambda *_a: _value({"longitudinal_file_path": LONG}), + ) + + +def _plant_like_b06(root: Path) -> None: + """B06 [측점 만들기]와 같은 길 — 서버 목록으로 뜨고 종단에 병합.""" + pipes = sections._load_pipe_points(root, LINE) + stations = sections.generate_irregular_sections( + root, + ROUTE, + "csf", + "tin", + False, + extra_stations=sections.resolve_extra_stations(root, pipes), + ) + confirm._merge_irregular_into_longitudinal(root, LONG, stations) + + +def _save(root: Path, screen: list[tuple[float, str]]) -> None: + """B05 [저장] — 화면은 관 · A군 목록만 실어 보냄.""" + request = RouteConfirmRequest( + filter_key="csf", + method="tin", + surface_model_id=1, + irregular_stations=[{"chainage_m": c, "structure": s} for c, s in screen], + ) + assert request.can_regenerate() + asyncio.run( + confirm._append_irregular_cross_sections( + None, "p", {"id": 1, "route_data_path": ROUTE}, request + ) + ) + + +def test_save_keeps_server_planted_stations(tmp_path, monkeypatch): + _project(tmp_path) + _patch(monkeypatch, tmp_path) + _plant_like_b06(tmp_path) + before = _irregular(tmp_path) + labels = {label for _key, label in before} + assert {"소단", "옹벽", "흙막이", "기슭막이"} <= labels + assert [k for k, label in before if label == "기슭막이"] == ["296.00", "301.00", "316.00"] + + _save(tmp_path, [(150.0, "파형강관 D800")]) + + assert _irregular(tmp_path) == before + # 소단 깊이(194 · 측점 patch)가 붙는 자리 = 소단 측점 — 그대로 섬 + assert [k for k, label in _irregular(tmp_path) if label == "소단"] == [ + "196.00", + "201.00", + "206.00", + ] + + +def test_save_follows_moved_pipe(tmp_path, monkeypatch): + _project(tmp_path) + _patch(monkeypatch, tmp_path) + _plant_like_b06(tmp_path) + before = _irregular(tmp_path) + + # 관 150 → 165 (관 초안이 [저장] 차례상 먼저 정본에 들어감 · design_save.ts) + _write_pipes(tmp_path, [{**PIPE, "chainage_m": 165.0}, REVET]) + _save(tmp_path, [(165.0, "파형강관 D800")]) + + after = _irregular(tmp_path) + expected = sorted( + [item for item in before if item[0] != "150.00"] + [("165.00", "파형강관 D800")], + key=lambda item: float(item[0]), + ) + assert after == expected