"""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}), ) monkeypatch.setattr(confirm, "sync_cross_section_rows", lambda *_a: _value(0)) 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