"""구조물 원단위 — **성분 계약과 갈 곳**(이중계상 경계) · 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", } # ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). 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")) def _num(value: Any, fallback: float = 0.0) -> float: return float(value) if isinstance(value, (int, float)) else fallback