시험을 `resources/tester` 로 옮긴 뒤 **한 건이 실패**하고 있었음 — `test_preview_cost` 가 짝 파일을 `tmp.tests.…` 로 불러 못 찾았음(옮긴 폴더에 없음). - `from resources.tester.test_preview_assets import …` 로 고침 - 실행 안내 문구 셋도 새 경로로(`diag_structures` · `helper_b06_fill_slope_length` · `test_common_util_crs`) - 정책 시험 설명은 **사실이 바뀌었으므로** 고쳐 적음 — 「git 밖이라」가 아니라 「옮겨서 함께 건너가되, 까닭은 여전히 값 옆(정본)에 두는 것이 옳다」 `resources/tester/` 1183 통과 / 22 건너뜀 / **실패 0**. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
566 lines
29 KiB
Python
566 lines
29 KiB
Python
"""레지스트리 정책 검증 — 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"),
|
||
# 사토장 쌓는 쪽 기본 "자동(성토 쪽)" — C군 side 와 같은 뜻이다. 도메인 확정값이 아니라
|
||
# 「흙은 성토 쪽에 쌓는다」는 지금까지의 동작을 이어받는 표식이고, 측점마다 좌·우로 바꾼다.
|
||
("spoil_bank", "side"),
|
||
# 추가 운반거리 기본 0 = 「추가 없음」. 노선 안 사토장은 측점 사이 누가거리로 거리가
|
||
# 그대로 나오므로(2026-09-09 사용자 확정 ③) 이 칸은 **외부 사토장 대비 여벌**이다.
|
||
# 0 은 도메인 수치가 아니라 lift_m·shift_m 과 같은 「안 더함」 표식이다.
|
||
("spoil_bank", "extra_distance_m"),
|
||
# 단 수 기본 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
|