Files
Aislo/resources/tester/test_b08_revegetation_unit.py
T
eomsangdonandClaude Opus 5 0d4f46ecbe feat(b05·b08): 비탈면 녹화 ㎡ 넷(새심기·평떼·줄떼·비탈덮기) — 구간 사면 면적 × 대상 면 칸 · 떼 11매/3.33매(품셈 5-22-1·5-23-1) · 초류종자 살포는 공법에서 빼고 칸 밑 까닭 · 단 직고·선떼 급수 칸 기본값 없음(12장 D 녹화 · 브레인 ⓐ~ⓓ)
- 겹친 사면(초류종자살포·면고르기)은 자동으로 안 빼고 맨 앞 사유 · 평떼 떼꽂이 5-22-3 [주] 읽기 사유
- 혼합석 사유 정정: 토사 = B06 측점 토사 토글(구성비는 암 몫만) · 사전터파기 「산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음」
- UnitQuantity 700줄 — 늦은 import 를 한 벌(_lazy)로

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-15 05:30:32 +09:00

116 lines
5.1 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.
"""비탈면 녹화 ㎡ 넷 — 새심기·평떼·줄떼·비탈덮기 (2026-09-15 브레인 12장 D 녹화 ⓐ~ⓓ).
근거
산림품셈 5-14 새심기(㎡당) · 5-22 평떼(떼 0.3×0.3 11매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) ·
5-23 줄떼(떼 3.33매/㎡ · 붙임 ㎡ · 떼꽂이 ㎡) · 5-25 거적덮기(㎡당) · 5-28 비탈덮기(짚망 · 매트)
판정 — 면적 = 구간 안 사면 면적(칸: 절토면/성토면/양쪽 · 기본값 없음) · 초류종자 살포는 공법에서 뺌
(토공집계가 5-24 로 사면 전체를 셈) · 단 직고·선떼 급수 칸은 기본값 없음 · 길이형은 다음 차례.
"""
from __future__ import annotations
import pytest
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Revegetation import (
revegetation_from_designs,
)
AREAS = {"b06_cut_area_m2": 40.0, "b06_fill_area_m2": 60.0}
def _row(**options) -> dict:
structure = {
"structure_id": "g1",
"type_id": "revegetation",
"start_m": 0.0,
"end_m": 20.0,
"options": {**AREAS, **options},
}
table = build_table([structure], {"revegetation": "비탈면 녹화"})
return table
def _parts(table: dict) -> dict:
row = next(
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "녹화" in r["name"]
)
return {part["code"]: part for part in row["composite_parts"] or []}
def test_평떼_양쪽은_면적과_떼_11매() -> None:
table = _row(method="평떼", face="양쪽")
row = table["structures"][0]
got = {(c["name"], c["unit"]): c["amount"] for c in row["components"]}
assert (row["billing_unit"], row["billing_quantity"]) == ("㎡", pytest.approx(100.0))
assert got[("평떼", "㎡")] == pytest.approx(100.0)
assert got[("떼", "매")] == pytest.approx(1100.0)
text = " ".join(row["notes"])
assert "겹친 몫을 자동으로 빼지 않음" in text and row["notes"][0].startswith("⚠")
assert "5-12 품에 걸린 것으로 읽음 — 5-22-3 떼꽂이는 따로 셈" in text
parts = _parts(table)
assert parts["FP-05-22-02"]["quantity"] == pytest.approx(100.0)
assert parts["FP-05-22-03"]["quantity"] == pytest.approx(100.0)
def test_줄떼_절토면은_3_33매() -> None:
table = _row(method="줄떼", face="절토면")
got = {(c["name"], c["unit"]): c["amount"] for c in table["structures"][0]["components"]}
assert got[("줄떼", "㎡")] == pytest.approx(40.0)
assert got[("떼", "매")] == pytest.approx(40.0 * 3.33)
assert set(_parts(table)) == {"FP-05-23-02", "FP-05-23-03"}
def test_새심기_성토면() -> None:
table = _row(method="새심기", face="성토면")
assert _parts(table)["FP-05-14"]["quantity"] == pytest.approx(60.0)
def test_비탈덮기는_덮개를_골라야_선다() -> None:
table = _row(method="비탈덮기", face="양쪽", cover_kind="거적")
assert _parts(table)["FP-05-25"]["quantity"] == pytest.approx(100.0)
unset = _row(method="비탈덮기", face="양쪽")["structures"][0]
assert not unset["components"] and any("덮개" in n for n in unset["notes"])
def test_면을_안_고르면_사유() -> None:
row = _row(method="평떼")["structures"][0]
assert not row["components"] and any("절토면" in n and "성토면" in n for n in row["notes"])
def test_길이형은_떼_매수표를_기다린다() -> None:
row = _row(method="선떼붙이기", face="양쪽")["structures"][0]
assert not row["components"]
assert any("사방기술교본" in n for n in row["notes"])
def test_구간_안_사면_면적은_B06_측점에서_보간() -> None:
designs = [
{"chainage_m": 0.0, "design": {}},
{"chainage_m": 20.0, "design": {}},
]
from B08_Quantity.B08_Quantity_Engine_SlopeLength import StationSlope
slopes = [
StationSlope(chainage_m=0.0, cut_length_m=2.0, fill_length_m=4.0),
StationSlope(chainage_m=20.0, cut_length_m=4.0, fill_length_m=0.0),
]
structure = {"type_id": "revegetation", "start_m": 5.0, "end_m": 20.0, "options": {}}
filled = revegetation_from_designs(structure, designs, slopes)
# 절토 5→2.5, 20→4 : (2.5+4)/2×15 · 성토 5→3, 20→0 : (3+0)/2×15
assert filled["options"]["b06_cut_area_m2"] == pytest.approx(48.75)
assert filled["options"]["b06_fill_area_m2"] == pytest.approx(22.5)
def test_등록부_공법에서_초류종자_살포를_빼고_까닭을_보인다() -> None:
options = {o.key: o for o in structure_type_map()["revegetation"].options}
assert "초류종자 살포" not in options["method"].choices
assert "토공집계" in (options["method"].default_basis or "")
assert (
options["face"].choices == ["절토면", "성토면", "양쪽"] and options["face"].default is None
)
for key in ("step_height_m", "sod_grade"):
assert options[key].default is None and "아직 표에 안 쓰임" in options[key].not_in_table