Files
Aislo/resources/tester/test_b05_structures_registry_policy.py
T

565 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""레지스트리 정책 검증 — 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. 높이는 **기본값 없음**(2026-09-14 브레인 판정): 비우면 관경 기준 최소
# 높이로 횡단도·구조물도·표가 같은 값을 씀(옛 2.5 는 횡단도가 안 쓰던 근거 없는 값이라 걷음).
for side in ("inlet", "outlet"):
assert options[f"{side}_revet_length_m"].default == 10
assert options[f"{side}_revet_height_m"].default is None
assert "관경 기준" in options[f"{side}_revet_height_m"].empty_means
# 유무·길이·관경·유입 구조 = 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"
# ⚠ 높이는 2026-09-14 부터 **설계자 입력**(기본 없음) — 계획홍수위 + 0.5~0.7m(사방기술교본
# 2-나:141 · 3-가:181) · 옛 2.5 는 근거 없어 걷음(브레인 판정).
expected = {"height_m": None, "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