feat(b09): 봉상후렉시블 셋(12-12 날개벽 · 12-13 면벽 · 12-16 맨홀) + 12-34-1 — 같은 진동기 줄 하나가 막던 네 공종을 판정표로 세움

· 12-12·12-13·12-16 — 12-15 집수정과 같은 모양(㎥ 줄 비고 인력 + 다짐기 줄) · 판정표 한 벌에 더함 · 맨홀은 비고가 끝 칸이 아니라 단위 뒤 칸 전체에서 찾음
· 비고가 인/㎥ 인 판정표는 밑수 없음 목록을 안 봄(12-12 는 「개소당」 머리조차 없음)
· 12-34-1 — 형태 판정 참조→소요량(「별도계상」 은 레미콘 줄 비고일 뿐) · 새 모양 per_m3_rows · 진동기(3.5HP) 대 2 → 4611-0350 × 2/5.4 · 봉상후렉시블 대 2 는 엔진식 진동기의 봉이라 안 셈 · 머리 개소당 ↔ 인력 0.17·0.29(12-16 ㎥당과 같음)·Q ㎥/hr ⇒ ㎥당으로 읽고 고른 쪽·버린 쪽을 사유에 나란히(브레인 판정)
· 범위 별칭 넷 → 엔진식 4611-0350 · 표의 나머지 줄(거푸집·철근·기초잡석·원형거푸집·설치비 등)은 원문 그대로 사유 한 줄(조용히 안 사라지게)
· 실무 영월 「콘크리트타설(임도품셈 5-5)」 호표가 같은 모양 — 인력 인/㎥ + 진동기 엔진식 1/Q · 손료 252천원×0.5101 · 노무 0
· 마스터 재생성 — 네 공종 갈래 키 · F0385 형태만

새 제목 다섯 · 바뀐 제목 0 · 프로젝트 내역 그대로 · 잴 시험 넷 빨강→초록 · 끄기 다섯 빨강 · 전체 시험 1699 + 337 통과

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 23:33:47 +09:00
co-authored by Claude Opus 5
parent fd0ea82975
commit 15d91be7b0
8 changed files with 242 additions and 15 deletions
@@ -110,6 +110,11 @@ FORM_JUDGMENTS: dict[str, tuple[str, str, str]] = {
# 절 제목으로 읽어 12-2 에 붙어 있던 표(빌더가 앞 절 이어받기로 고침). 형태 판정은 그대로.
"F0358": ("12-17-1", "reference", "시설유형 Type-Ⅰ~Ⅳ 적용 기준 설명"),
"F0360": ("12-17-1", "reference", "현장조건 Type-Ⅰ~Ⅲ 적용 기준 설명"),
"F0385": (
"12-34-1",
"requirement",
"인력(인)·기계(대, Q=5.4㎥/hr) 소요량 표 — 「별도계상」 은 레미콘 자재 줄 비고일 뿐",
),
"F0388": ("12-34-4", "reference", "「재료비 JOINT FILLER」 항목만 — 값이 없는 구성 안내"),
"F0390": ("12-36", "reference", "제작비·운송비·설치비 「견적처리」"),
"F0392": ("12-38-1", "coefficient", "사용횟수별 잔존율(12회 25%·25회 10%)"),
@@ -55,6 +55,33 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
"(제안 무한궤도 — 영월 실무 「06M3 B/H」 · 2026-09-14 브레인 ②).",
),
# 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만.
# 봉상후렉시블 셋(2026-09-14) — 판정표가 콘크리트 ㎥ 줄만 읽음 · 표의 나머지 줄이 조용히 안 사라지게 원문 그대로.
"FP-12-12": (
"표의 다른 줄",
"ⓘ 이 일위대가는 콘크리트 ㎥(콘크리트공·보통인부 + 진동기) 몫만 — 표의 거푸집(합판4회) · 철근(SD300 D13"
" · 철근가공조립 간단) · 기초잡석 운반·부설·다짐(소할 30% 브레이커 · 적사 굴착기 0.7㎥ · 운반 덤프트럭"
" 15톤 · 부설다짐 보통인부 0.6인/㎥) · 바닥정리는 안 넣음(따로 공종) · [주] 성토부 날개벽 인건비 30%"
" 할증은 안 걺.",
),
"FP-12-13": (
"표의 다른 줄",
"ⓘ 이 일위대가는 콘크리트 ㎥(콘크리트공·보통인부 + 진동기) 몫만 — 표의 거푸집(합판4회) · 기초잡석"
" 운반·부설·다짐(소할 30% 브레이커 · 적사 굴착기 0.7㎥ · 운반 덤프트럭 15톤 · 부설다짐 보통인부"
" 0.6인/㎥)은 안 넣음(따로 공종).",
),
"FP-12-16": (
"표의 다른 줄",
"ⓘ 이 일위대가는 구체·버림콘크리트 ㎥ 몫만 — 표의 원형거푸집(PE10회 · 각 1조/10) · 설치비(기초 및"
" 슬라브 특별인부 0.16·보통인부 0.45인/㎡ · 벽체 0.20·0.60인/㎡) · 거푸집(합판4회) · 철근(가공조립"
" 보통) · 설치비 재료비의 5% · 사다리설치(철근가공조립 간단 준용) · 인력 보통인부 0.004 는 안 넣음"
"(따로 공종·조립 몫).",
),
"FP-12-34-01": (
"원문 머리",
"ⓘ 원문 머리 「(단위: 개소당)」 ↔ 인력 콘크리트공 0.17·보통인부 0.29 가 12-16 맨홀 ㎥당(0.17인/㎥"
" · 0.29인/㎥)과 같고 기계도 Q=5.4㎥/hr ⇒ ㎥당으로 읽음(고른 쪽 ㎥당 · 버린 쪽 개소당) ·"
" 봉상후렉시블(45mm) 대 2 는 엔진식 진동기의 봉이라 두 번 안 셈(2026-09-14 브레인).",
),
"FP-12-04": (
"원문 [주]",
"ⓘ 「사용고재 평가기준 23%(합판과 각재의 설계단가 기준)」 은 원문이 셈을 안 줘 값으로 안 씀"
@@ -63,6 +63,39 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = {
"needs_machine": {"구체콘크리트": "다짐:봉상후렉시블(45mm)"},
"why": "원문 L6460 「구체·버림콘크리트 ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」",
},
# 12-15 와 같은 모양 셋 — 같은 봉상후렉시블 줄 하나가 셋을 막고 있었음(2026-09-14 브레인 · 672 다음).
"F0350": {
"code": "FP-12-12",
"shape": "remark_labor",
"prefix": "날개벽",
"needs_machine": {"콘크리트": "다짐:봉상후렉시블(45mm)"},
"why": "원문 L6419 12-12 「콘크리트(레미콘) ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」",
},
"F0351": {
"code": "FP-12-13",
"shape": "remark_labor",
"prefix": "면벽",
"needs_machine": {"콘크리트": "다짐:봉상후렉시블(45mm)"},
"why": "원문 L6436 12-13 「콘크리트(레미콘) ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」",
},
"F0354": {
"code": "FP-12-16",
"shape": "remark_labor",
"prefix": "맨홀",
# 칸이 하나 밀려 비고가 끝 칸이 아님(「구체콘크리트 | 철근 | ㎥ | | 비고 | 」).
"needs_machine": {"구체콘크리트": "봉상후렉시블(45mm)"},
"why": "원문 L6474 12-16 「구체·버림콘크리트 ㎥ — 비고 칸 콘크리트공·보통인부 인/㎥」",
},
# 12-34-1 — 머리 「(단위: 개소당)」 이나 인력 0.17·0.29 가 12-16 맨홀 ㎥당과 같고 기계가 Q ㎥/hr
# ⇒ ㎥당으로 읽음 · 「봉상후렉시블 대 2」 는 엔진식 진동기(엔진+플렉시블 한 대)의 봉(2026-09-14 브레인).
"F0385": {
"code": "FP-12-34-01",
"shape": "per_m3_rows",
"prefix": "콘크리트 타설",
# 엔진식 진동기(건설품셈 8-3 (4611) 엔진+플렉시블 한 대)의 봉 — 「진동기(3.5HP) 대 2」 로 셈.
"same_machine": ("봉상후렉시블(45mm)",),
"why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」",
},
}
#: 비고 칸 인력 — 「콘크리트공0.24인/㎥, 보통인부 0.42인/㎥」.
_RE_REMARK_LABOR = re.compile(r"([가-힣]+)\s*(\d+(?:\.\d+)?)\s*인/㎥")
@@ -122,7 +155,8 @@ def match_judged_table(
result.partial_items[code] = why
return True
if basis_quantity in (None, 0):
# 비고·Q 가 ㎥당을 적는 모양은 표 머리 밑수를 안 씀(12-12 날개벽은 「개소당」 머리조차 없음).
if basis_quantity in (None, 0) and judged["shape"] not in ("remark_labor", "per_m3_rows"):
return block("판정 표에 밑수가 없습니다")
if judged["shape"] == "header_row":
staged = _header_row(code, table, judged, rows, catalog, basis_quantity, unit)
@@ -130,6 +164,8 @@ def match_judged_table(
staged = _use_count(code, table, rows, catalog, basis_quantity, unit)
elif judged["shape"] == "remark_labor":
staged = _remark_labor(code, table, judged, rows, catalog)
elif judged["shape"] == "per_m3_rows":
staged = _per_m3_rows(code, table, judged, rows, catalog)
else:
staged = _merged_first(code, table, judged, rows, catalog, basis_quantity, unit)
if isinstance(staged, str):
@@ -240,7 +276,7 @@ def _remark_labor(code, table, judged, rows, catalog) -> list | str:
by_name = {"".join(cells[0].split()): cells for cells in rows if cells}
staged: list = []
for index, cells in enumerate(rows):
labors = _RE_REMARK_LABOR.findall(cells[-1] if cells else "")
labors = _RE_REMARK_LABOR.findall(" ".join(cells[3:])) # 비고가 끝 칸이 아닌 표(12-16)
if len(cells) < 3 or cells[2] != "" or not labors:
continue
variant = cells[0].split("(")[0].strip()
@@ -268,3 +304,27 @@ def _remark_labor(code, table, judged, rows, catalog) -> list | str:
for entry, amount in pieces:
staged.append(_row(code, table, entry, amount, "", index, variant))
return staged
def _per_m3_rows(code, table, judged, rows, catalog) -> list | str:
"""「인」 칸 앞 이름 · 뒤 수(인/㎥) · 「대」 칸 앞 이름 · 뒤 대수 ÷ Q — 칸이 밀린 줄도 단위 칸으로 찾음."""
table_id = str(table.get("pum_table_id", ""))
staged: list = []
for index, cells in enumerate(rows):
unit = next((i for i, c in enumerate(cells) if c in ("", "") and i > 0), None)
if unit is None:
continue
name = cells[unit - 1]
if "".join(name.split()) in judged.get("same_machine", {}):
continue # 같은 기계 두 번 안 셈 — 까닭은 공종 사유 한 줄(`KnownGaps`)이 화면에 보임
amount = next((parse_amount(c) for c in cells[unit + 1 :] if parse_amount(c)), None)
entry = _entry(catalog, name, code)
if amount is None or entry is None:
return f"{name}"
if cells[unit] == "":
found_q = _RE_Q.search(" ".join(cells))
if found_q is None:
return f"{name} Q"
amount = amount / Decimal(found_q.group(1))
staged.append(_row(code, table, entry, amount, "", index, ""))
return staged
+5 -1
View File
@@ -899,9 +899,13 @@ def build_unit_prices(
# 예외 = 사용자가 다른 품셈 밑수를 확정한 자리(9-21 「1,000㎡당」 · ÷ 는 ChooseOne 이 검) —
# 마스터 목록은 원문 사실이라 그대로 두고 여기서만 걷음(2026-09-14 브레인 · 사유는 비고).
borrowed = work_item_code.startswith(tuple(BORROWED_BASIS_PER))
from B09_Estimation.B09_Estimation_ResourceAxis_JudgedTable import JUDGED_TABLES
for row in rows:
section = missing_basis.get(str(row.pum_table_id))
if section and not borrowed:
# 비고가 「인/㎥」 로 밑수를 적은 판정표(12-12 날개벽 「개소당」 머리 없음)는 밑수가 ㎥ 로 섬.
per_remark = JUDGED_TABLES.get(str(row.pum_table_id), {}).get("shape") == "remark_labor"
if section and not borrowed and not per_remark:
build.basis_missing[work_item_code] = section
break
@@ -51,6 +51,38 @@
"pum_edition": "2026-01-01",
"basis": "12-15 집수정 표(원문 L6460)는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 건설품셈 8-3 (4611) 은 전기식 플렉시블형 ø45(0.75㎾)·엔진식 플렉시블형 ø45(2.6㎾) 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표(8-4)에 엔진식 4611-0350(휘발유 1.0L)만 있음 → 엔진식. 전기식이 필요하면 그때 엶. 2026-09-14 브레인 승인(661 뒤 ①)"
},
{
"axis": "resource",
"from": "다짐:봉상후렉시블(45mm)",
"to": "4611-0350",
"scope": "FP-12-12",
"pum_edition": "2026-01-01",
"basis": "12-12 날개벽(원문 L6419) 표는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 12-15 집수정과 같은 줄 · 같은 근거(건설품셈 8-3 (4611) 전기식·엔진식 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표에 엔진식 4611-0350 만) → 엔진식. 2026-09-14 브레인(672 다음 · 봉상후렉시블 셋)"
},
{
"axis": "resource",
"from": "다짐:봉상후렉시블(45mm)",
"to": "4611-0350",
"scope": "FP-12-13",
"pum_edition": "2026-01-01",
"basis": "12-13 면벽(원문 L6436) 표는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 12-15 집수정과 같은 줄 · 같은 근거(건설품셈 8-3 (4611) 전기식·엔진식 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표에 엔진식 4611-0350 만) → 엔진식. 2026-09-14 브레인(672 다음 · 봉상후렉시블 셋)"
},
{
"axis": "resource",
"from": "봉상후렉시블(45mm)",
"to": "4611-0350",
"scope": "FP-12-16",
"pum_edition": "2026-01-01",
"basis": "12-16 맨홀(원문 L6474) 표는 「봉상후렉시블(45mm) Q=5.4㎥/hr」 만 적고 전기·엔진을 안 적음 — 12-15 집수정과 같은 줄 · 같은 근거(건설품셈 8-3 (4611) 전기식·엔진식 둘 · 12-34-1 「콘크리트 진동기(3.5HP)」 = 2.6㎾ 엔진식 · 운전경비표에 엔진식 4611-0350 만) → 엔진식. 2026-09-14 브레인(672 다음 · 봉상후렉시블 셋)"
},
{
"axis": "resource",
"from": "콘크리트 진동기(3.5HP)",
"to": "4611-0350",
"scope": "FP-12-34-01",
"pum_edition": "2026-01-01",
"basis": "12-34-1 「콘크리트 진동기(3.5HP)」 — 3.5HP = 2.6㎾ · 건설품셈 8-3 (4611) 엔진식 플렉시블형 ø45(2.6㎾) · 운전경비표에 엔진식만 · 실무 영월 중기목록 「콘크리트 진동기 45φ(2.6㎾)엔진식플렉시블형」. 2026-09-14 브레인"
},
{
"axis": "variant",
"from": "0.2·소림",
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-14T22:25:14+09:00",
"generated_at": "2026-09-14T23:14:39+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": "2f0d8bfb3836597ee6f2fd755e2f2e172cb33df1e1e9061af81982a43497f9b2",
"size_bytes": 838655
"sha256": "708a6a80d016c42d3402bd2a6cb5266fa18e1e2b5301bac823e6ff79d17fb2b6",
"size_bytes": 838969
},
{
"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-14T22:25:14+09:00",
"generated_at": "2026-09-14T23:14:39+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -32888,7 +32888,9 @@
"formula_rows": [],
"special_glyphs": [],
"capacity_formula_here": false,
"variant_key": [],
"variant_key": [
"콘크리트"
],
"condition_note": [
"구 분",
"규 격",
@@ -32970,7 +32972,9 @@
]
}
],
"variant_keys": []
"variant_keys": [
"콘크리트"
]
},
{
"work_item_code": "FP-12-13",
@@ -32997,7 +33001,9 @@
"formula_rows": [],
"special_glyphs": [],
"capacity_formula_here": false,
"variant_key": [],
"variant_key": [
"콘크리트"
],
"condition_note": [
"구 분",
"규 격",
@@ -33065,7 +33071,9 @@
]
}
],
"variant_keys": []
"variant_keys": [
"콘크리트"
]
},
{
"work_item_code": "FP-12-14",
@@ -33242,7 +33250,10 @@
"formula_rows": [],
"special_glyphs": [],
"capacity_formula_here": false,
"variant_key": [],
"variant_key": [
"구체콘크리트",
"버림콘크리트"
],
"condition_note": [
"구 분",
"단위",
@@ -33349,7 +33360,10 @@
]
}
],
"variant_keys": []
"variant_keys": [
"구체콘크리트",
"버림콘크리트"
]
},
{
"work_item_code": "FP-12-17",
@@ -35258,8 +35272,8 @@
"pum_table_id": "F0385",
"section": "12-34-1. 콘크리트 타설(철근 진동기포함)",
"source_line": 6843,
"pum_form": "reference",
"form_basis": "'별도계상' — 값이 아니라 참조 지시",
"pum_form": "requirement",
"form_basis": "사람 판정 — 인력(인)·기계(대, Q=5.4㎥/hr) 소요량 표 — 「별도계상」 은 레미콘 자재 줄 비고일 뿐",
"basis_quantity": 1.0,
"basis_unit": "개소",
"basis_source": "본문",
@@ -0,0 +1,85 @@
"""봉상후렉시블 셋 + 12-34-1 — 2026-09-14 브레인(672 다음).
같은 「봉상후렉시블(45mm) Q=5.4㎥/hr」 줄 하나가 12-12 날개벽 · 12-13 면벽 · 12-16 맨홀을 막고 있었음.
세 표는 12-15 집수정과 같은 모양(㎥ 줄 비고 인력 + 다짐기 줄) — 판정표 한 벌에 더하고 엔진식 4611-0350 로 이음.
실무 영월 「콘크리트타설(임도품셈 5-5)」 호표가 같은 모양: 콘크리트공 0.15·보통인부 0.27 인/㎥ + 진동기 1/Q(5.4)
· 진동기 45φ(2.6㎾)엔진식플렉시블형 손료 252천원 × 0.5101 · 노무 0 — 엔진식·계수·조종원 없음이 실무 실증.
12-34-1 콘크리트 타설(철근 진동기 포함) — 실무 호표 없음 → 브레인 판정
① 「콘크리트 진동기(3.5HP) 대 2」 만 4611-0350 × 2대 · 「봉상후렉시블(45mm) 대 2」 는 같은 기계의 봉이라 안 셈
② 머리 「(단위: 개소당)」 ↔ 인력 0.17·0.29 가 12-16 맨홀 ㎥당과 같고 기계도 Q ㎥/hr ⇒ ㎥당으로 읽고 사유에 나란히
"""
from __future__ import annotations
from decimal import Decimal
from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of
VIBRATOR = "X-4611-0350"
PER_Q = Decimal(1) / Decimal("5.4")
def _rows(build, code: str) -> dict[str, Decimal]:
return {
r["ref_code"]: Decimal(r["quantity"])
for r in detail_of(build, code)["rows"]
if r.get("ref_code")
}
def test_날개벽_면벽은_콘크리트_갈래가_진동기와_함께_섬() -> None:
build = cached_build()
for code in ("B-FP-12-12#콘크리트", "B-FP-12-13#콘크리트"):
rows = _rows(build, code)
assert rows[VIBRATOR] == PER_Q and rows["1013"] == Decimal("0.15"), (code, rows)
assert rows["1002"] == Decimal("0.27"), (code, rows)
assert build.book.titles[code].unit == ""
assert (
"FP-12-12" not in build.basis_missing
) # 비고가 인/㎥ — 「개소당」 머리가 없어도 밑수는 ㎥
def test_맨홀은_비고가_끝_칸이_아니어도_구체_버림_갈래() -> None:
build = cached_build()
body = _rows(build, "B-FP-12-16#구체콘크리트")
assert (
body[VIBRATOR] == PER_Q
and body["1013"] == Decimal("0.17")
and body["1002"] == Decimal("0.29")
)
lean = _rows(build, "B-FP-12-16#버림콘크리트")
assert lean == {"1013": Decimal("0.15"), "1002": Decimal("0.27")}, lean
for code in ("FP-12-12", "FP-12-13", "FP-12-16"):
assert "봉상후렉시블" not in " ".join(build.unattached.get(code, [])), code
def test_12_34_1_진동기_두_대만_세고_봉은_두_번_안_셈() -> None:
build = cached_build()
rows = _rows(build, "B-FP-12-34-01")
assert rows[VIBRATOR] == 2 * PER_Q, rows
assert rows["1013"] == Decimal("0.17") and rows["1002"] == Decimal("0.29"), rows
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
assert "봉상후렉시블" in known_gap_note("FP-12-34-01") and "두 번 안 셈" in known_gap_note(
"FP-12-34-01"
)
refs = [r.get("ref_code") for r in detail_of(build, "B-FP-12-34-01")["rows"]]
assert refs.count(VIBRATOR) == 1, refs # 봉 줄이 기계로 또 안 섬(한 줄 · 2대 몫)
def test_12_34_1_은_개소당_머리를_세제곱미터당으로_읽은_까닭을_보임() -> None:
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
build = cached_build()
assert build.book.titles["B-FP-12-34-01"].unit == ""
note = known_gap_note("FP-12-34-01")
assert "개소당" in note and "㎥당" in note and "12-16" in note, note
def test_표의_나머지_줄은_조용히_안_사라지고_사유에_원문_그대로() -> None:
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
assert "원형거푸집" in known_gap_note("FP-12-16") and "0.16" in known_gap_note("FP-12-16")
assert "기초잡석" in known_gap_note("FP-12-12") and "30%" in known_gap_note("FP-12-12")
assert "기초잡석" in known_gap_note("FP-12-13")