From 72ec6c2135794d0fb17a121d9b6655559a22c368 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 18:27:36 +0900 Subject: [PATCH] =?UTF-8?q?feat(b09):=2012-4=20=ED=95=A9=ED=8C=90=EA=B1=B0?= =?UTF-8?q?=ED=91=B8=EC=A7=91=20=EC=82=AC=EC=9A=A9=ED=9A=9F=EC=88=98=201~6?= =?UTF-8?q?=ED=9A=8C=20=EA=B0=88=EB=9E=98=20=E2=80=94=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=88=98=EB=9F=89(1=ED=9A=8C)=20=C3=97=20=EC=9B=90=EB=AC=B8=20?= =?UTF-8?q?=EB=B9=84=EC=9C=A8(=EC=9E=AC=EB=A3=8C=20=EC=A4=84=20=EC=9E=AC?= =?UTF-8?q?=EB=A3=8C=20%=20=C2=B7=20=EC=9D=B8=EB=A0=A5=20=EC=A4=84=20?= =?UTF-8?q?=EB=85=B8=EB=AC=B4=EB=B9=84=20%)=20=C2=B7=20=EC=84=A4=EA=B3=84?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=89=EC=95=84=20=EB=91=94=20=ED=91=9C=20?= =?UTF-8?q?=ED=92=80=EB=A6=BC=20=C2=B7=20=EC=82=AC=EC=9A=A9=EA=B3=A0?= =?UTF-8?q?=EC=9E=AC=2023%=C2=B7=EB=8F=99=EB=B0=94=EB=A6=AC=C2=B7=EC=86=8C?= =?UTF-8?q?=ED=98=95=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=ED=95=A0=EC=A6=9D?= =?UTF-8?q?=EC=9D=80=20=EA=B0=92=20=EC=95=88=20=EC=A7=93=EA=B3=A0=20?= =?UTF-8?q?=EC=82=AC=EC=9C=A0=20=C2=B7=20=EC=9E=AC=EB=A3=8C=EB=8A=94=20?= =?UTF-8?q?=EC=B9=B4=ED=83=88=EB=A1=9C=EA=B7=B8=20=EC=97=86=EC=9D=8C/?= =?UTF-8?q?=EA=B7=9C=EA=B2=A9=20=EB=AF=B8=EC=A0=95=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=AA=BB=20=EB=B6=99=EC=9D=80=20=EC=A4=84(=EC=9D=B8=EB=A0=A5?= =?UTF-8?q?=20=ED=92=88=EB=A7=8C)=20=C2=B7=20=EB=A7=88=EC=8A=A4=ED=84=B0?= =?UTF-8?q?=20=EA=B0=88=EB=9E=98=20=ED=82=A4=20=EC=9E=AC=EC=83=9D=EC=84=B1?= =?UTF-8?q?(=EB=B0=94=EB=80=90=20=EA=B3=B5=EC=A2=85=2012-04=20=ED=95=98?= =?UTF-8?q?=EB=82=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일위대가 새 제목 6(1회 81,321 → 4회 32,528 → 6회 26,023원/㎡) · 나머지 462 변화 0 (브레인 ㉮) Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn --- B09_Estimation/B09_Estimation_KnownGaps.py | 16 +++--- ...B09_Estimation_ResourceAxis_JudgedTable.py | 43 +++++++++++++++ .../data_work_item_master/_manifest.json | 6 +- .../work_item_master_2026-01-01.json | 20 ++++++- .../tester/test_b09_formwork_use_count.py | 55 +++++++++++++++++++ resources/tester/test_b09_table_reading.py | 4 +- 6 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 resources/tester/test_b09_formwork_use_count.py diff --git a/B09_Estimation/B09_Estimation_KnownGaps.py b/B09_Estimation/B09_Estimation_KnownGaps.py index 6fc5102a..c34d40f2 100644 --- a/B09_Estimation/B09_Estimation_KnownGaps.py +++ b/B09_Estimation/B09_Estimation_KnownGaps.py @@ -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`). } diff --git a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py index d15d3f29..304f29ad 100644 --- a/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py +++ b/B09_Estimation/B09_Estimation_ResourceAxis_JudgedTable.py @@ -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 diff --git a/resources/data_work_item_master/_manifest.json b/resources/data_work_item_master/_manifest.json index 9026c366..0f31fe5e 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-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", 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 625475cf..a01f45d0 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 @@ -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", diff --git a/resources/tester/test_b09_formwork_use_count.py b/resources/tester/test_b09_formwork_use_count.py new file mode 100644 index 00000000..10ce1af6 --- /dev/null +++ b/resources/tester/test_b09_formwork_use_count.py @@ -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 diff --git a/resources/tester/test_b09_table_reading.py b/resources/tester/test_b09_table_reading.py index 05048bff..91825855 100644 --- a/resources/tester/test_b09_table_reading.py +++ b/resources/tester/test_b09_table_reading.py @@ -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