- 겹친 사면(초류종자살포·면고르기)은 자동으로 안 빼고 맨 앞 사유 · 평떼 떼꽂이 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
134 lines
6.5 KiB
Python
134 lines
6.5 KiB
Python
"""혼합석 부설 — 법령 조건 자동 · 연약·습윤 칸 · 「비포장 전 구간」 고르개 (2026-09-15 브레인 ⓑ).
|
||
|
||
근거
|
||
법령 별표2 Ⅰ.2.바.(2) — 「종단기울기가 8퍼센트를 초과하는 사질토양 또는 점토질 토양인 구간과
|
||
종단기울기가 8퍼센트 이하인 구간으로서 지반이 약하고 습한 구간에는 쇄석·자갈을 부설」
|
||
임도기술교본 3-2 — 「콘크리트 포장 구간 이외에는 혼합석 부설 다짐 후 0.10m 내외」(고르개)
|
||
임도기술교본 10-1 · 부록 7-2 — 포설 전 10㎝ 터파기 · 적용범위 「노면의 토사구간」
|
||
실무 소광 — 혼합석 C 0.85 · L 1.25 (원문 체적환산표에 혼합석 줄 없음 ·
|
||
역(礫) L 1.10~1.20 · C 1.05~1.10)
|
||
판정 — 토사 = 사질·점토 해당 · 암 = 비해당 · 폭 = 차도 표준 폭 + 확폭(노견 제외).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import SummaryInput, build_rows
|
||
from B08_Quantity.B08_Quantity_Engine_GravelSurfacing import gravel_material_rows, gravel_surfacing
|
||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
||
|
||
|
||
def _design(ground: str = "soil", widening: float = 0.0, paved: bool = False) -> dict:
|
||
return {
|
||
"ground_type": ground,
|
||
"carriageway_standard_width_m": 3.0,
|
||
"widening_left_m": widening,
|
||
"paved": paved,
|
||
}
|
||
|
||
|
||
DESIGNS = [
|
||
{"chainage_m": 0.0, "design": _design()},
|
||
{"chainage_m": 20.0, "design": _design()},
|
||
{"chainage_m": 40.0, "design": _design(widening=1.0)},
|
||
{"chainage_m": 60.0, "design": _design(paved=True)},
|
||
{"chainage_m": 80.0, "design": _design(ground="ripping_rock")},
|
||
]
|
||
GRADES = {0.0: 10.0, 20.0: 10.0, 40.0: 5.0, 60.0: 12.0, 80.0: 12.0}
|
||
|
||
|
||
def test_법령_조건만_자동_토사_8퍼센트_초과() -> None:
|
||
result = gravel_surfacing(DESIGNS, GRADES, {})
|
||
# 0·20 만 해당 — [0,20] (3+3)/2×20 + [20,40] (3+0)/2×20
|
||
assert result["area_m2"] == pytest.approx(90.0)
|
||
assert result["thickness_m"] == pytest.approx(0.10)
|
||
assert result["loose_m3"] == pytest.approx(90.0 * 0.10 * 1.25 / 0.85)
|
||
assert result["excavation_soil_m3"] == pytest.approx(9.0)
|
||
text = " ".join(result["notes"])
|
||
assert "별표2" in text and "토사 전부를 해당으로 봄" in text
|
||
# 토사/암은 B06 토사 토글이 정본 — 리핑암은 「암」의 자리표시(브레인 2026-09-15 ① 정정).
|
||
assert "토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)" in text
|
||
assert "원문 표에 혼합석 줄 없음" in text and "방향이 반대" in text
|
||
assert "다짐" in text
|
||
|
||
|
||
def test_연약_습윤_구간_칸이_8퍼센트_이하_측점을_더한다() -> None:
|
||
result = gravel_surfacing(
|
||
DESIGNS, GRADES, {"gravel_soft_wet_ranges": [{"from_m": 30, "to_m": 50}]}
|
||
)
|
||
# 40 추가(경사 5 · 확폭 1) — [20,40] (3+4)/2×20 + [40,60] (4+0)/2×20
|
||
assert result["area_m2"] == pytest.approx(60.0 + 70.0 + 40.0)
|
||
|
||
|
||
def test_비포장_전_구간_고르개는_포장만_뺀다_암은_사전터파기_안_셈() -> None:
|
||
result = gravel_surfacing(DESIGNS, GRADES, {"gravel_all_unpaved": True})
|
||
assert result["area_m2"] == pytest.approx(60.0 + 70.0 + 40.0 + 30.0)
|
||
# 암 측점(80)은 교본 부록 7-2 「토사구간」 밖 — 사전 터파기 밑수에서 빠짐
|
||
assert result["excavation_soil_m3"] == pytest.approx(170.0 * 0.10)
|
||
assert any("교본 3-2" in note for note in result["notes"])
|
||
assert any("토사구간" in note for note in result["notes"])
|
||
assert any(
|
||
"산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음" in note
|
||
for note in result["notes"]
|
||
)
|
||
|
||
|
||
def test_칸이_두께_C_L_을_덮어쓴다() -> None:
|
||
settings = {"gravel_thickness_m": 0.2, "gravel_conversion_c": 1.0, "gravel_conversion_l": 1.2}
|
||
result = gravel_surfacing(DESIGNS, GRADES, settings)
|
||
assert result["loose_m3"] == pytest.approx(90.0 * 0.2 * 1.2 / 1.0)
|
||
|
||
|
||
def test_토공집계_두_줄과_인계_자재() -> None:
|
||
result = gravel_surfacing(DESIGNS, GRADES, {})
|
||
rows = {row.group: row for row in build_rows(SummaryInput(gravel=result))}
|
||
assert rows["혼합석부설"].amount == pytest.approx(result["loose_m3"])
|
||
assert "토사" in rows["혼합석 사전터파기"].spec
|
||
assert rows["혼합석 사전터파기"].amount == pytest.approx(9.0)
|
||
materials = gravel_material_rows(result)
|
||
assert materials == [
|
||
{
|
||
"name": "혼합석",
|
||
"spec": "",
|
||
"unit": "㎥",
|
||
"amount": pytest.approx(result["loose_m3"]),
|
||
"destination": "material",
|
||
"source": "혼합석 부설",
|
||
}
|
||
]
|
||
|
||
|
||
def test_산출_조건_저장이_구간을_거르고_자재총괄에_혼합석이_선다() -> None:
|
||
from B08_Quantity.B08_Quantity_Router_Earthwork_Settings import (
|
||
NULLABLE_SETTING_KEYS,
|
||
clean_setting_values,
|
||
)
|
||
from B08_Quantity.B08_Quantity_Router_Material import material_table_for
|
||
|
||
values = {"gravel_soft_wet_ranges": [{"from_m": 50, "to_m": 30}, {"from_m": 1}, "x"]}
|
||
assert clean_setting_values(values) is None
|
||
assert values["gravel_soft_wet_ranges"] == [{"from_m": 30, "to_m": 50}]
|
||
assert {"gravel_thickness_m", "gravel_conversion_c", "gravel_conversion_l"} <= set(
|
||
NULLABLE_SETTING_KEYS
|
||
)
|
||
gravel = gravel_surfacing(DESIGNS, GRADES, {})
|
||
rows = material_table_for({"rows": []}, {}, None, gravel)["rows"]
|
||
assert [row for row in rows if row["name"] == "혼합석"]
|
||
|
||
|
||
def test_인계_코드() -> None:
|
||
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table
|
||
|
||
result = gravel_surfacing(DESIGNS, GRADES, {})
|
||
items = build_handoff(summary_table=build_table(SummaryInput(gravel=result)))["work_items"]
|
||
codes = {row["work_item_code"]: row for row in items if "혼합석" in str(row["name"])}
|
||
assert codes["FP-11-04"]["quantity"] == pytest.approx(result["loose_m3"])
|
||
assert codes["FP-09-03-02"]["quantity"] == pytest.approx(9.0)
|
||
assert codes["FP-11-04"]["in_bill"] and codes["FP-09-03-02"]["in_bill"]
|
||
# 해당 측점이 없으면(모두 암) 0 원 줄을 안 세움 — 936be972 실측에서 잡음.
|
||
rock = [{**d, "design": {**d["design"], "ground_type": "ripping_rock"}} for d in DESIGNS]
|
||
empty = gravel_surfacing(rock, GRADES, {})
|
||
zero = build_handoff(summary_table=build_table(SummaryInput(gravel=empty)))["work_items"]
|
||
assert not [r for r in zero if "혼합석" in str(r["name"]) and r["in_bill"]]
|