diff --git a/B04_PreProcess/B04_PreProcess_Router_Basins.py b/B04_PreProcess/B04_PreProcess_Router_Basins.py index e84ff0220..e6de81797 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Basins.py +++ b/B04_PreProcess/B04_PreProcess_Router_Basins.py @@ -30,7 +30,6 @@ from common_util.common_util_auth import get_optional_session from common_util.common_util_drainage_context import DrainageContext, load_drainage_context from common_util.common_util_drainage_detail import DrainageDetail, build_detail from common_util.common_util_drainage_pipes import ( - PIPE_FACILITY_PIPE, PIPE_SOURCE_USER, PipePoint, carry_facility_attributes, @@ -84,8 +83,8 @@ def _build( # 계산기는 chainage만 다뤄 시설 종류·구간·옵션이 사라진다 — 원본에서 되붙인다 # (2026-08-17 계곡 통과 시설: 배관/BOX암거/물넘이/세월교). carried = carry_facility_attributes(rebuilt, points or []) - # 되붙인 뒤에 추천을 얹는다 — 순서가 뒤바뀌면 승계된 사용자 값이 추천에 덮인다. - _apply_recommendations(detail, carried) + # 관경 · 시설 자동 지정 없음 — 10-16 사용자 지시로 끔 · 제안(recommended_*)만 응답에 실음. + # 새 관은 아래 등록부 기본값(구조물정의 배관 관경)을 받는다. flows = {round(basin.chainage_m, 2): basin.design_flow_m3s for basin in detail.basins} _apply_new_defaults([point for point, new in zip(carried, fresh) if new], flows) return detail, carried @@ -105,7 +104,7 @@ def _float(value: Any) -> float: def _apply_new_defaults( points: list[PipePoint], flows: dict[float, float | None] | None = None ) -> None: - """새로 놓인 관에만 등록부 기본값을 **빈 키에** 채움(PLAN 46-7 · 추천 뒤 — 추천 관경이 이김). + """새로 놓인 관에만 등록부 기본값을 **빈 키에** 채움(PLAN 46-7). 시설이 생길 때 기본값을 칸에 실제 값으로 넣어 두고 사용자가 고침 — 이미 있던 관의 빈 칸은 그대로 (옛 프로젝트 빈칸 = 미확정 유지 · 사용자 결정 2026-10-05). @@ -176,39 +175,6 @@ def _retag(pipes: list[StructureCandidate], points: list[PipePoint]) -> None: pipe.reason = source or PIPE_SOURCE_USER -def _apply_recommendations(detail: DrainageDetail, points: list[PipePoint]) -> None: - """유역별 추천(시설 종류·관경)을 관 지점 옵션의 **빈칸에만** 채운다. - - "초기 전처리 계산에서 해당값으로 설정"(2026-08-17 사용자 지시) — 자동 배치된 관이 - 유역 유량에 맞는 규격을 처음부터 갖고 있어야 폼을 열지 않아도 하류가 값을 받는다. - - 덮어쓰지 않는 두 경우: ① 사용자가 직접 놓거나 옮긴 관(`source == "user"`) ② - 그 옵션 키가 이미 있는 관. 설계자가 고른 값을 재계산이 되돌리면 안 되기 때문이다. - """ - by_chainage = {round(basin.chainage_m, 2): basin for basin in detail.basins} - for point in points: - if point.source == PIPE_SOURCE_USER: - continue - basin = by_chainage.get(round(point.chainage_m, 2)) - if basin is None: - continue - options = dict(point.options or {}) - # 시설 종류는 아직 손대지 않은 기본 배관일 때만 추천으로 바꾼다. - if ( - point.facility == PIPE_FACILITY_PIPE - and basin.recommended_facility != PIPE_FACILITY_PIPE - ): - point.facility = basin.recommended_facility - if ( - point.facility == PIPE_FACILITY_PIPE - and basin.recommended_diameter_mm is not None - and "pipe_diameter_mm" not in options - ): - options["pipe_diameter_mm"] = basin.recommended_diameter_mm - if options: - point.options = options - - def _payload( project_id: UUID, context: DrainageContext, diff --git a/resources/tester/test_b05_drainage_recommend.py b/resources/tester/test_b05_drainage_recommend.py index 6a3183a89..f145898a0 100644 --- a/resources/tester/test_b05_drainage_recommend.py +++ b/resources/tester/test_b05_drainage_recommend.py @@ -1,4 +1,4 @@ -"""배수 추천 구조물·관경과 자동 배치 주입 (PLAN 2026-08-17 「4」). +"""배수 추천 구조물·관경 (PLAN 2026-08-17 「4」) — 제안만 · 자동 지정 없음(342-3 · 10-16 사용자 지시). 추천 근거는 **유량(유효직경)뿐**이다 — 계곡 횡단경사·하천 차수는 지형 계산이 필요해 이번 범위에서 뺐고 화면이 "현장 확인"으로 안내한다(사용자 확정). @@ -8,9 +8,12 @@ D > 2,000㎜ 세월교·물넘이 검토 """ +from pathlib import Path +from types import SimpleNamespace + import pytest -from B04_PreProcess.B04_PreProcess_Router_Basins import _apply_recommendations +from B04_PreProcess import B04_PreProcess_Router_Basins as basins from B05_Profile.B05_Profile_Structures_Schema import load_structure_types from common_util.common_util_drainage_detail import ( DrainageDetail, @@ -22,9 +25,9 @@ from common_util.common_util_drainage_pipes import ( PIPE_FACILITY_FORD_BRIDGE, PIPE_FACILITY_PIPE, PIPE_SOURCE_STREAM, - PIPE_SOURCE_USER, PipePoint, ) +from common_util.common_util_route_geometry import StructureCandidate from config.config_system import DRAINAGE_RECOMMEND_DIAMETERS_MM @@ -58,13 +61,7 @@ def test_recommended_sizes_match_registry_choices(): assert list(DRAINAGE_RECOMMEND_DIAMETERS_MM) == choices -# ── 자동 배치 주입 ───────────────────────────────────────────────────────── - - -def _detail(*basins: WatershedBasin) -> DrainageDetail: - detail = DrainageDetail.__new__(DrainageDetail) - detail.basins = list(basins) - return detail +# ── 자동 지정 끔 — 제안은 응답에만 ───────────────────────────────────────────── def _basin(chainage: float, facility: str, diameter: int | None) -> WatershedBasin: @@ -78,58 +75,40 @@ def _basin(chainage: float, facility: str, diameter: int | None) -> WatershedBas ) -def test_auto_pipe_gets_recommended_diameter(): - """자동 배치 관은 폼을 열지 않아도 유역 유량에 맞는 관경을 갖는다.""" +def _run_build(monkeypatch, basin: WatershedBasin, points: list[PipePoint] | None): + detail = DrainageDetail( + pipes=[StructureCandidate(chainage_m=100.0, x=0.0, y=0.0, reason=PIPE_SOURCE_STREAM)], + basins=[basin], + ) + monkeypatch.setattr(basins, "build_detail", lambda *args: detail) + monkeypatch.setattr(basins, "drainage_dir", lambda path: Path(".")) + _, carried = basins._build("x", SimpleNamespace(vertices=[]), points) + return carried + + +def _master_pipe_diameter(): + pipe = {item.type_id: item for item in load_structure_types()}["pipe"] + return {option.key: option for option in pipe.options}["pipe_diameter_mm"].default + + +@pytest.mark.parametrize( + "facility, diameter", + [(PIPE_FACILITY_PIPE, 1500), (PIPE_FACILITY_BOX, None), (PIPE_FACILITY_FORD_BRIDGE, None)], +) +def test_new_pipe_gets_master_default_not_recommendation(monkeypatch, facility, diameter): + """새로 놓인 관 = 배관 · 마스터 기본 관경 — 추천(관경 · BOX암거 · 세월교)은 안 얹는다.""" + carried = _run_build(monkeypatch, _basin(100.0, facility, diameter), None) + assert carried[0].facility == PIPE_FACILITY_PIPE + assert carried[0].options["pipe_diameter_mm"] == _master_pipe_diameter() + + +def test_existing_auto_pipe_is_untouched(monkeypatch): + """이미 놓인 자동 관의 빈 관경 · 시설은 추천으로 안 채운다(옛 값 그대로).""" points = [PipePoint(chainage_m=100.0, source=PIPE_SOURCE_STREAM)] - _apply_recommendations(_detail(_basin(100.0, PIPE_FACILITY_PIPE, 1200)), points) - assert points[0].options == {"pipe_diameter_mm": 1200} - assert points[0].facility == PIPE_FACILITY_PIPE + carried = _run_build(monkeypatch, _basin(100.0, PIPE_FACILITY_BOX, None), points) + assert carried[0].facility == PIPE_FACILITY_PIPE + assert carried[0].options is None -def test_auto_point_switches_facility_to_box(): - """추천이 BOX암거면 시설 종류까지 바꾼다(관경은 붙이지 않는다).""" - points = [PipePoint(chainage_m=100.0, source=PIPE_SOURCE_STREAM)] - _apply_recommendations(_detail(_basin(100.0, PIPE_FACILITY_BOX, None)), points) - assert points[0].facility == PIPE_FACILITY_BOX - assert points[0].options is None - - -def test_user_placed_point_is_never_overwritten(): - """사용자가 직접 놓거나 옮긴 관은 재계산이 건드리지 않는다.""" - points = [PipePoint(chainage_m=100.0, source=PIPE_SOURCE_USER)] - _apply_recommendations(_detail(_basin(100.0, PIPE_FACILITY_BOX, None)), points) - assert points[0].facility == PIPE_FACILITY_PIPE - assert points[0].options is None - - -def test_existing_option_is_kept(): - """설계자가 고른 관경을 추천이 되돌리면 안 된다 — 빈칸만 채운다.""" - points = [ - PipePoint( - chainage_m=100.0, - source=PIPE_SOURCE_STREAM, - options={"pipe_diameter_mm": 800}, - ) - ] - _apply_recommendations(_detail(_basin(100.0, PIPE_FACILITY_PIPE, 1500)), points) - assert points[0].options == {"pipe_diameter_mm": 800} - - -def test_already_chosen_facility_is_kept(): - """이미 다른 시설로 바꿔 둔 지점은 추천이 되돌리지 않는다.""" - points = [ - PipePoint( - chainage_m=100.0, - source=PIPE_SOURCE_STREAM, - facility=PIPE_FACILITY_FORD_BRIDGE, - ) - ] - _apply_recommendations(_detail(_basin(100.0, PIPE_FACILITY_BOX, None)), points) - assert points[0].facility == PIPE_FACILITY_FORD_BRIDGE - - -def test_point_without_basin_is_untouched(): - """담당 유역이 없는 관(유역 미산출)은 그대로 둔다.""" - points = [PipePoint(chainage_m=500.0, source=PIPE_SOURCE_STREAM)] - _apply_recommendations(_detail(_basin(100.0, PIPE_FACILITY_PIPE, 1200)), points) - assert points[0].options is None +def test_master_pipe_default_is_1000(): + assert float(_master_pipe_diameter()) == 1000