- 브레인 판정: 값 없음을 감추면 조용히 빠지거나 조용히 부풂 — 없으면 없다고 보이기 - 관 지점 벽 치수를 등록부 기본값으로 채운 줄 = 미확정: 원단위 합·자재·터파기·되메우기·기초잡석·타설·잔토·채집석에서 뺌 - 인계 막힘 갈래 넷째 값 unconfirmed(계약 칸은 안 늘림) · B09 는 내역 제자리에 빈 금액 + 빨간 테두리 · 머리 뱃지 - 구조물도 그림에 표와 같은 기본값 사유 줄 · 「횡단도와 같은 값」 문구는 틀려 고침(B06 관 벽 높이는 관경 기준) - 936be972 내역 184,291,506 → 124,424,547 · 미확정 18건 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
149 lines
8.5 KiB
Python
149 lines
8.5 KiB
Python
"""유토곡선이 받아야 할 **구조물 몫** — 채집석 공제와 구조물 잔토 (2026-09-08).
|
||
|
||
⚠⚠ **B08 은 내기만 하고 빼거나 더하지 않는다.** 두 값 다 **양수 ㎥** 로 주고,
|
||
공제(빼기)와 사토 가산(더하기)은 **유토곡선(B06)에서 한 번씩만** 일어난다.
|
||
부호를 넘기면 받는 쪽에서 두 번 뒤집힌다 — 실무 시트가 `−274.66` 으로 적혀 있어
|
||
실제로 겪은 자리다.
|
||
|
||
채집석 공제는 사토에서 한 번만 뺀다.
|
||
구조물 잔토는 사토에 한 번만 더한다.
|
||
|
||
⚠ **측점별로도 낸다.** 총량 하나만 주면 받는 쪽이 잔량 크기에 비례해 나눌 수밖에 없고,
|
||
그러면 **운반거리가 틀어진다**(한 곳에 몰리면 거리가 어긋남 — 공제 때 이미 짚은 자리).
|
||
구조물은 구간(start~end)이라 **그 가운데 측점**을 자리로 본다.
|
||
|
||
⚠ **지반 갈래도 함께 낸다** — 새 판정이 아니라 **구조물터파기가 이미 쓰는 그 값**이다
|
||
(`design.ground_type` 에서 뽑아 「암절취 · 심도 2~3m」로 세운 그것). 그 터파기에서 나온
|
||
흙이 곧 이 잔토이므로 **같은 판정을 두 곳이 그대로 쓴다** — 두 벌로 짜면 갈린다.
|
||
⚠ **섞여서 못 고른 구조물은 「모름」(`None`)으로 보낸다** — 터파기에서 다수결로 안 고른
|
||
그 규칙 그대로다. 받는 쪽이 그 몫을 「모르는 몫」으로 드러내면 된다.
|
||
⚠ **한계도 그대로 넘어간다** — 「터파기 깊이가 암반 경계선보다 깊은지」는 보지 않는다.
|
||
터파기 토질에 이미 있는 한계이고 여기서 새로 생기는 것이 아니다.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Any
|
||
|
||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import COLLECTED_STONE_KEY, GROUND_TYPE_LABEL
|
||
|
||
#: 유토곡선이 읽는 칸 이름 — 양쪽이 같은 낱말을 써야 인계에서 어긋나지 않는다.
|
||
STRUCTURE_SPOIL_KEY = "structure_spoil_m3"
|
||
STRUCTURE_SPOIL_POINTS_KEY = "structure_spoil_points"
|
||
#: ⚠⚠ **이 값은 자연상태(본바닥)다.** 유토곡선 잔량은 **다짐상태**라 그냥 더하면 상태가
|
||
#: 섞인다(2026-09-09 랩탑 메인 지적). 받는 쪽이 `× C` 로 맞춰 담을 수 있게 **상태를 값으로
|
||
#: 적어 보낸다** — 말로만 두면 다음 사람이 못 본다.
|
||
#: 까닭: 터파기·되메우기가 **제자리 기하 부피**(H × 폭 × 연장)이고 잔토는 그 차이다.
|
||
#: ⚠ 엄밀히는 되메움에 **쓰이는 흙**은 다짐부피 ÷ C 라 조금 더 든다. 실무 정본 시트도
|
||
#: 그냥 빼므로(터파기 1.55 − 되메우기 0.30 = 잔토 1.25) 여기서도 그대로 두되,
|
||
#: **그 사실을 적어 둔다** — 값을 임의로 보정하지 않는다.
|
||
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"
|
||
#: ⭐ **축 맞춤의 검산** — 더하는 쪽(구조물 잔토)과 빼는 쪽(채집석 공제)에 **같은 계수**가
|
||
#: 걸리므로 사토 총량은 안 흔들린다. 실측: 잔토 126.625 ×1.15 = 145.62 담기고 공제
|
||
#: 64.75 ×1.15 = 74.46 빠져 사토(다짐) 71.16 → ÷1.15 = **61.887**, 축 맞추기 전 61.88 과 같다.
|
||
#: ⇒ **값을 안 흔들면서 축만 바로잡은 것**이다. 갈래가 갈리거나 모르는 몫이 섞이는
|
||
#: 자리에서만 값이 달라진다.
|
||
COLLECTED_STONE_AXIS_CHECK = (
|
||
"축 맞춤 검산 — 더하는 쪽과 빼는 쪽에 같은 계수가 걸려 사토 총량은 그대로"
|
||
"(잔토 ×C 담김 · 공제 ×C 빠짐 ⇒ ÷C 하면 같은 값)"
|
||
)
|
||
|
||
COLLECTED_STONE_BASIS_NOTE = (
|
||
"벽 입적(제자리 부피) — 자연 축으로 보고 받는 쪽이 ×C 로 다짐 축에 맞춰 뺌."
|
||
" ⚠ 벽 입적과 원바닥 암 부피의 관계를 정한 원문이 없음(캐면 부풀고 쌓으면 공극이 생기나"
|
||
" 그 값이 어디에도 없음). 실무 시트는 그냥 뺌 — 우리는 축만 맞춤"
|
||
)
|
||
|
||
|
||
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,
|
||
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 []:
|
||
if structure.get("unconfirmed"):
|
||
continue # 기본값으로 선 구조물 — 잔토·채집석이 사토 운반 금액으로 번지지 않게
|
||
amount = 0.0
|
||
stone = 0.0
|
||
for component in structure.get("components") or []:
|
||
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
|
||
chainage = _center(structure)
|
||
if chainage is None:
|
||
continue
|
||
# 지반 갈래 — 구조물터파기가 쓰는 그 판정을 그대로 싣는다(두 벌로 안 짠다).
|
||
ground = structure.get("ground_type") or None
|
||
points.append(
|
||
{
|
||
"chainage_m": chainage,
|
||
"spoil_m3": round(amount, 3),
|
||
VOLUME_BASIS_KEY: VOLUME_BASIS_NATURAL,
|
||
"ground_type": ground,
|
||
"ground_label": GROUND_TYPE_LABEL.get(str(ground)) if ground else None,
|
||
"ground_basis": str(structure.get("ground_type_basis") or ""),
|
||
}
|
||
)
|
||
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": f"{COLLECTED_STONE_BASIS_NOTE} · {COLLECTED_STONE_AXIS_CHECK}",
|
||
STRUCTURE_SPOIL_KEY: round(total, 3) if points or total else None,
|
||
STRUCTURE_SPOIL_POINTS_KEY: sorted(points, key=lambda row: row["chainage_m"]),
|
||
# 총량에도 상태를 적는다 — 측점값을 안 쓰는 쪽도 상태를 알아야 한다.
|
||
VOLUME_BASIS_KEY: VOLUME_BASIS_NATURAL,
|
||
}
|