"""콘크리트 포장 전개식 — 면적 × 두께 · 거푸집 양면 · 수축줄눈 (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 == ["있음", "없음"]