feat(B08): 채집석 공제를 갈래별로 냄 — 받는 쪽이 축을 맞춰 뺄 수 있게

- 축이 다른 값을 그냥 빼면 암에서 15% 어긋남(우리가 만드는 어긋남). 갈래를 실어 보내
  유토곡선이 ×C 로 다짐 축에 맞춰 빼게 함
- 갈래를 못 가른 구조물 몫은 계수가 없어 따로 냄(환산 안 함)
- ⚠ 벽 입적 ↔ 원바닥 암 부피의 관계를 정한 원문이 없음(캐면 부풀고 쌓으면 공극) —
  그 가정은 어느 쪽으로 가도 남으므로 가정은 그대로 두고 축만 맞춤.
  실무 시트가 그냥 뺀다는 사실까지 값과 함께 보냄
- tmp/tests 3건 추가

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 07:20:52 +09:00
co-authored by Claude Opus 5
parent 49fb30f61f
commit bafa652562
+38 -1
View File
@@ -41,6 +41,23 @@ VOLUME_BASIS_KEY = "volume_basis"
VOLUME_BASIS_NATURAL = "natural"
SPOIL_COMPONENT = "잔토처리"
COLLECTED_STONE_COMPONENT = "채집석"
#: 채집석 공제를 **갈래별로** 낸다 (2026-09-09 세 창 확정 — 축을 맞추기로).
#: ⚠ 이 값은 **벽 입적(제자리 완성 부피)**이다 — 자연·다짐 어느 축으로도 환산한 적 없다.
#: 굳이 가르면 자연 축 쪽이라 받는 쪽이 **×C 로 다짐 축에 맞춰** 뺀다.
#: ⚠⚠ **벽 입적과 원바닥 암 부피의 관계를 정한 원문이 없다** — 캐면 부풀고(L) 벽에 쌓으면
#: 공극이 생기는데 그 값이 품셈·교본·지식DB 어디에도 없다. **실무 시트도 그냥 뺀다**
#: (울진 「채집석 −274.66」이 사토에서 바로 빠진다). 그 가정은 어느 쪽으로 가도 남으므로
#: **가정은 그대로 두고 축만 맞춘다** — 축이 다른 값을 그냥 빼는 것은 우리가 만드는 어긋남이다.
#: ⚠ 갈래를 모르는 구조물 몫은 **환산하지 않는다**(계수가 없다) — 따로 낸다.
COLLECTED_STONE_BY_GROUND_KEY = "collected_stone_by_ground_m3"
COLLECTED_STONE_UNKNOWN_KEY = "collected_stone_ground_unknown_m3"
COLLECTED_STONE_BASIS_NOTE = (
"벽 입적(제자리 부피) — 자연 축으로 보고 받는 쪽이 ×C 로 다짐 축에 맞춰 뺌."
" ⚠ 벽 입적과 원바닥 암 부피의 관계를 정한 원문이 없음(캐면 부풀고 쌓으면 공극이 생기나"
" 그 값이 어디에도 없음). 실무 시트는 그냥 뺌 — 우리는 축만 맞춤"
)
def _center(structure: dict[str, Any]) -> float | None:
@@ -59,18 +76,33 @@ def haul_inputs(unit_quantity_table: dict[str, Any] | None) -> dict[str, Any]:
if not unit_quantity_table:
return {
COLLECTED_STONE_KEY: None,
COLLECTED_STONE_BY_GROUND_KEY: {},
COLLECTED_STONE_UNKNOWN_KEY: None,
STRUCTURE_SPOIL_KEY: None,
STRUCTURE_SPOIL_POINTS_KEY: [],
VOLUME_BASIS_KEY: VOLUME_BASIS_NATURAL,
}
total = 0.0
points: list[dict[str, Any]] = []
stone_by_ground: dict[str, float] = {}
stone_unknown = 0.0
for structure in unit_quantity_table.get("structures") or []:
amount = 0.0
stone = 0.0
for component in structure.get("components") or []:
if str(component.get("name") or "") != SPOIL_COMPONENT:
name = str(component.get("name") or "")
if name == COLLECTED_STONE_COMPONENT:
stone += float(component.get("amount") or 0.0)
if name != SPOIL_COMPONENT:
continue
amount += float(component.get("amount") or 0.0)
# 채집석 — 그 구조물의 지반 갈래로 담는다. 갈래를 못 가른 구조물 몫은 따로 둔다.
if stone > 0:
ground = str(structure.get("ground_type") or "")
if ground:
stone_by_ground[ground] = stone_by_ground.get(ground, 0.0) + stone
else:
stone_unknown += stone
if amount <= 0:
continue
total += amount
@@ -92,6 +124,11 @@ def haul_inputs(unit_quantity_table: dict[str, Any] | None) -> dict[str, Any]:
collected = unit_quantity_table.get(COLLECTED_STONE_KEY)
return {
COLLECTED_STONE_KEY: collected,
COLLECTED_STONE_BY_GROUND_KEY: {
key: round(value, 3) for key, value in sorted(stone_by_ground.items())
},
COLLECTED_STONE_UNKNOWN_KEY: round(stone_unknown, 3) if stone_unknown else 0.0,
"collected_stone_basis": COLLECTED_STONE_BASIS_NOTE,
STRUCTURE_SPOIL_KEY: round(total, 3) if points or total else None,
STRUCTURE_SPOIL_POINTS_KEY: sorted(points, key=lambda row: row["chainage_m"]),
# 총량에도 상태를 적는다 — 측점값을 안 쓰는 쪽도 상태를 알아야 한다.