"""운반 가중평균 검사 — PLAN 8-3·8-7 ㉡. 실무는 (운반수단 × 지반유형)별 **가중평균 1개**를 내역에 올린다. 단순평균이 아니다. 무대는 값은 내되 **내역 줄이 되지 않는다**(품셈 1-2-7). """ 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_HaulSummary import ( # noqa: E402 build_table, check_against_plan, summary_input_rows, ) def plan() -> dict: """띠 셋 + 장거리 이동 하나. 지반유형은 `HaulPlan` 이 이미 안분해 둔 값이다.""" return { "blocks": [ { "bands": [ { "equipment": "free_haul", "haul_distance_m": 12.0, "haul_from_m": 0.0, "haul_to_m": 12.0, "ea_m3": 800.0, "rr_m3": 0.0, "br_m3": 0.0, }, { "equipment": "dozer", "haul_distance_m": 40.0, "haul_from_m": 20.0, "haul_to_m": 60.0, "ea_m3": 600.0, "rr_m3": 400.0, "br_m3": 0.0, }, { "equipment": "dozer", "haul_distance_m": 50.0, "haul_from_m": 60.0, "haul_to_m": 110.0, "ea_m3": 400.0, "rr_m3": 0.0, "br_m3": 0.0, }, ] } ], "transfers": [ { "equipment": "dump_truck", "haul_distance_m": 300.0, "from_m": 100.0, "to_m": 400.0, "ea_m3": 1000.0, "rr_m3": 0.0, "br_m3": 500.0, } ], "hauled_m3": 2200.0, "transferred_m3": 1500.0, } def test_가중평균이지_단순평균이_아님() -> None: """도자 토사 = 600㎥@40m + 400㎥@50m → (600·40+400·50)/1000 = 44m.""" rows = build_table(plan())["rows"] dozer_soil = next(r for r in rows if r["equipment"] == "dozer" and r["ground"] == "토사") assert dozer_soil["volume_m3"] == pytest.approx(1000.0) assert dozer_soil["average_distance_m"] == pytest.approx(44.0) assert dozer_soil["average_distance_m"] != pytest.approx(45.0) # 단순평균이면 45 def test_지반유형별로_갈림() -> None: """`HaulPlan` 이 안분해 둔 토사·리핑암·발파암을 그대로 쓴다 — 다시 판정하지 않는다.""" rows = build_table(plan())["rows"] dozer = {r["ground"] for r in rows if r["equipment"] == "dozer"} assert dozer == {"토사", "리핑암"} dump = {r["ground"] for r in rows if r["equipment"] == "dump_truck"} assert dump == {"토사", "발파암"} def test_무대는_내역줄이_아님() -> None: """품셈 1-2-7 — 소운반 20m 는 품에 포함. 인력운반 10-6 도 「초과분」이다.""" table = build_table(plan()) free = next(r for r in table["rows"] if r["equipment"] == "free_haul") assert free["in_bill"] is False assert free["volume_m3"] == pytest.approx(800.0) # 값은 낸다 assert all(r["in_bill"] for r in table["rows"] if r["equipment"] != "free_haul") def test_내역줄_개수는_무대를_뺀_수() -> None: table = build_table(plan()) assert table["bill_row_count"] == len([r for r in table["rows"] if r["in_bill"]]) assert table["bill_row_count"] == 4 # 도자 토사·리핑암 · 덤프 토사·발파암 def test_근거줄이_함께_나옴() -> None: """어느 구간이 그 평균을 만들었는지 되짚을 수 있어야 한다.""" table = build_table(plan()) legs = table["legs"] assert len(legs) == 6 # 무대1 + 도자3 + 덤프2 assert {leg["source"] for leg in legs} == {"band", "transfer"} dozer_legs = [leg for leg in legs if leg["equipment"] == "dozer" and leg["ground"] == "토사"] assert sorted(leg["distance_m"] for leg in dozer_legs) == [40.0, 50.0] def test_줄_순서가_늘_같음() -> None: """내역 줄 순서가 매번 달라지면 대조를 못 한다.""" first = [(r["equipment"], r["ground"]) for r in build_table(plan())["rows"]] second = [(r["equipment"], r["ground"]) for r in build_table(plan())["rows"]] assert first == second assert first[0][0] == "free_haul" # 무대가 맨 앞 def test_검산_무대를_넣어야_합이_맞음() -> None: """무대를 안 내면 이 대조가 죽는다 — 그래서 값은 내고 내역 줄만 뺀다.""" table = build_table(plan()) check = check_against_plan(table, plan()) assert check.hauled_total_m3 == pytest.approx(3700.0) # 800+1000+400+1000+500 assert check.plan_total_m3 == pytest.approx(3700.0) assert check.difference_m3 == pytest.approx(0.0) assert check.details["free_haul"] == pytest.approx(800.0) def test_집계표_입력으로_줄임() -> None: """토공집계표는 근거 줄을 안 쓴다 — 내역 줄만 넘긴다. ⚠ 2026-09-09 에 칸이 **둘 늘었다** — 다짐·자연 두 상태를 함께 넘긴다 (`natural_m3`·`volume_basis`). 내역서 수량은 자연상태라(config 5-4-3) 받는 쪽이 어느 상태인지 스스로 봐야 한다. 근거 줄(`legs`)을 안 넘긴다는 계약은 그대로다. """ rows = summary_input_rows(build_table(plan())) assert all( set(row) == { "equipment", "ground", "volume_m3", "average_distance_m", "natural_m3", "volume_basis", # 쓴 계수도 함께 온다 — 받는 쪽이 되짚을 수 있어야 한다. "conversion_c", # 2026-09-14 ㉱ — 구성비로 가른 암 갈래(안 가른 줄은 None) · 집계표 공종 칸이 씀. "rock_class", } for row in rows ) assert len(rows) == 5 # 무대 포함(집계표에는 오른다) def test_계획이_비면_빈_표() -> None: table = build_table(None) assert table["rows"] == [] assert table["legs"] == [] # ── 저장 정본 모양 (2026-09-07 실증에서 잡은 자리) ────────────────── # # ⚠ 정본에 저장되는 것은 유토곡선 한 벌(`mass_haul`)이고 **배분은 그 안의 `haul_plan`** 이다. # 바깥 껍데기를 그대로 넘기면 `blocks` 를 못 찾아 **운반 표가 영영 0줄**이 된다. # [확정] 전에는 어차피 빈 표라 화면에서 티가 안 나던 자리다. def 저장정본() -> dict: """서버 계산이 실제로 내는 모양 — `mass_haul` 바깥에 `haul_plan` 이 들어 있다.""" return { "basis": "cross", "cut_natural_m3": 5000.0, "haul_plan": { "hauled_m3": 3724.0, "blocks": [ { "bands": [ { "equipment": "free_haul", "haul_distance_m": 10.0, "ea_m3": 3.9, "rr_m3": 0, "br_m3": 0, }, { "equipment": "dozer", "haul_distance_m": 40.0, "ea_m3": 7.8, "rr_m3": 0, "br_m3": 0, }, { "equipment": "dump_truck", "haul_distance_m": 288.12, "ea_m3": 3712.29, "rr_m3": 0, "br_m3": 0, }, ] } ], "transfers": [], }, } def test_바깥_껍데기를_그대로_주면_빈_표가_된다() -> None: """⚠ 이 시험이 그 사고를 못 박는다 — 빈 표는 「계획이 없다」와 구별이 안 된다.""" assert build_table(저장정본())["rows"] == [] def test_haul_plan_을_벗겨_주면_세_줄이_선다() -> None: table = build_table(저장정본()["haul_plan"]) equipment = [ row.equipment if hasattr(row, "equipment") else row["equipment"] for row in table["rows"] ] assert equipment == ["free_haul", "dozer", "dump_truck"] assert table["bill_row_count"] == 2 # 무대는 내역 줄이 아니다 # ── 암 운반 (2026-09-07 실증) ──────────────────────────────────────── # # 앞선 실증에서는 토사만 나와 **(수단 × 지반유형) 갈래가 실물로 갈리는 것을 못 봤다**. # 암이 유용토에 실리는 노선으로 다시 돌려 6줄로 갈리는 것과 보정계수가 물리는 것을 확인했고, # 그 모양을 여기 못 박는다. def 암_운반계획() -> dict: """서버 계산이 실제로 낸 모양(리핑암 노선) — 띠마다 지반별 물량이 함께 온다.""" return { "hauled_m3": 7179.0, "blocks": [ { "bands": [ { "equipment": "free_haul", "haul_distance_m": 10.0, "ea_m3": 14.16, "rr_m3": 18.09, "br_m3": 0.0, }, { "equipment": "dozer", "haul_distance_m": 40.0, "ea_m3": 28.32, "rr_m3": 36.19, "br_m3": 0.0, }, { "equipment": "dump_truck", "haul_distance_m": 439.92, "ea_m3": 988.08, "rr_m3": 6094.15, "br_m3": 0.0, }, ] } ], "transfers": [], } def test_수단과_지반유형으로_갈려_여섯_줄이_섬() -> None: table = build_table(암_운반계획()) pairs = [(row["equipment"], row["ground"]) for row in table["rows"]] assert ("dump_truck", "리핑암") in pairs assert ("free_haul", "리핑암") in pairs assert len(table["rows"]) == 6 def test_암도_무대는_내역에_안_섬() -> None: """지반이 암이어도 무대는 품에 포함이다 — 지반유형이 그 규칙을 바꾸지 않는다.""" table = build_table(암_운반계획()) free = [row for row in table["rows"] if row["equipment"] == "free_haul"] assert len(free) == 2 # 토사·리핑암 assert all(not row["in_bill"] for row in free) assert table["bill_row_count"] == 4 def test_지반별_합이_총_운반토량과_맞을것() -> None: table = build_table(암_운반계획()) total = sum(row["volume_m3"] for row in table["rows"]) assert total == pytest.approx(7179.0, abs=0.02) # 띠 값 자체의 반올림 몫 def test_보정계수가_다짐환산에_물릴것() -> None: """⚠ 실증 산수 — 리핑암 1.15 · 발파암 1.30 이 그대로 곱해진다. 자연토량 토사 1,240 · 암 5,560 인 같은 노선에서 리핑암: 1,240×0.9 + 5,560×1.15 = 1,116 + 6,394 = 7,510 발파암: 1,240×0.9 + 5,560×1.30 = 1,116 + 7,228 = 8,344 서버 계산이 낸 값과 자릿수까지 맞았다. 계수가 바뀌면 이 시험이 깨진다. """ from config.config_system_design import EARTHWORK_CONVERSION_FACTORS as F assert F["soil"]["compacted"] == 0.9 assert F["ripping_rock"]["compacted"] == 1.15 assert F["blasting_rock"]["compacted"] == 1.30 assert 1240 * 0.9 + 5560 * 1.15 == pytest.approx(7510.0) assert 1240 * 0.9 + 5560 * 1.30 == pytest.approx(8344.0)