From 87c710a27bb891beae3c8fe20d2bc96c2d271b4a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 9 Sep 2026 22:57:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(B09):=20=EB=85=B8=EC=B2=B4=EB=8B=A4?= =?UTF-8?q?=EC=A7=90(9-16-2)=20=EB=8B=A8=EA=B0=80=EA=B0=80=20=EC=84=9C?= =?UTF-8?q?=EA=B2=8C=20=ED=95=A8=20=E2=80=94=20=EB=8B=A4=EC=A7=90=20?= =?UTF-8?q?=EA=B3=B5=EC=8B=9D=EC=9D=80=20=EA=B5=B4=EC=B0=A9=C2=B7=EC=9A=B4?= =?UTF-8?q?=EB=B0=98=EA=B3=BC=20=EC=8B=9D=EC=9D=B4=20=EB=8B=A4=EB=A6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q = 1000 × V × W × E × D × f / N 로 서는 첫 공종. 계수 여섯은 표에 다 있고 기종만 [주]에 있어 마스터가 못 싣던 자리. - 롤러만 듦 — [주]③ 의 「굴착기와 진동롤러 조합」은 포설(9-16-1)과 다짐을 함께 쓰라는 뜻으로 읽음. 이 줄에 굴착기를 또 넣으면 포설에서 센 굴착기를 두 번 셈. - 계수가 하나라도 없으면 안 섬 — 채워 넣지 않음. - 조건([주]⑤ 대규모 성토지 층다짐)과 기종 근거를 줄에 남김. - ⚠ 인계에 그 줄이 없어 금액은 아직 안 섬 — 층다짐을 셀지는 설계 판단(사용자 확정 대기). 넣으면 13,150.2㎥ × 527.40 = 6,935,414원. Co-Authored-By: Claude Opus 5 (1M context) --- ...timation_MachineProductivity_Compaction.py | 125 ++++++++++++++++++ B09_Estimation/B09_Estimation_UnitPrice.py | 4 +- resources/tester/test_b09_compaction.py | 82 ++++++++++++ 3 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py create mode 100644 resources/tester/test_b09_compaction.py diff --git a/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py b/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py new file mode 100644 index 00000000..29773cd0 --- /dev/null +++ b/B09_Estimation/B09_Estimation_MachineProductivity_Compaction.py @@ -0,0 +1,125 @@ +"""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 [] diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 7d558618..233974fc 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -612,8 +612,10 @@ def build_unit_prices( build.component_gaps = dict(axis.partial_items) # 「작업량을 직접 준」 기계(깨기 대형브레이커)도 사용료 층을 세운다 — 안 세우면 # 그 줄이 붙을 데가 없어 암·발파암 갈래의 깨기 몫이 통째로 빠진다. + from B09_Estimation.B09_Estimation_MachineProductivity_Compaction import compaction_rows + capacity_rows = { - str(node.get("work_item_code", "")): direct_capacity_rows(node) + str(node.get("work_item_code", "")): direct_capacity_rows(node) + compaction_rows(node) for node in master.get("work_items", []) } capacity_rows = {code: rows for code, rows in capacity_rows.items() if rows} diff --git a/resources/tester/test_b09_compaction.py b/resources/tester/test_b09_compaction.py new file mode 100644 index 00000000..4c4b7397 --- /dev/null +++ b/resources/tester/test_b09_compaction.py @@ -0,0 +1,82 @@ +"""노체다짐(9-16-2) — 다짐 공식으로 서는 공종 (2026-09-09 밤). + +굴착·운반과 **식이 다르다** — 사이클(㎝)이 아니라 롤러가 지나간 넓이로 센다. + + Q = 1000 × V × W × E × D × f / N (㎥/시간) + +⚠ 겨누는 것 넷 + ① 계수 여섯을 표에서 그대로 읽는다 — 하나라도 없으면 **채워 넣지 않고 안 선다** + ② Q 가 원문 값과 같다 (1000×4×1.9×0.6×0.3×1.0÷6 = 228 ㎥/hr) + ③ **롤러만** 든다 — [주]③ 의 「굴착기와 롤러 조합」은 포설(9-16-1)과 다짐을 함께 쓰라는 + 뜻이라, 이 줄에 굴착기를 또 넣으면 **포설에서 센 굴착기를 두 번 센다** + ④ 조건([주]⑤ 대규모 성토지 층다짐)과 기종 근거가 줄에 남는다 +""" + +from __future__ import annotations + +import sys +from decimal import Decimal +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B09_Estimation.B09_Estimation_MachineProductivity_Compaction import ( # noqa: E402 + capacity_per_hour, + compaction_factors, + compaction_rows, +) +from B09_Estimation.B09_Estimation_UnitPrice import ( # noqa: E402 + cached_build, + load_work_item_master, +) + + +def _node(code: str) -> dict: + master = load_work_item_master() + return next(w for w in master["work_items"] if w["work_item_code"] == code) + + +def test_계수_여섯을_표에서_그대로_읽는다() -> None: + factors = compaction_factors(_node("FP-09-16-02")["tables"][0]) + assert factors == { + "V": Decimal("4"), + "W": Decimal("1.9"), + "E": Decimal("0.6"), + "D": Decimal("0.3"), + "f": Decimal("1.0"), + "N": Decimal("6"), + } + + +def test_하나라도_없으면_안_선다() -> None: + """① 채워 넣으면 조용히 틀린 작업량이 선다.""" + assert compaction_factors({"raw_row": [["V(다짐속도,km/hr)", "4", ""]]}) is None + + +def test_작업량이_원문_식과_같다() -> None: + factors = compaction_factors(_node("FP-09-16-02")["tables"][0]) + assert factors is not None + assert capacity_per_hour(factors) == Decimal(228) + + +def test_롤러만_든다() -> None: + """③ 굴착기를 또 넣으면 포설에서 센 것을 두 번 센다.""" + rows = compaction_rows(_node("FP-09-16-02")) + assert [row["machine_code"] for row in rows] == ["1306-0100"] # 진동롤러(자주식) 10ton + + +def test_근거와_조건이_줄에_남는다() -> None: + rows = compaction_rows(_node("FP-09-16-02")) + text = rows[0]["source_text"] + assert "9-16-2 [주]①" in text and "층다짐" in text and "두 번" in text + + +def test_단가가_선다() -> None: + build = cached_build() + title = build.book.titles.get("B-FP-09-16-02") + assert title is not None and title.unit == "㎥" + money = build.book.resolve("B-FP-09-16-02") + assert money.total > 0 + # 포설(굴착기)보다 싸야 한다 — 롤러 한 대가 228㎥/hr 로 도는 줄이다. + assert money.total < build.book.resolve("B-FP-09-16-01").total