diff --git a/B06_Section/B06_Section_Engine_Attach.py b/B06_Section/B06_Section_Engine_Attach.py index af14d1681..032a8fd95 100644 --- a/B06_Section/B06_Section_Engine_Attach.py +++ b/B06_Section/B06_Section_Engine_Attach.py @@ -141,9 +141,32 @@ def attach_shapes( return misses -def attach_saved_shapes(project_root: Path, sections: list[dict[str, Any]]) -> None: - """저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 구조물 정본을 못 읽으면 관만(횡단 조회는 이어 감).""" - pipes = read_pipe_points_file(pipe_points_file(project_root)) +def saved_pipes(project_root: Path, route_data_path: str | None = None) -> list[Any]: + """관 지점 정본 — 노선 경로를 주면 그 노선에 얹어 읽음(472 덧) · 없으면 파일 그대로. + + 측점 계획(`_load_pipe_points`) · 관 지점 GET · 화면과 같은 이월 — 지문이 어긋난 옛 저장분도 + 측점이 선 자리에 관 세트가 얹힘.""" + if route_data_path: + from B05_Profile.B05_Profile_Engine_Sections import ( + _load_pipe_points, + _load_route_polyline, + ) + + try: + return _load_pipe_points( + project_root, _load_route_polyline(project_root, route_data_path) + ) + except (FileNotFoundError, ValueError): + logger.warning("B06 관 지점: 노선을 못 읽어 저장분 그대로 얹음 (%s)", route_data_path) + return read_pipe_points_file(pipe_points_file(project_root)) + + +def attach_saved_shapes( + project_root: Path, sections: list[dict[str, Any]], route_data_path: str | None = None +) -> None: + """저장분 현황(관 지점 · 구조물 정본)으로 얹음 — 관 = 그 노선에 얹은 것(`saved_pipes`) · + 구조물 정본을 못 읽으면 관만(횡단 조회는 이어 감).""" + pipes = saved_pipes(project_root, route_data_path) try: structures = load_structures(str(project_root))[1] except Exception: # noqa: BLE001 — 정본을 못 읽어도 횡단 조회는 이어 간다 diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 03dd566c3..1c3f93d08 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -232,7 +232,9 @@ async def get_sections(project_id: UUID, route_id: int) -> SectionSummaryRespons ) -def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dict: +def _read_section_detail( + project_root: Path, longitudinal_file_path: str, route_data_path: str | None = None +) -> dict: """검증된 프로젝트 루트 안의 종단 및 횡단 JSON을 읽는다.""" root = project_root.resolve() longitudinal_path = (root / longitudinal_file_path).resolve() @@ -258,7 +260,7 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic ): raise ValueError("종횡단 상세 파일 형식이 올바르지 않습니다.") # 측점 그림 자료(관 세트 · 벽 · 포장 · 맹암거 · 빈칸 경고) = 형상 얹기 한 벌(323-4 · 화면 짝 attachShapes) - attach_saved_shapes(root, cross_sections) + attach_saved_shapes(root, cross_sections, route_data_path) relabel_pipe_stations(root, cross_sections) # 저장으로 종류만 바꾼 관 — 카드 제목(54-4) return {"longitudinal": longitudinal, "cross_sections": cross_sections} @@ -314,10 +316,11 @@ async def get_section_detail( try: # 서로 기다릴 이유가 없는 읽기 셋 — 원격 DB 라 순차로 내면 왕복이 그대로 더해진다 # (질의 하나 약 12ms, 2026-09-06 실측). 같이 보내 가장 느린 하나의 시간만 쓴다. - longitudinal, stored_path, designs = await asyncio.gather( + longitudinal, stored_path, designs, source = await asyncio.gather( run_with_connection(get_longitudinal_section, project_id, route_id), run_with_connection(get_project_storage_relative_path, project_id), run_with_connection(get_cross_section_designs, route_id), + run_with_connection(get_route_generation_source, project_id, route_id), ) if not longitudinal: return JSONResponse( @@ -329,6 +332,9 @@ async def get_section_detail( _read_section_detail, project_root, str(longitudinal["longitudinal_file_path"]), + (source or {}).get( + "route_data_path" + ), # 관 = 이 노선에 얹은 것(측점 계획 · 화면과 같음) ) # DB에만 있는 잠정 설계 지정을 chainage 근사로 각 횡단에 얹어 화면 복원을 돕는다. for record in designs: @@ -461,7 +467,7 @@ async def regenerate_route_sections( raise result = sections["result"] # 재생성 응답도 상세 조회와 같은 형상 얹기 한 벌(323-4) — 화면이 어긋나지 않게. - attach_saved_shapes(project_root, result["cross_sections"]) + attach_saved_shapes(project_root, result["cross_sections"], source["route_data_path"]) return SectionDetailResponse( longitudinal=result["longitudinal"], cross_sections=result["cross_sections"] ) @@ -517,12 +523,14 @@ async def preview_cross_designs( ) stored_path = await get_project_storage_relative_path(connection, project_id) designs = await get_cross_section_designs(connection, route_id) + source = await get_route_generation_source(connection, project_id, route_id) designs = _with_design_choices(designs, request.design_choices) project_root = Path(resolve_stored_project_path(stored_path)) detail = await asyncio.to_thread( _read_section_detail, project_root, str(longitudinal_row["longitudinal_file_path"]), + (source or {}).get("route_data_path"), ) def rebuild() -> None: diff --git a/docs/wiki/concepts/design_page/calculations.md b/docs/wiki/concepts/design_page/calculations.md index 4055c0dc2..51328fdfa 100644 --- a/docs/wiki/concepts/design_page/calculations.md +++ b/docs/wiki/concepts/design_page/calculations.md @@ -44,7 +44,7 @@ source: ["docs/raw/조사/2026-10-19_472_구조다시세움_목록.md", "PLAN.md | F1 | 시설 따라감(노선 바뀜) | `common_util_route_follow.ts:291` · `logic/B05_Profile_Pipeline_Follow.ts` · `logic/B05_Profile_Structure_Follow.ts` | `common_util_route_follow.py:120` | — | 짝 · `test_410_follow_xy` · `test_344_2_route_pairs` | | F2 | 추가 측점 따라감 | `B05_Profile_Api_PlusStations.ts` followPlusDraft | `B05_Profile_Plus_Stations.py:82` | — | 짝 · `test_459_route_seat_mirror` | | F3 | 시설 측점 계획 | `common_util_structure_station.ts` | `common_util_structure_station.py` | — | 짝 · `test_322_3_plan_mirror` | -| F4 | 형상 얹기(관 세트 · 벽 · 포장 그림 자료) | `common_util_structure_attach.ts:130` | `B06_Section_Engine_Attach.py:114` | — | 짝 · `test_323_4_attach_mirror` | +| F4 | 형상 얹기(관 세트 · 벽 · 포장 그림 자료) — 관 = 지금 노선에 얹은 저장분(서버 `saved_pipes` = 측점 계획 · 관 지점 GET 과 같은 이월) | `common_util_structure_attach.ts:130` | `B06_Section_Engine_Attach.py:114` | — | 짝 · `test_323_4_attach_mirror` · `test_472_d_pipe_route_attach` | | F5 | 관 세트(관경 · 날개벽 · 집수정) | `common_util_culvert_sets.ts` · `logic/B06_Section_UI_Cross_Culvert_Const.ts` | `B06_Section_Engine_Culvert.py` | Const = sc · co · ex | 짝 · `test_b06_culvert_sets_mirror` · `test_b06_pipe_wall_height_mirror` | | F6 | 벽 · 포장 | `common_util_structure_walls.ts` · `common_util_structure_pavement.ts` | `B06_Section_Engine_Structures_Wall.py` · `common_util_structure_pavement.py` | — | 짝 · `test_b06_structure_walls_mirror` · `test_b06_structure_pavement_mirror` | | F7 | 관 길이 · 구조물 면적 · 다단 벽 | `B06_Section_Structure_Layouts.ts:95 · 287` · `design_structure_pipe_calc.ts` · `common_util_cross_structure_areas.ts` | — | sc | ② · `test_b06_pipe_length` · `test_299_ford_span_pipe` | diff --git a/resources/tester/helper_472_1_paths.py b/resources/tester/helper_472_1_paths.py index 587b9b9d7..f49187936 100644 --- a/resources/tester/helper_472_1_paths.py +++ b/resources/tester/helper_472_1_paths.py @@ -234,7 +234,9 @@ def server_route(root: Path) -> dict[str, Any]: entries = compute_default_designs(root, LONG, chainages, None, inputs) by_m = {round(at, 3): {**design, **adjusts.get(round(at, 3), {})} for at, design in entries} # [확정] 상세 읽기(`get_section_detail`) — 행 설계(0.01 m) 얹기 · 없는 측점 기본값 - detail = _read_section_detail(root, LONG) + detail = _read_section_detail( + root, LONG, ROUTE + ) # 관 = 이 노선에 얹은 것(get_section_detail 과 같음) for section in detail["cross_sections"]: hit = next( (d for at, d in by_m.items() if abs(at - float(section["chainage_m"])) < 0.01), None diff --git a/resources/tester/test_472_d_pipe_route_attach.py b/resources/tester/test_472_d_pipe_route_attach.py new file mode 100644 index 000000000..10a16b47d --- /dev/null +++ b/resources/tester/test_472_d_pipe_route_attach.py @@ -0,0 +1,84 @@ +"""PLAN 472 덧 (요구 272) — 서버 상세 읽기 관 세트 = 측점 계획 · 화면과 같은 관 자리. + +관 지점 정본의 노선 지문이 지금 노선과 어긋남(옛 노선 누가거리 394 · 좌표는 지금 노선 351) — +측점 계획(`_load_pipe_points`) · 관 지점 GET · 화면은 좌표로 351 에 얹는데 서버 상세 읽기 +(`attach_saved_shapes`)만 파일 누가거리 394 로 찾아 351 측점에 관 세트를 못 붙였음(새로고침 = +관 세트 없음 · 관 지우기 ↶ = 화면이 다시 얹어 관 세트 있음 → 누가토량 갈림). + + ./venv/Scripts/python.exe -m pytest resources/tester/test_472_d_pipe_route_attach.py -q +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B05_Profile.B05_Profile_Engine_Sections import ( # noqa: E402 + _load_pipe_points, + _load_route_polyline, +) +from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes # noqa: E402 + +ROUTE = "B05_Profile/route/route_main.geojson" + + +def _project(root: Path) -> None: + route = root / ROUTE + route.parent.mkdir(parents=True) + route.write_text( + json.dumps( + { + "type": "Feature", + "geometry": {"type": "LineString", "coordinates": [[0, 0, 0], [1000, 0, 0]]}, + } + ), + encoding="utf-8", + ) + edits = root / "B04_PreProcess" / "drainage" / "edits" + edits.mkdir(parents=True) + pipe = { + "id": "p35100", + "chainage_m": 394.0, # 옛 노선 누가거리 + "x": 351.0, + "y": 0.3, + "options": {"pipe_kind": "파형강관", "pipe_diameter_mm": "1000"}, + } + (edits / "pipe_points.json").write_text( + json.dumps({"route_signature": "옛-노선", "points": [pipe]}), encoding="utf-8" + ) + + +def _sections() -> list[dict]: + return [ + {"chainage_m": at, "center_x": at, "center_y": 0.0, "structure": None} + for at in (340.0, 351.0, 360.0, 380.0, 400.0) + ] + + +def test_상세_읽기_관_세트_자리_측점_계획과_같음(tmp_path: Path) -> None: + _project(tmp_path) + planned = _load_pipe_points(tmp_path, _load_route_polyline(tmp_path, ROUTE)) + assert [round(p.chainage_m, 2) for p in planned] == [351.0] # 측점 계획 = 351 측점 + + sections = _sections() + attach_saved_shapes(tmp_path, sections, ROUTE) + owners = {s["chainage_m"]: s["culvert"]["chainage_m"] for s in sections if "culvert" in s} + assert owners == {351.0: 351.0} + + +def test_노선_모름_저장분_그대로(tmp_path: Path) -> None: + _project(tmp_path) + sections = _sections() + attach_saved_shapes(tmp_path, sections) # 노선 경로 없음 = 파일 누가거리(394 · 제 측점 없음) + assert not any("culvert" in s for s in sections) + + +def test_노선_파일_없음_저장분_그대로(tmp_path: Path) -> None: + _project(tmp_path) + sections = _sections() + attach_saved_shapes(tmp_path, sections, "B05_Profile/route/없음.geojson") + assert not any("culvert" in s for s in sections)