Files
Aislo/resources/tester/test_b08_preparation.py
T
eomsangdonandClaude Opus 5 82755260e2 feat(b08): 표토제거 실무대로 — 노면 + 절토 사면 ㎡ · 운반은 20 m 넘는 몫만 · 층따기 제안값 경고
- 표토제거: 답외구간 FP-09-15-02 · 밑수 ㎡ · 대상 노면·절토대상지(성토 사면 안 셈) · 노면 폭이 빈 측점이 있으면 안 섬
- 표토 운반·적치: 제거 ㎡ × 두께 · 거리 − 압토 20 m · 20 m 이내면 제거 품에 듦
- 층따기: 「제안값(확정 아님)」 경고 줄 · 칸에 자동으로 안 넣음
- 시험: 옛 ㎥ 기대값 넷 갱신 · 노면 면적 평균단면적 시험

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
2026-09-13 23:58:38 +09:00

232 lines
11 KiB
Python

"""준비공·사방공 검사 — PLAN 8-3 의 ❌ 둘 (2026-09-07 ⑪).
여기서 하는 일은 **줄을 세우고 못 서는 줄의 사유를 적는 것**이다. 빈 표를 내면
「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다.
⚠⚠ 벌목은 이미 토공집계의 「지장목제거」로 선다 — 여기서 또 세우면 같은 나무를 두 번 벤다.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_Preparation import ( # noqa: E402
EROSION_CONTROL_TYPES,
STATUS_COUNTED_ELSEWHERE,
STATUS_NOT_APPLICABLE,
STATUS_PENDING,
build_table,
erosion_rows,
)
def (table: dict, item: str) -> dict:
return next(row for row in table["rows"] if row["item"] == item)
# ── 이중계상 방어 ───────────────────────────────────────────────────
def test_벌목은_값을_내지_않고_가리키기만_함() -> None:
"""⚠ 이 파일의 핵심 — 토공집계의 「지장목제거」와 겹치면 같은 나무를 두 번 벤다."""
table = build_table({"tree_removal_fill": 1000.0, "tree_removal_cut": 500.0})
row = (table, "벌목·지장목제거")
assert row["amount"] is None # 값은 안 낸다
assert row["status"] == STATUS_COUNTED_ELSEWHERE
assert "이중계상" in row["reason"]
# 참고 면적은 보인다 — 어느 값과 겹치는지 사람이 알아야 한다.
assert row["reference_amount"] == pytest.approx(1500.0)
# ── 못 서는 줄은 사유를 적는다 ─────────────────────────────────────
def test_못_서는_줄이_목록에_남을것() -> None:
"""빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다."""
table = build_table()
items = [row["item"] for row in table["rows"]]
assert "표토제거" in items and "제근·뿌리다듬기" in items
# 규준틀은 둘로 갈렸다 — 비탈(11-2)과 수평(11-3)의 설치 기준이 다르다.
assert "비탈 규준틀" in items and "수평 규준틀" in items
def test_사유가_비어_있지_않을것() -> None:
for row in build_table()["rows"]:
assert row["reason"], f"{row['item']} 에 사유가 없다"
def test_값이_없어도_공종코드는_알려줄것() -> None:
"""무엇으로 서게 될 줄인지 알아야 나중에 잇는다."""
assert (build_table(), "표토제거")["work_item_code"] == "FP-09-15-02"
# ── 사방공 ──────────────────────────────────────────────────────────
def test_사방_시설이_없으면_해당_없음() -> None:
"""있는 것처럼 0 을 적지 않는다."""
row = (build_table(structures=[{"type_id": "masonry_wet"}]), "사방 시설")
assert row["status"] == STATUS_NOT_APPLICABLE
assert row["amount"] is None
def test_사방_시설이_있으면_줄이_섬() -> None:
table = build_table(structures=[{"type_id": "erosion_check"}, {"type_id": "revetment"}])
사방 = [row for row in table["rows"] if row["group"] == "사방공"]
# 키는 `type_id` 칸으로 오가고, 화면에 뜨는 `item` 은 사람 이름이다.
assert [row["type_id"] for row in 사방] == ["erosion_check", "revetment"]
assert [row["item"] for row in 사방] == ["골막이", "기슭막이"]
assert all(row["status"] == STATUS_PENDING for row in table["rows"] if row["group"] == "사방공")
def test_사방_종류_이름이_레지스트리와_맞을것() -> None:
"""⚠ 이름을 지어내면 영영 안 걸린다 — 레지스트리의 실제 `type_id` 여야 한다."""
import json
registry = json.loads(
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
)
types = registry if isinstance(registry, list) else (registry.get("types") or [])
known = {t["type_id"] for t in types}
assert EROSION_CONTROL_TYPES <= known, EROSION_CONTROL_TYPES - known
def test_셈이_맞을것() -> None:
table = build_table()
assert table["row_count"] == len(table["rows"])
assert table["pending_count"] == sum(1 for r in table["rows"] if r["status"] == STATUS_PENDING)
# ── 화면에 안 보이던 확정 항목을 드러내기 (2026-09-07) ─────────────
#
# ⚠ 목록에만 있고 화면에 없으면 **사용자는 그것이 잠정인 줄도 모른다.**
# 「확인 필요」만 있어도 안 된다 — **지금 무슨 값으로 돌고 있는지**를 함께 적어야
# 사용자가 무엇을 정할지 안다(원단위 미확보에서 「표에 있는 규격을 함께 알린」 그 방식).
def test_지장목제거_줄에_공종_자리가_적힐것() -> None:
"""⭐ 2026-09-13 판정 — 잡관목제거는 단목베기(4-2-2), 뿌리뽑기는 제근(9-21)."""
row = (build_table(), "벌목·지장목제거")
assert "FP-04-02-02" in row["reason"] and "FP-09-21" in row["reason"]
# ── 규준틀 개소 — 원문이 기준을 정해 둠 (2026-09-07 ㉒) ────────────
#
# 11-2 [주]① 「비탈길이 10m 이상 20m마다 설치한다」
# 11-3 [주]① 「중심점에서 성토 높이 5m 이상에 설치한다」
# ⚠ 재료량은 [주]④ 「설계수량에 따른다」 — **개소만 내고 재료는 미확보**.
def 사면줄(*pairs: tuple[float, float]) -> list[dict]:
"""(그 측점의 사면길이, 앞 측점까지 거리) 목록."""
return [
{"lengths": {"face_dressing_fill": length}, "distance_m": dist} for length, dist in pairs
]
def test_비탈길이_10m_이상_구간만_셀것() -> None:
"""⚠ 「10m 이상」은 **비탈길이** 조건이고 「20m마다」는 **노선 거리** 간격이다.
둘을 섞으면 짧은 사면 구간까지 세어 개소가 부푼다."""
from B08_Quantity.B08_Quantity_Engine_Preparation import batter_frame_count
# 사면길이 12m 인 구간 40m + 사면길이 3m 인 구간 100m → 40m 만 센다.
count, notes = batter_frame_count(사면줄((12.0, 20.0), (12.0, 20.0), (3.0, 100.0)))
assert count == 3 # 40 ÷ 20 + 1
assert "11-2" in notes[0]
def test_긴_사면이_없으면_안_설것() -> None:
from B08_Quantity.B08_Quantity_Engine_Preparation import batter_frame_count
count, notes = batter_frame_count(사면줄((4.0, 100.0)))
assert count == 0
assert notes
def test_비탈_규준틀이_표에_값으로_설것() -> None:
# ⚠ 실제 사면표는 **첫 측점의 distance_m 이 0** 이다(앞 측점이 없다). 그 모양으로 넣는다.
table = build_table(slope_rows=사면줄((12.0, 0.0), (12.0, 20.0)))
row = (table, "비탈 규준틀")
assert row["amount"] == 2.0 # 20 ÷ 20 + 1
assert row["status"] == "값 있음"
# 재료는 여전히 미확보임을 같은 줄에 적는다.
assert "설계수량" in row["reason"]
def test_수평_규준틀은_성토고_칸이_없으면_못_셈() -> None:
"""⚠ 0 으로 때우지 않는다 — 「없음」과 「못 셈」은 다르다."""
row = (build_table(slope_rows=[{"lengths": {}, "distance_m": 20.0}]), "수평 규준틀")
assert row["amount"] is None
assert "성토고" in row["reason"] and "11-3" in row["reason"]
def test_수평_규준틀은_성토고_5m_이상_측점마다() -> None:
"""⚠ 비탈규준틀(「10m 이상 20m마다」)과 **기준이 다르다** — 이쪽은 **지점 조건**이라
간격이 없다. 두 기준을 같은 식으로 쓰면 조용히 틀린다."""
rows = [
{"fill_height_m": 6.0, "distance_m": 20.0},
{"fill_height_m": 2.0, "distance_m": 20.0},
{"fill_height_m": 5.0, "distance_m": 20.0},
]
row = (build_table(slope_rows=rows), "수평 규준틀")
assert row["amount"] == 2.0 # 6.0 · 5.0 두 곳(5.0 은 「이상」이라 든다)
assert row["status"] == "값 있음"
def test_성토고가_다_낮으면_0개소() -> None:
"""0 이 **값**인 자리다 — 「못 셈」과 달리 「없다」가 맞는 답이다."""
row = (build_table(slope_rows=[{"fill_height_m": 1.0, "distance_m": 20.0}]), "수평 규준틀")
assert row["amount"] == 0.0
assert row["status"] == "값 있음"
# ── 표토제거 두께 — 품셈이 아니라 설계가 정함 (2026-09-07 ㉓) ───────
#
# 9-15 [주]② 「Q=Q1/T … T : 표토두께(m)」 — **공식의 입력 변수**다.
# ⚠ 안 넣으면 0 으로 때우지 않고 물량을 안 낸다.
def test_표토제거는_노면과_절토대상지_면적이고_두께를_안_곱한다() -> None:
"""⭐ 2026-09-13 판정 「실무대로」 — 밑수 ㎡ · 대상 노면+절토대상지 · 성토 사면은 안 셈."""
table = build_table(
{"face_dressing_fill": 1000.0, "face_dressing_cut": 500.0},
topsoil_thickness_m=0.15,
road_surface_area_m2=4000.0,
)
row = (table, "표토제거")
assert (row["unit"], row["amount"], row["status"]) == ("㎡", pytest.approx(4500.0), "값 있음")
assert row["work_item_code"] == "FP-09-15-02" # 답외구간
assert "노면·절토대상지" in row["reason"] and "20m 압토" in row["reason"]
def test_노면_면적을_못_세면_물량을_안_낼것() -> None:
"""노체 끝이 없는 측점이 있으면 절토 사면만으로 세우지 않는다 — 합계가 그럴듯하게 틀림."""
row = (build_table({"face_dressing_cut": 500.0}), "표토제거")
assert row["amount"] is None and "노면 면적" in row["reason"]
assert row["reference_amount"] == pytest.approx(500.0)
def test_면적이_0이면_안_낼것() -> None:
"""0 은 「없음」과 구별이 안 된다 — 0㎡ 를 내면 「표토가 없는 노선」으로 읽힌다."""
row = (build_table({}, road_surface_area_m2=0.0), "표토제거")
assert row["amount"] is None
def test_erosion_row_uses_registry_name_not_type_id():
"""사방공 줄 이름에 개발자 키가 새지 않는다 (2026-09-08 ㉕ 통과에서 `soil_guard` 로 뜬 자리)."""
rows = erosion_rows([{"type_id": "soil_guard"}], {"soil_guard": "흙막이"})
assert [row["item"] for row in rows] == ["흙막이"]
assert rows[0]["type_id"] == "soil_guard"
def test_erosion_row_falls_back_to_wording_table_without_registry():
"""이름표가 없어도 문구표가 받아 준다 — 없으면 키를 보이되 지어내지 않는다."""
assert erosion_rows([{"type_id": "soil_guard"}])[0]["item"] == "흙막이"
assert erosion_rows([{"type_id": "erosion_check"}])[0]["item"] == "골막이"