feat(b09): 12-4 합판거푸집 사용횟수 1~6회 갈래 — 기준수량(1회) × 원문 비율(재료 줄 재료 % · 인력 줄 노무비 %) · 설계로 막아 둔 표 풀림 · 사용고재 23%·동바리·소형구조물 할증은 값 안 짓고 사유 · 재료는 카탈로그 없음/규격 미정으로 못 붙은 줄(인력 품만) · 마스터 갈래 키 재생성(바뀐 공종 12-04 하나)

일위대가 새 제목 6(1회 81,321 → 4회 32,528 → 6회 26,023원/㎡) · 나머지 462 변화 0 (브레인 ㉮)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
2026-09-14 18:27:36 +09:00
co-authored by Claude Opus 5
parent 1550c7caf6
commit 72ec6c2135
6 changed files with 129 additions and 15 deletions
+9 -7
View File
@@ -54,6 +54,14 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
"ⓘ 성토면 · 기계: 원문이 굴착기 0.6㎥ 형식을 안 적어 무한궤도·타이어 두 갈래로 세움"
"(제안 무한궤도 — 영월 실무 「06M3 B/H」 · 2026-09-14 브레인 ②).",
),
# 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만.
"FP-12-04": (
"원문 [주]",
"ⓘ 「사용고재 평가기준 23%(합판과 각재의 설계단가 기준)」 은 원문이 셈을 안 줘 값으로 안 씀"
" — [주]① 2회 이상 사용 고재량은 재료비 비율에 기포함. "
"ⓘ 재료(합판·못 카탈로그 없음 · 각재·철선·박리제 규격 미정)는 못 붙은 줄 — 지금 값은"
" **인력 품만** · [주]③ 동바리 별도 · [주]⑥ 소형구조물 인력품 30% 할증(선택)은 안 걺.",
),
"FP-12-25": (
"운반거리 미정",
"⚠ 이 값에는 **운반 몫이 빠져 있습니다** — 품셈 12-25 는 「운반 | 덤프트럭(15ton)」 줄을 "
@@ -76,13 +84,7 @@ CONDITIONAL_INCLUDED: dict[str, str] = {
#: (표 읽기 사유 「박리제 줄을 못 풀었습니다」는 사람을 엉뚱한 데로 보낸다).
#: ⚠ 이 구현이 서면 그 줄을 지운다.
BLOCKED_BY_DESIGN: dict[str, str] = {
# 2026-09-13 브레인 판정 — B09 일위대가 일감(B08 Formwork 머리말 「횟수별 재료 환산은 여기서
# 하지 않는다」). 표를 그대로 풀면 1회 사용 값(재료·노무 100 %)으로 조용히 비싸진다.
"FP-12-04": (
"거푸집 **사용횟수별 비율**(12-4 「1회 100 % · 4회 40 %…」)을 일위대가에 거는 셈이"
" 아직 없어"
" 풀지 않았습니다 — 풀면 1회 사용 값으로 비싸게 섭니다"
),
# (비어 있음) 12-4 합판거푸집은 2026-09-14 사용횟수 갈래로 풀려 걷음(`_JudgedTable`).
}
@@ -5,6 +5,8 @@
F0288 1. 절토면 고르기 자원 이름이 **첫 자료 줄**(「보통인부 (인)」 …) · 규격은 [주]① 에만
· 「·」 = 그 토질엔 그 자원 없음
F0289 2. 성토면 고르기 병합 첫 칸(시공) 탓에 둘째 줄이 한 칸 앞당겨 옴
F0336 12-4 합판거푸집 기준수량(1회) × 사용횟수별 비율 — 재료 줄은 재료 %, 인력 줄은 노무비 %
(원문 L6187 · 2026-09-14 브레인 ㉮ — 비율 셈이 없어 막아 뒀던 표)
⚠ 일반 보정으로 넓히지 않음 — 밀린 줄 고쳐 읽기는 목재틀흙막이 503만원 전례(`_Transposed` 머리말).
**적어 둔 표만** 읽고, 칸이 판정과 다르면 한 줄도 안 세우고 막음.
@@ -47,7 +49,17 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = {
"form_split": {"굴착기": ("무한궤도", "타이어")},
"why": "원문 L5430 「2. 성토면 고르기 (단위: 10㎡당)」 — 시공 칸 병합으로 둘째 줄 앞당김",
},
"F0336": {
"code": "FP-12-04",
"shape": "use_count",
"prefix": "합판거푸집",
"why": "원문 L6187 「기준수량(1회사용) · 사용횟수별기준수량에대한 비율(%) 재료별·노무비」",
},
}
#: 비율 줄 — 「1회사용시 2회사용시 …」 칸.
_RE_USE_COUNT = re.compile(r"(\d+)회사용시")
#: 값으로 안 읽는 줄 — 사용고재 평가기준(원문이 셈을 안 줌 · 사유는 `KNOWN_GAPS`) · 비고.
_NOT_RESOURCE_ROWS = ("사용고재평가기준", "비고", "횟수별")
#: 「보통인부 (인)」 의 단위 꼬리 — 규격이 아님.
_UNIT_TAIL = re.compile(r"\s*[(](?:인|시간)[)]\s*$")
@@ -103,6 +115,8 @@ def match_judged_table(
return block("판정 표에 밑수가 없습니다")
if judged["shape"] == "header_row":
staged = _header_row(code, table, judged, rows, catalog, basis_quantity, unit)
elif judged["shape"] == "use_count":
staged = _use_count(code, table, rows, catalog, basis_quantity, unit)
else:
staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit)
if isinstance(staged, str):
@@ -176,3 +190,32 @@ def _merged_first(code, table, judged, rows, catalog, basis, unit) -> list | str
named = f"{variant} · {form}" if form else variant
staged.append(_row(code, table, entry, amount / basis, unit, index, named))
return staged
def _use_count(code, table, rows, catalog, basis, unit) -> list | str:
"""[이름, 단위, 기준수량, …] + 비율 줄 하나 → 사용횟수마다 갈래(재료 % · 노무비 %)."""
ratio_row = next((cells for cells in rows if _RE_USE_COUNT.search(" ".join(cells))), None)
if ratio_row is None:
return "사용횟수 비율 줄"
counts = _RE_USE_COUNT.findall(" ".join(ratio_row))
number = re.compile(r"\d+(?:\.\d+)?")
material = [Decimal(x) for x in number.findall(ratio_row[4] if len(ratio_row) > 4 else "")]
labor = [Decimal(x) for x in number.findall(ratio_row[5] if len(ratio_row) > 5 else "")]
if not counts or not (len(counts) == len(material) == len(labor)):
return f"비율 칸 수(횟수 {len(counts)} · 재료 {len(material)} · 노무비 {len(labor)})"
staged: list = []
for index, cells in enumerate(rows):
name = "".join(cells[0].split()) if cells else ""
base = parse_amount(cells[2]) if len(cells) > 2 else None
if not name or name in _NOT_RESOURCE_ROWS or base is None:
continue
entry = _entry(catalog, cells[0], code)
if entry is None:
reason = unmatched_reason(catalog, cells[0])
staged.append(UnmatchedRow(code, str(table.get("pum_table_id", "")), cells[0], reason))
continue
ratios = labor if entry.kind == "labor" else material
for count, ratio in zip(counts, ratios):
amount = base * ratio / Decimal(100) / basis
staged.append(_row(code, table, entry, amount, unit, index, f"{count}"))
return staged
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-14T16:09:37+09:00",
"generated_at": "2026-09-14T18:26:57+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "a942f7ad412441ba0c7ad3f8149fd143b10502dae5d20053e5d98029ab31c6b4",
"size_bytes": 838243
"sha256": "fa4d9bc80627d33a46b203f8910f9cc802c1249f2c6c8f09d2e818fce4332287",
"size_bytes": 838421
},
{
"file": "form_undetermined_2026-01-01.json",
@@ -3,7 +3,7 @@
"dataset_id": "work_item_master_forest",
"effective_date": "2026-01-01",
"pum_edition": "2026-01-01",
"generated_at": "2026-09-14T16:09:37+09:00",
"generated_at": "2026-09-14T18:26:57+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -31615,7 +31615,14 @@
"formula_rows": [],
"special_glyphs": [],
"capacity_formula_here": false,
"variant_key": [],
"variant_key": [
"1회",
"2회",
"3회",
"4회",
"5회",
"6회"
],
"condition_note": [
"구 분",
"단위",
@@ -31717,7 +31724,14 @@
]
}
],
"variant_keys": []
"variant_keys": [
"1회",
"2회",
"3회",
"4회",
"5회",
"6회"
]
},
{
"work_item_code": "FP-12-05",
@@ -0,0 +1,55 @@
"""합판거푸집 12-4 사용횟수별 비율 — 2026-09-14 브레인 ㉮ 승인.
원문 L6187~6211: 기준수량(1회사용) × 「사용횟수별 기준수량 비율(%)」 — 재료별
100·57.0·46.1·40.1·37.1·34.7 · 노무비 100·60.0·47.1·40.0·34.2·32.0 (1~6회).
종전엔 비율 셈이 없어 풀면 1회 값으로 비싸게 서므로 일부러 막아 뒀음(BLOCKED_BY_DESIGN).
⇒ 사람 판정표로 1~6회 갈래를 세우고 재료 줄엔 재료 비율 · 인력 줄엔 노무비 비율.
「사용고재 평가기준 23%」 는 원문이 셈을 안 줘([주]① 2회 이상 비율에 기포함) 값으로 안 씀.
"""
from __future__ import annotations
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of
def _labor(code: str) -> dict[str, float]:
rows = detail_of(cached_build(), code)["rows"]
return {r["ref_code"]: float(r["quantity"]) for r in rows if r.get("kind") == "labor"}
def test_사용횟수_여섯_갈래가_서고_인력은_노무비_비율() -> None:
titles = cached_build().book.titles
variants = sorted(c for c in titles if c.startswith("B-FP-12-04#"))
assert variants == [f"B-FP-12-04#{n}" for n in range(1, 7)], variants
assert _labor("B-FP-12-04#1회") == {"1007": 0.22, "1002": 0.12}
assert _labor("B-FP-12-04#4회") == {"1007": 0.088, "1002": 0.048} # 40.0 %
assert _labor("B-FP-12-04#6회") == {"1007": 0.0704, "1002": 0.0384} # 32.0 %
def test_재료는_재료_비율로_읽고_못_붙은_재료는_사유() -> None:
from B09_Estimation.B09_Estimation_ResourceAxis import (
build_resource_axis,
load_combined_catalog,
load_work_item_master,
)
axis = build_resource_axis(load_work_item_master(), load_combined_catalog())
left = {u.cell for u in axis.unmatched if u.work_item_code == "FP-12-04"}
# 합판·못 은 카탈로그 없음 · 각재·철선·박리제는 규격 미정 — 값을 짓지 않고 못 붙은 줄로
assert {"합판", "", "각재", "철선", "박리제"} <= {c.replace(" ", "") for c in left}, left
build = cached_build()
assert "FP-12-04" not in build.component_gaps and "FP-12-04" not in build.partial_ratio
note = known_gap_note("FP-12-04")
assert "사용고재" in note and "23" in note, note
def test_거푸집_갈래가_내역에서_금액이_섬() -> None:
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
item = {"work_item_code": "FP-12-04", "name": "합판거푸집", "spec": "", "unit": "",
"quantity": "10", "in_bill": True, "variant_axis": "use_count",
"variant_value": "4회"} # fmt: skip
line = next(r for r in build_bill({"work_items": [item], "materials": []}).rows
if r.code == "FP-12-04") # fmt: skip
assert line.price_code == "B-FP-12-04#4회" and line.amount_krw, line.note
+2 -2
View File
@@ -100,8 +100,8 @@ def test_단목베기_5m_미만이_실무값_근처로_선다():
build = build_unit_prices()
assert "FP-04-02-02" not in build.partial_ratio
# 합판거푸집 — 일부러 안 푼 표는 **그 까닭**이 사유로 뜬다(사용횟수 비율 미구현)
assert "사용횟수" in (build.component_gaps.get("FP-12-04") or "")
# 합판거푸집 — 2026-09-14 사용횟수 갈래로 풀림(`test_b09_formwork_use_count`) · 막힘 사유 없음
assert "FP-12-04" not in build.component_gaps
total = build.book.resolve("B-FP-04-02-02#5m미만").total
# 영월 설계내역 1.9.2 「잡관목제거 벌목(5m미만)」 @882 — 노임 연도 차이로 ±5 % 안이면 같은 읽기
assert Decimal("838") <= total <= Decimal("926"), total