Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
167 lines
8.9 KiB
Python
167 lines
8.9 KiB
Python
"""구조물 원단위 — **성분 계약과 갈 곳**(이중계상 경계) · 2026-09-14 `UnitQuantity` 에서 가름.
|
|
|
|
⚠⚠ 이 파일의 `DESTINATION`·`MIX_COMPONENTS` 가 이중계상 경계다 — 규칙 셋은 `UnitQuantity` 머리글.
|
|
㉢ 배합을 분해하지 않음(`MIX_COMPONENTS`) · ㉠ 할증 안 붙임 ·
|
|
터파기·되메우기·잔토는 토공으로(`DESTINATION`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from B08_Quantity.B08_Quantity_Engine_ObservedUnit import BASIS_DERIVED
|
|
|
|
#: 기초잡석 줄 이름 — 갈 곳 표(`DESTINATION`)가 이 이름을 키로 씀. 두께 상수는 `_StoneSpec`.
|
|
RUBBLE_BASE_NAME = "기초잡석"
|
|
|
|
|
|
DESTINATION = {
|
|
"터파기": "earthwork",
|
|
"되메우기": "earthwork",
|
|
"잔토처리": "earthwork",
|
|
"돌쌓기": "unit_price",
|
|
"돌붙임": "unit_price",
|
|
"깬돌": "material",
|
|
"고임돌": "material",
|
|
"막자갈": "material",
|
|
# ⭐ 2026-09-09 사용자 확정 3차 ⑥ — **콘크리트는 자재 축에 세운다**(사용자 명시).
|
|
# ⚠ 타설 줄(품셈 12-1)은 **품만** 주고 재료를 안 준다(서브 일위대가도 재료 0원).
|
|
# 그래서 자재로 안 보내면 **재료비가 통째로 빠진다** — B09 매김에서 드러난 자리다.
|
|
# ⚠ 배합을 분해하지 않는 규칙(㉢)은 그대로다 — 「콘크리트 ㎥」에서 멈춘다.
|
|
"콘크리트": "material",
|
|
"채움콘크리트": "material",
|
|
"버림콘크리트": "material",
|
|
"모르터": "unit_price",
|
|
"거푸집": "unit_price",
|
|
"물구멍관": "material",
|
|
# ⚠ 자재도 토공도 아닌 자리 — **유토곡선이 사토에서 뺄 밑수**다. 자재총괄은
|
|
# `material` 만 모으므로 여기 섞이지 않는다.
|
|
"채집석": "haul_deduction", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약)
|
|
# ⚠ **보여 주기만 하는 줄** — 자재도 토공도 일위대가도 아니다. 정본 계산표 좌측 열에
|
|
# 「입적」으로 실려 있어 그 이름 그대로 낸다(2026-09-08 확정 ⑦). 값은 이미 식 안에
|
|
# 있던 것을 줄로 꺼낸 것뿐이고, `material`·`earthwork` 어디에도 안 섞인다.
|
|
# ⚠ 「석적」은 정본에 없고 소광리 시트에만 있으나 **확정 5차로 세운다** — 같은
|
|
# 보여 주기 줄이고, 「소광리에만 있는 줄」임을 사유에 적는다.
|
|
"입적": "reference",
|
|
"석적": "reference",
|
|
# 돌 줄은 **이름 「돌」 + 규격(`spec`)에 종류**다(명세 13장 Ⓒ) — 정본 계산표 줄 이름 그대로
|
|
# (「돌 ℓ3=45cm」). 자재총괄이 (이름·규격·단위)로 묶으므로 종류 이름을 따로 두지 않는다.
|
|
"돌": "material",
|
|
# 기초잡석(품셈 12-25) — 운반·부설·다짐 품이 붙는 **공종**이라 일위대가로 간다.
|
|
# 자재총괄로 보내면 같은 잡석이 재료로 한 번 더 선다.
|
|
RUBBLE_BASE_NAME: "unit_price",
|
|
# ⭐ 2026-09-14 — 갈 곳 기본값을 걷자 드러난 넷(L형수로·개거·떼흙막이 정본 줄).
|
|
# 관측 원단위(`structure_unit_observed`)가 같은 이름에 둔 갈 곳과 맞춤(두 벌이 안 갈리게).
|
|
# ⚠ 「이형철근 D13」 은 이름에 규격이 섞인 글자 그대로 — 인계 대응표 `from_components` 가
|
|
# 그 글자로 조각을 찾아 지금 가르면 옹벽 철근 조각이 끊김(이름 가르기는 대응표와 함께 따로).
|
|
"이형철근 D13": "material",
|
|
"유로폼": "unit_price", # 거푸집 계열(품셈 12-38) — 설치·해체 품
|
|
"면목": "material",
|
|
"떼": "material", # 사면 떼(자재총괄 extra)와 같은 자리 · 할증 10%(1-3-1)
|
|
}
|
|
|
|
#: ⛔ **갈 곳 기본값 금지**(명세 13장 「빠진 것이 조용해짐」) — 표에 없는 이름은 막고 사유를 냄.
|
|
#: 종전엔 `DESTINATION.get(name, "quantity")` 로 떨어져 **아무 축에도 안 가고** 자재총괄
|
|
#: 건너뛴 수로만 셈(2026-09-14 L형수로 이형철근·떼흙막이 떼가 통째로 빠져 있던 자리).
|
|
DESTINATION_MISSING = (
|
|
"「{name}」 갈 곳이 표(`DESTINATION`)에 없어 물량을 어느 축에도 안 보냄"
|
|
" — 표에 올릴 것(명세 13장 갈 곳 기본값 금지)"
|
|
)
|
|
|
|
|
|
def routed(name: str, notes: list[str]) -> str | None:
|
|
"""성분 이름의 갈 곳 — 표에 없으면 `None` 이고 사유 한 줄을 `notes` 에 더함(이름마다 한 번)."""
|
|
destination = DESTINATION.get(name)
|
|
if destination is None:
|
|
text = DESTINATION_MISSING.format(name=name)
|
|
if text not in notes:
|
|
notes.append(text)
|
|
return destination
|
|
|
|
|
|
# ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다.
|
|
# ⚠ **정확히 같은 이름**으로만 본다. 부분문자열로 재면 `막자갈`(뒤채움 재료)이 배합 `자갈` 로
|
|
# 오탐된다 — 개발 중 실제로 걸렸던 자리다.
|
|
MIX_COMPONENTS = frozenset(
|
|
{"시멘트", "모래", "자갈", "친모래", "친자갈", "잔골재", "굵은골재", "부순돌"}
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class Component:
|
|
"""전개 결과 한 성분. `basis` 는 어떤 식으로 나왔는지 사람이 읽는 근거다."""
|
|
|
|
name: str
|
|
unit: str
|
|
amount: float
|
|
destination: str
|
|
basis: str = ""
|
|
# ⚠ 값이 어디서 왔나 — `derived`(치수에서 식으로) / `observed`(실무 관측 원단위표).
|
|
# 두 근거가 한 표에 섞이므로 줄마다 단다. 안 적으면 나중에 못 되짚는다.
|
|
basis_kind: str = BASIS_DERIVED
|
|
source: str = ""
|
|
#: 규격 — 같은 이름이라도 단가가 갈리는 값(채움 콘크리트 강도 등). 받는 쪽이 읽는다.
|
|
spec: str = ""
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class StructureQuantity:
|
|
"""구조물 하나의 원단위 전개 결과."""
|
|
|
|
structure_id: str | None
|
|
type_id: str
|
|
name: str
|
|
length_m: float = 0.0
|
|
height_m: float = 0.0
|
|
# 측점 — 내역 줄에 「어디부터 어디까지」를 적으려면 여기서 따라가야 한다(B09 인계).
|
|
start_m: float | None = None
|
|
end_m: float | None = None
|
|
# 저장된 제원 — **형식(반중력식…)처럼 뒤 단계가 봐야 하는 값**이 여기 있다.
|
|
# 치수를 다시 쓰라는 뜻이 아니라 **읽으라고** 실어 나른다(치수 정본은 여전히 하나).
|
|
options: dict[str, Any] = field(default_factory=dict)
|
|
components: list[Component] = field(default_factory=list)
|
|
notes: list[str] = field(default_factory=list)
|
|
#: 내역 줄이 설 **단위와 그 단위로 센 수량**. 관측 원단위가 「개소당」·「㎡당」인
|
|
#: 종류는 연장(m)으로 세면 축이 어긋난다(2026-09-08 ㉕ 실증).
|
|
#: 비어 있으면 종전대로 「m · 연장」으로 선다.
|
|
billing_unit: str = ""
|
|
billing_quantity: float = 0.0
|
|
#: 구조물이 놓인 자리의 지반 갈래(`soil`·`ripping_rock`·`blasting_rock`)와 그 판정 근거.
|
|
#: 품셈 9-13 구조물터파기의 **토질 축**이 이 값으로 갈린다. 못 가르면 `None` 이다.
|
|
ground_type: str | None = None
|
|
ground_type_basis: str = ""
|
|
#: 파는 깊이(m) — 단면으로 터파기를 세운 종류만(옹벽 기초분). 비면 인계가 직고 + 기초 깊이로 봄.
|
|
trench_depth_m: float | None = None
|
|
#: 양식으로 성분을 세웠으면 그 양식 이름(PLAN 3장 ④-2). 비면 지금 전개 값.
|
|
library_item: str = ""
|
|
#: 치수가 정본에 없어 **등록부 기본값으로 선** 까닭(2026-09-14 브레인 판정) — 차 있으면 줄·표·그림은
|
|
#: 서되 **금액·자재·토공·운반 합에는 안 듦**(`is_unconfirmed`). 설계자가 치수를 적으면 비고 금액이 섬.
|
|
unconfirmed: str = ""
|
|
|
|
|
|
def is_unconfirmed(structure: dict[str, Any]) -> bool:
|
|
"""금액 합에서 뺄 구조물인가 — 합을 내는 자리마다 이 한 벌로 가림."""
|
|
return bool(structure.get("unconfirmed"))
|
|
|
|
|
|
#: 높이가 물량 밑수인 벽 — 높이를 안 적고 놓아도 줄은 서되 금액 밖(2026-09-14 브레인 판정 ①
|
|
#: 「설계자가 자리부터 잡고 치수를 뒤에 넣는 길을 막지 말 것」). 흙막이(떼 개소당)는 높이를 안 씀.
|
|
HEIGHT_DRIVEN_TYPES = frozenset({"retaining_wall", "masonry_wet", "masonry_dry", "boulder_masonry"})
|
|
UNCONFIRMED_NO_HEIGHT = "높이를 안 적고 놓았음 — 높이를 적으면 금액이 섬"
|
|
|
|
|
|
def missing_height_reason(structure: dict[str, Any]) -> str:
|
|
"""높이 없이 놓인 벽이면 미확정 까닭 한 줄, 아니면 빈 글."""
|
|
if structure.get("type_id") not in HEIGHT_DRIVEN_TYPES:
|
|
return ""
|
|
try:
|
|
height = float((structure.get("options") or {}).get("height_m") or 0.0)
|
|
except (TypeError, ValueError):
|
|
height = 0.0
|
|
return "" if height > 0 else UNCONFIRMED_NO_HEIGHT
|
|
|
|
|
|
def _num(value: Any, fallback: float = 0.0) -> float:
|
|
return float(value) if isinstance(value, (int, float)) else fallback
|