"""배수 추천 구조물·관경과 자동 배치 주입 (PLAN 2026-08-17 「4」). 추천 근거는 **유량(유효직경)뿐**이다 — 계곡 횡단경사·하천 차수는 지형 계산이 필요해 이번 범위에서 뺐고 화면이 "현장 확인"으로 안내한다(사용자 확정). D ≤ 1,500㎜ 배관 (레지스트리 선택지로 스냅, 하한 800㎜) 1,500 < D ≤ 2,000 BOX암거 후보 D > 2,000㎜ 세월교·물넘이 검토 """ import pytest from B04_PreProcess.B04_PreProcess_Router_Basins import _apply_recommendations from B05_Profile.B05_Profile_Structures_Schema import load_structure_types from common_util.common_util_drainage_detail import ( DrainageDetail, WatershedBasin, recommend_structure, ) from common_util.common_util_drainage_pipes import ( PIPE_FACILITY_BOX, PIPE_FACILITY_FORD_BRIDGE, PIPE_FACILITY_PIPE, PIPE_SOURCE_STREAM, PIPE_SOURCE_USER, PipePoint, ) from config.config_system import DRAINAGE_RECOMMEND_DIAMETERS_MM # ── 추천 판정 ────────────────────────────────────────────────────────────── @pytest.mark.parametrize( "diameter_mm, expected", [ (None, (PIPE_FACILITY_PIPE, None)), # 강우량표 없음 — 관경 미정 (300.0, (PIPE_FACILITY_PIPE, 800)), # 하한 800㎜ 아래로는 안 내린다 (800.0, (PIPE_FACILITY_PIPE, 800)), (800.1, (PIPE_FACILITY_PIPE, 1000)), # 규격을 넘으면 바로 위 규격 (1000.0, (PIPE_FACILITY_PIPE, 1000)), (1100.0, (PIPE_FACILITY_PIPE, 1200)), (1500.0, (PIPE_FACILITY_PIPE, 1500)), # 경계값은 아직 배관 (1500.1, (PIPE_FACILITY_BOX, None)), # 초과 → BOX암거 후보 (2000.0, (PIPE_FACILITY_BOX, None)), # 경계값은 아직 BOX암거 (2000.1, (PIPE_FACILITY_FORD_BRIDGE, None)), # 관 최대 규격 초과 ], ) def test_recommend_structure_thresholds(diameter_mm, expected): assert recommend_structure(diameter_mm) == expected def test_recommended_sizes_match_registry_choices(): """추천 관경은 폼에서 고를 수 있어야 한다 — 레지스트리 선택지와 같은 목록.""" by_id = {item.type_id: item for item in load_structure_types()} options = {option.key: option for option in by_id["pipe"].options} choices = [int(value) for value in options["pipe_diameter_mm"].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: return WatershedBasin( index=1, chainage_m=chainage, outlet_x=0.0, outlet_y=0.0, recommended_facility=facility, recommended_diameter_mm=diameter, ) def test_auto_pipe_gets_recommended_diameter(): """자동 배치 관은 폼을 열지 않아도 유역 유량에 맞는 관경을 갖는다.""" 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 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