From 93546b850aa21c4b47169ffc6fe1a9e6522900dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 08:23:03 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=A4=80=EB=B9=84=EA=B3=B5=C2=B7?= =?UTF-8?q?=EC=82=AC=EB=B0=A9=EA=B3=B5=EC=9D=84=20=EC=9D=B8=EA=B3=84?= =?UTF-8?q?=EC=97=90=20=EC=8B=A4=EC=9D=8C=20=E2=80=94=20=EB=AA=BB=20?= =?UTF-8?q?=EB=82=B4=EB=8A=94=20=EC=A4=84=EC=9D=80=20=EC=82=AC=EC=9C=A0?= =?UTF-8?q?=EC=99=80=20=ED=95=A8=EA=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보조 창 요청에서 출발했는데, 보다 보니 표가 통째로 안 가고 있었음. 표토제거(값 있음·FP-09-15)·규준틀(개소·FP-11-02)이 화면에는 서는데 인계에는 없었음. ⚠ 줄을 빼면 「빠졌다는 사실조차 안 보임」 — 받는 쪽에서 「내역서에 원래 없는 것」과 「우리가 아직 못 내는 것」이 구별되지 않음. - 값이 서는 줄 → in_bill: True, 공종코드와 수량 그대로 - 못 내는 줄 → in_bill: False + blocked_kind + blocked_reason (금액은 안 붙되 「무엇이 채워지면 풀리는지」가 함께 감) - ⚠ 「다른 표에서 이미 섬」(벌목)은 blocked_kind: None — 막힌 것이 아니라 여기서 세면 안 되는 줄임. 막힘으로 보내면 「만들어야 할 것」으로 오해됨 - origin: "preparation" 로 갈래를 나눔 시험 넷 — 값 서는 줄 · 못 내는 줄의 사유 · 이중계상 방지 줄은 막힘 아님 · ⚠ 표를 안 주면 빈 줄을 지어내지 않을 것(좁게 고치는 짝 시험). 시험: 681 passed · 24 skipped (B05 코리도 1건 기존 깨짐, 무관). Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 78 ++++++++++++++++++++ B08_Quantity/B08_Quantity_Router_Material.py | 2 + 2 files changed, 80 insertions(+) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index 5079967d..7b69b2f5 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -45,6 +45,14 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Iterable +from B08_Quantity.B08_Quantity_Engine_Preparation import ( + STATUS_COUNTED_ELSEWHERE as PREP_COUNTED_ELSEWHERE, +) +from B08_Quantity.B08_Quantity_Engine_Preparation import ( + STATUS_NOT_APPLICABLE as PREP_NOT_APPLICABLE, +) +from B08_Quantity.B08_Quantity_Engine_Preparation import STATUS_PENDING as PREP_PENDING +from B08_Quantity.B08_Quantity_Engine_Preparation import STATUS_READY as PREP_READY from common_util.common_util_quantity_spread import spread_by_unit DATASET_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_work_item_mapping" @@ -149,6 +157,7 @@ ORIGIN_EARTHWORK = "earthwork" ORIGIN_STRUCTURE = "structure" ORIGIN_SLOPE = "slope" ORIGIN_HAUL = "haul" +ORIGIN_PREPARATION = "preparation" #: 암 시공법 → 매핑표의 지반 이름. 품셈이 **긁어내기와 터뜨리기를 다른 공종**으로 두기 때문에 #: 갈래 이름(연암·보통암…)만으로는 공종을 못 고른다(2026-09-07 일감 9 실서버에서 드러남). @@ -777,6 +786,71 @@ def _placing_rows( return rows, notes +def _preparation_rows(preparation_table: dict[str, Any]) -> list[dict[str, Any]]: + """준비공·사방공 줄 — **값이 서는 줄도, 못 서는 줄도** 함께 보낸다. + + ⚠ **줄을 빼면 「빠졌다는 사실조차 안 보인다」** (2026-09-08 보조 창 제보). + 받는 쪽 화면에서 「내역서에 원래 없는 것」과 「우리가 아직 못 내는 것」이 구별되지 않는다. + 그래서 못 내는 줄도 `in_bill: False` + `blocked_reason` 으로 실어 보낸다 — + **금액은 안 붙되 「무엇이 채워지면 풀리는지」가 함께 간다.** + + ⚠ 이 표가 통째로 안 가고 있었다 — 표토제거(값 있음·`FP-09-15`)·규준틀(개소·`FP-11-02`)이 + 화면에는 서는데 인계에는 없었다. 「사유를 실어 달라」는 요청을 보다 드러났다. + """ + rows: list[dict[str, Any]] = [] + for row in preparation_table.get("rows") or []: + status = str(row.get("status") or "") + amount = row.get("amount") + ready = status == PREP_READY and amount is not None + rows.append( + { + "work_item_code": row.get("work_item_code"), + "name": str(row.get("item") or ""), + "spec": str(row.get("group") or ""), + "unit": str(row.get("unit") or ""), + "quantity": float(amount or 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": str(row.get("group") or ""), + "composite_parts": None, + "structure_kind": None, + # ⚠ 못 서는 까닭을 그대로 넘긴다 — 받는 쪽이 「만들어야 할 것」 목록에 얹는다. + "blocked_kind": None if ready else _prep_blocked_kind(status), + "blocked_reason": "" if ready else str(row.get("reason") or status), + "variant_axis": None, + "variant_value": None, + "spec_class": None, + "spec_class_basis": str(row.get("reason") or ""), + "composite_not_ready": None, + # 값이 없는 줄은 **내역에 세우지 않는다** — 0 원 줄을 만들면 더 나쁘다. + "in_bill": ready, + "in_bill_reason": "" if ready else str(row.get("reason") or status), + "origin": ORIGIN_PREPARATION, + } + ) + return rows + + +def _prep_blocked_kind(status: str) -> str | None: + """준비공 줄의 상태를 **막힌 갈래 셋** 중 하나로 옮긴다. 모르는 상태는 `None`.""" + if status == PREP_PENDING: + return BLOCKED_INPUT_MISSING + if status == PREP_COUNTED_ELSEWHERE: + # 다른 표에서 이미 선 줄 — 막힌 것이 아니라 **여기서 세면 안 되는** 줄이다. + return None + if status == PREP_NOT_APPLICABLE: + return None + return BLOCKED_UNIT_DATA_MISSING + + def _material_rows(material_table: dict[str, Any]) -> list[dict[str, Any]]: """자재 줄 — **공종코드를 붙이지 않는다.** 자재 축은 B09 카탈로그가 잇는다(8-7).""" rows: list[dict[str, Any]] = [] @@ -804,6 +878,7 @@ def build_handoff( haul_table: dict[str, Any] | None = None, unit_quantity_table: dict[str, Any] | None = None, material_table: dict[str, Any] | None = None, + preparation_table: dict[str, Any] | None = None, mapping: WorkItemMapping | None = None, ground_class_set: str | None = None, ground_classes: list[str] | None = None, @@ -829,6 +904,9 @@ def build_handoff( work_items.extend(rows) unmatched.extend(misses) + # 준비공·사방공 — **못 내는 줄도 사유와 함께** 보낸다(빼면 빠진 줄이 안 보인다). + work_items.extend(_preparation_rows(preparation_table or {})) + # 콘크리트 타설 — 품은 이 줄, 재료는 자재 쪽. 겹치지 않는다(위 `_placing_rows` 주석). placing_rows, placing_notes = _placing_rows( unit_quantity_table or {}, table, concrete_placing_method diff --git a/B08_Quantity/B08_Quantity_Router_Material.py b/B08_Quantity/B08_Quantity_Router_Material.py index e76451e8..aee5eb14 100644 --- a/B08_Quantity/B08_Quantity_Router_Material.py +++ b/B08_Quantity/B08_Quantity_Router_Material.py @@ -159,6 +159,8 @@ async def get_handoff(project_id: UUID) -> JSONResponse: haul_table=earthwork.get("haul"), unit_quantity_table=unit_table, material_table=material_table, + # 준비공·사방공 — 값이 서는 줄도, 못 내는 줄도 함께 넘긴다(빼면 빠진 줄이 안 보임). + preparation_table=earthwork.get("preparation"), ground_class_set=settings.get("rock_class_set"), ground_classes=rock_classes(settings), ground_methods={name: rock_method(settings, name) for name in rock_classes(settings)},