From 9a758c4d27d96aa0918ee21f385e773037b44e20 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 13 Sep 2026 19:51:40 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(axis):=20=EB=AA=A8=EB=A5=B4=ED=84=B0?= =?UTF-8?q?=20=EC=A4=84=20=EA=B3=B5=EC=A2=85=20=EC=BD=94=EB=93=9C=20AX-WK-?= =?UTF-8?q?c0842a0d=20=EB=AA=A8=EB=A5=B4=ED=83=80=EB=A5=B4=20=EB=B0=B0?= =?UTF-8?q?=ED=95=A9=201:3=20=E2=80=94=20=EC=B0=B0=EC=8C=93=EA=B8=B0=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=ED=91=9C=EC=9D=98=20?= =?UTF-8?q?=EB=A7=89=ED=9E=8C=20=EC=A4=84=EC=9D=B4=20=EC=97=B4=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 산림 품셈엔 배합 절이 없어 건설 품셈 [건축] 9-1-1 을 옮김 — 보통인부 0.66인/㎥(모래체가름 포함). 시멘트 510kg·모래 1.10㎥ 는 자재 단가 층이 없어 안 붙은 줄로 드러냄(원문 할증 포함 값). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B09_Estimation/B09_Estimation_UnitPrice.py | 4 ++ B09_Estimation/B09_Estimation_WorkItems_AX.py | 59 +++++++++++++++++++ resources/library_structure/masonry_wet.json | 4 +- .../tester/test_b08_structure_unit_price.py | 4 +- resources/tester/test_b09_work_items_ax.py | 25 ++++++++ 5 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 B09_Estimation/B09_Estimation_WorkItems_AX.py create mode 100644 resources/tester/test_b09_work_items_ax.py diff --git a/B09_Estimation/B09_Estimation_UnitPrice.py b/B09_Estimation/B09_Estimation_UnitPrice.py index 0207f461..24029293 100644 --- a/B09_Estimation/B09_Estimation_UnitPrice.py +++ b/B09_Estimation/B09_Estimation_UnitPrice.py @@ -929,6 +929,10 @@ def build_unit_prices( from B09_Estimation.B09_Estimation_ParentSteps import attach_parent_steps attach_parent_steps(build, master) + # 산림 품셈에 절이 없는 공종(모르타르 배합 등) — 명세 2장 ② 갈래 WK. + from B09_Estimation.B09_Estimation_WorkItems_AX import attach_work_items_ax + + attach_work_items_ax(build) # 기계 수송비 — 기계경비의 셋째 몫(손료·운전경비·**수송비**). 거리가 있어야 선다. from B09_Estimation.B09_Estimation_Transport import attach_transport diff --git a/B09_Estimation/B09_Estimation_WorkItems_AX.py b/B09_Estimation/B09_Estimation_WorkItems_AX.py new file mode 100644 index 00000000..43f77128 --- /dev/null +++ b/B09_Estimation/B09_Estimation_WorkItems_AX.py @@ -0,0 +1,59 @@ +"""B09 원가계산 — **산림 품셈에 절이 없는 공종**(`AX-WK-*`)의 일위대가 (명세 2장 ② 갈래 WK). + +산림 품셈이 **부르기만 하고 품을 안 주는** 자리를 원문이 있는 다른 품셈에서 그대로 옮긴다. +⚠ 코드는 난수 8자리 · 동등 비교만(명세 2장 ④) — 읽기는 이름 칸이 맡음. +⚠ 값은 원문 그대로 · 줄마다 출처. 자재는 단가 층이 없어 **드러내기만** 함(치즐과 같은 자리). +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_PriceBook import PriceDetail, PriceKind, PriceTitle + +#: 모르타르 배합 1:3 — 찰쌓기·찰붙임 줄눈메꿈(산림 품셈 13-4-4 [주]④ · 13-5-1 [주]③ 0.009㎥/㎡). +MORTAR_MIX = "AX-WK-c0842a0d" + +WORK_ITEMS: dict[str, dict[str, Any]] = { + MORTAR_MIX: { + "name": "모르타르 배합", + "spec": "1:3", + "unit": "㎥", + "basis": ( + "건설공사 표준품셈(2026) [건축부문] 9-1-1 모르타르 배합(㎥당)" + " — 산림 품셈엔 배합 절이 없음 · 배합비 1:3 은 돌쌓기 시방(시멘트:잔골재 부피비) · 실무 울진 대흥(2024)·소광(2025)" + " 「모르타르배합 1:3」 과 같은 짜임" + ), + # (노임 코드, 수량, 칸) — 원문 [주]② 「배합이 포함된 것이며, 비빔은 제외」. + "labor": (("1002", "0.66", "보통인부 — 모래체가름 포함 칸(제외 칸 0.43)"),), + # 원문 참고자료 1:3 줄 · ※ 「위 재료량은 할증이 포함된 것이다」 — 할증 전 값이 아님. + "materials": ("시멘트 510 kg", "모래 1.10 ㎥"), + }, +} + + +def attach_work_items_ax(build: Any) -> None: + """`B-AX-WK-*` 제목을 세운다 — 노임 층이 없으면 안 세우고 사유를 남긴다.""" + for code, item in WORK_ITEMS.items(): + missing = [ref for ref, _, _ in item["labor"] if ref not in build.book.titles] + if missing: + build.component_gaps[code] = f"노임 단가 층이 없습니다 — {', '.join(missing)}" + continue + title_code = f"B-{code}" + build.book.add_title( + PriceTitle( + code=title_code, + kind=PriceKind.UNIT_PRICE, + name=item["name"], + spec=item["spec"], + unit=item["unit"], + ) + ) + for ref, quantity, cell in item["labor"]: + build.book.add_detail( + PriceDetail(title_code, ref, Decimal(quantity), note=f"{cell} · {item['basis']}") + ) + build.unattached[code] = [ + f"{material} — 자재 단가 층 없음(원문 할증 포함 값)" for material in item["materials"] + ] diff --git a/resources/library_structure/masonry_wet.json b/resources/library_structure/masonry_wet.json index 75e53841..b9c1cc9a 100644 --- a/resources/library_structure/masonry_wet.json +++ b/resources/library_structure/masonry_wet.json @@ -263,7 +263,7 @@ } ], "unit_price": { - "note": "하단 일위대가 틀(PLAN 3장 하단 ①) — 갈 곳 unit_price 줄마다 한 줄. 수량은 원단위 줄(from_row), 단가는 B09 단가표. 공종 코드는 인계 대응표(찰쌓기 FP-13-04-05 · 갈래 뒷길이)와 10장 판정(기초잡석 FP-12-25)을 따름. 모르터는 코드 미정이라 막힘으로 둠", + "note": "하단 일위대가 틀(PLAN 3장 하단 ①) — 갈 곳 unit_price 줄마다 한 줄. 수량은 원단위 줄(from_row), 단가는 B09 단가표. 공종 코드는 인계 대응표(찰쌓기 FP-13-04-05 · 갈래 뒷길이)와 10장 판정(기초잡석 FP-12-25)을 따름. 모르터는 산림 품셈에 배합 절이 없어 AX-WK-c0842a0d 모르타르 배합 1:3(건설 품셈 건축 9-1-1)", "rows": [ { "seq": 1, @@ -272,7 +272,7 @@ "work_item_code": "FP-13-04-05", "variant_from": "L3" }, - { "seq": 2, "name": "모르터", "from_row": 8 }, + { "seq": 2, "name": "모르터", "from_row": 8, "work_item_code": "AX-WK-c0842a0d" }, { "seq": 3, "name": "기초잡석", "from_row": 15, "work_item_code": "FP-12-25" } ] } diff --git a/resources/tester/test_b08_structure_unit_price.py b/resources/tester/test_b08_structure_unit_price.py index f930019c..5cb92450 100644 --- a/resources/tester/test_b08_structure_unit_price.py +++ b/resources/tester/test_b08_structure_unit_price.py @@ -254,4 +254,6 @@ def test_창구가_실제_단가표로_찰쌓기_갈래를_찾는다(client: Tes assert rows[1]["ref_code"] == "B-FP-13-04-05#55cm이하" assert rows[1]["labor"] > 0 and rows[1]["quantity"] == pytest.approx(2.5 * 1.09**0.5) assert rows[3]["ref_code"] == "B-FP-12-25" - assert rows[2]["reason"] == "공종 코드 미정" and table["complete"] is False + # 모르터 — 산림 품셈에 배합 절이 없어 AX-WK 모르타르 배합 1:3(2026-09-13) + assert rows[2]["ref_code"] == "B-AX-WK-c0842a0d" and rows[2]["labor"] > 0 + assert table["complete"] is True diff --git a/resources/tester/test_b09_work_items_ax.py b/resources/tester/test_b09_work_items_ax.py new file mode 100644 index 00000000..35ed26a2 --- /dev/null +++ b/resources/tester/test_b09_work_items_ax.py @@ -0,0 +1,25 @@ +"""산림 품셈에 절이 없는 공종 `AX-WK-*` (명세 2장 ② 갈래 WK · 2026-09-13). + +지키는 것 — 모르타르 배합 1:3 = 보통인부 0.66인/㎥(건설 품셈 건축 9-1-1, 모래체가름 포함) · +시멘트·모래는 자재 단가 층이 없어 금액에 안 들고 「안 붙은 줄」로 드러남. +""" + +from __future__ import annotations + +from decimal import Decimal + +from B09_Estimation.B09_Estimation_UnitPrice import cached_build +from B09_Estimation.B09_Estimation_WorkItems_AX import MORTAR_MIX + + +def test_모르타르_배합은_보통인부_066인이고_자재는_드러내기만() -> None: + build = cached_build() + title = build.book.title(f"B-{MORTAR_MIX}") + assert (title.name, title.spec, title.unit) == ("모르타르 배합", "1:3", "㎥") + money = build.book.resolve(f"B-{MORTAR_MIX}") + wage = build.book.resolve("1002").labor # 보통인부 + assert money.labor == wage * Decimal("0.66") and money.material == 0 + assert [label.split(" —")[0] for label in build.unattached[MORTAR_MIX]] == [ + "시멘트 510 kg", + "모래 1.10 ㎥", + ] From 980c5440a068f412b8b9d423f6bb67cd2bd745ac Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 13 Sep 2026 20:00:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(axis):=20=ED=91=9C=20=EC=9D=BD=EA=B8=B0?= =?UTF-8?q?=20=E2=80=94=20=EB=8B=A8=EB=81=8A=EA=B8=B0=205-16-1=20=EA=B3=B5?= =?UTF-8?q?=EC=A0=95=20=EC=A4=84=20=ED=95=A9=EC=82=B0=20=C2=B7=20=E3=80=8C?= =?UTF-8?q?(=EC=9D=B8/100m=EB=8B=B9)=E3=80=8D=20=EA=BC=B4=20=EB=B0=91?= =?UTF-8?q?=EC=88=98=2022=ED=91=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 단끊기는 공정 네 줄을 토질 갈래마다 더하고 [주]⑤ 작업반장 1인/보통인부 20인을 더함. 원문 합계 줄은 절취를 ㎥당으로 더한 값이라 안 씀(까닭을 적은 공종만 섬). 같은 모양 뭉기기 13-12-1 은 안 더함. 「단위:」 없이 분모만 적은 본문 밑수를 읽어 조림·시비·떼심기 22표가 밑수 없음에서 풀림. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- .../B08_Quantity_Build_WorkItemMaster.py | 3 +- B09_Estimation/B09_Estimation_ResourceAxis.py | 6 + .../B09_Estimation_ResourceAxis_ProcessSum.py | 120 +++++++++++++++ .../data_work_item_master/_manifest.json | 10 +- .../basis_missing_2026-01-01.json | 132 ---------------- .../work_item_master_2026-01-01.json | 144 +++++++++--------- resources/tester/test_b09_table_reading.py | 39 +++++ 7 files changed, 244 insertions(+), 210 deletions(-) create mode 100644 B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py diff --git a/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py b/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py index e87898d8..cfa70987 100644 --- a/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py +++ b/B08_Quantity/B08_Quantity_Build_WorkItemMaster.py @@ -276,8 +276,9 @@ BASIS_RE = re.compile(r"(\d[\d,.]*)\s*(㎥|m3|㎡|m2|㏊|ha|km|㎞|m|인|본|개 # ⚠ **「당」 또는 「단위:」 가 있어야 밑수다.** 둘 다 없으면 규격일 뿐이다 — # 만들다 실제로 걸렸다: `(무한궤도,0.7㎥)` 를 「0.7㎥당」으로 읽어 5건이 잘못 잡혔다. #: ⚠ `(단위: 인/㎡당)` 꼴 — **분모가 밑수**다. 값의 단위(인)를 밑수로 읽으면 뜻이 뒤집힌다. +#: ⚠ 「단위:」 없이 `(인/100m당)` 으로만 적은 표가 22 곳이다(조림 5장·단끊기 5-16-1 등, 2026-09-13). SOURCE_BASIS_RATIO_RE = re.compile( - r"[((]\s*단위\s*[::]\s*[^))/]+/\s*([\d,]*\.?\d*)\s*" + r"[((]\s*(?:단위\s*[::]\s*)?[^))/::]+/\s*([\d,]*\.?\d*)\s*" r"(㎥|m3|㎡|m2|㏊|ha|㎞|km|m|매|본|개소|개|인|공|kg|㎏|톤|ton|주|식|대)\s*당\s*[))]" ) diff --git a/B09_Estimation/B09_Estimation_ResourceAxis.py b/B09_Estimation/B09_Estimation_ResourceAxis.py index 1ef60584..e65014c9 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis.py @@ -441,6 +441,12 @@ def match_table( # 「구 분 | 콘크리트공(인) | 보통인부(인)」처럼 **열이 자원**이고 행은 규격 갈래 # (무근·철근·소형구조물)다. 행을 자원으로 읽는 길로 보내면 통째로 못 맞춘다 — # 콘크리트 타설(12-1)이 그래서 하나도 안 서고 있었다. + # 공정 줄을 더해 한 품이 되는 표(단끊기 5-16-1) — 사람이 적은 공종만. 아래 길은 표째 버린다. + from B09_Estimation.B09_Estimation_ResourceAxis_ProcessSum import match_process_sum_table + + if match_process_sum_table(node, table, catalog, result, basis_quantity, unit): + return + from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import match_transposed_table if match_transposed_table(node, table, catalog, result, basis_quantity, unit): diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py b/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py new file mode 100644 index 00000000..8cdc0f83 --- /dev/null +++ b/B09_Estimation/B09_Estimation_ResourceAxis_ProcessSum.py @@ -0,0 +1,120 @@ +"""B09 원가계산 — **공정 줄을 더해 한 품이 되는 표** 읽기 (자원 축 보조, 2026-09-13). + + 공정별 | 보통인부(인) | 비고 + 보통토사 | 경질ㆍ고사점토 및 자갈섞인 점토 | 호박돌 섞인 토사 + 절 취 | 2.4 | 3.3 | 5.4 + 수평잡기 및 단정리 | 0.34 | 0.34 | 0.34 + … + 합계 | 2.03 | 2.09 | 2.23 + +행은 공정, 열은 토질 갈래, 자원은 머리 한 칸. 갈래마다 공정 줄을 **더한 것**이 한 품이다. +지금은 「숫자 칸이 자원 열보다 많다」로 표째 버려져 단끊기 5-16-1 이 한 줄도 안 섰다. + +⚠ **모양만으로는 「더하는 표」인지 못 가른다** — 뭉기기 13-12-1 은 같은 모양인데 줄마다 단위가 + 달라(절취 ㎥ · 면고르기 시간/㎡) 더하면 틀린다. 그래서 **사람이 적은 공종만** 더한다. +⚠ 원문 합계 줄은 안 읽는다 — 줄 합과 다르면 **까닭을 적어 둔 공종만** 서고, 아니면 막는다. +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from B09_Estimation.B09_Estimation_ResourceAxis import ( + AxisResult, + ResourceCatalog, + ResourceRow, + UnmatchedRow, + parse_amount, +) + +_TOTAL_LABELS = ("계", "합계", "소계", "총계") + +#: 공종 → 근거 · 원문 합계 줄과 다를 때의 까닭 · [주]가 더하라는 직종(코드, 보통인부 몇 인당 1인). +PROCESS_SUM: dict[str, dict[str, Any]] = { + "FP-05-16-01": { + "basis": ( + "품셈 5-16-1 표 머리 (인/100m당) · [주]④ 「수평잡기 및 단정리, 잡석 및 뿌리정리," + " 성토면고르기, 고르기품은 100m당 품」 · [주]③ 절취량 0.15㎥/m 기준" + ), + "total_mismatch": ( + "원문 합계 줄(2.03·2.09·2.23)은 절취를 ㎥당(2.4÷15=0.16)으로 더한 값이라 줄 합과 다름" + " — 100m당 줄을 더함(STmate 단끊기 산출과 같음)" + ), + "per_workers": ("1001", Decimal(20), "[주]⑤ 작업반장 1인/보통인부 20인 가산"), + }, +} + + +def _tight(text: Any) -> str: + return "".join(str(text or "").split()) + + +def match_process_sum_table( + node: dict[str, Any], + table: dict[str, Any], + catalog: ResourceCatalog, + result: AxisResult, + basis_quantity: Decimal | None, + unit: str, +) -> bool: + """적어 둔 공종의 공정 합산 표를 읽는다. 그 공종이 아니면 `False`.""" + from B09_Estimation.B09_Estimation_ResourceAxis_Transposed import transposed_columns + + code = str(node.get("work_item_code") or "") + spec = PROCESS_SUM.get(code) + if spec is None: + return False + table_id = str(table.get("pum_table_id", "")) + rows = [[str(c).strip() for c in row] for row in (table.get("raw_row") or [])] + columns = transposed_columns(table, catalog) + + def block(cell: str, why: str) -> bool: + result.unmatched.append(UnmatchedRow(code, table_id, cell, why)) + result.partial_items[code] = why + return True + + labels = [cell for cell in (rows[0] if rows else []) if cell] + if len(columns) != 1 or len(labels) < 2 or basis_quantity in (None, 0): + return block(code, "공정 합산 표 모양이 아닙니다(자원 한 칸 · 갈래 줄 · 밑수)") + sums = [Decimal(0)] * len(labels) + totals: list[Decimal] | None = None + for cells in rows[1:]: + numbers = [value for value in (parse_amount(c) for c in cells[1:]) if value is not None] + if not cells or not cells[0] or not numbers: + continue + if len(numbers) != len(labels): + return block(cells[0], f"값 {len(numbers)} 개가 갈래 {len(labels)} 개와 안 맞습니다") + if _tight(cells[0]) in _TOTAL_LABELS: + totals = numbers + continue + sums = [total + value for total, value in zip(sums, numbers)] + if totals is not None and totals != sums and not spec.get("total_mismatch"): + return block("합계", f"원문 합계 {totals} 가 줄 합 {sums} 와 다릅니다") + + entry = columns[0][1] + members = [(entry, Decimal(1))] + if spec.get("per_workers"): + chief_code, workers, _note = spec["per_workers"] + chief = next((e for e in catalog.entries if e.code == chief_code), None) + if chief is None: + return block(chief_code, "[주]가 더하라는 직종이 카탈로그에 없습니다") + members.append((chief, Decimal(1) / workers)) + for label, total in zip(labels, sums): + for member, ratio in members: + result.rows.append( + ResourceRow( + work_item_code=code, + pum_table_id=table_id, + pum_form=str(table.get("pum_form", "")), + resource_kind=member.kind, + resource_code=member.code, + resource_name=member.name, + resource_spec=member.spec, + amount=total * ratio / basis_quantity, + amount_unit=unit, + raw_row_index=0, + variant=label, + ) + ) + return True diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 85381040..4169de1c 100644 --- a/resources/data_work_item_master/_manifest.json +++ b/resources/data_work_item_master/_manifest.json @@ -1,7 +1,7 @@ { "schema_version": "1.0", "dataset_id": "data_work_item_master_manifest", - "generated_at": "2026-09-13T18:54:39+09:00", + "generated_at": "2026-09-13T19:52:49+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": "11c49ecde0a611307a82fd98b3625ee6577c01c2f77db6e1f515b41bf83ce685", - "size_bytes": 861050 + "sha256": "6d1b46ce4e619f0f828dbf111e05a50c0bc22cff969fca1e38d5736f6e7dd440", + "size_bytes": 861142 }, { "file": "form_undetermined_2026-01-01.json", @@ -22,8 +22,8 @@ }, { "file": "basis_missing_2026-01-01.json", - "sha256": "cb97edff0f47183173e23549c77f98f0509441edecc291bfcabba23254e60e9d", - "size_bytes": 18770 + "sha256": "bc3b92c991e03ddd6a2ca837e4a03a0729e3022fb783f741e6c504f5cb3a2813", + "size_bytes": 15905 } ] } \ No newline at end of file diff --git a/resources/data_work_item_master/basis_missing_2026-01-01.json b/resources/data_work_item_master/basis_missing_2026-01-01.json index 9f31bba8..97f9d2b6 100644 --- a/resources/data_work_item_master/basis_missing_2026-01-01.json +++ b/resources/data_work_item_master/basis_missing_2026-01-01.json @@ -130,24 +130,6 @@ "pum_form": "requirement", "line": 2062 }, - { - "pum_table_id": "F0092", - "section": "5-1-1. 관목굴취", - "pum_form": "requirement", - "line": 2081 - }, - { - "pum_table_id": "F0093", - "section": "5-1-2. 교목굴취(나무높이)", - "pum_form": "requirement", - "line": 2099 - }, - { - "pum_table_id": "F0094", - "section": "5-1-3. 교목굴취(근원직경)", - "pum_form": "requirement", - "line": 2125 - }, { "pum_table_id": "F0095", "section": "5-1-3. 교목굴취(근원직경)", @@ -160,108 +142,18 @@ "pum_form": "requirement", "line": 2177 }, - { - "pum_table_id": "F0098", - "section": "5-2. 뿌리돌림", - "pum_form": "requirement", - "line": 2194 - }, - { - "pum_table_id": "F0099", - "section": "5-3-1. 나무식재", - "pum_form": "requirement", - "line": 2219 - }, - { - "pum_table_id": "F0102", - "section": "5-3-2. 관목식재(단식)", - "pum_form": "requirement", - "line": 2254 - }, - { - "pum_table_id": "F0103", - "section": "5-3-3. 관목식재(군식)", - "pum_form": "requirement", - "line": 2272 - }, - { - "pum_table_id": "F0104", - "section": "5-3-4. 교목식재(나무높이)", - "pum_form": "requirement", - "line": 2291 - }, - { - "pum_table_id": "F0106", - "section": "5-3-5. 교목식재(흉고직경)", - "pum_form": "requirement", - "line": 2322 - }, { "pum_table_id": "F0108", "section": "5-3-5. 교목식재(흉고직경)", "pum_form": "requirement", "line": 2355 }, - { - "pum_table_id": "F0109", - "section": "5-4. 파종조림", - "pum_form": "requirement", - "line": 2366 - }, - { - "pum_table_id": "F0110", - "section": "5-5. 천연하종갱신", - "pum_form": "requirement", - "line": 2383 - }, - { - "pum_table_id": "F0111", - "section": "5-6. 움싹갱신", - "pum_form": "requirement", - "line": 2396 - }, - { - "pum_table_id": "F0112", - "section": "5-7. 생태보완조림", - "pum_form": "requirement", - "line": 2409 - }, - { - "pum_table_id": "F0113", - "section": "5-8. 큰나무 공익조림", - "pum_form": "requirement", - "line": 2426 - }, - { - "pum_table_id": "F0114", - "section": "5-9. 해안조림", - "pum_form": "requirement", - "line": 2439 - }, - { - "pum_table_id": "F0116", - "section": "5-11. 사초심기", - "pum_form": "requirement", - "line": 2475 - }, { "pum_table_id": "F0117", "section": "5-12. 떼붙임(재배잔디)", "pum_form": "requirement", "line": 2496 }, - { - "pum_table_id": "F0118", - "section": "5-13. 떼심기", - "pum_form": "requirement", - "line": 2511 - }, - { - "pum_table_id": "F0121", - "section": "5-16-1. 단끊기", - "pum_form": "requirement", - "line": 2565 - }, { "pum_table_id": "F0126", "section": "5-19-1. 표토절취 및 모으기", @@ -364,24 +256,6 @@ "pum_form": "requirement", "line": 3244 }, - { - "pum_table_id": "F0164", - "section": "6-6. 가지치기 및 수형교정", - "pum_form": "requirement", - "line": 3276 - }, - { - "pum_table_id": "F0165", - "section": "6-7-1. 교목 시비", - "pum_form": "requirement", - "line": 3304 - }, - { - "pum_table_id": "F0166", - "section": "6-7-2. 관목 시비", - "pum_form": "requirement", - "line": 3318 - }, { "pum_table_id": "F0170", "section": "7-1-1. 수확", @@ -490,12 +364,6 @@ "pum_form": "requirement", "line": 3763 }, - { - "pum_table_id": "F0201", - "section": "8-1-1. 약제주입기", - "pum_form": "requirement", - "line": 3945 - }, { "pum_table_id": "F0202", "section": "8-1-2. 약제주입병", diff --git a/resources/data_work_item_master/work_item_master_2026-01-01.json b/resources/data_work_item_master/work_item_master_2026-01-01.json index e5c9082c..086aa87f 100644 --- a/resources/data_work_item_master/work_item_master_2026-01-01.json +++ b/resources/data_work_item_master/work_item_master_2026-01-01.json @@ -2,7 +2,7 @@ "schema_version": "1.0", "dataset_id": "work_item_master_forest", "effective_date": "2026-01-01", - "generated_at": "2026-09-13T18:54:39+09:00", + "generated_at": "2026-09-13T19:52:49+09:00", "dataset_version": { "dataset_id": "pum_forest", "effective_date": "2026-01-01", @@ -21,9 +21,9 @@ "tables_attached": 456, "tables_orphan": 19, "form_undetermined": 77, - "basis_found": 182, - "basis_missing": 138, - "basis_grouped": 37 + "basis_found": 204, + "basis_missing": 116, + "basis_grouped": 48 }, "orphan_tables": [ { @@ -12716,7 +12716,7 @@ "form_basis": "직종 표기((인)·인부·공)", "basis_quantity": 100.0, "basis_unit": "본", - "basis_source": "절 이름", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -12873,7 +12873,7 @@ "form_basis": "직종 표기((인)·인부·공)", "basis_quantity": 1000.0, "basis_unit": "㎡", - "basis_source": "절 이름", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13185,9 +13185,9 @@ "source_line": 2081, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13247,9 +13247,9 @@ "source_line": 2099, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13351,9 +13351,9 @@ "source_line": 2125, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13737,9 +13737,9 @@ "source_line": 2194, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -13893,9 +13893,9 @@ "source_line": 2219, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1000.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14088,9 +14088,9 @@ "source_line": 2254, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14150,9 +14150,9 @@ "source_line": 2272, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14212,9 +14212,9 @@ "source_line": 2291, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14389,9 +14389,9 @@ "source_line": 2322, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14647,9 +14647,9 @@ "source_line": 2366, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14722,9 +14722,9 @@ "source_line": 2383, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14777,9 +14777,9 @@ "source_line": 2396, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14822,9 +14822,9 @@ "source_line": 2409, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14881,9 +14881,9 @@ "source_line": 2426, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -14928,9 +14928,9 @@ "source_line": 2439, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -15062,9 +15062,9 @@ "source_line": 2475, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -15158,9 +15158,9 @@ "source_line": 2511, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "㎡", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -15362,9 +15362,9 @@ "source_line": 2565, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "m", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -18216,9 +18216,9 @@ "source_line": 3276, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "본", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -18331,9 +18331,9 @@ "source_line": 3304, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 10.0, + "basis_unit": "주", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -18396,9 +18396,9 @@ "source_line": 3318, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 100.0, + "basis_unit": "㎡", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], @@ -21434,9 +21434,9 @@ "source_line": 3945, "pum_form": "requirement", "form_basis": "직종 표기((인)·인부·공)", - "basis_quantity": null, - "basis_unit": null, - "basis_source": null, + "basis_quantity": 1.0, + "basis_unit": "ha", + "basis_source": "본문", "resource_shares": {}, "partial_ratio": false, "expression_cells": [], diff --git a/resources/tester/test_b09_table_reading.py b/resources/tester/test_b09_table_reading.py index 0d06686c..d49d576b 100644 --- a/resources/tester/test_b09_table_reading.py +++ b/resources/tester/test_b09_table_reading.py @@ -105,3 +105,42 @@ def test_단목베기_5m_미만이_실무값_근처로_선다(): total = build.book.resolve("B-FP-04-02-02#5m미만").total # 영월 설계내역 1.9.2 「잡관목제거 벌목(5m미만)」 @882 — 노임 연도 차이로 ±5 % 안이면 같은 읽기 assert Decimal("838") <= total <= Decimal("926"), total + + +def test_단위_글자_없는_분모형_밑수도_본문에서_읽는다(): + """「(인/100m당)」 — 「단위:」 없이 분모만 적은 22 곳(2026-09-13). 「인」은 밑수가 아니다.""" + from B08_Quantity.B08_Quantity_Build_WorkItemMaster import basis_from_source + + assert basis_from_source(["(인/100m당)", "", "| 공정별 |"], 3) == (100.0, "m") + assert basis_from_source(["(인/본당)", "| 규격 |"], 2) == (1.0, "본") + assert basis_from_source(["(무한궤도/0.7㎥)", "| 규격 |"], 2) == (None, None) # 「당」 없음 + + +_단끊기 = { + "pum_table_id": "F-단끊기", + "pum_form": "requirement", + "basis_quantity": 100.0, + "basis_unit": "m", + "condition_note": ["공정별", "보통인부(인)", "비고"], + "raw_row": [ + ["보통토사", "경질ㆍ고사점토 및 자갈섞인 점토", "호박돌 섞인 토사", "", ""], + ["절 취", "2.4", "3.3", "5.4", ""], + ["수평잡기 및 단정리", "0.34", "0.34", "0.34", ""], + ["잡석 및 뿌리 정리", "0.36", "0.36", "0.36", ""], + ["절·성토면 고르기", "1.17", "1.17", "1.17", ""], + ["합계", "2.03", "2.09", "2.23", ""], + ], +} + + +def test_단끊기는_공정_줄을_더하고_작업반장을_20인당_1인_더한다(): + catalog = ResourceCatalog(entries=[*MACHINES, CatalogEntry("1001", "작업반장", "labor")]) + result = AxisResult() + match_table({"work_item_code": "FP-05-16-01"}, _단끊기, catalog, result) + assert _codes(result, "보통토사") == {"1002": Decimal("0.0427"), "1001": Decimal("0.002135")} + assert _codes(result, "호박돌 섞인 토사")["1002"] == Decimal("0.0727") + assert "FP-05-16-01" not in result.partial_items + # 적어 두지 않은 공종은 같은 모양이어도 더하지 않는다(뭉기기 13-12-1 은 줄마다 단위가 다름) + other = AxisResult() + match_table({"work_item_code": "FP-13-12-01"}, _단끊기, catalog, other) + assert not other.rows