fix(배수유역): 342-3 관경 · 시설 자동 지정 끔 — 새 관 = 마스터 기본 · 추천은 응답 제안만

- _apply_recommendations 지움(시설 BOX암거 · 세월교 바꿈 · 관경 넣기) · 새 관은 _apply_new_defaults 가 구조물정의 배관 기본(1000)
- 응답 recommended_facility · recommended_diameter_mm 그대로(제안)
- 시험 test_b05_drainage_recommend 고쳐 씀(추천 얹지 않음 · 옛 관 그대로 · 마스터 기본 1000)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 11:03:37 +09:00
co-authored by Claude Opus 5.5
parent 0efc77380e
commit b8ed082874
2 changed files with 44 additions and 99 deletions
+3 -37
View File
@@ -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,
+41 -62
View File
@@ -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