From 4deaf0239ca4e59a2a503f270d756d23fe5d4802 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 04:35:31 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=EC=9D=B8=EA=B3=84=20=EC=A4=84?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=EB=A7=89=ED=9E=8C=20=EA=B9=8C=EB=8B=AD?= =?UTF-8?q?=EC=9D=84=20=EC=8B=A4=EC=9D=8C=20+=20=EA=B3=84=EC=95=BD=20?= =?UTF-8?q?=EC=B9=B8=20=EB=8C=80=EC=A1=B0=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서브 화면에 「왜 안 서는지」가 안 뜨던 줄 셋(돌쌓기·흙막이·큰돌쌓기)을 고침. - `blocked_reason` · `blocked_kind` 를 줄마다 실음. 갈래는 셋 — `input_missing`(입력하면 풀림) / `unit_data_missing`(자료 없음) / `formula_missing`(산출식 없음). 받는 쪽이 「사용자가 입력하면 풀리는 것」과 「우리가 만들어야 하는 것」을 화면에서 갈라야 함(8-27 표와 같은 축). - 문구는 `B08_Quantity_Wording` 것을 그대로 씀 — 두 벌로 짜면 갈림. - 토공·운반 줄은 막힐 자리가 없어도 **칸은 둠** — 계약이 한 모양이어야 함. ⚠ 계약 칸 대조 시험 추가(`test_b08_handoff_contract.py`). 오늘 이 병이 계약 양쪽에서 하나씩 났음 — 우리 쪽은 레지스트리 키와 엔진이 읽는 키가 달라 저장값이 안 닿았고, 받는 쪽은 우리가 보낸 `composite_parts`· `quantity_gross` 를 안 읽고 버렸음. **「보내는 쪽은 보냈는데 받는 쪽이 안 읽는」 자리는 양쪽 다 조용함.** 키를 늘릴 때 계약에 안 적고 늘리면 받는 쪽이 영영 모름. 자재 줄에 공종코드가 없는 것도 계약으로 못 박음. 검증 — 전체 638 passed. 실물 프로젝트(`5601e828`)에서 네 줄 다 사유가 실림. Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_Engine_Handoff.py | 53 ++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index 11273cb0..bdfcde7c 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -128,6 +128,12 @@ NOTE_METHOD_MISSING = "시공법 미지정으로 공종을 못 고름" #: 아니라 공종 이름이라 성분 목록에는 안 온다. REBAR_PREFIXES = ("이형철근", "원형철근", "철근") +#: 줄이 왜 막혔나 — **받는 쪽이 「사용자가 입력하면 풀리는 것」과 「우리가 만들어야 하는 것」을 +#: 화면에서 갈라야** 한다(2026-09-07 3자 확정). 8-27 표에서 이미 가른 그 축이다. +BLOCKED_INPUT_MISSING = "input_missing" # 저장 제원 칸이 비어 있음 — 입력하면 풀림 +BLOCKED_UNIT_DATA_MISSING = "unit_data_missing" # 원단위·표준 물량 자료가 없음 +BLOCKED_FORMULA_MISSING = "formula_missing" # 수량 산출식 자체가 없음 + #: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다. SLOPE_GROUPS = frozenset({"성토면다짐", "초류종자살포", "지장목제거", "층따기", "면고르기"}) @@ -459,6 +465,14 @@ def _earthwork_rows( "station_from": None, "station_to": None, "spec_detail": "", + "composite_parts": None, + "structure_kind": None, + "spec_class": None, + "spec_class_basis": "", + # 토공 줄은 막힐 자리가 없다 — 그래도 **칸은 둔다**(계약이 한 모양이어야 한다). + "blocked_kind": None, + "blocked_reason": "", + "composite_not_ready": None, # 합계 줄과 무대 줄은 값은 내되 내역에 안 선다. "in_bill": bool(row.get("in_bill", True)) and not is_subtotal, "excavation_method": methods.get(ground) if ground else None, @@ -499,9 +513,17 @@ def _haul_rows( "ground_class": row.get("ground") or None, "haul_distance_m": float(row.get("average_distance_m") or 0.0), "haul_equipment": equipment, + "excavation_method": None, "station_from": None, "station_to": None, "spec_detail": "", + "composite_parts": None, + "structure_kind": None, + "spec_class": None, + "spec_class_basis": "", + "blocked_kind": None, + "blocked_reason": "", + "composite_not_ready": None, "in_bill": in_bill, "in_bill_reason": str(entry.get("reason") or ""), "origin": ORIGIN_HAUL, @@ -510,6 +532,30 @@ def _haul_rows( return rows, unmatched +def blocked_of(structure: dict[str, Any], class_basis: str = "") -> tuple[str | None, str]: + """(막힌 갈래, 사유). 안 막혔으면 `(None, "")`. + + ⚠ 사유 문구는 **`B08_Quantity_Wording` 것을 그대로** 쓴다 — 두 벌로 짜면 갈린다. + 전개 알림(`notes`)에 이미 사람 말로 적혀 있으므로 그것을 그대로 옮긴다. + """ + notes = [str(note) for note in structure.get("notes") or []] + if structure.get("components"): + # 물량은 섰는데 **단가 갈래**를 못 고른 자리(돌쌓기 뒷길이 등). + if class_basis and "입력되지 않았습니다" in class_basis: + return BLOCKED_INPUT_MISSING, class_basis + return None, "" + for note in notes: + if "입력되지 않았습니다" in note: + return BLOCKED_INPUT_MISSING, note + if "자료에 없습니다" in note or "표준 물량 자료" in note: + return BLOCKED_UNIT_DATA_MISSING, note + if "산출식이 아직 없습니다" in note: + return BLOCKED_FORMULA_MISSING, note + if notes: + return BLOCKED_UNIT_DATA_MISSING, notes[0] + return None, "" + + def _structure_rows( unit_quantity_table: dict[str, Any], mapping: WorkItemMapping ) -> tuple[list[dict[str, Any]], list[str]]: @@ -534,9 +580,10 @@ def _structure_rows( composite = mapping.composite_for(type_id) if code is None else None kind = structure_kind(structure) if composite else None parts: list[dict[str, Any]] | None = None - parts_missing: list[str] = [] + parts_missing: list[dict[str, Any]] = [] if composite: parts, parts_missing = composite_quantities(structure, composite, mapping) + blocked_kind, blocked_reason = blocked_of(structure, class_basis) if code is None and composite is None: unmatched.append(f"{wording_type_label(type_id)} — 품셈 공종을 아직 못 이었습니다") elif entry.get("class_from") == "back_length" and class_key is None: @@ -558,11 +605,15 @@ def _structure_rows( "haul_equipment": None, "station_from": structure.get("start_m"), "station_to": structure.get("end_m"), + "excavation_method": None, "spec_detail": _spec_detail(structure), # 품셈에 그 이름의 공종이 없어 여러 공종을 묶는 자리 — 빈 코드와 구별한다. "composite_parts": parts, # 철근이 있나 없나로 자동 판정 — 사람이 고르는 값이 아니다. "structure_kind": kind, + # ⚠ 줄마다 **왜 막혔는지**를 싣는다 — 안 실으면 받는 쪽 화면이 빈다. + "blocked_kind": blocked_kind, + "blocked_reason": blocked_reason, # 규격 갈래(뒷길이 …㎝ 이하) — 못 고르면 사유가 남는다. "spec_class": class_key, "spec_class_basis": class_basis,