- build_table 이 양식 있는 종류(찰쌓기) 성분을 양식 풀이 값으로 갈음. 양식 없는 종류는 전개 그대로 - 구조물에 library_item — 원단위 탭 「양식」·구조물도 머리 「양식 있음/없음」으로 가림 - 뒤 단계가 아직 이름으로 찾으므로 돌 줄은 종류 이름으로 넘김(1장 코드 잇기 뒤 걷을 자리) · 자료 출처·근거 문구는 전개 것을 그대로 둠 - 검증 프로젝트 전·후 대조: 구조물 5 중 찰쌓기 2 갈음 · 움직인 값 없음 · 자재총괄 최대 차 7e-15 · 채집석 공제 58.674·잔토 91.75 그대로. Node 가 안 돌면 전개 값 + 사유 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
249 lines
11 KiB
Python
249 lines
11 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 _downstream_name(result: dict[str, Any]) -> str:
|
|
"""뒤 단계(자재총괄·인계)가 찾는 이름 — 돌 줄만 종류 이름으로.
|
|
|
|
⚠ 명세 13장 Ⓒ 는 이름 고정 「돌」 + `spec` 이지만, 자재총괄·할증표·인계가 아직 **이름으로**
|
|
찾음(1장 「문자열에서 코드로」 일감이 끝나기 전). 그 일감이 끝나면 이 갈음을 걷을 것.
|
|
"""
|
|
if result["name"] == "돌" and result.get("spec"):
|
|
return str(result["spec"])
|
|
return str(result["name"])
|
|
|
|
|
|
def replace_with_templates(
|
|
quantities: list[Any],
|
|
inputs: list[dict[str, Any]],
|
|
section_modes: dict[float, str] | None,
|
|
rubble_base_thickness_m: float | None,
|
|
) -> None:
|
|
"""`build_table` 이 부름 — 양식이 있는 종류의 성분을 **양식 풀이 값으로 갈음**(자리에서).
|
|
|
|
⚠ 전개가 성분을 하나도 못 낸 구조물(표에 없는 뒷길이 등)은 그대로 둠 — 전개 사유가 이미 드러냄.
|
|
⚠ Node 가 안 돌면 전개 값을 두고 **사유를 남김** — 조용히 섞이지 않게(브레인 챙길 것 ②).
|
|
⚠ 오류 난 양식 줄은 성분에서 빼고 사유로 — 전개도 원문 「-」 줄을 안 세우고 사유로 둠.
|
|
"""
|
|
from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets
|
|
from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of
|
|
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, _section_mode_at
|
|
from common_util.common_util_structure_face_role import structure_face_role
|
|
|
|
settings = {"rubble_base_thickness_m": rubble_base_thickness_m}
|
|
targets: list[tuple[Any, dict[str, Any]]] = []
|
|
for quantity, item in zip(quantities, inputs):
|
|
template = load_template(quantity.type_id)
|
|
if template is None or not quantity.components:
|
|
continue
|
|
options = item.get("options") or {}
|
|
face, face_reason = structure_face_role(
|
|
_section_mode_at(item, section_modes), options.get("side")
|
|
)
|
|
sheet = {
|
|
"type_id": quantity.type_id,
|
|
"height_m": quantity.height_m,
|
|
"options": options,
|
|
"face": face,
|
|
"face_reason": face_reason,
|
|
}
|
|
structure = {
|
|
"height_m": quantity.height_m,
|
|
"length_m": quantity.length_m,
|
|
"options": options,
|
|
}
|
|
values = template_vars(template, structure, slope_of(sheet)[0], settings)
|
|
targets.append((quantity, template_sheet(template, values)))
|
|
if not targets:
|
|
return
|
|
|
|
solved = evaluate_sheets([body for _quantity, body in targets])
|
|
for index, (quantity, _body) in enumerate(targets):
|
|
if solved is None:
|
|
quantity.notes.append("⚠ 식 풀이기를 못 돌려 양식 대신 지금 전개 값으로 섰음")
|
|
continue
|
|
template = load_template(quantity.type_id) or {}
|
|
by_seq = {row["seq"]: row for row in template.get("rows") or []}
|
|
# 값의 **자료 출처**(야면석 무게 = 울진 관측 등)는 전개가 붙인 그대로.
|
|
# 양식이냐는 구조물 단위(`library_item`)로 따로 둠.
|
|
engine_source = {c.name: c.source for c in quantity.components}
|
|
# 근거 문구도 전개 것을 씀 — 「강도 210 — 안 정해 기본값」·「판정 1:0.3 성토」처럼
|
|
# **그 구조물에서 왜 그 값인지**가 들어 있어 양식의 붙박이 설명보다 많이 말함(값은 같음).
|
|
# ⚠ ⑤ 에서 사용자가 식을 고친 줄은 전개 문구가 틀리게 되므로 그때 양식 설명으로 갈 것.
|
|
engine_basis = {c.name: c.basis for c in quantity.components}
|
|
components = []
|
|
for result in solved[index]:
|
|
if result.get("skipped"):
|
|
continue
|
|
source = by_seq.get(result["seq"]) or {}
|
|
if result.get("error"):
|
|
quantity.notes.append(f"양식 줄 「{result['name']}」이 안 섬 — {result['error']}")
|
|
continue
|
|
name = _downstream_name(result)
|
|
components.append(
|
|
Component(
|
|
name,
|
|
str(source.get("unit") or ""),
|
|
float(result["amount"]),
|
|
str(source.get("destination") or ""),
|
|
engine_basis.get(name) or str(source.get("formula_text") or ""),
|
|
source=engine_source.get(name, ""),
|
|
spec="" if name != result["name"] else str(result.get("spec") or ""),
|
|
)
|
|
)
|
|
quantity.components = components
|
|
# 「양식 있음/없음」을 화면이 가리게 — 조용히 섞이면 왜 값이 다른지 못 찾음.
|
|
quantity.library_item = str(template.get("name") or quantity.type_id)
|
|
|
|
|
|
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")}
|