diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff.py b/B08_Quantity/B08_Quantity_Engine_Handoff.py index e3b7f446..8f1535a5 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff.py @@ -54,6 +54,7 @@ from B08_Quantity.B08_Quantity_Engine_Handoff_Trench import ( from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import ( # noqa: F401 BLOCKED_FORMULA_MISSING, BLOCKED_INPUT_MISSING, + BLOCKED_UNCONFIRMED, BLOCKED_UNIT_DATA_MISSING, METHOD_TO_GROUND, NOTE_METHOD_MISSING, diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py index 96598873..cda537d3 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Mapping.py @@ -132,6 +132,9 @@ REBAR_PREFIXES = ("이형철근", "원형철근", "철근") BLOCKED_INPUT_MISSING = "input_missing" # 저장 제원 칸이 비어 있음 — 입력하면 풀림 BLOCKED_UNIT_DATA_MISSING = "unit_data_missing" # 원단위·표준 물량 자료가 없음 BLOCKED_FORMULA_MISSING = "formula_missing" # 수량 산출식 자체가 없음 +#: 치수가 없어 **등록부 기본값으로 선** 구조물 줄(2026-09-14 브레인 판정) — 수량은 보이되 금액·합계엔 +#: 안 듦. B09 가 내역 제자리에 빈 금액 + 빨간 테두리로 세우고 「미확정 N건 — 금액에 안 들어감」. +BLOCKED_UNCONFIRMED = "unconfirmed" #: 사면 계열 — 토공집계에 함께 실리지만 출처가 사면적이다. #: ⚠ `item` 칸이 **지반 갈래**인 공종 — 그 밖의 공종에서 `item` 은 **작업 갈래**다 diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py index 68adef0a..d353c8c7 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Rows.py @@ -13,6 +13,7 @@ from B08_Quantity.B08_Quantity_Engine_BasisUnit import normalize_unit, unit_for_ from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import ( BLOCKED_FORMULA_MISSING, BLOCKED_INPUT_MISSING, + BLOCKED_UNCONFIRMED, BLOCKED_UNIT_DATA_MISSING, GROUND_SPLIT_GROUPS, HAUL_SUMMARY_GROUPS, @@ -33,6 +34,9 @@ from B08_Quantity.B08_Quantity_Engine_Handoff_Mapping import ( ) from B08_Quantity.B08_Quantity_Wording import type_label as wording_type_label +#: 기본값으로 선 구조물 줄 사유 머리 — B09 「미확정 N건 — 금액에 안 들어감」과 같은 말. +UNCONFIRMED_LABEL = "미확정 — 금액에 안 들어감" + def _spec_detail(structure: dict[str, Any]) -> str: """규격 표기 — 저장된 제원에서 만든다. 없는 값은 적지 않는다.""" @@ -447,6 +451,13 @@ def _structure_rows( if notes else "물량이 0 이라 내역에 안 세움 — 저장 제원에서 치수·면적을 넣으면 값이 섭니다" ) + # ⭐ 치수가 없어 기본값으로 선 구조물(2026-09-14 브레인 판정) — **줄은 서되 금액은 안 듦.** + # B09 가 내역 제자리에 빈 금액 + 빨간 테두리로 세우고 「미확정 N건 — 금액에 안 들어감」. + unconfirmed = str(structure.get("unconfirmed") or "") + if unconfirmed: + in_bill = False + zero_reason = f"{UNCONFIRMED_LABEL} — {unconfirmed}" + blocked_kind, blocked_reason = BLOCKED_UNCONFIRMED, zero_reason rows.append( { "work_item_code": code, @@ -491,7 +502,7 @@ def _structure_rows( "origin": ORIGIN_STRUCTURE, } ) - if sheet_entry is not None: + if sheet_entry is not None and not unconfirmed: rows[-1] = template_row(rows[-1], structure, sheet_entry) return rows, unmatched @@ -541,8 +552,10 @@ def _placing_rows( # 2026-09-08 B09 가 「철근이 겹치나」를 물어 그 김에 드러난 자리다 — # 철근은 안 겹치고(자재는 재료·묶음 조각은 품, 재료 0원) **타설이 겹쳤다.** # ⚠ 묶음이 아직 미확보라 지금은 값이 안 걸렸을 뿐, 묶음이 서는 날 두 번이 된다. - if mapping.composite_for(str(structure.get("type_id") or "")): - continue + if mapping.composite_for(str(structure.get("type_id") or "")) or structure.get( + "unconfirmed" + ): + continue # 기본값으로 선 구조물도 — 금액에 안 듦 # ⚠ 버림은 **따로 센다** — 실무 내역이 「레미콘타설(장비) **무근,버림**」으로 갈라 # 적는다(봉화 제50호표, 2026-09-09 데스크탑 보조 확인). 같은 공종·같은 단가라 # 금액은 안 움직이고 **이름만 맞추는 것**이다. diff --git a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py index 55ccbc65..28cfe826 100644 --- a/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py +++ b/B08_Quantity/B08_Quantity_Engine_Handoff_Trench.py @@ -166,8 +166,10 @@ def rubble_base_rows( total = 0.0 bases: list[str] = [] for structure in unit_quantity_table.get("structures") or []: - if mapping.composite_for(str(structure.get("type_id") or "")): - continue + if mapping.composite_for(str(structure.get("type_id") or "")) or structure.get( + "unconfirmed" + ): + continue # 묶음 조각이 품음 · 기본값으로 선 구조물은 금액에 안 듦 sheet_entry = priced.get(str(structure.get("structure_id"))) if sheet_entry is not None and RUBBLE_GROUP in sheet_entry["covered"]: continue @@ -219,6 +221,8 @@ def structure_earthwork_rows( # 사유도 안 났다). 관측 원단위로 가는 종류는 표에 터파기 줄이 없으면 이렇게 된다. without_trench: list[str] = [] for structure in unit_quantity_table.get("structures") or []: + if structure.get("unconfirmed"): + continue # 기본값으로 선 구조물 — 제 줄에 사유가 섬, 토공 금액엔 안 듦 options = structure.get("options") or {} height = float(structure.get("height_m") or options.get("height_m") or 0.0) # 단면으로 판 깊이가 있으면 그것(옹벽은 기초분만 팜 · 비탈분 제외) — 없으면 직고 + 기초 깊이. diff --git a/B08_Quantity/B08_Quantity_Engine_HaulInputs.py b/B08_Quantity/B08_Quantity_Engine_HaulInputs.py index cc9b4aa3..3e26dd0f 100644 --- a/B08_Quantity/B08_Quantity_Engine_HaulInputs.py +++ b/B08_Quantity/B08_Quantity_Engine_HaulInputs.py @@ -97,6 +97,8 @@ def haul_inputs(unit_quantity_table: dict[str, Any] | None) -> dict[str, Any]: stone_by_ground: dict[str, float] = {} stone_unknown = 0.0 for structure in unit_quantity_table.get("structures") or []: + if structure.get("unconfirmed"): + continue # 기본값으로 선 구조물 — 잔토·채집석이 사토 운반 금액으로 번지지 않게 amount = 0.0 stone = 0.0 for component in structure.get("components") or []: diff --git a/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py b/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py index 869caf3d..b1471b29 100644 --- a/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py +++ b/B08_Quantity/B08_Quantity_Engine_MaterialSummary.py @@ -286,6 +286,10 @@ def _supply_setting(supply: dict[str, Any], row: MaterialRow) -> Any: return None +#: 건너뛴 까닭 칸 — 치수가 없어 기본값으로 선 구조물(구조물 수로 셈). +UNCONFIRMED_SKIP = "미확정(기본값으로 선 구조물)" + + def _collect( unit_quantity_table: dict[str, Any], ) -> tuple[dict[tuple[str, str, str], MaterialRow], dict[str, int]]: @@ -293,6 +297,10 @@ def _collect( rows: dict[tuple[str, str, str], MaterialRow] = {} skipped: dict[str, int] = {} for structure in unit_quantity_table.get("structures", []): + if structure.get("unconfirmed"): + # 기본값으로 선 구조물 — 자재에 안 듦(2026-09-14 브레인 판정 「없으면 없다고 보이기」). + skipped[UNCONFIRMED_SKIP] = skipped.get(UNCONFIRMED_SKIP, 0) + 1 + continue label = str(structure.get("name") or structure.get("type_id") or "") for component in structure.get("components", []): destination = str(component.get("destination") or "") or "(없음)" diff --git a/B08_Quantity/B08_Quantity_Engine_Pipe.py b/B08_Quantity/B08_Quantity_Engine_Pipe.py index e0b5ca9e..4c4e1722 100644 --- a/B08_Quantity/B08_Quantity_Engine_Pipe.py +++ b/B08_Quantity/B08_Quantity_Engine_Pipe.py @@ -208,9 +208,14 @@ REVET_ROLES = ( ("inlet", "유입부 기슭막이", "유입 칸 벽"), ("outlet", "유출부 기슭막이", "유출 칸 벽"), ) +#: ⚠ 「횡단도와 같은 값」이 아님 — B06 은 관 벽 높이를 **관경 기준 최소 높이**로 따로 그림 +#: (`revetWallSpec`), 독립 기슭막이도 저장 높이가 없으면 근입 깊이로 그림. 형태·길이만 같은 기본값. NOTE_DEFAULT_WALL = ( - "⚠ 시설 지점에 {filled} 을 안 적어 등록부 기본값으로 섰음 — 횡단도 그림과 같은 값" + "⚠ 시설 지점에 {filled} 을 안 적어 등록부 기본값으로 섰음 — 미확정이라 금액에 안 들어감" + " · 횡단도 벽 높이는 관경 기준으로 따로 그려져 다를 수 있음" ) +#: 금액에서 뺄 까닭(2026-09-14 브레인 판정) — 줄·표·그림은 서되 내역 합에는 안 듦. +UNCONFIRMED_WALL = "벽 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬" NOTE_SIDE_UNKNOWN = ( "⚠ 설치 측 「{side}」인데 두 칸(유입·유출) 값이 달라 어느 칸이 그쪽인지 서버가 못 가림" "(횡단 지형이 정함) — 값을 안 세움 · 두 칸을 같게 적으면 섬" @@ -231,7 +236,7 @@ def _num_or_zero(value: Any) -> float: def _revet_values( options: dict[str, Any], role: str, defaults: dict[str, Any], legacy: bool ) -> tuple[dict[str, Any], list[str]]: - """벽 한쪽 제원과 등록부 기본값으로 채운 칸 — B06 횡단 그림(`_side_spec`)과 같은 채움.""" + """벽 한쪽 제원과 등록부 기본값으로 채운 칸 — B06 세트 스펙(`_side_spec`)과 같은 채움.""" values: dict[str, Any] = {} filled: list[str] = [] for name, label in REVET_FIELDS: @@ -250,7 +255,8 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: """계곡 통과 시설(`pipe_points.json`) → 원단위 전개가 읽는 구조물 줄 (A1, 2026-09-14). ⚠ 관 자체는 안 냄 — `build_rows` 가 관 연장으로 셈. - ⚠ 안 적힌 벽 칸은 **등록부 기본값** — 횡단 그림이 쓰는 값과 같고, 그 사실을 줄 사유로 붙임. + ⚠ 안 적힌 벽 칸은 **등록부 기본값** — 그 사실을 줄 사유로 붙이고 `unconfirmed` 로 금액 합에서 뺌 + (2026-09-14 브레인 판정 「줄은 서되 금액은 실제 값이 있을 때만」). ⚠ 집수정·기슭막이 터파기·되메우기는 전개 성분(`destination: earthwork`)이라 토공집계로만 감. """ from B05_Profile.B05_Profile_Structures_Schema import structure_type_map @@ -303,17 +309,17 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: notes = list(inlet_notes) if role == "inlet" else [] if filled: notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled))) - walls.append([role, own_label if legacy else pipe_label, values, notes, False]) + walls.append([role, own_label if legacy else pipe_label, values, notes, False, filled]) side = str(options.get("side") or "") if legacy else "" if side in ("좌", "우"): # 좌·우가 유입/유출 어느 칸인지는 **횡단 지형**이 정함 — 두 칸이 같을 때만 한 벽으로 셈. same = walls[0][2] == walls[1][2] - walls = [[walls[0][0], f"{side} 벽", walls[0][2], walls[0][3], not same]] + walls = [[walls[0][0], f"{side} 벽", walls[0][2], walls[0][3], not same, walls[0][5]]] if not same: walls[0][3].append(NOTE_SIDE_UNKNOWN.format(side=side)) foundation = options.get("foundation" if legacy else "revet_foundation") kept = {k: v for k, v in options.items() if not k.startswith(("inlet_", "outlet_"))} - for role, label, values, notes, withheld in walls: + for role, label, values, notes, withheld, filled in walls: before, after = _num_or_zero(values["before_m"]), _num_or_zero(values["after_m"]) row = child_row(base, "revetment", label, f"{role}_revet") row.update( @@ -322,6 +328,7 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]: options={**(kept if legacy else {}), **values, "foundation": foundation}, notes=notes, withheld=withheld, + unconfirmed=UNCONFIRMED_WALL.format(filled=" · ".join(filled)) if filled else "", ) rows.append(row) return rows diff --git a/B08_Quantity/B08_Quantity_Engine_StructureFigure.py b/B08_Quantity/B08_Quantity_Engine_StructureFigure.py index edd922e7..91ed57c9 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureFigure.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureFigure.py @@ -26,6 +26,7 @@ from __future__ import annotations from typing import Any +from B08_Quantity.B08_Quantity_Engine_Pipe import NOTE_DEFAULT_WALL from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of from B08_Quantity.B08_Quantity_Engine_UnitQuantity import ( _back_length, @@ -283,9 +284,14 @@ def _masonry_figure(sheet: dict[str, Any]) -> list[dict[str, Any]]: _LABEL_SIZE, ), ] - if default_note: - # 「이 값이 어디서 왔나」 — 표 사유 줄과 **글자까지 같게**(브레인 판정). - shapes.append(_text(default_note, (0.0, -0.80 - base_d), "left")) + # 「이 값이 어디서 왔나」 — 표 사유 줄과 **글자까지 같게**(브레인 판정). 벽 치수 자체가 + # 기본값인 관 지점 기슭막이는 그 줄도 — 그림이 설계값처럼 보이면 더 위험함(2026-09-14). + wall_prefix = NOTE_DEFAULT_WALL.split("{")[0] + lines = [default_note] + [ + str(n) for n in sheet.get("notes") or [] if str(n).startswith(wall_prefix) + ] + for index, line in enumerate(line for line in lines if line): + shapes.append(_text(line, (0.0, -0.80 - base_d - index * 0.18), "left")) return shapes diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index dcd043c2..a7feeb03 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -51,6 +51,7 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import ( # noqa: F401 Component, StructureQuantity, _num, + is_unconfirmed, ) from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Masonry import ( # noqa: F401 — 다시 내보냄 boulder_masonry, @@ -528,6 +529,7 @@ def build_table( if rubble is not None: quantity.components.append(rubble) _append_section_trench(quantity, item, observed, rubble_base_thickness_m) + quantity.unconfirmed = str(item.get("unconfirmed") or "") quantities.append(quantity) if use_templates: # 늦게 부름 — 양식 모듈이 이 모듈을 부르므로 맨 위에서 부르면 맞물림. @@ -544,7 +546,9 @@ def build_table( violations = verify_no_mix_components(quantities) totals: dict[str, dict[str, Any]] = {} - for item in quantities: + # 합은 **금액에 드는 구조물만** — 기본값으로 선 구조물(`unconfirmed`)은 줄에만 보임. + priced = [item for item in quantities if not item.unconfirmed] + for item in priced: for component in item.components: key = f"{component.name}|{component.spec}|{component.unit}" entry = totals.setdefault( @@ -581,6 +585,8 @@ def build_table( "trench_depth_m": item.trench_depth_m, # 양식 있음/없음 — 화면이 가림(비면 지금 전개). "library_item": item.library_item, + # 기본값으로 선 까닭 — 차 있으면 금액·자재·토공·운반 합에서 빠짐(`is_unconfirmed`). + "unconfirmed": item.unconfirmed, "notes": item.notes, "components": [ { @@ -619,7 +625,7 @@ def build_table( COLLECTED_STONE_KEY: round( sum( component.amount - for item in quantities + for item in priced for component in item.components if component.name == "채집석" ), diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py index c57cd173..5bd4177b 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Base.py @@ -108,6 +108,14 @@ class StructureQuantity: trench_depth_m: float | None = None #: 양식으로 성분을 세웠으면 그 양식 이름(PLAN 3장 ④-2). 비면 지금 전개 값. library_item: str = "" + #: 치수가 정본에 없어 **등록부 기본값으로 선** 까닭(2026-09-14 브레인 판정) — 차 있으면 줄·표·그림은 + #: 서되 **금액·자재·토공·운반 합에는 안 듦**(`is_unconfirmed`). 설계자가 치수를 적으면 비고 금액이 섬. + unconfirmed: str = "" + + +def is_unconfirmed(structure: dict[str, Any]) -> bool: + """금액 합에서 뺄 구조물인가 — 합을 내는 자리마다 이 한 벌로 가림.""" + return bool(structure.get("unconfirmed")) def _num(value: Any, fallback: float = 0.0) -> float: diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities.py b/B09_Estimation/B09_Estimation_BillOfQuantities.py index 4712202a..9a73aa88 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities.py @@ -64,8 +64,12 @@ SUPPLY_OWNER = "owner_supplied" #: 그것을 「우리가 만들어야 하는 것」에 얹으면 **결국 이중계상으로 간다**(㉠~㉦ 규칙). _NOT_OUR_ROW = "not_our_row" +#: B08 `BLOCKED_UNCONFIRMED` 와 같은 글 — 치수 없이 기본값으로 선 줄. +BLOCKED_UNCONFIRMED = "unconfirmed" + _BLOCKED_LABELS = { _NOT_OUR_ROW: "여기서 세지 않는 줄", + BLOCKED_UNCONFIRMED: "미확정 — 금액에 안 들어감", "input_missing": "입력이 필요합니다", "unit_data_missing": "원단위가 없습니다(우리가 만들 것)", "formula_missing": "전개식이 없습니다(우리가 만들 것)", @@ -123,6 +127,12 @@ class HandoffWorkItem: def display_name(self) -> str: return f"{self.name} {self.spec}".strip() + @property + def unconfirmed(self) -> bool: + """B08 이 치수 없이 기본값으로 세운 줄(2026-09-14 브레인 판정) — 내역 제자리에 + **빈 금액 + 빨간 테두리**로 서고 합계에 안 듦 · 머리에 「미확정 N건 — 금액에 안 들어감」.""" + return self.blocked_kind == BLOCKED_UNCONFIRMED + @dataclass(frozen=True) class HandoffMaterial: @@ -262,6 +272,8 @@ class BillResult: material_sheet: Any = None #: 수동 단가로 선 자리 — 내역서 끝 「미확정 N건」(PLAN 확정 ⑦). 줄마다 `{name, count}`. unconfirmed: list[dict[str, Any]] = field(default_factory=list) + #: 치수 없이 기본값으로 선 구조물 줄 — 제자리에 빈 금액으로 서고 **합계에 안 듦**(`{name, reason}`). + unpriced: list[dict[str, Any]] = field(default_factory=list) @property def direct_material_krw(self) -> Decimal: @@ -435,7 +447,8 @@ def build_bill( composites: list[HandoffWorkItem] = [] templated: list[HandoffWorkItem] = [] for item in work_items: - if not item.in_bill: + if not item.in_bill and not (item.unconfirmed and item.work_item_code in index): + # (미확정 줄은 제자리 나무로 감 — `_leaf_row` 가 금액 없이 세우고 `unpriced` 에 셈) # ⚠ 코드 유무보다 **먼저** 가른다. 보정량계는 공종코드가 없어서가 아니라 # **검산용 줄이라서** 금액이 없는 것이다 — `missing` 으로 새면 「단가를 구해야 할 # 줄」로 잘못 읽힌다. @@ -669,6 +682,8 @@ def bill_summary(result: BillResult) -> dict[str, Any]: "missing": result.missing, "unconfirmed": result.unconfirmed, "unconfirmed_count": sum(int(entry["count"]) for entry in result.unconfirmed), + "unpriced": result.unpriced, + "unpriced_count": len(result.unpriced), "body_total_krw": str(result.body_total_krw), "direct_material_krw": str(result.direct_material_krw), "direct_labor_krw": str(result.direct_labor_krw), diff --git a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py index 7a456b66..7c319c3e 100644 --- a/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py +++ b/B09_Estimation/B09_Estimation_BillOfQuantities_Rows.py @@ -267,6 +267,10 @@ def _leaf_row( # 검산용 줄 — 수량은 보이되 **단가를 안 붙인다**(PLAN 8-7 ㉡ 와 같은 성격). row.quantity = item.quantity row.add_note("", item.in_bill_reason or "합계 검산용 줄 — 금액을 매기지 않습니다.") + if item.unconfirmed: + # 치수 없이 기본값으로 선 줄 — 빨간 테두리 + 머리 「미확정 N건 — 금액에 안 들어감」. + row.unconfirmed = 1 + result.unpriced.append({"name": item.display_name, "reason": item.blocked_reason}) result.excluded.append(row) return row diff --git a/B09_Estimation/B09_Estimation_UI_Store.ts b/B09_Estimation/B09_Estimation_UI_Store.ts index 1473748e..1d415ee4 100644 --- a/B09_Estimation/B09_Estimation_UI_Store.ts +++ b/B09_Estimation/B09_Estimation_UI_Store.ts @@ -129,6 +129,8 @@ export interface BillDto { detail_rows: number; missing: MissingDto[]; unconfirmed_count: number; + /** 치수 없이 기본값으로 선 구조물 줄 — 제자리에 빈 금액으로 서고 합계에 안 듦. */ + unpriced_count?: number; notes: string[]; material_sheet: MaterialSheetDto | null; }; diff --git a/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts b/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts index 6332de5a..56458849 100644 --- a/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts +++ b/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts @@ -202,6 +202,15 @@ function drawBill(ctx: B09TabContext, bill: BillDto, reload: () => void): void { ); if (bill.summary.unconfirmed_count > 0) bar.append(unconfirmedBadge(bill.summary.unconfirmed_count)); + // 치수 없이 기본값으로 선 줄 — 빨간 테두리 줄로 서고 금액·합계엔 안 듦(브레인 판정 2026-09-14). + if (bill.summary.unpriced_count) + bar.append( + el( + "span", + "b09s-badge", + `${L("B09_Sheet_Unconfirmed")} ${bill.summary.unpriced_count}${L("B09_Sheet_Count")} — ${L("B09_Sheet_Unpriced")}`, + ), + ); const rateToggle = el("button", "b09s-undo", L("B09_Sheet_Rate")); rateToggle.type = "button"; rateToggle.addEventListener("click", () => { diff --git a/resources/tester/test_b08_facility_structures.py b/resources/tester/test_b08_facility_structures.py index 8a684a5a..6ad5f803 100644 --- a/resources/tester/test_b08_facility_structures.py +++ b/resources/tester/test_b08_facility_structures.py @@ -42,10 +42,41 @@ def test_관_하나에_기슭막이_둘이_기본값_사유와_함께_서고_관 work = {row["name"]: row for row in build_handoff(unit_quantity_table=table)["work_items"]} assert work["배수관 · 유입부 기슭막이"]["work_item_code"] == "FP-13-04-05" assert work["배수관 · 유출부 기슭막이"]["work_item_code"] == "FP-13-04-02" - assert ( - work["배수관 · 유입부 기슭막이"]["in_bill"] - and work["배수관 · 유입부 기슭막이"]["unit"] == "㎡" + assert work["배수관 · 유입부 기슭막이"]["unit"] == "㎡" + + +def test_기본값으로_선_벽은_줄만_서고_금액_합에는_안_든다() -> None: + """브레인 판정(2026-09-14) — 줄은 서야 채울 자리가 보이고, 금액은 실제 값이 있을 때만.""" + point = {"chainage_m": 100.0, "options": {"pipe_diameter_mm": 1000}} + table = _table([point]) + assert all(s["unconfirmed"] and s["components"] for s in table["structures"]) + assert table["totals"] == [] # 원단위 합(자재·채집석 밑수)에도 안 듦 + handoff = build_handoff(unit_quantity_table=table) + rows = handoff["work_items"] + walls = [r for r in rows if "기슭막이" in r["name"]] + assert len(walls) == 2 and all( + not r["in_bill"] and r["blocked_kind"] == "unconfirmed" for r in walls ) + assert walls[0]["quantity"] > 0 # 수량은 보임 + # 터파기·되메우기·기초잡석·버림 타설이 금액 줄로 번지지 않음 + assert not [r for r in rows if r["in_bill"] and r["quantity"] > 0] + + from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill + + bill = build_bill(handoff) + placed = [r for r in bill.rows if not r.is_group and "기슭막이" in r.name] + assert len(placed) == 2 and all(r.unconfirmed == 1 and r.amount_krw is None for r in placed) + summary = bill_summary(bill) + assert summary["unpriced_count"] == 2 and summary["body_total_krw"] == "0" + + # 치수를 다 적으면 미확정이 풀림 + filled = { + f"{side}_revet_{key}": value + for side in ("inlet", "outlet") + for key, value in (("form", "돌쌓기(메)"), ("height_m", 1.5), ("length_m", 6)) + } + confirmed = _table([{**point, "options": {**point["options"], **filled}}]) + assert not any(s["unconfirmed"] for s in confirmed["structures"]) def test_유입구가_집수정이면_집수정_줄이_사유로_서고_유입_기슭막이는_없다() -> None: diff --git a/resources/tester/test_b08_handoff_contract.py b/resources/tester/test_b08_handoff_contract.py index f27312bf..8f4d9d8a 100644 --- a/resources/tester/test_b08_handoff_contract.py +++ b/resources/tester/test_b08_handoff_contract.py @@ -131,10 +131,18 @@ def test_막힌_까닭이_세_갈래_중_하나일것() -> None: from B08_Quantity.B08_Quantity_Engine_Handoff import ( BLOCKED_FORMULA_MISSING, BLOCKED_INPUT_MISSING, + BLOCKED_UNCONFIRMED, BLOCKED_UNIT_DATA_MISSING, ) - allowed = {None, BLOCKED_INPUT_MISSING, BLOCKED_UNIT_DATA_MISSING, BLOCKED_FORMULA_MISSING} + # ⭐ 넷째 갈래(2026-09-14 브레인 판정) — 치수 없이 기본값으로 선 줄: 수량은 보이되 금액 밖. + allowed = { + None, + BLOCKED_INPUT_MISSING, + BLOCKED_UNIT_DATA_MISSING, + BLOCKED_FORMULA_MISSING, + BLOCKED_UNCONFIRMED, + } for row in _rows()["work_items"]: assert row["blocked_kind"] in allowed diff --git a/ui_template/ui_template_locale_b3.ts b/ui_template/ui_template_locale_b3.ts index 5b7c9ef9..b79d3809 100644 --- a/ui_template/ui_template_locale_b3.ts +++ b/ui_template/ui_template_locale_b3.ts @@ -26,6 +26,7 @@ export const ui_locales_b3 = { B09_Sheet_Won: ["원", " KRW"], B09_Sheet_Count: ["건", ""], B09_Sheet_Unconfirmed: ["미확정", "Unconfirmed"], + B09_Sheet_Unpriced: ["금액에 안 들어감", "not in the amount"], B09_Sheet_Missing: ["금액을 못 세운 줄", "Rows without a price"], B09_Sheet_Reload: ["다시 불러오기", "Reload"], B09_Sheet_Level: ["보이는 레벨", "Show levels"],