From acb0a3dfcf46e1d25151fbd57344691987d31e0e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 22:22:54 +0900 Subject: [PATCH] =?UTF-8?q?fix(B08):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=ED=84=B0=ED=8C=8C=EA=B8=B0=C2=B7=EB=90=98=EB=A9=94=EC=9A=B0?= =?UTF-8?q?=EA=B8=B0=C2=B7=EC=9E=94=ED=86=A0=EB=A5=BC=20=EC=9D=B8=EA=B3=84?= =?UTF-8?q?=20=EC=B6=95=EC=9C=BC=EB=A1=9C=20=EC=98=AC=EB=A6=BC=20=E2=80=94?= =?UTF-8?q?=20=EB=B9=A0=EB=9C=A8=EB=A0=B8=EB=8D=98=20=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 구조물 전개가 destination `earthwork` 로 내던 셋을 아무도 받지 않아 내역서에 한 줄도 안 나갔음(두께 식·기초 몫을 맞춰도 금액 0원). 실무 토적집계에는 서는 줄 (울진 대흥 1공구 D12~D14 구조물터파기 토사 1,248/암 30 · 되메우기 739㎥) - 새 파일 `Engine_Handoff_Trench.py` — 700줄 제한 탓에 Rows 에 안 넣음 - 구조물터파기: 심도(직고 + 기초 깊이)로 갈라 냄. 토질·용수 칸이 없어 품셈 9-13 의 18구분 중 하나를 못 골라 상위 코드 + `input_missing` 사유로 둠 - 되메우기: FP-09-14-01 로 붙어 금액이 섬 - 잔토처리: 사토로 갈 몫이라 in_bill=False, ⚠ 그 통로가 아직 없다는 사실을 사유에 적음 - 매핑에 구조물터파기 줄 추가(FP-09-13 · 원문 L5022~5241) - tmp/tests/test_b08_structure_trench_rows.py 신설(6건) Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 7 + .../B08_Quantity_Engine_Handoff_Trench.py | 181 ++++++++++++++++++ .../work_item_mapping_2026-01-01.json | 16 +- 3 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index e6d72e12..b8c6d7ca 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -43,6 +43,7 @@ from __future__ import annotations from typing import Any, Iterable from B08_Quantity.B08_Quantity_Engine_BasisUnit import verify_unit_matches_basis +from B08_Quantity.B08_Quantity_Engine_Handoff_Trench import structure_earthwork_rows # ⚠ 파일만 갈랐고 **계약은 그대로다** — 종전에 이 이름으로 가져다 쓰던 곳이 그대로 돌게 # 여기서 다시 내보낸다(2026-09-08 분리). @@ -124,6 +125,12 @@ def build_handoff( rows, misses = _structure_rows(unit_quantity_table, table) work_items.extend(rows) unmatched.extend(misses) + # 구조물이 낸 터파기·되메우기·잔토 — **공종 축으로 올린다.** + # ⚠ 종전에는 성분으로만 있고 아무도 안 받아 **내역서에 한 줄도 안 나갔다** + # (2026-09-08 B09 매김에서 드러남). 실무 토적집계에는 서는 줄이다(울진 D12~D14). + rows, misses = structure_earthwork_rows(unit_quantity_table, table) + work_items.extend(rows) + unmatched.extend(misses) # 준비공·사방공 — **못 내는 줄도 사유와 함께** 보낸다(빼면 빠진 줄이 안 보인다). # B군 종단배수 — 겹침을 합친 연장으로 종류별 한 줄(위 `_length_rows` 주석). diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py new file mode 100644 index 00000000..90c8cc62 --- /dev/null +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py @@ -0,0 +1,181 @@ +"""구조물 터파기·되메우기·잔토 인계 줄 (2026-09-08). + +⚠⚠ **빠뜨렸던 자리다.** 구조물 전개는 이 셋을 `destination: earthwork` 로 내는데, + 토공집계표는 **토적표만** 읽어 만들어져 그 성분을 아무도 받지 않았다. 그래서 두께 식· + 기초 몫을 아무리 맞춰도 **내역서에 한 줄도 안 나갔다**(2026-09-08 B09 매김에서 드러남). + +실무 내역에는 서는 줄이다 — 울진 대흥 1공구 토적집계 D12~D14: + `구조물터파기 토사 1,248 / 암 30 ㎥` · `되메우기 739 ㎥` + +⚠ **품셈 9-13 은 18구분이다**(토질 3 × 육상/용수 × 심도 3). 우리는 **심도만** 안다 + (터파기 깊이 = 직고 + 기초 깊이). 토질·용수는 저장 제원에 칸이 없으므로 **지어내지 않고** + 상위 코드로 세운 뒤 사유를 붙인다 — 심도 갈래는 미리 갈라 두어 칸이 생기는 날 바로 붙는다. + +⚠ **잔토는 내역 줄로 세우지 않는다** — 사토로 실어 내는 몫이라 유토곡선(B06)이 세야 겹치지 + 않는다. 다만 **지금 그 통로가 없다**(채집석 공제만 있다). 값을 버리지 않고 `in_bill=False` + 로 넘기며 그 사실을 사유에 적는다 — 빼 버리면 빠진 줄을 아무도 못 찾는다. +""" + +from __future__ import annotations + +from typing import Any + +from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import ( + BLOCKED_INPUT_MISSING, + ORIGIN_EARTHWORK, + WorkItemMapping, +) +from common_util.common_util_excavation import ( + WALL_BLINDING_DEPTH_M, + WALL_FOUNDATION_DEPTH_M, +) + +#: 품셈 9-13 심도 갈래 — 원문 표기(`0~1m · 1~2m · 2~3m`)를 그대로 쓴다. +DEPTH_BANDS: tuple[tuple[float, str], ...] = ((1.0, "0~1m"), (2.0, "1~2m"), (3.0, "2~3m")) +DEPTH_OVER = "3m 초과" + +TRENCH_GROUP = "구조물터파기" +BACKFILL_GROUP = "되메우기" +SPOIL_GROUP = "잔토처리" + +TRENCH_BLOCKED_REASON = ( + "토질(토사·암절취·발파암)과 용수 유무가 저장 제원에 없어 품셈 9-13 의 18구분 중 어느 칸인지" + " 못 고름 — 심도만 구조물 제원(직고 + 기초 깊이)에서 갈라 둠" +) +SPOIL_REASON = ( + "사토로 실어 내는 몫이라 유토곡선(B06)이 세야 겹치지 않음 — ⚠ 지금 그 통로가 없어" + " 어디에도 안 실림. 값을 버리지 않고 사유와 함께 넘김" +) + + +def _depth_band(depth_m: float) -> str: + for limit, label in DEPTH_BANDS: + if depth_m <= limit: + return label + return DEPTH_OVER + + +def _foundation_depth(options: dict[str, Any]) -> float: + """기초 깊이 — 「기초유」 0.5 · 「기초버림」 0.1 · 안 고르면 0(비탈분만).""" + value = str(options.get("foundation") or "").strip() + if value == "기초유": + return WALL_FOUNDATION_DEPTH_M + if value == "기초버림": + return WALL_BLINDING_DEPTH_M + return 0.0 + + +def _row(**fields: Any) -> dict[str, Any]: + """줄 한 벌 — 계약이 요구하는 칸을 **모두** 채운다(빌더마다 같은 모양이라야 한다).""" + row: dict[str, Any] = { + "work_item_code": None, + "name": "", + "spec": "", + "unit": "㎥", + "quantity": 0.0, + "quantity_gross": None, + "application_ratio_pct": None, + "application_ratio_breakdown": None, + "quantity_breakdown": None, + "ground_class": None, + "haul_distance_m": None, + "haul_equipment": None, + "station_from": None, + "station_to": None, + "excavation_method": None, + "spec_detail": "", + "composite_parts": None, + "structure_kind": None, + "blocked_kind": None, + "blocked_reason": "", + "variant_axis": None, + "variant_value": None, + "secondary_axes": None, + "spec_class": None, + "spec_class_basis": "", + "composite_not_ready": None, + "in_bill": True, + "in_bill_reason": "", + "origin": ORIGIN_EARTHWORK, + } + row.update(fields) + return row + + +def structure_earthwork_rows( + unit_quantity_table: dict[str, Any], mapping: WorkItemMapping +) -> tuple[list[dict[str, Any]], list[str]]: + """구조물이 낸 터파기·되메우기·잔토를 **공종 축**으로 올린다.""" + trench: dict[str, float] = {} + backfill = 0.0 + spoil = 0.0 + for structure in unit_quantity_table.get("structures") or []: + options = structure.get("options") or {} + height = float(structure.get("height_m") or options.get("height_m") or 0.0) + band = _depth_band(height + _foundation_depth(options)) + for component in structure.get("components") or []: + if str(component.get("destination") or "") != "earthwork": + continue + name = str(component.get("name") or "") + amount = float(component.get("amount") or 0.0) + if amount <= 0: + continue + if name == "터파기": + trench[band] = trench.get(band, 0.0) + amount + elif name == "되메우기": + backfill += amount + elif name == "잔토처리": + spoil += amount + + rows: list[dict[str, Any]] = [] + unmatched: list[str] = [] + if trench: + entry = mapping.for_earthwork(TRENCH_GROUP, None) + code = (entry or {}).get("work_item_code") + if code is None: + unmatched.append(TRENCH_GROUP) + for _, band in (*DEPTH_BANDS, (0.0, DEPTH_OVER)): + amount = trench.get(band) + if not amount: + continue + rows.append( + _row( + work_item_code=code, + name=TRENCH_GROUP, + spec=f"심도 {band}", + spec_detail=f"구조물 전개 합 · 심도 {band}", + quantity=amount, + blocked_kind=BLOCKED_INPUT_MISSING, + blocked_reason=TRENCH_BLOCKED_REASON, + in_bill=False, + in_bill_reason=TRENCH_BLOCKED_REASON, + ) + ) + if backfill > 0: + entry = mapping.for_earthwork(BACKFILL_GROUP, None) + code = (entry or {}).get("work_item_code") + if code is None: + unmatched.append(BACKFILL_GROUP) + rows.append( + _row( + work_item_code=code, + name=BACKFILL_GROUP, + spec="구조물", + spec_detail="구조물 전개 합", + quantity=backfill, + in_bill=code is not None, + in_bill_reason="" if code else "품셈 공종을 아직 못 이었습니다", + ) + ) + if spoil > 0: + rows.append( + _row( + name=SPOIL_GROUP, + spec="구조물", + spec_detail="구조물 전개 합 (터파기 − 되메우기)", + quantity=spoil, + in_bill=False, + in_bill_reason=SPOIL_REASON, + ) + ) + return rows, unmatched diff --git a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json index bb8f95fe..8f7acc16 100644 --- a/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json +++ b/resources/data_work_item_mapping/work_item_mapping_2026-01-01.json @@ -109,6 +109,14 @@ "group": "되메우기", "work_item_code": "FP-09-14-01", "master_name": "되메우기 및 다짐 > 되메우기" + }, + { + "group": "구조물터파기", + "work_item_code": "FP-09-13", + "basis_unit": "㎥", + "basis_source": "산림사업 표준품셈(고시 2025-82) 9-13 구조물터파기 — 원문 L5022~5241.", + "master_name": "구조물터파기", + "note": "⚠ 상위 코드다. 품은 18구분(토질 3 × 육상/용수 × 심도 3) 하위에만 있어 그대로는 금액이 안 선다 — 토질·용수 칸이 생기면 FP-09-13-01~18 중 하나로 내려간다. 심도는 구조물 제원(직고 + 기초 깊이)에서 이미 갈라 둔다." } ], "haul": [ @@ -137,7 +145,9 @@ "structure": [ { "type_id": "masonry_wet", - "secondary_axes": ["stone_kind"], + "secondary_axes": [ + "stone_kind" + ], "secondary_axes_note": "⚠ 돌 종류는 **갈래 축이 하나 더**인 자리다. 품셈 13-4-2·13-4-5 [주]② 가 「본 품은 **깬돌 및 깬 잡석**의 돌쌓기 기준」이라 못 박고, 돌 종류로 갈리는 표는 **13-5 돌붙임**에 따로 있다(뒷길이 7 × 돌종류 6). 어느 쪽 공종으로 볼지는 **사용자 확정 대기** — 여기서 공종을 바꾸지 않고 **저장 원본값만 실어 보낸다**(2026-09-08 계약과 같은 방식).", "billing_component": "돌쌓기", "billing_note": "⚠ 품셈 밑수가 「㎡당」이라 **연장(m)으로 세면 안 된다** — 받는 쪽이 ㎡ 단가를 m 수량에 곱해 금액이 2.6배로 섰다(2026-09-08 실증: 10m × 52,938.9 = 529,389원 / 26.101㎡ × 52,938.9 = 1,381,753원). 내역 줄 수량은 이 성분(비탈면적)으로 센다. 고임돌·야면석·막자갈은 **자재 축**으로 따로 가므로 여기서 빠지지 않는다.", @@ -149,7 +159,9 @@ }, { "type_id": "masonry_dry", - "secondary_axes": ["stone_kind"], + "secondary_axes": [ + "stone_kind" + ], "secondary_axes_note": "⚠ 돌 종류는 **갈래 축이 하나 더**인 자리다. 품셈 13-4-2·13-4-5 [주]② 가 「본 품은 **깬돌 및 깬 잡석**의 돌쌓기 기준」이라 못 박고, 돌 종류로 갈리는 표는 **13-5 돌붙임**에 따로 있다(뒷길이 7 × 돌종류 6). 어느 쪽 공종으로 볼지는 **사용자 확정 대기** — 여기서 공종을 바꾸지 않고 **저장 원본값만 실어 보낸다**(2026-09-08 계약과 같은 방식).", "billing_component": "돌쌓기", "billing_note": "⚠ 품셈 밑수가 「㎡당」이라 **연장(m)으로 세면 안 된다** — 받는 쪽이 ㎡ 단가를 m 수량에 곱해 금액이 2.6배로 섰다(2026-09-08 실증: 10m × 52,938.9 = 529,389원 / 26.101㎡ × 52,938.9 = 1,381,753원). 내역 줄 수량은 이 성분(비탈면적)으로 센다. 고임돌·야면석·막자갈은 **자재 축**으로 따로 가므로 여기서 빠지지 않는다.",