Merge remote-tracking branch 'origin/main_desktop_1' into sub_desktop_1

This commit is contained in:
2026-09-09 22:58:28 +09:00
3 changed files with 210 additions and 1 deletions
@@ -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 []
+3 -1
View File
@@ -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}
+82
View File
@@ -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