Files
Aislo/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py
T
eomsangdonandClaude Opus 5 1375d8c290 feat(b08): 구조물도 탭에 양식 식을 실음 — 산출 기초·식·갈 곳·안 섬
- 양식이 있는 종류(찰쌓기)는 장마다 양식으로 줄을 다시 세움(L=1 m당) — 식·설명·반올림·
  갈 곳·출처·안 섬 까닭을 줄에 실음. 양식 없는 종류는 지금 전개 그대로(고정형 모양)
- 화면 표: 산출 근거 밑에 식 한 줄 · 갈 곳 칸 · 안 선 줄은 「안 섬」과 까닭
- 전개 결함 둘 고침(브레인 판정): 「실무 관행」 계수가 돌종류를 지우던 것 ·
  기초잡석이 서는데 「두께가 없어 안 섬」 사유가 함께 뜨던 것
- 검증 프로젝트 찰쌓기 15줄 — 양식 m당×10 = 원단위 탭 합계 전부 일치

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-13 17:23:12 +09:00

155 lines
6.6 KiB
Python

"""구조물도 **양식형 항목** 읽기 — 양식 파일 + 구조물 제원 → 식 풀이기에 넘길 장 한 벌.
양식은 `resources/library_structure/<type_id>.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")}