From 401598277eb771318603295fceb5320fdb4db1c9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 22 Oct 2026 20:31:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(512-6=20=EB=B0=B0=EC=88=98=EC=9C=A0?= =?UTF-8?q?=EC=97=AD=20=EC=98=9B=20=EA=B7=B8=EB=A6=BC):=20=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20[=EC=A0=80=EC=9E=A5]=20=EB=92=A4=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C=20=EC=97=B0=20B05=20=3D=20=EB=A7=88=EC=A7=80=EB=A7=89?= =?UTF-8?q?=20[=EC=A0=80=EC=9E=A5]=20=EC=84=B8=EB=B6=80=20=EC=9C=A0?= =?UTF-8?q?=EC=97=AD=20=EC=A0=80=EC=9E=A5=EB=B3=B8=EC=9D=98=20=EC=98=9B=20?= =?UTF-8?q?=EC=9C=A0=EC=97=AD=20=C2=B7=20=EC=98=9B=20=EB=85=B8=EC=84=A0?= =?UTF-8?q?=EC=84=A0=20+=20=EA=B4=80=EB=A7=8C=20=EC=A7=80=EA=B8=88=20?= =?UTF-8?q?=EB=85=B8=EC=84=A0=20=EC=9C=84=20=C2=B7=20=EB=82=A1=EC=9D=8C=20?= =?UTF-8?q?=EC=A0=90=20=C2=B7=20[=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8]=20?= =?UTF-8?q?=EB=A1=9C=EB=A7=8C=20=EB=B0=94=EA=BF=88=20=C2=B7=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=B3=B8=20=EC=97=86=EC=9D=8C=20=C2=B7=20=EA=B9=A8?= =?UTF-8?q?=EC=A7=90=20=3D=20=EA=B4=80=20=EC=9E=90=EB=A6=AC=EB=A7=8C(?= =?UTF-8?q?=EC=98=9B=20=EA=BC=B4)=20=C2=B7=20=EC=9A=94=EA=B5=AC=20326-6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../B04_PreProcess_Router_Basins.py | 50 ++++++++++--- .../tester/test_512_6_drainage_saved_old.py | 72 +++++++++++++++++++ 2 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 resources/tester/test_512_6_drainage_saved_old.py diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index 8dec4add1..24928ccb5 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -16,6 +16,7 @@ from __future__ import annotations import asyncio import copy +import dataclasses import hashlib import json import logging @@ -27,6 +28,7 @@ from uuid import UUID from fastapi import APIRouter, Depends from fastapi.responses import JSONResponse +from pyproj import Transformer from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import STAGES, drainage_dir from B05_Profile.B05_Profile_Structures_Schema import DEFINITION_PATH, structure_type_map @@ -121,14 +123,18 @@ def _rebuilt(detail: DrainageDetail) -> list[PipePoint]: def _build_stale(context: DrainageContext, points: list[PipePoint]) -> DrainageDetail: - """낡은 분석 — 세부 유역 없이 관 자리만(지금 노선 위) · 시설 속성은 넘겨받은 그대로.""" + """낡은 분석 — 마지막 [저장]의 옛 유역 그림(저장본 파일 · 그 노선) + 관 자리만 지금 노선 위 · + 저장본이 없으면 관 자리만(512-6 · 새로 연 화면도 옛 그림 · [업데이트] 로만 바꿈).""" + pipes = pipes_from_chainages(context.vertices, [point.chainage_m for point in points]) + _retag(pipes, points) + saved = _read_cache_file(context.stored_path) + if saved is not None: + return dataclasses.replace(saved["value"][0], pipes=pipes, pipe_candidates=[]) to_lonlat = context.to_lonlat - detail = DrainageDetail( + return DrainageDetail( route_lonlat=[list(to_lonlat(vertex.x, vertex.y)) for vertex in context.vertices], - pipes=pipes_from_chainages(context.vertices, [point.chainage_m for point in points]), + pipes=pipes, ) - _retag(detail.pipes, points) - return detail # 세부 유역 저장본(342-3 · 부록 4 R15) — 열쇠 = 관 목록 · 노선(평면 · 종단 Z) · 분석 산출물 · @@ -165,6 +171,26 @@ def _cache_path(stored_path: str): return drainage_dir(stored_path) / DETAIL_CACHE_FILENAME +def _read_cache_file(stored_path: str) -> dict[str, Any] | None: + """[저장] 때 남긴 세부 유역 저장본 `{key, value: (detail, 관 목록)}` — 없거나 깨지면 None.""" + try: + with _cache_path(stored_path).open("rb") as file: + saved = pickle.load(file) + saved["value"][0].basins # 모양 확인 + return saved + except ( + OSError, + ValueError, + pickle.PickleError, + EOFError, + AttributeError, + KeyError, + TypeError, + IndexError, + ): + return None + + def _remember(key: str, value: tuple[DrainageDetail, list[PipePoint]]) -> None: _DETAIL_CACHE.pop(key, None) _DETAIL_CACHE[key] = value @@ -179,12 +205,8 @@ def _build_cached( key = _detail_key(context, points) hit = _DETAIL_CACHE.get(key) if hit is None: - try: - with _cache_path(context.stored_path).open("rb") as file: - saved = pickle.load(file) - hit = saved["value"] if saved.get("key") == key else None - except (OSError, pickle.PickleError, EOFError, AttributeError, KeyError, TypeError): - hit = None + saved = _read_cache_file(context.stored_path) + hit = saved["value"] if saved is not None and saved.get("key") == key else None if hit is None: detail, carried = _build(context.stored_path, context, points) if detail is None: @@ -312,6 +334,12 @@ def _payload( payload["route_signature"] = route_signature(context.vertices) payload["pipe_signature"] = pipe_signature([pipe.chainage_m for pipe in detail.pipes]) payload["stale"] = STALE_MESSAGE if stale else None + if stale: # 노선선 = 옛 그림의 노선(512-6 · 저장본 없으면 지금 노선 그대로) + back = Transformer.from_crs("EPSG:4326", context.crs, always_xy=True) + payload["route_xy"] = [ + [round(x, 3), round(y, 3)] + for x, y in (back.transform(lon, lat) for lon, lat in detail.route_lonlat) + ] return round_floats(payload, LONLAT_DIGITS) diff --git a/resources/tester/test_512_6_drainage_saved_old.py b/resources/tester/test_512_6_drainage_saved_old.py new file mode 100644 index 000000000..ed15a22dd --- /dev/null +++ b/resources/tester/test_512_6_drainage_saved_old.py @@ -0,0 +1,72 @@ +"""512-6(요구 326-6) 노선 [저장] 뒤 새로 연 B05 = 저장된 옛 유역 그림 + 낡음. + +- 낡은 분석(노선 지문 다름)이면 마지막 [저장]의 세부 유역 저장본(`04_detail_cache.pkl`)으로 + 옛 유역 · 옛 노선선을 돌려줌 · 관 자리만 지금 노선 위 · 저장본 없으면 관 자리만(옛 꼴). +- 화면은 열 때 그림이 없으니 그 답을 그대로 그림 + 낡음 점([업데이트] 로만 바꿈 · test_512_2). +""" + +import pickle +from pathlib import Path +from uuid import uuid4 + +from pyproj import Transformer + +from B04_PreProcess import B04_PreProcess_Router_Basins as basins +from common_util.common_util_drainage_context import DrainageContext +from common_util.common_util_drainage_detail import DrainageDetail, WatershedBasin +from common_util.common_util_route_geometry import RouteVertex + +CRS = "EPSG:5186" +TO = Transformer.from_crs(CRS, "EPSG:4326", always_xy=True) + + +def _context(tmp_path: Path, monkeypatch, shift: float) -> DrainageContext: + monkeypatch.setattr(basins, "drainage_dir", lambda _stored: tmp_path) + vertices = [ + RouteVertex(200000.0 + shift, 500000.0, 100.0, 0.0), + RouteVertex(200300.0 + shift, 500000.0, 90.0, 300.0), + ] + return DrainageContext( + stored_path="x", + project_root=tmp_path, + vertices=vertices, + crs=CRS, + to_lonlat=lambda x, y: TO.transform(x, y), + ) + + +def test_저장본_있으면_옛_그림과_옛_노선(tmp_path, monkeypatch): + old = DrainageDetail( + route_lonlat=[ + list(TO.transform(200000.0, 500000.0)), + list(TO.transform(200300.0, 500000.0)), + ], + basins=[WatershedBasin(index=1, chainage_m=120.0, outlet_x=200120.0, outlet_y=500000.0)], + ) + with (tmp_path / basins.DETAIL_CACHE_FILENAME).open("wb") as file: + pickle.dump({"key": "옛", "value": (old, [])}, file) + context = _context(tmp_path, monkeypatch, shift=17.0) # 노선 17 m 옮김 + points = [basins.PipePoint(chainage_m=150.0, source="user")] + detail = basins._build_stale(context, points) + assert [basin.chainage_m for basin in detail.basins] == [120.0] + assert detail.pipe_candidates == [] + assert abs(detail.pipes[0].x - 200167.0) < 1e-6 # 관 = 지금 노선 위 + payload = basins._payload(uuid4(), context, detail, points, True, stale=True) + assert payload["stale"] and len(payload["basins"]) == 1 + xy = payload["route_xy"] + assert abs(xy[0][0] - 200000.0) < 0.01 and abs(xy[1][0] - 200300.0) < 0.01 # 옛 노선 + + +def test_저장본_없으면_관_자리만(tmp_path, monkeypatch): + context = _context(tmp_path, monkeypatch, shift=17.0) + points = [basins.PipePoint(chainage_m=150.0, source="user")] + detail = basins._build_stale(context, points) + assert detail.basins == [] + payload = basins._payload(uuid4(), context, detail, points, True, stale=True) + assert abs(payload["route_xy"][0][0] - 200017.0) < 0.01 # 지금 노선 + + +def test_깨진_저장본은_없음과_같음(tmp_path, monkeypatch): + (tmp_path / basins.DETAIL_CACHE_FILENAME).write_bytes(b"broken") + monkeypatch.setattr(basins, "drainage_dir", lambda _stored: tmp_path) + assert basins._read_cache_file("x") is None