diff --git a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py index 71c3d762..ad2d25f6 100644 --- a/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py +++ b/B08_Quantity/B08_Quantity_Engine_ObservedUnit.py @@ -72,6 +72,8 @@ class ObservedUnitTable: entries: list[dict[str, Any]] = field(default_factory=list) sources: dict[str, Any] = field(default_factory=dict) not_found: dict[str, Any] = field(default_factory=dict) + #: 값을 바꾸는 **설계 조건**인데 우리 제원에 칸이 없는 것 — 화면이 보이게 한다. + pending_choices: dict[str, Any] = field(default_factory=dict) def find(self, type_id: str, spec: dict[str, Any]) -> dict[str, Any] | None: """규격이 **모두** 맞는 줄만 돌려준다. 하나라도 어긋나면 없는 것으로 본다.""" @@ -101,6 +103,7 @@ def load_observed_table(path: Path | None = None) -> ObservedUnitTable: entries=list(payload.get("entries") or []), sources=payload.get("sources") or {}, not_found=payload.get("not_found") or {}, + pending_choices=payload.get("pending_choices") or {}, ) diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py index 164f1516..c1e5d697 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity.py @@ -288,7 +288,21 @@ OBSERVED_SPEC_KEYS: dict[str, tuple[str, ...]] = { EXPANDERS = { "masonry_wet": lambda h, l, o: stone_masonry(h, l, o, wet=True), "masonry_dry": lambda h, l, o: stone_masonry(h, l, o, wet=False), - "boulder_masonry": lambda h, l, o: stone_masonry(h, l, o, wet=False), + # ⚠⚠ **큰돌쌓기(`boulder_masonry`)를 여기에 두지 않는다** (2026-09-07 발견). + # 큰돌쌓기는 품셈 **13-6** 이고 돌쌓기는 **13-4** 다 — **규격 축이 다르다.** + # 돌쌓기는 **뒷길이**(35·45·55·60㎝), 큰돌쌓기는 **직경**(40~60·60~80·80~100㎝). + # 앞서 `stone_masonry(dry)` 로 전개하고 있었는데, 그러면 직경 60~80㎝ 짜리가 + # **「뒷길이 45㎝」 계수로 돌아 조용히 틀린 값**이 나온다(고임돌 0.15·야면석 0.88 …). + # ⚠ 값이 나오기는 하므로 어떤 시험도 안 잡던 자리다 — 「값이 있기는 하니 안 보이는」 그것. + # 전개식·관측 원단위가 설 때까지 **미확보로 드러낸다.** +} + +#: 전개식을 일부러 안 두는 종류 — 왜 안 두는지 사람이 읽게 적는다. +EXPANDER_WITHHELD = { + "boulder_masonry": ( + "큰돌쌓기는 품셈 13-6 이고 규격 축이 **직경**(40~60·60~80·80~100㎝)이다. " + "돌쌓기(13-4)의 **뒷길이** 계수로 돌리면 조용히 틀린 값이 나온다 — 전개식 미확보." + ), } @@ -363,6 +377,11 @@ def expand( end_m=end if structure.get("end_m") is not None else None, options=dict(options), ) + withheld = EXPANDER_WITHHELD.get(type_id) + if withheld: + result.notes.append(f"전개식 미확보 — {withheld}") + return result + expander = EXPANDERS.get(type_id) if expander is None: # 전개식이 없으면 **관측 원단위표**를 본다(치수가 저장돼 있지 않은 종류). @@ -456,6 +475,9 @@ def build_table( "formwork_reuse_missing": formwork_missing, # 동바리 — 대상이 없으면 0 이 아니라 「없음」이라고 말한다. "shoring": shoring_status(), + # ⚠ 값을 바꾸는 설계 조건인데 우리 제원에 칸이 없는 것 — 화면에 드러낸다. + # 「무엇을 정해야 하는지」만으로는 부족하고 **「정하면 얼마나 달라지는지」**까지. + "pending_choices": (observed.pending_choices or {}).get("items") or [], "totals": sorted(totals.values(), key=lambda entry: entry["name"]), # 할증 전 값임을 응답에 못 박는다 — 자재총괄이 한 번만 붙인다(㉠). "surcharge_applied": False, diff --git a/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts b/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts index 35c2db87..a288291d 100644 --- a/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_MaterialGrid.ts @@ -102,6 +102,14 @@ export interface FormworkInfo { formwork_notes?: string[]; formwork_reuse_missing?: string[]; shoring?: { applicable: boolean; reason: string; pending_types: string[] }; + /** 값을 바꾸는 설계 조건인데 우리 제원에 칸이 없는 것 — 화면에 드러낸다. */ + pending_choices?: { + label: string; + default?: unknown; + where?: string; + effect?: string; + scope?: string; + }[]; } /** 성분이 어디로 가는지 — 화면에서도 보이게 한다. 규칙이 코드에만 있으면 잊힌다. */ @@ -349,6 +357,19 @@ export function renderUnitQuantityGrid(response: MaterialResponse): HTMLElement wrap.append(line); } + // ⚠ 값을 바꾸는 설계 조건인데 칸이 없는 것 — 「무엇을 정해야 하는지」만으로는 부족하고 + // **「정하면 얼마나 달라지는지」**까지 보여야 사용자가 판단한다. + for (const choice of info.pending_choices ?? []) { + const line = document.createElement("p"); + line.className = "b08-quantity__notice"; + const parts = [`⚠ 미확정: ${choice.label}`]; + if (choice.effect) parts.push(choice.effect.replace(/\*\*/g, "")); + if (choice.where) parts.push(`근거 ${choice.where}`); + if (choice.scope) parts.push(choice.scope.replace(/\*\*/g, "")); + line.textContent = parts.join(" · "); + wrap.append(line); + } + // ⚠ 품셈에 그 이름의 공종이 없어 **여러 공종으로 나뉘어 서는** 구조물 — 무엇으로 // 나뉘는지와 각 조각의 물량·갈래를 보인다. 코드만으로는 사람이 검증할 수 없다. for (const group of response.composite ?? []) { diff --git a/resources/data_structure_unit/structure_unit_observed_2026-01-01.json b/resources/data_structure_unit/structure_unit_observed_2026-01-01.json index 9d97d33a..f1898673 100644 --- a/resources/data_structure_unit/structure_unit_observed_2026-01-01.json +++ b/resources/data_structure_unit/structure_unit_observed_2026-01-01.json @@ -304,5 +304,32 @@ "source": "품셈 13-6-2 [주]⑩ 「큰돌쌓기(찰쌓기)의 뒤채움콘크리트량은 0.2㎥기준으로 하고 현지여건에 따라 0.3㎥까지 적용할 수 있다」" } ] + }, + "pending_choices": { + "note": "값을 바꾸는 **설계 조건**인데 우리 제원에 칸이 없는 것. 기본값과 「정하면 얼마나 달라지는지」를 함께 적어 화면이 보이게 한다.", + "items": [ + { + "key": "anti_suction_sheet", + "label": "흡출방지재·차수시트 시공", + "default": false, + "where": "품셈 13-6·13-7 [주]② — 「흡출방지재 또는 차수시트를 시공하는 경우는 ( )의 값을 적용한다」", + "effect": "인부 수량이 갈림 — 큰돌쌓기 메쌓기 직경 40~60㎝ 기준 보통인부 1.04 → 1.17 인/10㎡ (약 +12.5 %)", + "scope": "⚠ 큰돌쌓기(13-6)·큰돌붙이기(13-7)에만 걸린다. 지금 쓰는 돌쌓기(13-4)에는 괄호 값 자체가 없다." + }, + { + "key": "timber_crib_unit", + "label": "목재틀흙막이 원단위", + "default": "품셈 13-13-1 그대로", + "where": "품셈 13-13-1 (단위: 인/㎥당)", + "effect": "1㎥당 건축목공 16.975인 + 보통인부 1.848인. ⚠ 실무 감각에 맞는지 아무도 판단 못 했고, 각재·판재 자재가 카탈로그에 없어 **지금 값은 모자란 값**이다." + }, + { + "key": "bill_quantity_digits", + "label": "내역서 수량 표시 자릿수", + "default": null, + "where": "`단수처리_규칙.md` 에 **금액 자리만 있고 수량 자리가 없음**", + "effect": "표시 자릿수와 계산 자릿수가 다르면 보는 사람이 반올림해 곱해 보고 「틀렸다」고 한다(2026-09-07 실제로 그렇게 오진한 일이 있었다)." + } + ] } }