"""B09 원가계산 — **표토제거 답외구간** 9-15-2 (2026-09-14 브레인 ㉰ 첫째). B08 준비공 「표토제거」 줄이 부르는 코드인데 표가 계수표(T·L·E·q0·e·f·V1·V2·t)라 일위대가가 안 섰음. q = q0 × e · ㎝ = L/V1 + L/V2 + t · Q1 = 60 × q × f × E / ㎝ (㎥/hr) · Q = Q1 / T (㎡/hr) [주]① 무한궤도 불도저(19ton) · ③ 건설품셈 8-2-1 불도저 참조 → 불도저 식(`dozer_hourly_output`) 그대로 + T 로 나눔 기종은 표의 q0·V1·V2(1단)로 8-2-1 표에서 되짚음(`resolve_dozer`) — [주]① 19ton 과 맞는지 시험이 봄 ⚠ 실무 영월 호표 Q=576.95 는 E 자리에 e(0.96)를 넣은 값 — 원문이 또렷해 원문 E 로 셈(까닭은 `KnownGaps`). """ from __future__ import annotations import re from decimal import Decimal from typing import Any CODE = "FP-09-15-02" TABLE = "F0282" _RE_SYMBOL = re.compile(r"^([A-Za-z]\d?)\s*\(") _RE_GEAR = re.compile(r"(\d+)\s*단") def _factors(node: dict[str, Any]): """(불도저 계수, T) — 칸이 모자라거나 기종이 안 좁혀지면 까닭 글.""" from B09_Estimation.B09_Estimation_MachineProductivity import parse_measure from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import DozerFactors, resolve_dozer table = next((t for t in node.get("tables") or [] if t.get("pum_table_id") == TABLE), {}) values: dict[str, Decimal] = {} gear = 1 for row in table.get("raw_row") or []: found = _RE_SYMBOL.match(str(row[0]).strip()) if row else None value = parse_measure(str(row[1])) if found and len(row) > 1 else None if value is not None: values[found.group(1)] = value shift = _RE_GEAR.search(str(row[1])) gear = int(shift.group(1)) if shift else gear missing = [k for k in ("T", "L", "E", "q0", "e", "f", "V1", "V2") if k not in values] if missing: return f"9-15-2 표 칸 없음: {', '.join(missing)}" machine = resolve_dozer(values["q0"], values["V1"], values["V2"], gear) if machine is None: return f"삽날 {values['q0']}㎥ · {values['V1']}/{values['V2']}m/분({gear}단) 으로 불도저가 안 좁혀짐" factors = DozerFactors( work_item_code=CODE, blade_capacity_m3=values["q0"], distance_factor=values["e"], volume_factor=values["f"], efficiency=values["E"], haul_distance_m=values["L"], forward_speed_m_min=values["V1"], reverse_speed_m_min=values["V2"], machine_code=machine[0], machine_name=machine[1], ) return factors, values["T"] def topsoil_output(node: dict[str, Any] | None = None) -> tuple[Decimal, Decimal]: """(Q1 ㎥/hr, Q ㎡/hr) — 둘 다 소수 2자리로 확정한 뒤 씀(명세 7장).""" from B09_Estimation.B09_Estimation_MachineProductivity import fix2 from B09_Estimation.B09_Estimation_MachineProductivity_Dozer import dozer_hourly_output 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) found = _factors(node) if isinstance(found, str): raise ValueError(found) factors, thickness = found q1 = dozer_hourly_output(factors) return q1, fix2(q1 / thickness) def attach_topsoil_removal(build: Any, nodes_by_code: dict[str, dict[str, Any]]) -> None: """`B-FP-09-15-02` — 불도저 1/Q hr/㎡ 한 줄(D). 기계 층이 없거나 표가 달라지면 까닭만.""" from B09_Estimation.B09_Estimation_PriceBook import PriceKind, PriceTitle node = nodes_by_code.get(CODE) title_code = f"B-{CODE}" if node is None or title_code in build.book.titles: return found = _factors(node) if isinstance(found, str): build.component_gaps[CODE] = found return factors, thickness = found hourly = f"X-{factors.machine_code}" if hourly not in build.book.titles: build.component_gaps[CODE] = f"{factors.machine_name} 시간당 사용료가 안 섬" return q1, q = topsoil_output(node) build.book.add_title( PriceTitle( code=title_code, kind=PriceKind.UNIT_PRICE, name=str(node.get("name") or CODE), spec="표토제거", unit="㎡", ) ) build.book.add_output_detail( title_code, hourly, Decimal(1) / q, f"{factors.formula_text} → Q = Q1 {q1} ÷ T {thickness}m = {q} ㎡/hr" " (산림품셈 9-15-2 [주]②③ · 건설 8-2-1)", output=q, ) if CODE in build.skipped: build.skipped.remove(CODE)