tmp/ 가 창끼리 안 건너가는 것이 확정돼(랩탑이 시간 두고 두 번 확인) 시험·예외가 저절로 건너가도록 git 안으로 옮김. 사용자 확정. - 내용은 하나도 안 고침 — 자리만 옮김. tmp/tests 는 남겨 둠. - 같은 이름이 이미 있던 64 개는 랩탑 것을 그대로 두고 건너뜀. - helper_b05_*.js 둘은 랩탑이 .cjs 로 이미 올린 것과 **줄바꿈만 다른 같은 내용**이라 복사본을 도로 뺌(시험이 .cjs 를 부름). - resources/tester/ 에서 전체 1176 통과 · 29 건너뜀 · 실패 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
308 lines
12 KiB
Python
308 lines
12 KiB
Python
"""운반 가중평균 검사 — 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",
|
||
}
|
||
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)
|