"""레지스트리 정책 검증 — phase 단계 분리·선택지 재편·기본값 원칙. 2026-08-17 선택지 재편: B05는 유무·종류·위치만 고르고 상세 치수는 B06/B07에서 받는다. 종단배수(B군)·생태/녹화(F군)·노면공(G군)은 B05 선택지에서 빠지고 `enabled: false`로 보존된다(B06 횡단도 옵션 재사용). 바닥막이·생태연못은 임도 미사용으로 완전 삭제. 배관·BOX암거는 부속 옵션 정의를 갖는다. """ from B05_Profile.B05_Profile_Structures_Schema import load_structure_types # 기본값을 남겨 둘 수 있는 항목 — 법정 단일값·사용자 확정값·표시용 문자열뿐이다. ALLOWED_DEFAULT_KEYS = { # 돌 조달 「채집」 기본 — **사용자 확정 ②(2026-09-09) 「기본은 캔다, 구조물마다 바꿀 수 # 있게」**. 법도 그쪽을 권한다: 별표2 「석축 등에 필요한 야면석 등은 가급적 현장에서 # 채취·사용하도록 운반거리를 조사한다」. 실무 견적 다섯 권에도 야면석 구입 단가가 0건. ("masonry_wet", "stone_supply"), ("masonry_dry", "stone_supply"), ("boulder_masonry", "stone_supply"), ("erosion_check", "stone_supply"), ("bed_sill", "stone_supply"), ("revetment", "stone_supply"), # 독립 기슭막이 설치 측(2026-08-28 사용자 확정: 자동 판정 없이 수동으로 받는다). # "좌"는 셀렉트 첫 항목일 뿐 도메인 확정값이 아니다 — 측점마다 사용자가 고른다. ("revetment", "side"), # C군 다섯(옹벽·돌쌓기 찰/메·흙막이·큰돌쌓기)의 설치 측 기본 "자동(성토 쪽)" # (2026-09-07 사용자 지시로 폼 칸 신설). **도메인 확정값이 아니라 「지금까지의 동작」** # 이다 — 칸이 생기기 전에도 기하는 성토 쪽에 세우고 있었고("좌"/"우" 를 고르면 그쪽), # 그 행동을 그대로 이어받는 표식이다. 사용자가 측점마다 좌·우로 바꾼다. ("retaining_wall", "side"), ("masonry_wet", "side"), ("masonry_dry", "side"), ("soil_guard", "side"), ("boulder_masonry", "side"), # 흙막이 — **기슭막이 칸을 한 벌로 옮긴 것**(2026-09-09 사용자 확정 4차 원문: # 「흙막이는 횡단도에서 표현방식들과 옵션들은 동일하게 반영 · 형상은 동일 · # 데이터는 분리하여 계산」). 기본값의 까닭도 기슭막이 줄과 **같다** — # 단 수 1 = 다단 없음 · 올림/이동 0 = 자동 자리 그대로 · 조달 채집 = 별표2 권고. ("soil_guard", "tiers"), ("soil_guard", "lift_m"), ("soil_guard", "shift_m"), ("soil_guard", "stone_supply"), # ⚠ 사토장 둘(`side`·`extra_distance_m`)은 **여기서 뺐다**(2026-09-09) — 정본에 # `default_basis` 로 까닭이 적혀 있어 이 목록이 없어도 통과한다. **앞으로도 여기 넣지 말 것.** # 단 수 기본 1 = 단일 벽(다단 아님). 도메인 수치가 아니라 "다단 없음" 표식이다. ("revetment", "tiers"), # 자리 이동 기본 0 = 자동 자리(성토면 끝) 그대로. 이동량은 사용자 조작값이다. ("revetment", "lift_m"), ("revetment", "shift_m"), # 포장 구간 길이 10m / 전·후 5m — 기슭막이와 같은 출발값(2026-08-28 사용자 확정). # 지식DB에 포장 구간 길이 기준은 없다. 화면 출발값일 뿐 도메인 확정값이 아니다. ("pavement_concrete", "length_m"), ("pavement_concrete", "before_m"), ("pavement_concrete", "after_m"), # ── 소단(berm·ditch_berm) 기본값 — 근거 확인 끝(2026-09-07, 보조 창 구현 + 원문 대조) ── # 폭 0.5m · 간격(사면길이) 3m = **별표2 범위 안에서 가장 적게 파는 조합**이다. # 지식DB `01_임도/02_상세설계/절토_비탈면.md` §2: 「사면길이 **2~3m마다** 폭 **50~100㎝**」. # 범위의 양 끝(폭 최소·간격 최대)을 골랐다 — 기본값은 되돌리기 쉬운 쪽이어야 하기 때문이다 # (더 넣는 것은 폼에서 한 번이지만, 이미 판 것을 되돌리면 전 측점 재계산이다). # 실효 경사 차이도 크다(설계 1:1): 0.5·3 → 1:1.24 / 1.0·3 → 1:1.47 / 1.0·2 → 1:1.71. ("berm", "width_m"), ("berm", "interval_m"), ("ditch_berm", "width_m"), ("ditch_berm", "interval_m"), # 안쪽 기울기 기본 **0°** = 「기본으로 아무것도 안 넣음」. 기울이는 것은 실무이나 # **법령·교본 근거가 없어** 박지 않고 폼에서 받는다(2026-09-07 사용자 재확정 — 처음 2° # 로 잡았다가 내렸다). 근거 없는 값을 안 넣으려는 값이라 이 원칙의 반대편이 아니다. ("berm", "slope_deg"), ("ditch_berm", "slope_deg"), # 구간 길이 10m·전·후 5m — 소단만의 값이 아니라 **C군 구간형 폼 공통 출발값**이다 # (포장 구간·기슭막이와 같은 값). 도메인 확정값이 아니다. ("berm", "length_m"), ("berm", "before_m"), ("berm", "after_m"), ("ditch_berm", "length_m"), ("ditch_berm", "before_m"), ("ditch_berm", "after_m"), ("ditch_side", "depth_cm"), ("erosion_check", "length_m"), ("erosion_check", "height_m"), ("refuge", "length_m"), ("work_yard", "width_m"), ("work_yard", "length_m"), ("turnaround", "width_m"), ("etc", "name"), # 별표2 관 지름 1,000㎜ 이상 원칙 + 2026-08-17 사용자 확정 기본값. ("pipe", "pipe_diameter_mm"), # 보호공(돌붙임) 계열은 삭제 — 기슭막이는 사면 공정이라 바닥 돌붙임이 붙지 # 않는다(2026-08-19 사용자 지시 1·3). 바닥 보호는 바닥막이(bed_sill) 구조물 몫. # 기슭막이 형태 기본 — 유입 돌쌓기(찰) / 유출 돌쌓기(메) (2026-08-17 사용자 확정). ("pipe", "inlet_revet_form"), ("pipe", "outlet_revet_form"), # 유출구 구조는 현재 기슭막이 하나뿐 — 양식 일원화용 단일 선택지. ("pipe", "outlet_type"), # 관종 기본 = 파형강관, 유입구 구조 기본 = 기슭막이 (2026-08-17 사용자 확정). ("pipe", "pipe_kind"), ("pipe", "inlet_type"), # 집수정 종방향 기본 — 길이 2m·기준측점 전/후 각 1m (2026-08-24 사용자 확정). ("pipe", "inlet_basin_before_m"), ("pipe", "inlet_basin_after_m"), # 기슭막이 기본 치수 — 길이 10m·높이 2.5m·보호공 면적 10㎡ (2026-08-17 사용자 확정). ("pipe", "inlet_revet_length_m"), ("pipe", "inlet_revet_height_m"), ("pipe", "outlet_revet_length_m"), ("pipe", "outlet_revet_height_m"), # 독립 기슭막이(D4) — C군과 같은 범위 계산 방식, 기본 돌쌓기(메)·2.5·10·5·5 # (2026-08-19 사용자 지시 3). ("revetment", "form"), ("revetment", "height_m"), ("revetment", "length_m"), ("revetment", "before_m"), ("revetment", "after_m"), # 바닥막이 부활 — 돌붙임(메)·면적 10㎡·높이 0.3m (2026-08-19 사용자 지시 2·5). ("bed_sill", "form"), ("bed_sill", "area_m2"), ("bed_sill", "height_m"), # 세월교 구체 내 배관도 배수관과 같은 관종·관경을 쓴다 (2026-08-17 사용자 확정). ("ford_bridge", "pipe_kind"), ("ford_bridge", "pipe_diameter_mm"), # 세월교 날개벽 — BOX암거와 같은 한 벌을 그대로 쓴다 (2026-08-25 사용자 확정). ("ford_bridge", "wing_in"), ("ford_bridge", "wing_in_height_m"), ("ford_bridge", "wing_in_length_m"), ("ford_bridge", "wing_in_angle_deg"), ("ford_bridge", "wing_out"), ("ford_bridge", "wing_out_height_m"), ("ford_bridge", "wing_out_length_m"), ("ford_bridge", "wing_out_angle_deg"), # 기슭막이 종방향 전/후 각 5m·집수정 길이 2m — 확정값인데 목록에 빠져 있었다 # (2026-08-17·08-24 사용자 확정, 2026-08-25 보충). ("pipe", "inlet_revet_before_m"), ("pipe", "inlet_revet_after_m"), ("pipe", "outlet_revet_before_m"), ("pipe", "outlet_revet_after_m"), ("pipe", "inlet_basin_length_m"), # C군 기본값 — 높이 2.5·길이 10·전길이 5 (2026-08-19 사용자 확정). *( (type_id, key) for type_id in ( "retaining_wall", "masonry_wet", "masonry_dry", "soil_guard", "boulder_masonry", ) for key in ("height_m", "length_m", "before_m", "after_m") ), # BOX암거 본체 2.0×2.0(실무 관측 규격 프리셋의 첫 항목)과 날개벽 기본 제원 # — 설치 있음·짧은쪽 높이 1m·길이 2m·각도 45° (2026-08-17 사용자 확정). ("box_culvert", "body_width_m"), ("box_culvert", "body_height_m"), ("box_culvert", "wing_in"), ("box_culvert", "wing_in_height_m"), ("box_culvert", "wing_in_length_m"), ("box_culvert", "wing_in_angle_deg"), ("box_culvert", "wing_out"), ("box_culvert", "wing_out_height_m"), ("box_culvert", "wing_out_length_m"), ("box_culvert", "wing_out_angle_deg"), } # B06 이관 보류 — 정의 보존을 위해 삭제하지 않고 enabled=False로 숨긴다. DISABLED_TYPE_IDS = { "ditch_side", "ditch_ridge", "ditch_berm", "chute", "slope_drain", "underdrain", "wildlife_path", "revegetation", "gravel_surfacing", } # 임도 미사용 확정 — 레지스트리에서 완전 삭제. 바닥막이(bed_sill)는 2026-08-19 # 사용자 지시로 부활해 D군(계류 사방)에 되살렸다. REMOVED_TYPE_IDS = {"eco_pond"} def _quick_add_types(): """우클릭 한 번으로 넣을 수 있는 타입 — 정본 관리·비활성 타입만 제외한다.""" return [item for item in load_structure_types() if not item.managed_by and item.enabled] def test_quick_add_types_exclude_pipe_family(): """계곡 통과 시설(배관·BOX암거·물넘이·세월교)은 관 지점 정본 소관.""" quick_ids = {item.type_id for item in _quick_add_types()} for type_id in ("pipe", "box_culvert", "ford_pavement", "ford_bridge"): assert type_id not in quick_ids def test_all_enabled_non_managed_types_are_quick_addable(): """잔존 B05 선택지 = A군 2(노출형·개거) + C군 5 + D군 3 + E군 10 + G군 1(콘크리트 포장) + 기타 1. 콘크리트 포장은 2026-08-28 사용자 지시로 되살렸다 — 포장 구간을 기준측점+길이+전/후로 받는 정본이 됐다(종단경사 자동 포장 적용을 대체). D군이 4에서 3으로 줄었다 — 독립 기슭막이가 관 지점 시설로 옮겨가(2026-08-28, `managed_by: pipe_points`) 우클릭 추가 대상에서 빠졌다. C군이 5에서 6으로 늘었다 — **소단(`berm`)** 이 들어왔다(2026-09-07, 계획서 3-9). 소단 규격은 법령·교본마다 갈려 프로그램이 판정하지 않고 **사용자가 구간에 놓는다** — 그래서 C군 구간형 시설과 같은 자리에 선다. (측구쪽 `ditch_berm` 은 우클릭 추가 대상이 아니라 여기 수에 안 잡힌다.) """ assert len(_quick_add_types()) == 23 def test_removed_types_are_absent(): """바닥막이·생태연못 — 임도 미사용 확정으로 레지스트리에서 삭제.""" present = {item.type_id for item in load_structure_types()} leftovers = REMOVED_TYPE_IDS & present assert not leftovers, f"삭제 대상 잔존: {leftovers}" def test_b06_deferred_types_are_disabled_but_preserved(): """B·F·G군은 B05 선택지에서 빠지되 정의는 남는다(B06 횡단도 옵션 재사용).""" by_id = {item.type_id: item for item in load_structure_types()} for type_id in DISABLED_TYPE_IDS: assert type_id in by_id, f"{type_id} 정의가 삭제됨 — 보존 대상" assert by_id[type_id].enabled is False, f"{type_id} enabled=False여야 함" for type_id, item in by_id.items(): if type_id not in DISABLED_TYPE_IDS: assert item.enabled is True, f"{type_id}는 활성이어야 함" def test_b05_phase_options_are_never_required(): """B05 배치 시 필수 입력이 남아 있으면 유무 단계가 깨진다 — 필수는 전부 detail. C군 높이·길이는 한때 B05 필수였으나 기본값(2.5/10/5)이 사용자 확정되면서 필수가 풀렸다(2026-08-19) — 원칙은 다시 예외 없이 성립한다.""" offenders = [ f"{item.type_id}.{option.key}" for item in load_structure_types() for option in item.options if option.required and option.phase != "detail" ] assert offenders == [], f"B05 단계 필수 옵션: {offenders}" def test_slope_group_computes_range_from_length(): """C군 5종 — 전부 구간형이고 B05 길이·높이 옵션을 갖는다(2026-08-19 지시). 프론트(hasComputedRange)는 'B05 phase의 length_m 존재'로 범위 계산 UI를 켠다.""" by_id = {item.type_id: item for item in load_structure_types()} for type_id in ( "retaining_wall", "masonry_wet", "masonry_dry", "soil_guard", "boulder_masonry", ): item = by_id[type_id] assert item.group == "C" and item.placement == "interval", type_id # 기본값 = 높이 2.5·길이 10·전길이 5 (2026-08-19 사용자 확정 — 기본값이 # 생기며 required는 풀렸다). 후길이 = 길이 − 전길이는 화면 계산값이라 옵션이 # 아니다. 순서는 높이 → 길이 → 전길이. options = {option.key: option for option in item.options} expected = {"height_m": 2.5, "length_m": 10, "before_m": 5} for key, default in expected.items(): assert key in options, f"{type_id}.{key} 옵션 없음" assert options[key].phase == "b05", f"{type_id}.{key}는 b05 phase" assert not options[key].required, f"{type_id}.{key}는 기본값 확정으로 비필수" assert options[key].default == default, f"{type_id}.{key} 기본값" assert options[key].input == "number" and options[key].unit == "m", key keys = [option.key for option in item.options if option.phase == "b05"] assert keys[:3] == ["height_m", "length_m", "before_m"], f"{type_id} 옵션 순서" def test_retaining_wall_renamed_plain(): """옹벽(철근콘크리트) → 옹벽 — 전역 명칭 단순화(2026-08-19 사용자 지시).""" by_id = {item.type_id: item for item in load_structure_types()} assert by_id["retaining_wall"].name == "옹벽" def test_required_options_moved_to_detail_phase(): """크로스체크 2차의 필수 원칙이 detail phase로 유지되는지 확인.""" detail_required = [ (item.type_id, option.key) for item in load_structure_types() for option in item.options if option.phase == "detail" and option.required ] assert len(detail_required) >= 18 def test_only_confirmed_values_keep_defaults(): """미협의 재료·형식 선택지에 기본값이 남아 있으면 안 된다(지식DB 원칙). ⚠ 인정하는 길은 둘이다 — 아래 목록에 있거나, **정본에 `default_basis` 로 까닭을 적었거나**. 뒤쪽이 2026-09-09 신설이고 **앞으로는 그쪽을 쓴다** — 이 파일은 git 밖이라 목록만 고치면 다른 창에서 같은 시험이 깨진다(실제로 두 번 그랬다). """ offenders = [ f"{item.type_id}.{option.key}={option.default!r}" for item in load_structure_types() for option in item.options if option.default not in (None, "") and (item.type_id, option.key) not in ALLOWED_DEFAULT_KEYS and not option.default_basis ] assert offenders == [], f"근거 없는 기본값: {offenders}" def test_default_basis_is_a_real_sentence(): """까닭이 한 낱말이면 다음 사람이 못 되짚는다.""" thin = [ f"{item.type_id}.{option.key}" for item in load_structure_types() for option in item.options if option.default_basis is not None and len(option.default_basis) < 12 ] assert thin == [], f"까닭이 너무 짧음: {thin}" def test_options_without_default_are_required(): """기본값도 없고 필수도 아니면 빈 값이 조용히 저장된다 — 어느 phase든 동일. managed_by 타입(계곡 통과 시설)은 정본이 pipe_points라 structures.json 저장 검증을 거치지 않는다 — 배관 부속·세월교 관 옵션은 B05 유무 단계 입력이라 강제하지 않는다. ⚠⚠ **세 번째 갈래가 있다**(2026-09-09 신설) — **「비워 두는 것이 뜻인 칸」**. 비면 계산 쪽이 기준값으로 돌고 그 사실이 화면에 사유로 뜨며, 값을 넣으면 그 값이 이긴다 (전면 기울기 · 물빼기 구멍 · 뒷길이 …). 그런 칸은 **정본에 `empty_means` 로 까닭을 적어** 둔다. ⚠ **예외 목록을 이 파일에 두지 않는다.** 예전에는 이 파일이 `tmp/tests` 라 **git 밖**이어서, 정본(추적됨)만 바뀌고 예외(추적 안 됨)는 안 따라가 **다른 창에서만 시험이 깨졌다** (2026-09-09 실제로 두 번). 그 뒤 시험을 `resources/tester` 로 옮겨 함께 건너가게 됐으나, **까닭은 여전히 값 옆(정본)에 두는 것이 옳다** — 칸을 여는 사람이 곧 까닭을 아는 사람이고, 목록은 시간이 지나면 무엇을 왜 넣었는지 아무도 못 되짚는다. """ loose = [ f"{item.type_id}.{option.key}" for item in load_structure_types() if not item.managed_by for option in item.options if option.default in (None, "") and not option.required and option.input != "text" and not option.empty_means ] assert loose == [], f"기본값·필수·`empty_means` 어느 쪽도 아닌 옵션: {loose}" def test_empty_means_is_a_real_sentence(): """까닭이 한 낱말이면 다음 사람이 못 되짚는다 — 어디서 오는 기준인지 적혀야 한다.""" thin = [ f"{item.type_id}.{option.key}" for item in load_structure_types() for option in item.options if option.empty_means is not None and len(option.empty_means) < 12 ] assert thin == [], f"까닭이 너무 짧음: {thin}" def test_a6_a7_types_exist_for_manual_placement(): """A6 노출형 횡단수로·A7 개거 — 수동 구조물 정본 소속 (2026-08-17 확정).""" by_id = {item.type_id: item for item in load_structure_types()} for type_id in ("cross_drain_exposed", "open_ditch"): assert type_id in by_id, f"{type_id} 타입 없음" item = by_id[type_id] assert item.group == "A" and item.managed_by is None assert item.placement == "point" def test_ford_bridge_shares_pipe_kind_and_diameter(): """세월교 구체 내 배관 = 배수관과 같은 관종·관경 선택지·기본값 (2026-08-17 지시).""" by_id = {item.type_id: item for item in load_structure_types()} ford = {option.key: option for option in by_id["ford_bridge"].options} pipe = {option.key: option for option in by_id["pipe"].options} for key in ("pipe_kind", "pipe_diameter_mm"): assert ford[key].choices == pipe[key].choices, key assert ford[key].default == pipe[key].default, key assert "pipe_count" in ford, "세월교 수량(련)은 사용자 지정으로 남는다" def test_valley_crossing_facilities_exist_as_managed_types(): """BOX암거·물넘이·세월교 — 표시용 레지스트리 항목 (정본은 pipe_points).""" by_id = {item.type_id: item for item in load_structure_types()} for type_id in ("box_culvert", "ford_pavement", "ford_bridge"): assert type_id in by_id, f"{type_id} 타입 없음" item = by_id[type_id] assert item.managed_by == "pipe_points" assert item.style.get("abbr"), "종단 마크 약호 필요" def test_pipe_accessory_options_defined(): """배관 부속 옵션 — 관종·관경(기본 1000) + 유입구/유출구 그룹 (2026-08-17 UI 개편). 유입구는 집수정 또는 기슭막이 택일, 유출구는 기슭막이만(현재 다른 선택지 없음). 재질과 표면처리(찰/메)는 분리된 축이다 — 재질에 따라 표면처리가 달라진다 (돌쌓기 §1·§2: 찰=모르타르 채움, 메=건쌓기). """ by_id = {item.type_id: item for item in load_structure_types()} options = {option.key: option for option in by_id["pipe"].options} for key in ( "pipe_kind", "pipe_diameter_mm", "inlet_type", "inlet_basin_form", "inlet_basin_material", "inlet_revet_form", "inlet_revet_length_m", "inlet_revet_height_m", "outlet_revet_form", "outlet_revet_length_m", "outlet_revet_height_m", ): assert key in options, f"pipe.{key} 옵션 없음" # 보호공(돌붙임·도수로) 계열은 삭제 — 기슭막이는 사면 공정(2026-08-19 지시 1). for gone in ( "inlet_protection", "inlet_protection_area_m2", "inlet_protection_width_m", "outlet_protection", "outlet_protection_area_m2", "outlet_protection_width_m", ): assert gone not in options, f"pipe.{gone}는 삭제됐다" # 유형(flow_type)은 삭제 — 유입구 구조 선택으로 대체(2026-08-17 사용자 지시). assert "flow_type" not in options # 재질·표면처리는 하나로 합쳐 "형태"가 됐다(2026-08-17 사용자 지시 1). for gone in ("inlet_revet_material", "inlet_revet_finish", "outlet_revet_finish"): assert gone not in options, f"pipe.{gone}는 형태로 합쳐졌다" assert options["inlet_type"].choices == ["기슭막이", "집수정"] assert options["inlet_type"].default == "기슭막이" assert options["pipe_diameter_mm"].default == "1000" assert options["pipe_diameter_mm"].choices == ["800", "1000", "1200", "1500"] # 돌붙임은 바닥 구조라 형태 목록에서 뺐다 — 별도 항목이 있다(2026-08-17 지시 1). assert options["inlet_revet_form"].choices == [ "돌쌓기(찰)", "돌쌓기(메)", "콘크리트", "돌망태", "통나무·목재틀", "바자", ] assert options["inlet_revet_form"].default == "돌쌓기(찰)" assert options["outlet_revet_form"].default == "돌쌓기(메)" # 유출구도 구조 항목을 둔다 — 선택지는 기슭막이뿐이지만 양식을 맞춘다(지시 2). assert options["outlet_type"].choices == ["기슭막이"] # 구 두 축(있음/없음 + 표면처리)은 남아 있으면 안 된다. for gone in ( "inlet_pitching", "inlet_pitching_finish", "inlet_pitching_area_m2", "outlet_pitching", "outlet_pitching_finish", "outlet_pitching_area_m2", ): assert gone not in options, f"pipe.{gone}는 보호공으로 합쳐졌다" # 기본 치수 — 길이 10m·높이 2.5m. for side in ("inlet", "outlet"): assert options[f"{side}_revet_length_m"].default == 10 assert options[f"{side}_revet_height_m"].default == 2.5 # 유무·길이·관경·유입 구조 = b05, 형태·치수 제원 = detail. for key in ("inlet_type", "inlet_revet_length_m", "outlet_revet_length_m"): assert options[key].phase == "b05", f"pipe.{key}는 b05 phase" assert options["pipe_kind"].default == "파형강관" for key in ( "inlet_basin_form", "inlet_basin_material", "inlet_revet_form", "outlet_revet_form", "outlet_revet_height_m", ): assert options[key].phase == "detail", f"pipe.{key}는 detail phase" def test_revetment_range_options_like_slope_group(): """독립 기슭막이(D4) — C군과 같은 범위 계산 방식(2026-08-19 사용자 지시 3). 옵션 = 형태·높이·길이·기준측점 전/후, 기본 돌쌓기(메)·2.5·10·5·5. 보호공(돌붙임) 계열은 삭제 — 기슭막이는 사면 공정이다. 형태 선택지는 배수관 부속과 공유. 2026-08-28 이관: 정본이 관 지점(`pipe_points`)으로 옮겨가 배치형태가 점형이 됐다. 구간은 여기 옵션(전/후)이 만든다 — 관 시설의 start_m/end_m가 그 값이다.""" by_id = {item.type_id: item for item in load_structure_types()} item = by_id["revetment"] options = {option.key: option for option in item.options} pipe_options = {option.key: option for option in by_id["pipe"].options} assert item.placement == "point" and item.managed_by == "pipe_points" expected = {"height_m": 2.5, "length_m": 10, "before_m": 5, "after_m": 5} for key, default in expected.items(): assert key in options, f"revetment.{key} 옵션 없음" assert options[key].phase == "b05" and not options[key].required, key assert options[key].default == default, f"revetment.{key} 기본값" assert options["form"].phase == "b05" and options["form"].default == "돌쌓기(메)" for gone in ("protection", "protection_area_m2", "protection_width_m"): assert gone not in options, f"revetment.{gone}는 삭제됐다" # 같은 선택지를 써야 배수관 부속과 수량 산출이 어긋나지 않는다. assert options["form"].choices == pipe_options["inlet_revet_form"].choices def test_bed_sill_revived_as_point_type(): """바닥막이 부활(2026-08-19 사용자 지시 2·4·5) — D군 점형, 기준측점만. 옵션 = 형태(돌붙임 찰/메)·면적·높이, 기본 돌붙임(메)·10㎡·0.3m.""" by_id = {item.type_id: item for item in load_structure_types()} item = by_id["bed_sill"] assert item.group == "D" and item.placement == "point" and item.enabled options = {option.key: option for option in item.options} assert options["form"].choices == ["돌붙임(찰)", "돌붙임(메)"] assert options["form"].default == "돌붙임(메)" and options["form"].phase == "b05" assert options["area_m2"].default == 10 and options["area_m2"].unit == "㎡" assert options["height_m"].default == 0.3 for option in item.options: # 돌 조달·전면 기울기(2026-09-09 신설)는 상세 제원이라 detail phase 다 # — B05 는 유무·위치만 받는다. if option.key in { "stone_supply", "face_slope_ratio", "foundation", "stone_coeff_basis", "fill_concrete_mpa", }: continue assert option.phase == "b05" and not option.required, option.key def test_box_culvert_wing_wall_options_defined(): """BOX암거 날개벽 — 유입·유출 개별: 설치 유무 + 짧은쪽 높이/길이/각도. 2026-08-17 사용자 확정으로 기본값이 붙었다: 설치 있음·높이 1m·길이 2m·각도 45°. 기본값이 있으니 required는 풀고, B05 서브폼이 실제로 받는 값이라 phase도 b05다 (이전의 detail 표기는 폼과 어긋난 정본이었다). """ by_id = {item.type_id: item for item in load_structure_types()} options = {option.key: option for option in by_id["box_culvert"].options} expected = {"height_m": 1, "length_m": 2, "angle_deg": 45} for side in ("in", "out"): toggle = options[f"wing_{side}"] assert toggle.phase == "b05" and not toggle.required assert toggle.choices == ["있음", "없음"] assert toggle.default == "있음", f"wing_{side} 기본 설치" for dim, value in expected.items(): option = options[f"wing_{side}_{dim}"] assert option.phase == "b05" and not option.required, f"wing_{side}_{dim}" assert option.default == value, f"wing_{side}_{dim} 기본값" def test_box_culvert_body_size_defaults_to_first_preset(): """BOX암거 본체 — 프리셋 2.0×2.0·3.0×3.0 중 첫 항목이 기본값 (2026-08-17 확정). 프리셋 선택은 폼 전용이라 레지스트리에 별도 키를 두지 않는다 — 저장 키는 프리셋이든 사용자 지정이든 body_width_m·body_height_m 숫자 그대로다. """ by_id = {item.type_id: item for item in load_structure_types()} options = {option.key: option for option in by_id["box_culvert"].options} for key in ("body_width_m", "body_height_m"): assert options[key].phase == "b05" and not options[key].required assert options[key].default == 2.0, key assert options[key].input == "number" # 규격 프리셋 전용 키를 만들면 하류가 폭·높이 대신 문자열을 읽게 된다. assert "body_size" not in options