feat(b09): ㉡-3 시간 줄 판정표 — 14-1 입목뿌리 밑막이 · 14-2 근주이식이 섬 · 막힌 셋(물푸기·모노레일·케이블크레인)은 옛 값이 왜 틀렸는지 사유

· 판정표 모양 hour_rows — 장비 h 그대로 · 인력 h ÷ 8(총칙 「일일 작업시간 8시간」) · ÷ 10본
· 원문 「0.8㎥ 굴착기」 가 형식을 안 적어 무한궤도 0201-0080 · 타이어 0211-0080 두 갈래 · 제안 없음(브레인 판정 ①)
· 14-1 우드그랩 ↔ 카탈로그 부착용 집게 7206-0070(규격 0.6∼0.8) — 이름이 다름을 사유에(②) · 조합이라 본체 #조합
· 옛 14-2 11,356원은 0.66h 를 인으로 셈(8배) + 굴착기 빠짐 → 무한궤도 4,431 · 타이어 4,631원/본 · 14-1 8,603 → 3,192 · 3,316
· 막힌 셋 사유(③): 12-26 가동시간 원문 없음 · 옛 값 보통인부 1인뿐 / 10-7-4 운전 단가표 보통인부 2 는 적재·짐부리기 2인과 같은 사람 · 옛 값 두 번 센 4인 / 10-8-3 ℓ/kWh 원문 없음 · 옛 값 특별인부 1인뿐
· 마스터 재생성 — 14-1 · 14-2 갈래 키만

잴 시험 넷 빨강→초록 · 끄기 둘 빨강 · 전체 시험 1721 + 392 통과

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-15 03:29:21 +09:00
co-authored by Claude Opus 5
parent ea81b7644e
commit 42e8b4f0a9
6 changed files with 175 additions and 10 deletions
@@ -57,6 +57,33 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
# 2026-09-14 ㉮ — 사용횟수 갈래로 푼 뒤 남는 원문 몫. 값을 짓지 않고 말만.
# 봉상후렉시블 셋의 표 나머지 줄은 판정표 자동 목록(`_unread_rows`)이 맡음 — 줄이 아닌 [주] 만 여기.
"FP-12-12": ("원문 [주]", "ⓘ [주] 「성토부 날개벽 설치시 인건비 30% 할증」 은 안 걺(선택)."),
# ㉡-3(2026-09-15) — 시간 줄 표 둘 · 막힌 셋은 옛 값이 왜 틀렸는지(풀리는 날 두 번 세지 않게 · 브레인 ③).
"FP-14-01": (
"원문 표",
"ⓘ 원문 「0.8㎥ 굴착기(우드그랩 부착)」 가 굴착기 형식을 안 적어 무한궤도·타이어 두 갈래 · 우드그랩 ↔ 카탈로그"
" 「부착용 집게 7206-0070(0.60.8)」 — 이름이 다름 · 인력 「h」 는 시간이라 총칙 일일 작업시간 8시간으로 ÷8.",
),
"FP-14-02": (
"원문 표",
"ⓘ 원문 「0.8㎥ 굴착기」 가 형식을 안 적어 무한궤도·타이어 두 갈래 · 인력 「h」 는 시간이라 총칙 일일 작업시간"
" 8시간으로 ÷8 · 옛 11,356원은 0.66h 를 인으로 셈(8배) + 굴착기 줄이 빠진 값.",
),
"FP-12-26": (
"막힘 까닭",
"ⓘ 양수기(150㎜)·디젤엔진(15HP 11.19kW) 가동시간이 원문 표에 없음 · 카탈로그 양수기는 1.49㎾ 뿐 · 엔진(디젤)"
" 손료계수 없음 → 금액 안 세움 · 옛 172,068원은 보통인부 1인 값뿐(기계·목도 4인 빠짐).",
),
"FP-10-07-04": (
"막힘 까닭",
"ⓘ 운전 단가표(1일) 보통인부 2 는 적재·짐부리기 보통인부 2인(「운전조작」 포함)과 같은 사람 — 두 번 세지 말 것 ·"
" 연료 「ps × 0.253ℓ × 6h」 의 ps 입력 없음 · 본기계·레일은 임대료·견적 → 금액 안 세움 · 옛 688,272원은"
" 보통인부를 두 번 센 4인 값.",
),
"FP-10-08-03": (
"막힘 까닭",
"ⓘ 연료 = 윈치 기관 출력([주]② 36~73kW) × 운전시간([주]① 4.3~6.7h) × /kWh — /kWh 가 원문에 없음 · 보통인부는"
" 10-8-1 짐내리기 품 · 운반기구 손료 별도 → 금액 안 세움 · 옛 226,122원은 특별인부 1인 값뿐.",
),
"FP-08-06-01": (
"원문 [주]",
"ⓘ 2-1-8 [주]② 「소방관서의 급수 지원을 받은 경우에는 휘발유(양수기)는 미반영」 — 해당하면 양수기 몫을"
@@ -96,7 +96,27 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = {
"same_machine": ("봉상후렉시블(45mm)",),
"why": "원문 L6839 12-34-1 「인력 콘크리트공·보통인부 인 · 기계 대 (Q=5.4㎥/hr)」",
},
# 시간 줄 표 — 장비·인력이 다 「h」(2026-09-15 브레인 ㉡-3). 굴착기 형식을 원문이 안 적어 두 갈래 · 제안 없음(①).
"F0450": {
"code": "FP-14-01",
"shape": "hour_rows",
"prefix": "입목뿌리 밑막이",
"forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")),
# 원문 「우드그랩 부착」 ↔ 카탈로그 「부착용 집게 0.6∼0.8」 — 이름이 다름(②).
"attachment": "7206-0070",
"why": "원문 L7798 14-1 「0.8㎥ 굴착기(우드그랩 부착) h · 보통인부 h」(10본당)",
},
"F0451": {
"code": "FP-14-02",
"shape": "hour_rows",
"prefix": "근주이식",
"forms": (("무한궤도", "0201-0080"), ("타이어", "0211-0080")),
"why": "원문 L7809 14-2 「0.8㎥ 굴착기 h · 보통인부 h」(10본당)",
},
}
#: 총칙 L530 「본 품셈에서 제시된 품은 일일 작업시간 8시간을 기준」 — 인력 시간 ÷ 8 = 인.
HOURS_PER_DAY = Decimal(8)
_RE_SPEC_FIRST_MACHINE = re.compile(r"^\d+(?:\.\d+)?㎥굴착기")
#: 줄 첫 칸이 분류 딱지인 표(12-34-1 「자재 | 콘크리트(레미콘)」) — 이름은 다음 칸.
_ROW_CATEGORIES = ("자재", "인력", "기계")
#: 자원이 아닌 머리 줄(12-04 「횟수별 | 재료별(%) | 노무비(%)」).
@@ -206,6 +226,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"] == "hour_rows":
staged = _hour_rows(code, table, judged, rows, catalog, basis_quantity)
elif judged["shape"] == "per_m3_rows":
staged = _per_m3_rows(code, table, judged, rows, catalog)
else:
@@ -370,3 +392,46 @@ def _per_m3_rows(code, table, judged, rows, catalog) -> list | str:
amount = amount / Decimal(found_q.group(1))
staged.append(_row(code, table, entry, amount, "", index, ""))
return staged
def _hour_rows(code, table, judged, rows, catalog, basis_quantity) -> list | str:
"""장비·인력이 다 시간인 표 — 장비 h 그대로 · 인력 h ÷ 8 · ÷ 밑수 · 굴착기 형식마다 갈래."""
entries = {entry.code: entry for entry in catalog.entries}
unit = str(table.get("basis_unit") or "")
machine: tuple[int, Decimal] | None = None
labor: list = []
for index, cells in enumerate(rows):
if len(cells) < 3 or "".join(cells[1].split()) != "h":
continue
hours = parse_amount(cells[2])
if hours is None:
return f"{cells[0]} 시간"
if _RE_SPEC_FIRST_MACHINE.match("".join(cells[0].split())):
machine = (index, hours)
continue
entry = _entry(catalog, cells[0], code)
if entry is None:
return f"{cells[0]}"
labor.append((index, entry, hours / HOURS_PER_DAY))
if machine is None:
return "장비 줄"
staged: list = []
for form, machine_code in judged["forms"]:
codes = [machine_code, *([judged["attachment"]] if judged.get("attachment") else [])]
if any(c not in entries for c in codes):
return f"{form} 기종 코드"
for machine_code_ in codes:
staged.append(
_row(
code,
table,
entries[machine_code_],
machine[1] / basis_quantity,
unit,
machine[0],
form,
)
)
for index, entry, amount in labor:
staged.append(_row(code, table, entry, amount / basis_quantity, unit, index, form))
return staged
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-14T23:14:39+09:00",
"generated_at": "2026-09-15T03:10:55+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": "708a6a80d016c42d3402bd2a6cb5266fa18e1e2b5301bac823e6ff79d17fb2b6",
"size_bytes": 838969
"sha256": "480f9510cdd2fe223b54818fb7180f8c6114d6e33e45cd3c6aa829c64de27eb3",
"size_bytes": 839153
},
{
"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-14T23:14:39+09:00",
"generated_at": "2026-09-15T03:10:55+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -40210,7 +40210,10 @@
"formula_rows": [],
"special_glyphs": [],
"capacity_formula_here": false,
"variant_key": [],
"variant_key": [
"무한궤도",
"타이어"
],
"condition_note": [
"장 비",
"단 위",
@@ -40233,7 +40236,10 @@
]
}
],
"variant_keys": []
"variant_keys": [
"무한궤도",
"타이어"
]
},
{
"work_item_code": "FP-14-02",
@@ -40260,7 +40266,10 @@
"formula_rows": [],
"special_glyphs": [],
"capacity_formula_here": false,
"variant_key": [],
"variant_key": [
"무한궤도",
"타이어"
],
"condition_note": [
"장 비",
"단 위",
@@ -42099,7 +42108,10 @@
]
}
],
"variant_keys": []
"variant_keys": [
"무한궤도",
"타이어"
]
}
]
}
+60
View File
@@ -0,0 +1,60 @@
"""시간 줄 판정표 — 14-1 입목뿌리 밑막이 · 14-2 근주이식(2026-09-15 브레인 ㉡-3 판정 ①②③).
원문(10본당): 14-2 「0.8㎥ 굴착기 | h | 0.25 · 보통인부 | h | 0.66」 · 14-1 「0.8㎥ 굴착기(우드그랩 부착) | h | 0.17 · 보통인부 | h | 0.5」
⚠ 인력 단위가 **시간** — 옛 14-2 11,356원은 0.66 을 인으로 셈(8배) + 굴착기 줄이 말없이 빠짐.
장비 h 그대로 · 인력 h ÷ 8(총칙 「일일 작업시간 8시간」) · ÷ 10본
① 「0.8㎥ 굴착기」 형식을 원문이 안 적음 — 무한궤도 0201-0080 · 타이어 0211-0080 두 갈래 · 제안 없음
② 우드그랩 = 부착용 집게 7206-0070(규격 「0.60.8」 이 0.8 을 품음) — 이름이 다름을 사유에
③ 막힌 셋(12-26 · 10-7-4 · 10-8-3)은 옛 값이 왜 틀렸는지 사유에
"""
from __future__ import annotations
from decimal import Decimal
from B09_Estimation.B09_Estimation_KnownGaps import known_gap_note
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
def _rows(book, title: str) -> dict[str, Decimal]:
return {r.ref_code: r.quantity for r in book.details[title]}
def test_근주이식은_두_갈래_장비_시간과_인력_시간_나누기_8() -> None:
build = cached_build()
book = build.book
crawler = _rows(book, "B-FP-14-02#무한궤도")
tire = _rows(book, "B-FP-14-02#타이어")
assert crawler["X-0201-0080"] == Decimal("0.25") / 10, crawler
assert tire["X-0211-0080"] == Decimal("0.25") / 10, tire
assert crawler["1002"] == Decimal("0.66") / 8 / 10, crawler
assert "FP-14-02" not in build.partial_ratio
assert "FP-14-02" not in build.default_variants # 제안 없음(①)
def test_입목뿌리_밑막이는_부착용_집게를_같은_시간만큼() -> None:
book = cached_build().book
rows = _rows(book, "B-FP-14-01#무한궤도")
# 부착용 집게와 조합 — 본체는 잡재료 16% 층(`#조합` · 품셈 제8장 [주]⑤)
assert (
rows["X-0201-0080#조합"] == Decimal("0.17") / 10
and rows["X-7206-0070"] == Decimal("0.17") / 10
)
assert rows["1002"] == Decimal("0.5") / 8 / 10
note = known_gap_note("FP-14-01")
assert "우드그랩" in note and "부착용 집게" in note and "7206-0070" in note, note
def test_사유는_시간_단위와_옛_값의_잘못을_밝히고_무한궤도를_권하지_않음() -> None:
note = known_gap_note("FP-14-02")
assert "시간" in note and "8시간" in note and "8배" in note, note
assert "영월" not in note and "제안" not in note and "10-12" not in note, note
def test_막힌_셋은_옛_값이_왜_틀렸는지를_사유에() -> None:
build = cached_build()
assert "보통인부 1인 값뿐" in known_gap_note("FP-12-26")
assert "같은 사람" in known_gap_note("FP-10-07-04") and "두 번" in known_gap_note("FP-10-07-04")
assert "특별인부 1인 값뿐" in known_gap_note("FP-10-08-03")
for code in ("FP-12-26", "FP-10-07-04", "FP-10-08-03"):
assert code in build.partial_ratio, code
@@ -15,7 +15,8 @@ from B09_Estimation.B09_Estimation_UnitPrice import cached_build
def test_기계_연료_줄이_안_붙은_다섯_공종은_일부만으로_막히고_사유() -> None:
build = cached_build()
for code in ("FP-12-26", "FP-14-02", "FP-14-01", "FP-10-08-03", "FP-10-07-04"):
# 14-1·14-2 는 뒤이어 시간 줄 판정표가 읽어 풀림(`test_b09_hour_rows` · 2026-09-15 ㉡-3)
for code in ("FP-12-26", "FP-10-08-03", "FP-10-07-04"):
assert code in build.partial_ratio, code
assert "기계·연료 줄" in (build.component_gaps.get(code) or ""), (
code,
@@ -23,7 +24,7 @@ def test_기계_연료_줄이_안_붙은_다섯_공종은_일부만으로_막히
)
left = " ".join(build.unattached.get("FP-12-26", []))
assert "디젤엔진" in left and "양수기" in left, left
assert "0.8㎥ 굴착기" in " ".join(build.unattached.get("FP-14-02", []))
assert "FP-14-02" not in build.partial_ratio and "FP-14-01" not in build.partial_ratio
def test_머리_줄과_인력만인_정상_공종은_안_막힘() -> None: