Files
Aislo/resources/tester/test_b08_pavement_concrete_unit.py
eomsangdonandClaude Opus 5 3cfe3ef58a feat(b05·b08): 콘크리트 포장 수량식 — 폭·확폭·두께 B06 잇기 + 칸 덮어쓰기 · 면적×두께 · 거푸집 2L · 수축줄눈 6m · 비닐·철망 고르기(12장 D 포장 둘 · 브레인 판정 ①~⑤·ⓐ)
- 등록부: 두께 칸 필수 걷고 「비면 B06 포장층 두께」 · 폭·확폭 면적 칸(비면 B06) · 줄눈 간격 제안 6m(교본 부록 4-7 4-6m · 소광) · 비닐·철망 고르기(기본값 없음)
- 연장은 구간(끝 − 시작) — 물넘이로 나뉜 조각에 길이 칸이 복사되는 자리
- 묶음 12-06#두께 · 12-08 · 12-07-01(12-7-2 줄눈설치는 안 씀 — 소광 실무가 컷팅)

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

159 lines
6.7 KiB
Python
Raw Permalink 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 브레인 포장 둘 ①~⑤·ⓐ).
근거
임도기술교본 3-2 기본설계 — 포장 폭 3.0m · 두께 0.2m · 곡선부 확폭
임도기술교본 부록 4-7 7-1 3.5.1 — 「수축줄눈의 간격은 4-6m를 기준」
산림품셈 12-6 콘크리트 포장(인력) · 12-7-1 포장절단 · 12-8 콘크리트 포장 거푸집
실무 울진소광 「포장및난간개거」 — 면적 = B × L · 거푸집(1면기준) = 2L ·
수축줄눈 = 내림(L÷6) × B (103m·B 3.5 → 59.5 · 418m → 241.5) · 「포장확폭」 줄눈 = 면적 ÷ 6
판정 — 폭·확폭·두께는 **B06 에서 잇고 칸이 이김**(한 값 원칙) · 비닐·철망은 고르기(기본값 없음).
"""
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_Pavement import pavement_from_designs
FILLED = {
"width_m": 3.0,
"widening_area_m2": 6.0,
"thickness_cm": 20,
"joint_spacing_m": 6,
"separation_sheet": "있음",
"wire_mesh": "없음",
}
def _row(options: dict, start: float = 100.0, end: float = 130.0) -> dict:
structure = {
"structure_id": "p1",
"type_id": "pavement_concrete",
"start_m": start,
"end_m": end,
# 구간을 물넘이로 나누면 길이 칸(10)이 조각마다 그대로 복사됨 — 연장은 구간에서 읽음.
"options": {"length_m": 10, **options},
}
return build_table([structure], {"pavement_concrete": "콘크리트 포장"})["structures"][0]
def test_칸이_찬_구간은_면적_두께_거푸집_줄눈이_선다() -> None:
row = _row(FILLED)
got = {c["name"]: c for c in row["components"]}
area = 3.0 * 30 + 6.0
assert (row["billing_unit"], row["billing_quantity"]) == ("㎡", pytest.approx(area))
assert got["콘크리트"]["amount"] == pytest.approx(area * 0.2)
assert got["콘크리트"]["destination"] == "material"
assert got["포장거푸집"]["amount"] == pytest.approx(2 * 30)
# 내림(30 ÷ 6) × 3.0 + 확폭 6 ÷ 6
assert got["수축줄눈"]["amount"] == pytest.approx(5 * 3.0 + 1.0)
assert got["비닐"]["amount"] == pytest.approx(area) and "철망" not in got
text = " ".join(row["notes"])
assert "12-7-2 줄눈설치는 안 씀 — 소광 실무가 컷팅" in text
@pytest.mark.parametrize(("length", "joints"), [(103.0, 59.5), (418.0, 241.5)])
def test_수축줄눈은_소광_관측과_같다(length: float, joints: float) -> None:
row = _row({**FILLED, "width_m": 3.5, "widening_area_m2": 0}, 0.0, length)
got = {c["name"]: c["amount"] for c in row["components"]}
assert got["수축줄눈"] == pytest.approx(joints)
def test_비닐_철망을_안_고르면_사유() -> None:
row = _row({k: v for k, v in FILLED.items() if k not in ("separation_sheet", "wire_mesh")})
names = {c["name"] for c in row["components"]}
assert "비닐" not in names and "철망" not in names and "콘크리트" in names
text = " ".join(row["notes"])
assert "비닐" in text and "철망" in text and "12-6" in text
DESIGNS = [
{"chainage_m": 90.0, "design": {"carriageway_standard_width_m": 3.0, "paved": False}},
{
"chainage_m": 100.0,
"design": {
"carriageway_standard_width_m": 3.0,
"widening_right_m": 1.0,
"paved": True,
"pavement_thickness_m": 0.2,
},
},
{
"chainage_m": 120.0,
"design": {
"carriageway_standard_width_m": 3.0,
"widening_left_m": 0.5,
"paved": True,
"pavement_thickness_m": 0.2,
},
},
{
"chainage_m": 140.0,
"design": {"carriageway_standard_width_m": 3.0, "paved": True, "pavement_thickness_m": 0.2},
},
]
def _structure(**options) -> dict:
return {
"type_id": "pavement_concrete",
"start_m": 100.0,
"end_m": 130.0,
"options": {"length_m": 10, **options},
}
def test_빈_칸은_B06_폭_확폭_두께를_잇는다() -> None:
filled = pavement_from_designs(_structure(), DESIGNS)
options = filled["options"]
assert options["width_m"] == pytest.approx(3.0)
assert options["thickness_cm"] == pytest.approx(20.0)
# 100 → 1.0 · 120 → 0.5 · 130 → 0.25(120~140 사이 보간): (1.0+0.5)/2×20 + (0.5+0.25)/2×10
assert options["widening_area_m2"] == pytest.approx(15.0 + 3.75)
assert any("B06" in note for note in filled["notes"])
# 상세 칸은 배치 폼에 없어 저장에 없음 — 등록부 제안값 6m 을 사유와 함께 채움.
assert options["joint_spacing_m"] == 6 and any("4-6m" in note for note in filled["notes"])
def test_칸을_적으면_칸이_이긴다() -> None:
filled = pavement_from_designs(_structure(width_m=3.5, widening_area_m2=0), DESIGNS)
assert filled["options"]["width_m"] == 3.5 and filled["options"]["widening_area_m2"] == 0
assert filled["options"]["thickness_cm"] == pytest.approx(20.0)
def test_B06_설계도_칸도_없으면_안_선다() -> None:
filled = pavement_from_designs(_structure(), [])
row = build_table([filled])["structures"][0]
assert row["components"] == []
assert any("폭" in note for note in row["notes"])
def test_인계_묶음은_12_6_두께_갈래_12_8_12_7_1() -> None:
table = build_table(
[{**_structure(**FILLED), "structure_id": "p1"}], {"pavement_concrete": "콘크리트 포장"}
)
row = next(
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "포장" in r["name"]
)
parts = {part["code"]: part for part in row["composite_parts"] or []}
area = 3.0 * 30 + 6.0
assert parts["FP-12-06#20㎝"]["quantity"] == pytest.approx(area * 0.2)
assert parts["FP-12-08"]["quantity"] == pytest.approx(60.0)
assert parts["FP-12-07-01"]["quantity"] == pytest.approx(16.0)
def test_등록부_칸() -> None:
options = {o.key: o for o in structure_type_map()["pavement_concrete"].options}
assert not options["thickness_cm"].required and "B06" in options["thickness_cm"].empty_means
assert "B06" in options["width_m"].empty_means and "3.5" in options["width_m"].empty_means
assert "B06" in options["widening_area_m2"].empty_means
assert (
options["joint_spacing_m"].default == 6
and "4-6m" in options["joint_spacing_m"].default_basis
)
for key in ("separation_sheet", "wire_mesh"):
assert options[key].default is None and options[key].choices == ["있음", "없음"]