"""B09 — **다짐 공식**으로 서는 공종 (산림품셈 9-16-2 노체다짐). 굴착·운반과 **식이 다르다.** 굴착기는 사이클(㎝)로 세는데 다짐은 롤러가 지나간 넓이로 센다. Q = 1000 × V × W × E × D × f / N (㎥/시간) V 다짐속도(㎞/hr) · W 롤러 유효폭(m) · E 작업효율 · D 펴는 흙의 두께(m) f 토량환산계수 · N 소요다짐횟수 계수는 **표에 다 있고**(9-16-2 여섯 줄), 기종만 [주]에 있어 마스터가 못 싣는다. **⚠⚠ [주]③ 을 어떻게 읽었나 — 이중계상이 갈리는 자리** 원문 [주]③ 「임도에서는 굴착기(0.7㎥, 무한궤도)와 진동롤러(자주식 10ton)을 **조합**하는 것을 적용한다」. ⇒ **포설(9-16-1)의 굴착기와 다짐(9-16-2)의 롤러를 함께 쓰라는 뜻**으로 읽는다. 다짐 줄 안에 굴착기를 **또** 넣으면 **굴착기가 두 번 선다**(포설에서 이미 셈). 그래서 이 줄은 **롤러만** 든다. **⚠ 이 줄이 언제 서나 — [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」** 조건부 공종이라 늘 서는 것이 아니다. 지금 B08 인계에 이 줄이 **안 온다** — 실무 내역 셋(영월·울진·봉화)에 「노체포설·노체다짐」으로 가른 줄이 없어 **성토 본체 한 줄 + 성토면다짐(㎡) 따로**로 맞춰 둔 결정이다(2026-09-08 매핑 기록). ⚠ **다만 실무의 그 한 줄은 단가 안에 적사→성토→다짐 3단계를 품고 있고, 우리 성토 단가는 포설(굴착기)뿐이다** — 층다짐을 넣기로 하면 이 단가가 그날 바로 선다. """ from __future__ import annotations import re from decimal import Decimal from typing import Any _ZERO = Decimal(0) #: 계수 이름 — 표 첫 칸이 「V(다짐속도,km/hr)」처럼 기호+설명이라 **앞 글자**로 가른다. _KEYS = ("V", "W", "E", "D", "f", "N") _RE_HEAD = re.compile(r"^([VWEDfN])\s*[((]") _RE_NUMBER = re.compile(r"^-?\d+(?:\.\d+)?$") #: 다짐 공식으로 서는 공종 — 기종은 [주] 원문 그대로. #: ⚠ **롤러만** 든다(위 [주]③ 읽기). 넓히면 굴착기가 두 번 선다. COMPACTION_ITEMS: dict[str, dict[str, Any]] = { "FP-09-16-02": { "machine_code": "1306-0100", # 진동롤러(자주식) 10ton "machine_note": "산림품셈 9-16-2 [주]① 「장비는 자주식 진동롤러(10ton)를 적용한다」", "condition_note": ( "⚠ [주]⑤ 「대규모 성토지로서 층다짐이 필요한 경우 적용한다」 — 늘 서는 줄이 아님" ), "pair_note": ( "⚠ [주]③ 의 「굴착기와 진동롤러 조합」은 포설(9-16-1)과 다짐(9-16-2)을 함께 쓰라는" " 뜻으로 읽음 — 이 줄에 굴착기를 또 넣으면 포설에서 센 굴착기를 두 번 셈" ), }, } def _number(cells: list[str]) -> Decimal | None: for cell in cells: text = str(cell or "").strip() if _RE_NUMBER.match(text): return Decimal(text) return None def compaction_factors(table: dict[str, Any]) -> dict[str, Decimal] | None: """표에서 여섯 계수를 뽑는다. 하나라도 없으면 `None` — **채워 넣지 않는다.**""" found: dict[str, Decimal] = {} for row in table.get("raw_row") or []: cells = [str(c or "").strip() for c in row] if not cells: continue head = _RE_HEAD.match(cells[0].replace(" ", "")) if not head: continue value = _number(cells[1:]) if value is not None: found.setdefault(head.group(1), value) if set(found) != set(_KEYS): return None return found def capacity_per_hour(factors: dict[str, Decimal]) -> Decimal: """Q = 1000 × V × W × E × D × f / N (㎥/시간).""" return ( Decimal(1000) * factors["V"] * factors["W"] * factors["E"] * factors["D"] * factors["f"] / factors["N"] ) def compaction_rows(node: dict[str, Any]) -> list[dict[str, Any]]: """그 공종이 다짐 공식으로 서면 붙일 기계 줄. 아니면 빈 목록.""" entry = COMPACTION_ITEMS.get(str(node.get("work_item_code") or "")) if entry is None: return [] for table in node.get("tables") or []: factors = compaction_factors(table) if factors is None or factors["N"] == _ZERO: continue capacity = capacity_per_hour(factors) if capacity <= 0: continue formula = ( f"Q = 1000 × {factors['V']:g} × {factors['W']:g} × {factors['E']:g}" f" × {factors['D']:g} × {factors['f']:g} ÷ {factors['N']:g} = {capacity:g} ㎥/시간" ) return [ { "machine_code": entry["machine_code"], "capacity_per_hour": capacity, "cell": "노체다짐 Q(㎥/hr)", "ratio_pct": None, "table_id": str(table.get("pum_table_id") or ""), "row_cells": [], "source_text": ( f"{formula} · {entry['machine_note']} · {entry['pair_note']}" f" · {entry['condition_note']}" ), } ] return []