"""B09 원가계산 — **이동식 임목 파쇄** 8-11 (2026-09-15 브레인 판정 · ㉰ 둘째). 원문 L4651 표: 이동식 임목 파쇄기 93.25KW · Q = 3.5 ㎥/hr · 비고 「잡재료 : 주연료비의 16% · 소모품비(파쇄기날) 0.00125개/hr」 · [주]① 1일 8시간 ② 1일 기계운전자 1인·보통인부 2인 ③ 장비 운반비 별도 ④ 우드그랩 별도 ⑤ 연료 「10.8 + 16.3ℓ / 2 = 13.5ℓ (디젤)」. ⚠ 건설품셈 운전경비표(8-4 L3458) 7205-0125 93.25㎾ 줄은 연료·잡품 칸이 「-」 → 운전경비 레코드가 없어 기계 층이 안 서고 일위대가도 조용히 없었음. 8-11 [주]⑤·비고가 **유일한 값**(부딪히는 원문 없음)이라 그대로 씀. 기계운전자는 기계 층 조종원(건설기계운전사 · 8-4 잠정 규칙과 같은 직종)으로 한 번만 셈. """ from __future__ import annotations import re from decimal import Decimal from typing import Any CODE = "FP-08-11" MACHINE = "7205-0125" BLADE = "AR-M-249d0a01" TABLE = "F0237" #: [주]⑤ — 표 칸이 아니라 [주] 에만 있어 마스터 원문 줄에 안 실림. 원문 그대로 한 곳. FUEL_LITERS_PER_HOUR = Decimal("13.5") #: [주]① 1일 8시간 · [주]② 1일 보통인부 2인(기계운전자 1인은 기계 층 조종원). HOURS_PER_DAY = Decimal(8) LABORERS_PER_DAY = Decimal(2) LABORER_CODE = "1002" # 보통인부 BLADE_MISSING = "파쇄기날 — 규격·단가 없음 · 「자재 단가」 탭에서 넣으면 0.00125개/hr ÷ Q 로 붙음" _RE_Q = re.compile(r"Q\s*=\s*(\d+(?:\.\d+)?)") _RE_MISC = re.compile(r"주연료비의\s*(\d+(?:\.\d+)?)\s*%") _RE_BLADE = re.compile(r"(\d+(?:\.\d+)?)\s*개\s*/\s*hr") def _table_values(node: dict[str, Any]) -> tuple[Decimal, Decimal, Decimal] | None: """(Q ㎥/hr, 잡재료 %, 파쇄기날 개/hr) — 표 한 줄에서. 칸이 달라지면 `None`.""" table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == TABLE), {}) text = " ".join(str(c) for row in table.get("raw_row") or [] for c in row) found = [pattern.search(text) for pattern in (_RE_Q, _RE_MISC, _RE_BLADE)] if not all(found): return None return tuple(Decimal(m.group(1)) for m in found) # type: ignore[return-value] def operating_record(node: dict[str, Any] | None = None): """7205-0125 운전경비 — 8-11 [주]⑤ 연료 · 비고 잡재료 · [주]② 기계운전자 1인.""" from B09_Estimation.B09_Estimation_MachineOperating import ( _CATALOG_SUBPATH, OperatingRecord, _operator_code, _read_json, ) from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master if node is None: node = next(n for n in load_work_item_master()["work_items"] if n["work_item_code"] == CODE) values = _table_values(node) if values is None: return None aliases = _read_json(*_CATALOG_SUBPATH, "labor_const_2026-01-01.json")["variables"]["aliases"] return OperatingRecord( machine_code=MACHINE, machine_name="이동식 임목파쇄기", specification="93.25", fuel_liters_per_hour=FUEL_LITERS_PER_HOUR, fuel_kind="경유", misc_material_percent=values[1], operator_person_days=Decimal(1), operator_occupation_code=_operator_code("이동식 임목파쇄기", aliases), ) def attach_wood_chipping(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None: """`B-FP-08-11` ㎥당 — 파쇄기 1/Q(D) · 보통인부 2인 ÷ 8시간 ÷ Q · 파쇄기날(단가가 들면).""" from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle node = nodes_by_code.get(CODE) values = _table_values(node or {}) book = build.book uses = build.material_uses.setdefault(BLADE, []) if CODE not in uses: uses.append(CODE) if node is None or values is None or f"X-{MACHINE}" not in book.titles: build.component_gaps[CODE] = "8-11 표 칸이 달라졌거나 파쇄기 기계 층이 안 섬" return q, _misc, blade_per_hour = values title = f"B-{CODE}" book.add_title( PriceTitle(code=title, kind=PriceKind.UNIT_PRICE, name=str(node.get("name")), unit="㎥") ) book.add_output_detail( title, f"X-{MACHINE}", Decimal(1) / q, f"Q = {q} ㎥/hr (산림품셈 8-11)", output=q ) book.add_detail( PriceDetail( title, LABORER_CODE, LABORERS_PER_DAY / HOURS_PER_DAY / q, note=f"[주]② 1일 보통인부 {LABORERS_PER_DAY}인 ÷ [주]① {HOURS_PER_DAY}시간 ÷ Q {q}", ) ) # 표의 파쇄기 줄은 이 모듈이 씀 — 자원 축이 「기계·연료 줄이 안 붙음」 으로 올린 것을 걷음. labels = [ label for label in build.unattached.get(CODE, []) if "파쇄기날" not in label and "".join(label.split()) != "이동식임목파쇄기" ] if "파쇄기" in (build.component_gaps.get(CODE) or ""): build.component_gaps.pop(CODE) build.partial_ratio.pop(CODE, None) if BLADE in book.titles: book.add_detail( PriceDetail( title, BLADE, blade_per_hour / q, note=f"파쇄기날 {blade_per_hour}개/hr ÷ Q" ) ) else: labels.append(BLADE_MISSING) build.unattached[CODE] = labels if CODE in build.skipped: build.skipped.remove(CODE)