"""유토곡선이 받아야 할 **구조물 몫** — 채집석 공제와 구조물 잔토 (2026-09-08). ⚠⚠ **B08 은 내기만 하고 빼거나 더하지 않는다.** 두 값 다 **양수 ㎥** 로 주고, 공제(빼기)와 사토 가산(더하기)은 **유토곡선(B06)에서 한 번씩만** 일어난다. 부호를 넘기면 받는 쪽에서 두 번 뒤집힌다 — 실무 시트가 `−274.66` 으로 적혀 있어 실제로 겪은 자리다. 채집석 공제는 사토에서 한 번만 뺀다. 구조물 잔토는 사토에 한 번만 더한다. ⚠ **측점별로도 낸다.** 총량 하나만 주면 받는 쪽이 잔량 크기에 비례해 나눌 수밖에 없고, 그러면 **운반거리가 틀어진다**(한 곳에 몰리면 거리가 어긋남 — 공제 때 이미 짚은 자리). 구조물은 구간(start~end)이라 **그 가운데 측점**을 자리로 본다. """ from __future__ import annotations from typing import Any from B08_Quantity.B08_Quantity_Engine_UnitQuantity import COLLECTED_STONE_KEY #: 유토곡선이 읽는 칸 이름 — 양쪽이 같은 낱말을 써야 인계에서 어긋나지 않는다. STRUCTURE_SPOIL_KEY = "structure_spoil_m3" STRUCTURE_SPOIL_POINTS_KEY = "structure_spoil_points" SPOIL_COMPONENT = "잔토처리" def _center(structure: dict[str, Any]) -> float | None: start, end = structure.get("start_m"), structure.get("end_m") if start is None and end is None: return None values = [float(v) for v in (start, end) if v is not None] return sum(values) / len(values) def haul_inputs(unit_quantity_table: dict[str, Any] | None) -> dict[str, Any]: """(채집석 공제, 구조물 잔토, 측점별 잔토). 값이 없으면 `None` — 0 으로 눅이지 않는다. ⚠ `None` 과 `0.0` 은 다르다. 「아직 안 옴」과 「없음」을 받는 쪽이 갈라 봐야 한다. """ if not unit_quantity_table: return { COLLECTED_STONE_KEY: None, STRUCTURE_SPOIL_KEY: None, STRUCTURE_SPOIL_POINTS_KEY: [], } total = 0.0 points: list[dict[str, float]] = [] for structure in unit_quantity_table.get("structures") or []: amount = 0.0 for component in structure.get("components") or []: if str(component.get("name") or "") != SPOIL_COMPONENT: continue amount += float(component.get("amount") or 0.0) if amount <= 0: continue total += amount chainage = _center(structure) if chainage is not None: points.append({"chainage_m": chainage, "spoil_m3": round(amount, 3)}) collected = unit_quantity_table.get(COLLECTED_STONE_KEY) return { COLLECTED_STONE_KEY: collected, STRUCTURE_SPOIL_KEY: round(total, 3) if points or total else None, STRUCTURE_SPOIL_POINTS_KEY: sorted(points, key=lambda row: row["chainage_m"]), }