"""구조물도 **양식형 항목** 읽기 — 양식 파일 + 구조물 제원 → 식 풀이기에 넘길 장 한 벌. 양식은 `resources/library_structure/.json`(4장 프로그램 기본 자리 · 명세 13장 식 칸 계약). 풀이는 여기 없음 — `B08_Quantity_Engine_Formula.evaluate_sheets`(TS 한 벌을 Node 로)가 풂. ⚠ 규격(높이·뒷길이·돌종류)마다 항목을 늘리지 않음 — 제원은 `vars` 로 들어가고 같은 식이 수량을 다시 냄(명세 16장 「한 조합 + 규격별 수량표」). """ from __future__ import annotations import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] TEMPLATE_DIR = ROOT / "resources" / "library_structure" def load_template(type_id: str) -> dict[str, Any] | None: """프로그램 기본 양식. 없는 종류면 `None` — 그 종류는 지금 전개(고정형 모양)로 섬.""" path = TEMPLATE_DIR / f"{type_id}.json" if not path.is_file(): return None return json.loads(path.read_text(encoding="utf-8")) def _typed(value: Any, default: Any) -> Any: """제원 값을 양식 기본값과 같은 꼴로 — 수 칸은 수, 글 칸은 글.""" if isinstance(default, (int, float)) and not isinstance(default, bool): try: return float(value) except (TypeError, ValueError): return default return str(value) def template_vars( template: dict[str, Any], structure: dict[str, Any], judged_slope: float, settings: dict[str, Any] | None = None, ) -> dict[str, Any]: """양식이 적은 제원 칸을 구조물·산출 조건에서 채움. 빈 칸은 양식 기본값. ⚠ 전면 기울기는 **전개가 판정한 값**을 받음 — 표준경사 판정(성절토·직고)은 식 언어 밖이라 판정 한 벌(`face_slope_ratio`)을 그대로 씀. """ options = structure.get("options") or {} values: dict[str, Any] = {} for name, spec in (template.get("vars") or {}).items(): default = spec.get("default") source = spec.get("source") if source == "judged_face_slope": values[name] = float(judged_slope) continue if source: values[name] = float(structure.get(source) or 0.0) continue if "option" in spec: raw = options.get(spec["option"]) else: raw = (settings or {}).get(spec.get("setting")) values[name] = default if raw in (None, "") else _typed(raw, default) return values def template_sheet(template: dict[str, Any], values: dict[str, Any]) -> dict[str, Any]: """식 풀이기가 받는 장 한 벌 — 줄·표는 양식 그대로, 제원만 끼움.""" return { "rows": template.get("rows") or [], "vars": values, "tables": template.get("tables") or {}, } #: 양식이 m당으로 풀리는 단위 — 연장 L=1 로 풀면 곧 단위당 값(모든 줄이 L 에 비례). _PER_LENGTH_UNITS = frozenset({"m"}) def _library_rows( template: dict[str, Any], solved: list[dict[str, Any]], billing: float ) -> list[dict[str, Any]]: """풀이 결과를 구조물도 줄 모양으로 — 식·설명·반올림·갈 곳·안 섬까지 실음(명세 13장).""" by_seq = {row["seq"]: row for row in template.get("rows") or []} rows: list[dict[str, Any]] = [] for result in solved: source = by_seq.get(result["seq"]) or {} unit_amount = float(result["amount"]) if result.get("amount") is not None else None rows.append( { "no": result["seq"], "name": result["name"], "spec": result.get("spec") or "", "basis": source.get("formula_text") or "", "formula": source.get("formula") or "", "unit_amount": unit_amount, "amount": None if unit_amount is None else unit_amount * billing, "unit": source.get("unit") or "", "basis_kind": "derived", "source": source.get("source") or "library", "destination": source.get("destination") or "", "rounding": source.get("rounding"), "skipped": bool(result.get("skipped")), "reason": result.get("reason") or "", "error": result.get("error") or "", } ) return rows def apply_templates(payload: dict[str, Any], settings: dict[str, Any] | None = None) -> None: """구조물도 장마다 **양식이 있으면 양식으로 줄을 다시 세움**(자리에서 고침). ⚠ 양식이 없는 종류는 지금 전개 줄 그대로 — `formula` 빈칸 = 고정형 모양(명세 13장). ⚠ Node 풀이가 안 돌면 전개 줄을 두고 **그 사실을 장 사유에 적음** — 조용히 넘기지 않음. ⚠ 장은 제원 조합 하나라 **L=1(m당)** 으로 풂 — 연장은 제원이 아니고 모든 줄이 L 에 비례. """ from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of targets: list[tuple[dict[str, Any], dict[str, Any]]] = [] for sheet in payload.get("sheets") or []: template = load_template(str(sheet.get("type_id") or "")) if template is None or sheet.get("billing_unit") not in _PER_LENGTH_UNITS: for row in sheet.get("rows") or []: row.setdefault("formula", "") row["source"] = row.get("source") or "auto" continue structure = { "height_m": sheet.get("height_m"), "length_m": 1.0, "options": sheet.get("options"), } values = template_vars(template, structure, slope_of(sheet)[0], settings) targets.append((sheet, template_sheet(template, values))) if not targets: return solved = evaluate_sheets([body for _sheet, body in targets]) for index, (sheet, _body) in enumerate(targets): template = load_template(str(sheet.get("type_id") or "")) or {} if solved is None: sheet["notes"].append("⚠ 식 풀이기를 못 돌려 양식 대신 지금 전개 값으로 섰음") continue members = sheet.get("members") or [] billing = float(members[0].get("billing_quantity") or 0.0) if members else 0.0 sheet["rows"] = _library_rows(template, solved[index], billing) sheet["unpriced_rows"] = [ row["name"] for row in sheet["rows"] if row["unit_amount"] is None and not row["skipped"] ] sheet["library_item"] = {"type_id": template.get("type_id"), "name": template.get("name")}