From f3630ab1e7e20fab95453fb6ef9ee83dda556df4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 08:03:12 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(b09):=20=EA=B7=9C=EA=B2=A9=EC=9D=B4=20?= =?UTF-8?q?=EC=97=B4=EB=A1=9C=20=EC=84=A0=20=ED=91=9C(=EA=B4=80=EB=B6=80?= =?UTF-8?q?=EC=84=A4=2012-11)=20=E2=80=94=20=EA=B7=9C=EA=B2=A9=20=EC=B9=B8?= =?UTF-8?q?=C2=B7=EC=9B=90=EB=AC=B8=20=EB=B9=84=EA=B3=A0=EB=A5=BC=20?= =?UTF-8?q?=EC=9D=BD=EC=96=B4=20=EC=A4=84=EC=9D=80=20=EC=84=9C=EA=B3=A0=20?= =?UTF-8?q?=EB=AA=BB=20=EB=84=A3=EB=8A=94=20=EC=A4=84=EC=9D=80=20=EA=B9=8C?= =?UTF-8?q?=EB=8B=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원문 비고 「별산」·「별도계산」·「설계수량」(〃·병합 칸 포함)·「필요시적용」은 안 넣는 줄로 까닭을 남김 - 규격 칸(1:2)과 관경 열(∅800mm)을 조인 키로 — 접합몰탈·고무링이 자재 줄로 섬(단가 층은 아직 없음) - 크레인은 기종이 여럿이라 고르지 않음 — 「규격 미정 · 원문 규격 10ton/5ton」(브레인 판정 대기) - 옆 칸 규격 「40.64㎝」 = 카탈로그 절단기 40.64(기계 운전 자료가 없어 층은 못 섬 — 사유로 드러남) - 카탈로그 밖 자원 목록에 고무링 ∅800/1000/1200mm · 지수활제 보탬(원문 F0346 · 단가 없음) - 범위 81공종 상태 전후 같음(못 붙은 줄의 까닭만 바뀜) · 시험 4건 · 전체 1749 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- .../B09_Estimation_ResourceAxis_ChooseOne.py | 73 +++++++++++++++---- .../B09_Estimation_ResourceAxis_Sources.py | 3 +- .../resource_catalog_ext_2026-01-01.json | 52 +++++++++++++ .../tester/test_b09_spec_column_table.py | 62 ++++++++++++++++ 4 files changed, 173 insertions(+), 17 deletions(-) create mode 100644 resources/tester/test_b09_spec_column_table.py diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py b/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py index bcdb40ee..e1627f85 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_ChooseOne.py @@ -269,34 +269,75 @@ def match_spec_column_table( if len(specs) < 2 or not all(_SPEC_HEAD.match(spec) for spec in specs): return False + from B09_Estimation.B09_Estimation_ResourceAxis_Join import resolve_family, unmatched_reason + unit = table.get("basis_unit") or "" work_item_code = str(node.get("work_item_code", "")) table_id = str(table.get("pum_table_id", "")) form = str(table.get("pum_form", "")) made = 0 + previous_note = "" + + def unmatched(cell: str, reason: str) -> None: + result.unmatched.append( + UnmatchedRow( + work_item_code=work_item_code, pum_table_id=table_id, cell=cell, reason=reason + ) + ) + for index, row in enumerate(rows[1:], start=1): cells = [_clean(cell) for cell in row] - if not cells: + if not cells or not cells[0]: continue - name, spec_text = split_name_and_spec(cells[0]) - entry = catalog.resolve(name, spec_text) or catalog.resolve(name, "") - if entry is None: - result.unmatched.append( - UnmatchedRow( - work_item_code=work_item_code, - pum_table_id=table_id, - cell=cells[0], - reason="카탈로그에 없는 이름 (규격이 열로 선 표) — 0 으로 때우지 않습니다.", - ) + # 줄 모양 — | 구분 | 규격 | 단위 | 관경별 값 … | 비고 | (2026-09-14 · 원문 12-11 표 머리) + values = [ + value for value in (_cell_amount(cell) for cell in cells[1:]) if value is not None + ] + note = cells[-1] if len(cells) > 1 and _cell_amount(cells[-1]) is None else "" + note = previous_note if note == "〃" else note + spec_cell = cells[1] if len(cells) > 1 and _cell_amount(cells[1]) is None else "" + if not values and not note and previous_note.replace(" ", "") == "설계수량": + note = previous_note # 원문 병합 칸 — 바로 위 줄 비고를 따름(흄관 거푸집) + previous_note = note + tight_note = note.replace(" ", "") + # ⚠ 원문 비고가 「이 일위대가 밖」인 줄 — 못 찾은 게 아니라 안 넣는 것 · 까닭을 남김. + if tight_note in ("별산", "별도계산"): + unmatched( + cells[0], f"「{note}」 — 원문 비고대로 이 일위대가 밖에서 따로 셈 · 여기 안 넣음" ) continue - values = [_cell_amount(cell) for cell in cells[1:]] - values = [value for value in values if value is not None] - if len(values) < len(specs): - # 값이 빈 규격이 있다 — 「별산」 줄이거나 표가 덜 찼다. 짐작해 채우지 않는다. + if tight_note == "설계수량": + unmatched(cells[0], "「설계수량」 — 원문 비고대로 설계 물량으로 따로 셈 · 여기 안 넣음") continue - for spec_name, amount in zip(specs, values[-len(specs) :]): + if len(values) < len(specs): + why = f"「{note}」 — " if note else "" + unmatched(cells[0], f"{why}관경별 값이 비어 짐작해 채우지 않음") + continue + name, inline_spec = split_name_and_spec(cells[0]) + row_specs = [text for text in (spec_cell, inline_spec) if text] + entries: list[CatalogEntry] = [] + for spec_name in specs: + entry = ( + next( + ( + found + for text in (*row_specs, spec_name) + if (found := catalog.resolve(name, text)) is not None + ), + None, + ) + or catalog.resolve(name, "") + or resolve_family(catalog, cells[0], cells) + ) + if entry is None: + break + entries.append(entry) + if len(entries) < len(specs): + origin = f" · 원문 규격 {spec_cell}" if spec_cell else "" + unmatched(cells[0], f"{unmatched_reason(catalog, name)}{origin} (규격이 열로 선 표)") + continue + for spec_name, amount, entry in zip(specs, values[-len(specs) :], entries): result.rows.append( ResourceRow( work_item_code=work_item_code, diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_Sources.py b/B09_Estimation/B09_Estimation_ResourceAxis_Sources.py index c15d7c09..f696c0c6 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_Sources.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_Sources.py @@ -168,7 +168,8 @@ def spec_candidates(cells: list[str]) -> list[str]: if spec: found.append(spec) continue - token = _RE_SIZE_TOKEN.fullmatch(text.rstrip("㎥㎡톤tonm³")) + # 「40.64㎝」(절단기 12-11-2) — 카탈로그는 수만 적음(7620-0003 절단기 40.64). + token = _RE_SIZE_TOKEN.fullmatch(text.rstrip("㎥㎡톤tonm³㎝")) if token: found.append(token.group(0)) return found diff --git a/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json b/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json index cd7c679b..a780760c 100644 --- a/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json +++ b/resources/data_resource_catalog/resource_catalog_ext_2026-01-01.json @@ -259,6 +259,58 @@ "F0243" ] } + }, + { + "code": "AR-M-423cfb79", + "kind": "material", + "name": "고무링", + "spec": "∅800mm", + "unit": "개", + "source": { + "pum_edition": "2026-01-01", + "pum_table_ids": [ + "F0346" + ] + } + }, + { + "code": "AR-M-18c0ce85", + "kind": "material", + "name": "고무링", + "spec": "∅1000mm", + "unit": "개", + "source": { + "pum_edition": "2026-01-01", + "pum_table_ids": [ + "F0346" + ] + } + }, + { + "code": "AR-M-d9d5aa16", + "kind": "material", + "name": "고무링", + "spec": "∅1200mm", + "unit": "개", + "source": { + "pum_edition": "2026-01-01", + "pum_table_ids": [ + "F0346" + ] + } + }, + { + "code": "AR-M-0cc44237", + "kind": "material", + "name": "지수활제", + "spec": "", + "unit": "g", + "source": { + "pum_edition": "2026-01-01", + "pum_table_ids": [ + "F0346" + ] + } } ] } diff --git a/resources/tester/test_b09_spec_column_table.py b/resources/tester/test_b09_spec_column_table.py new file mode 100644 index 00000000..270a8ba6 --- /dev/null +++ b/resources/tester/test_b09_spec_column_table.py @@ -0,0 +1,62 @@ +"""규격이 열로 선 표(관부설 12-11) — 규격 칸·비고를 읽어 줄은 서고 못 넣는 줄은 까닭으로 드러남. + +2026-09-14 축 C 1장 ② (브레인 차례 · 판정 불필요). 원문 = 산림사업 표준품셈 12-11-1·2·3. + ① 원문 비고 「별산」·「별도계산」·「설계수량」(〃 · 병합 칸 포함)은 **안 넣는 줄**로 까닭을 남김 + ② 규격 칸(「1:2」)과 관경 열(「∅800mm」)이 조인 키 — 접합몰탈·고무링이 자재 줄로 섬 + ③ 기종이 여럿인 크레인은 **고르지 않고** 「규격 미정 · 원문 규격 10ton」 + ④ 옆 칸 규격 「40.64㎝」 = 카탈로그 절단기 40.64 +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B09_Estimation.B09_Estimation_ResourceAxis import ( # noqa: E402 + build_resource_axis, + load_combined_catalog, + load_work_item_master, +) + +AXIS = build_resource_axis(load_work_item_master(), load_combined_catalog()) + + +def _unmatched(code: str) -> dict[str, str]: + return {u.cell: u.reason for u in AXIS.unmatched if u.work_item_code == code} + + +def _rows(code: str) -> list: + return [r for r in AXIS.rows if r.work_item_code == code] + + +def test_원문_비고대로_안_넣는_줄은_까닭을_남긴다() -> None: + vr = _unmatched("FP-12-11-01") + assert "별도계산" in vr["VR관"] and "설계수량" in vr["기초콘크리트"] + assert "설계수량" in vr["거 푸 집"] # 「〃」 + hume = _unmatched("FP-12-11-02") + assert "별산" in hume["흄 관"] and "설계수량" in hume["거 푸 집"] # 병합 칸 + corrugated = _unmatched("FP-12-11-03") + assert "필요시적용" in corrugated["커플링밴드"] and "설계수량" in corrugated["모래부설"] + + +def test_규격_칸과_관경_열이_조인_키() -> None: + rings = { + r.variant: r.resource_spec for r in _rows("FP-12-11-01") if r.resource_name == "고무링" + } + assert rings == {"∅800mm": "∅800mm", "∅1000mm": "∅1000mm", "∅1200mm": "∅1200mm"} + mortar = [r for r in _rows("FP-12-11-02") if r.resource_name == "접합몰탈"] + assert {r.resource_spec for r in mortar} == {"1:2"} and len(mortar) == 3 + + +def test_크레인은_고르지_않고_원문_규격을_보인다() -> None: + reason = _unmatched("FP-12-11-03")["크레인"] + assert "규격 미정" in reason and "원문 규격 5ton" in reason + assert not any(r.resource_name.startswith("크레인") for r in _rows("FP-12-11-03")) + + +def test_절단기_40_64cm_는_카탈로그_40_64() -> None: + cutters = [r for r in _rows("FP-12-11-02") if r.resource_name == "절단기"] + assert [(r.resource_code, str(r.amount)) for r in cutters] == [("7620-0003", "0.93")] From fd324ce53181dad574e343fb7d36c70f5cb40c72 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 08:03:13 +0900 Subject: [PATCH 2/2] auto: 2026-09-14 08:03 (ESD_LAPTOP) --- docs/raw/verification/2026-09-13_축C_명세.md | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/raw/verification/2026-09-13_축C_명세.md b/docs/raw/verification/2026-09-13_축C_명세.md index 2b3a1862..bcb21055 100644 --- a/docs/raw/verification/2026-09-13_축C_명세.md +++ b/docs/raw/verification/2026-09-13_축C_명세.md @@ -90,6 +90,24 @@ FP-<절>-<절>-<절> 예) FP-04-02-02 (벌목, 5m 미만) - 정본은 **품셈 절 번호**. 표의 행은 그 절의 **조건별 변형**(`variant` 칸)임 - **이름은 표시용, 잇기는 코드로.** 이름으로 맞추는 코드가 남아 있으면 그것이 곧 결함임 + +### ⭐ 어느 품셈 판인가 — 2026-09-14 확인 (조문 근거 있음) + +**`FP-` 는 산림사업 표준품셈 한 판임.** 건설공사 표준품셈은 **공종 축이 아니라 기계 층에만** 들어옴. + +``` +공종 축 477/477 FP- · 원천 pum_forest_2026.json (산림청고시 2025-82호, 2026-01-01) +기계 층 pum_const_2026.json · mach_base_2026.json (건설품셈 제8장 — 취득가·손료계수·연료·운전원) +빌려 온 몇 곳 「교차 참조」 딱지를 붙임 (예: 뿌리뽑기 밑수 3-9-2) +``` + +**근거 — 같은 고시 총칙 7**: 「본 품셈에 명시되지 않은 품은 타 부문(토목, 건축, 전기, 기계 등)의 +표준품셈에 명시된 부분의 품을 적용하고, **타 부문과 유사한 공종의 품은 본 품셈을 우선하여 적용**한다.」 + +⇒ 지금 얼개가 이 조문 그대로임. **건설품셈 절번호로 선 공종 줄은 0 건**(소스에 「8-2-8」 참조 0건). +⚠ 자료를 찾을 때 건설품셈만 보면 「값이 없다」고 잘못 결론 내기 쉬움 — 덤프 운반이 그랬음 +(건설 8-2-8 에는 단위중량·적재용량 표가 없으나 **산림 10-12 [주]에 토사 1.9 · 암 2.4 ton/㎥ · 덤프 15 ton 이 있었음**). +**막히면 산림품셈 쪽을 먼저 볼 것.** - 부모·잎이 있음. ⚠ **「부모에 수량 금지」는 반쪽만 맞았음** — 2026-09-13 정정. **부모가 두 모양이고 규칙이 다름**(랩탑 메인 실측). @@ -334,6 +352,31 @@ B08 인계본 `ground_class_aliases` — **B09 가 안 읽음**)을 한 벌로 | **단가산출 성분단가** | 소수 1자리 절사 | 211건 중 137건 설명 | | 내부 계산 | 은행가(최근접-짝수) | STmate 내부. ⚠ 뽑은 엑셀은 사사오입이라 20개 경계 중 10개가 갈림 | +### ⭐⭐ 2026-09-14 — 위 실측이 **고시 조문과 그대로 맞았음** + +**산림청고시 제2025-82호 「산림사업 표준품셈」 1-2-2 「2. 금액의 단위표준」**: + +| 종목 | 단위 | 지위 | 비고 | 우리 실측과 | +| --- | --- | --- | --- | --- | +| 설계서의 **총액** | 원 | **1,000** | 미만버림 | ⚠ **아직 확인 안 됨** — 데스크탑 서브가 재는 중 | +| 설계서의 소계 | 원 | 1 | 미만버림 | ✅ | +| 설계서의 **금액란** | 원 | 1 | 미만버림 | ✅ 「내역에 실릴 때 정수 절사」와 같음 | +| **일위대가표의 계금** | 원 | 1 | 미만버림 | ✅ 「호표 안 성분 소계 절사」와 같음 | +| **일위대가표의 금액란** | 원 | **0.1** | 미만버림 | ✅ 「단가산출 성분단가 소수 1자리 절사」와 같음 | + +⇒ **STmate 를 재서 얻은 규칙이 법 조문이었음.** 경쟁사를 베낀 것이 아니라 둘 다 같은 고시를 따른 것임. + +**같은 고시 1-2-2 「1. 수량의 단위표준」 [주]**: + +``` +① 설계서 수량의 단위와 소수자리 표시는 본 표에 따르며, 반올림하여 적용 +② 품목이 제시한 소숫자리가 본 표와 다르면 항목 쪽을 우선 +③ 본 표에 없는 품은 유사품의 규격·단위수량을 참고, C.G.S 단위 원칙 +``` + +⇒ 아래 「규정이 없는 자리 = 소수 2자리」는 **이 표에도 없는 자리에만** 적용할 것. +표에 실린 품목(관류 길이 m 소수 2 · 수로연장 m 소수 1 · 옹벽 ㎡ 소수 1 …)은 **표가 규정임.** + ### ⭐ 규정이 없는 자리 — 소수점 2자리가 기본값 (2026-09-13 사용자 확정) **단수처리에 법적 근거가 없음이 확인됐음** — 기재부 **예정가격작성기준 전 조문에