- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/logic/remove`) → 로직 목록 · 부르는 곳(다른 수식 문서 · 일위대가 조합 · M02 단가산출 바탕)이 있으면 확인 창 전에 막고 까닭(`/logic/users`) - 머리 단추 다섯 — [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장] · [저장] 한 번 = 기본정보 + 수식 · [되돌림] = 둘 다 저장 뒤 상태 · 줄 쌓기 [고침 버림] · [수식 저장]은 M01 에서 걷음(M02 사본은 그대로) - 합계 상자 화면 아래 고정 · 원문 손잡이 브라우저 오른쪽 끝 - 왼쪽: 그룹 머리 한 줄(접어도 [+] · X 자리 그대로) · 「비우면 번호 없음」 · 가나다 체크 걷음 · 금액 줄 한 줄 배치 · 이름 단추 = 그 이름 찾은 찾기 창 · 수량 기호 = 값 + 출처 꼬리표 · [+] 메뉴(단가산출 · 율 · 식 · 근거 글 · 틀 찾기 걷음) · 식 글 눌러 그 자리 고침([식 고치기] 걷음) · 단위 = 공용 단위 고르기(친 글도 씀) · 만약 안 탄 갈래 숨김 - 오른쪽 기계 줄: 비목 수 링크 걷음 · 제목 줄 누름 = 시간당 경비 펼침 · 펼친 제목 보통 글 - 요소 키가 기호 이름으로 샌 것(`k_EQ0006`) — 변환기가 자원 이름으로(`k_하베스터`) · 수식 문서 다시 빌드(10 파일 · 78 기호 · 금액 같음) - 시험 페이지 `tools/line-stack-test` · main.py 마운트 · vite 프록시 · M01 왼쪽 「시험: 줄 쌓기」 걷음 - 단위 표준 표 읽기 = 로그인만(M02 일반 사용자 단위 목록) - 시험: test_804_7 새로 · 804_2 · 804_4 이음 글 고침(createStackScreen 줄 나뉨) · 552_3 여는 단추 기본 ⋯ 꼴로 고침(부르는 쪽이 [+] 로 바꿀 수 있음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
630 lines
25 KiB
Python
630 lines
25 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""옛 M01 로직 줄 → 수식 문서 변환 (PLAN 802-2 · 요구서_664 1 장 3 · 꼴 `_수식문서_틀.md`).
|
||
|
||
빌더 `build_수식문서_802_2.py` 가 부름 · 다시 돌리면 같은 결과.
|
||
- 기호 = 800-1 · 800-5 기호(수식 탭이 보이는 것
|
||
· `master_equation.export` + `master_equation_sign.settle`) ·
|
||
못 내보내는 로직은 같은 규칙(`master_symbol`)으로 · 한 문서 안 겹침 = 아래첨자 끝 숫자.
|
||
- 입력 = 선택(고르기) · 입력 · 값 = 기본 옵션(`master_equation.given_of`) · 중간 = 식 줄
|
||
· 한 마디 중간
|
||
(표 찾기 · 요소 · 로직 부름 하나) = 그 기호에 바로 연결 · 표 찾기 · 요소
|
||
· 로직 부름 = 기호 + 연결.
|
||
- 호표 = 금액 줄(수량 × 단가 · 단가 = 요소 · 재료 고르기 · 단가산출 묶음)
|
||
· 덧줄 = 금액 줄(단가 = 식) ·
|
||
`줄.'이름'` · 앞 덧줄 이름 = 그 금액 줄 기호 · 끝수 결과 · 표시 = 식 안 반올림
|
||
· 금액 끝수 = 문서 `끝수`.
|
||
옛 엔진 셈은 안 함(기호 · 이름 보기만 export 를 씀).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from decimal import Decimal
|
||
|
||
import master_doc as md
|
||
import master_equation as meq
|
||
import master_equation_edit as mee
|
||
import master_equation_sign as mes
|
||
import master_formula as mf
|
||
import master_material as mm
|
||
import master_symbol as msym
|
||
|
||
_ERR = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError, IndexError)
|
||
SUBMAX = msym.SUBMAX
|
||
|
||
|
||
def _harvest(master, key: str) -> dict:
|
||
"""수식 탭 내보내기 기호 — (꼴, 갈래, 식 | 열쇠) → 마디 · 못 내보내면 빈 것."""
|
||
try:
|
||
ex = mes.settle(meq.export(master, key))
|
||
except _ERR:
|
||
return {}
|
||
out: dict = {}
|
||
|
||
def visit(t):
|
||
if isinstance(t, list):
|
||
for x in t:
|
||
visit(x)
|
||
elif isinstance(t, dict):
|
||
if t.get("꼴") in ("변수", "블록"):
|
||
out.setdefault((t["꼴"], t.get("갈래"), str(t.get("식") or t.get("열쇠"))), t)
|
||
for k, v in t.items():
|
||
if k != "나무" and isinstance(v, (dict, list)):
|
||
visit(v)
|
||
|
||
for line in ex.get("줄") or []:
|
||
visit(line.get("이름식"))
|
||
if isinstance(line.get("결과"), dict):
|
||
head = line["결과"]
|
||
out.setdefault(("머리", line.get("갈래"), str(head.get("열쇠"))), head)
|
||
return out
|
||
|
||
|
||
_CODE = re.compile(r"^(?:EQ|MT|LB)\d")
|
||
|
||
|
||
def res_low(master, link: dict, low: str) -> str:
|
||
"""요소 키가 아래첨자로 샌 것(`k_EQ0006` · 804-7 · 요구 687-18) → 자원 이름 끝 낱말(`k_하베스터`) ·
|
||
괄호 · 쉼표 뒤 · 숫자로 시작하는 규격 낱말 뺌 · 겹침은 부른 쪽이 2 · 3."""
|
||
if not _CODE.match(low or "") or (link or {}).get("갈래") != "요소":
|
||
return low
|
||
try:
|
||
row = master.get(str(link.get("키") or ""))
|
||
except _ERR:
|
||
return low
|
||
name = re.sub(r"\(.*?\)", "", str(row.get("이름") or "")).split(",")[0]
|
||
words = [w for w in name.split() if not w[:1].isdigit()]
|
||
got = msym.short(words[-1]) if words else ""
|
||
return got if got and got != "·" else low
|
||
|
||
|
||
def _clean(text: str) -> str:
|
||
"""기호 · 첨자 글 다듬기 — 낱말 아닌 글 뺌."""
|
||
return re.sub(r"[^\w′'·]|_", "", str(text or ""))
|
||
|
||
|
||
class _Reg:
|
||
"""기호 자리 — 열쇠마다 (기호, 아래, 위) 제안 → 겹침 없는 이름."""
|
||
|
||
def __init__(self):
|
||
self.props: dict[tuple, tuple[str, str, str]] = {}
|
||
self.ids: dict[tuple, str] = {}
|
||
self.used: set[str] = set()
|
||
self.notes: list[str] = []
|
||
|
||
def want(self, key: tuple, parts: tuple[str, str, str]) -> None:
|
||
self.props.setdefault(key, parts)
|
||
|
||
def settle(self) -> None:
|
||
for key, (sign, low, up) in self.props.items():
|
||
if key in self.ids:
|
||
continue
|
||
if not md.good_id(md.sym_id(sign, "", "")):
|
||
fixed = _clean(sign)
|
||
fixed = fixed if fixed and md.good_id(fixed) else "X"
|
||
self.notes.append(f"기호 「{sign}」 → 「{fixed}」")
|
||
sign = fixed
|
||
n, base = 1, low
|
||
while md.sym_id(sign, low, up) in self.used or not md.good_id(md.sym_id(sign, low, up)):
|
||
n += 1
|
||
low = (base or "")[: SUBMAX - len(str(n))] + str(n)
|
||
if n > 99:
|
||
raise ValueError(f"기호 못 가름 {key}")
|
||
self.used.add(md.sym_id(sign, low, up))
|
||
self.ids[key] = md.sym_id(sign, low, up)
|
||
|
||
def id(self, key: tuple) -> str:
|
||
return self.ids.get(key, "?")
|
||
|
||
|
||
class _Conv:
|
||
def __init__(self, master, row: dict, given: dict):
|
||
self.master, self.row, self.given = master, row, given
|
||
self.key = str(row["키"])
|
||
self.found = _harvest(master, self.key)
|
||
self.table = {v["열쇠"]: v for v in meq.table(row, master)}
|
||
self.reg = _Reg()
|
||
self.links: dict[tuple, dict] = {} # 열쇠 → 변수 칸(이름 · 단위 · 연결)
|
||
self.order: list[tuple] = []
|
||
self.alias: dict[str, tuple] = {} # 한 마디 중간 식 → ('중간', 이름)
|
||
self.names = {s["이름"] for s in row.get("입력") or []} | {
|
||
s["이름"] for s in row.get("중간") or []
|
||
}
|
||
self.kinds = {s["이름"]: "입력" for s in row.get("입력") or []}
|
||
self.kinds |= {s["이름"]: "중간" for s in row.get("중간") or []}
|
||
self.notes: list[str] = []
|
||
self.used_lines: set[tuple] = set() # 식이 부른 금액 줄 — ('합' | '덧', 이름)
|
||
self.quiet = "" # 한 마디 중간 연결만 뜰 때(그 마디 식) — 기호 자리 안 잡음
|
||
self.last: dict = {}
|
||
|
||
# 기호 제안
|
||
def _look(self, kind: str, raw: str) -> dict | None:
|
||
for k in (("변수", kind, raw), ("블록", None, raw), ("변수", "요소", raw)):
|
||
if k in self.found:
|
||
return self.found[k]
|
||
return None
|
||
|
||
def _var(self, key: tuple, family: str, raw: str, name: str, link: dict, unit: str = "") -> str:
|
||
if self.quiet and self.quiet == raw:
|
||
self.quiet, self.last = "", link
|
||
return "?"
|
||
if key not in self.links:
|
||
node = self._look(family, raw) or {}
|
||
sign = str(node.get("기호") or "")
|
||
if sign and node.get("기호") != node.get("열쇠"):
|
||
parts = _merge(sign, str(node.get("아래") or ""), str(node.get("위") or ""))
|
||
else:
|
||
fam = {"블록": "결과"}.get(family, family)
|
||
parts = msym.lookup(name or raw, fam if fam in msym.FAMILY else "요소")
|
||
parts = (parts[0], res_low(self.master, link, parts[1]), parts[2])
|
||
self.reg.want(key, parts)
|
||
self.order.append(key)
|
||
label = str(node.get("이름") or name or raw)
|
||
self.links[key] = {
|
||
"이름": label,
|
||
"단위": str(node.get("단위") or unit or ""),
|
||
"연결": link,
|
||
}
|
||
self.links[key]["연결"] = link # 두 번째 돌이 = 겹침 가른 이름으로 쓴 연결
|
||
return self.reg.id(key)
|
||
|
||
def own(self, name: str) -> str:
|
||
"""입력 · 중간 이름 → 기호."""
|
||
if name not in self.names:
|
||
self.notes.append(f"모르는 이름 「{name}」")
|
||
return name if md.good_id(name) else "X_" + _clean(name)
|
||
return self.reg.id((self.kinds[name], name))
|
||
|
||
def holes(self, text):
|
||
if not isinstance(text, str):
|
||
return text
|
||
return md.HOLE.sub(lambda m: "{" + self.own(m.group(1)) + "}", text)
|
||
|
||
def _place(self) -> dict:
|
||
out = {}
|
||
if mm.REGION in self.kinds:
|
||
out["지역"] = self.own(mm.REGION)
|
||
if mm.TARIFF in self.kinds:
|
||
out["계약종별"] = self.own(mm.TARIFF)
|
||
return out
|
||
|
||
# 식
|
||
def emit(self, node, money: dict | None = None):
|
||
kind = node[0]
|
||
if kind in ("num", "str"):
|
||
return node
|
||
if kind == "name":
|
||
name = node[1]
|
||
if money is not None and name in md.COSTS:
|
||
return ("var", name)
|
||
if money is not None and name in money:
|
||
self.used_lines.add(money[name])
|
||
return ("var", self.reg.id(money[name]))
|
||
return ("var", self.own(name))
|
||
if kind == "ref":
|
||
return ("var", self.element(node[1]))
|
||
if kind == "neg":
|
||
return ("neg", self.emit(node[1], money))
|
||
if kind == "bin":
|
||
return ("bin", node[1], self.emit(node[2], money), self.emit(node[3], money))
|
||
if kind == "call":
|
||
return ("call", node[1], [self.emit(x, money) for x in node[2]])
|
||
if kind == "find":
|
||
return ("var", self.find(node, None))
|
||
if kind == "logic":
|
||
return ("var", self.block(node, None))
|
||
base, col = node[1], node[2]
|
||
if base[0] == "find":
|
||
return ("var", self.find(base, col))
|
||
if base[0] == "logic":
|
||
return ("var", self.block(base, col))
|
||
if base[0] == "ref":
|
||
return ("var", self.element(base[1], col))
|
||
if base[0] == "name" and base[1] == "줄" and money is not None:
|
||
self.used_lines.add(("합", col))
|
||
return ("var", self.reg.id(("합", col)))
|
||
if base[0] == "field" and base[1][0] == "name" and base[1][1] in ("번지", "단계"):
|
||
return ("field", ("field", ("var", base[1][1]), base[2]), col)
|
||
return ("field", self.emit(base, money), col)
|
||
|
||
def text(self, raw, money: dict | None = None) -> str:
|
||
return md.text_of(self.emit(mf.parse(raw), money))
|
||
|
||
# 연결 마디
|
||
def find(self, node, col) -> str:
|
||
raw = mee.raw_text(node if col is None else ("field", node, col))
|
||
if raw in self.alias:
|
||
return self.reg.id(self.alias[raw])
|
||
link = {"갈래": "표", "표": self.holes(node[1])}
|
||
link["조건"] = {k: md.text_of(self.emit(v)) for k, v in node[2]}
|
||
if col is None:
|
||
self.notes.append(f"칸 없는 찾기 「{raw}」")
|
||
else:
|
||
link["칸"] = col
|
||
return self._var(("찾기", raw), "찾기", raw, col or raw, link)
|
||
|
||
def block(self, node, col) -> str:
|
||
raw = mee.raw_text(node if col is None else ("field", node, col))
|
||
if raw in self.alias:
|
||
return self.reg.id(self.alias[raw])
|
||
link = {"갈래": "단가산출", "키": self.holes(node[1])}
|
||
link["입력"] = {k: md.text_of(self.emit(v)) for k, v in node[2]}
|
||
if col is not None:
|
||
link["칸"] = col
|
||
try:
|
||
sub = self.master.logic(mf.fill(node[1], {})) if "{" not in node[1] else {}
|
||
except _ERR:
|
||
sub = {}
|
||
unit = str(sub.get("결과단위") or "")
|
||
return self._var(("블록", raw), "블록", raw, str(sub.get("이름") or node[1]), link, unit)
|
||
|
||
def element(self, ref: str, col: str | None = None, unit: str | None = None) -> str:
|
||
raw = ref if col is None else mee.raw_text(("field", ("ref", ref), col))
|
||
if raw in self.alias and unit is None:
|
||
return self.reg.id(self.alias[raw])
|
||
link = {"갈래": "요소", "키": self.holes(ref)}
|
||
if col is not None:
|
||
link["칸"] = col
|
||
if unit is not None:
|
||
link["단위"] = unit
|
||
link |= self._place()
|
||
try:
|
||
label = self.master.label(ref) if "{" not in ref else ref
|
||
except _ERR:
|
||
label = ref
|
||
return self._var(("요소", raw, unit), "요소", raw, label, link)
|
||
|
||
def material(self, at: int, item: dict) -> str:
|
||
cond = {k: self.holes(v) for k, v in item["요소"].items()}
|
||
link = {"갈래": "요소", "재료": cond, "단위": item.get("단위") or ""} | self._place()
|
||
name = str(item.get("이름") or "")
|
||
return self._var(("재료", at), "요소", f"단가:{name}", f"{name}단가", link)
|
||
|
||
def lone(self, raw: str):
|
||
"""한 마디 식(표 찾기 · 요소 · 로직 부름 하나) — 마디 · 아니면 None."""
|
||
try:
|
||
node = mf.parse(raw)
|
||
except mf.FormulaError:
|
||
return None
|
||
if node[0] in ("ref", "logic"):
|
||
return node
|
||
if node[0] == "field" and node[1][0] in ("find", "logic", "ref"):
|
||
return node
|
||
return None
|
||
|
||
|
||
def _merge(sign: str, low: str, up: str) -> tuple[str, str, str]:
|
||
"""기호 칸에 첨자까지 든 것(「t_1」 · 「C_m」)을 가름."""
|
||
if "_" in sign or "^" in sign:
|
||
head, low0, up0 = md.split_id(sign)
|
||
if head != sign:
|
||
return head, low0 + low, up or up0
|
||
return sign, low, up
|
||
|
||
|
||
def _parts(var: dict) -> tuple[str, str, str]:
|
||
return _merge(
|
||
str(var.get("기호") or var["열쇠"]), str(var.get("아래") or ""), str(var.get("위") or "")
|
||
)
|
||
|
||
|
||
def _line_parts(c: _Conv, name: str) -> tuple[str, str, str]:
|
||
head = c.found.get(("머리", "호표", name)) or c.found.get(("머리", "덧줄", name)) or {}
|
||
if head.get("기호") and head.get("기호") != name:
|
||
return str(head["기호"]), str(head.get("아래") or ""), str(head.get("위") or "")
|
||
return msym.lookup(name, "합")
|
||
|
||
|
||
def convert(master, row: dict, given: dict | None = None) -> tuple[dict, list[str]]:
|
||
"""로직 줄 → (수식 문서, 알림). given = 기본 옵션(없으면 `given_of`)."""
|
||
given = meq.given_of(master, row) if given is None else given
|
||
c = _Conv(master, row, given)
|
||
for spec in row.get("입력") or []:
|
||
c.reg.want(
|
||
("입력", spec["이름"]), _parts(c.table.get(spec["이름"], {"열쇠": spec["이름"]}))
|
||
)
|
||
for step in row.get("중간") or []:
|
||
c.reg.want(
|
||
("중간", step["이름"]), _parts(c.table.get(step["이름"], {"열쇠": step["이름"]}))
|
||
)
|
||
for item in row.get("호표") or []:
|
||
c.reg.want(("합", str(item.get("이름") or "")), _line_parts(c, str(item.get("이름") or "")))
|
||
for item in row.get("덧줄") or []:
|
||
c.reg.want(("덧", str(item.get("이름") or "")), _line_parts(c, str(item.get("이름") or "")))
|
||
lone_steps = {}
|
||
for step in row.get("중간") or []:
|
||
node = c.lone(step["식"])
|
||
if node is not None and step.get("표시") is None:
|
||
raw = mee.raw_text(node)
|
||
if raw not in c.alias:
|
||
c.alias[raw] = ("중간", step["이름"])
|
||
lone_steps[step["이름"]] = node
|
||
doc = None
|
||
for _ in range(2): # 한 번 = 기호 모음 · 두 번 = 겹침 가른 이름으로 글
|
||
c.reg.settle()
|
||
doc = _build(c, row, given, lone_steps)
|
||
c.notes += c.reg.notes
|
||
return slim_doc(doc), sorted(set(c.notes))
|
||
|
||
|
||
def _is(node, value) -> bool:
|
||
return node[0] == "num" and node[1] == value
|
||
|
||
|
||
def slim(node):
|
||
"""옮김 군더더기 걷기(802-3) — `+ 0` · `- 0` · `× 1` · `÷ 1` 지움 · `A × k ÷ 100` → `A × k%`(값 같음)."""
|
||
kind = node[0]
|
||
if kind == "bin":
|
||
op, a, b = node[1], node[2], node[3]
|
||
if op == "/" and _is(b, 100) and a[0] == "bin" and a[1] == "*" and a[3][0] == "num":
|
||
return ("bin", "*", slim(a[2]), ("pct", a[3]))
|
||
a, b = slim(a), slim(b)
|
||
if (op in ("+", "-") and _is(b, 0)) or (op in ("*", "/") and _is(b, 1)):
|
||
return a
|
||
if (op == "+" and _is(a, 0)) or (op == "*" and _is(a, 1)):
|
||
return b
|
||
return ("bin", op, a, b)
|
||
if kind in ("neg", "pct"):
|
||
return (kind, slim(node[1]))
|
||
if kind == "field":
|
||
return ("field", slim(node[1]), node[2])
|
||
if kind == "call":
|
||
return ("call", node[1], [slim(x) for x in node[2]])
|
||
return node
|
||
|
||
|
||
def _slim_text(text):
|
||
if not isinstance(text, str) or not text.strip():
|
||
return text
|
||
try:
|
||
return md.text_of(slim(md.parse(text)))
|
||
except md.DocError:
|
||
return text
|
||
|
||
|
||
def slim_doc(doc: dict) -> dict:
|
||
"""식 줄 · 금액 줄(수량 · 단가) 글에 `slim`."""
|
||
for line in doc.get("식") or []:
|
||
line["식"] = _slim_text(line.get("식"))
|
||
for line in doc.get("금액") or []:
|
||
for k in ("수량", "단가"):
|
||
if k in line:
|
||
line[k] = _slim_text(line[k])
|
||
return doc
|
||
|
||
|
||
def _link_of(c: _Conv, node) -> dict:
|
||
"""한 마디 중간의 연결 — 찾기 · 요소 · 로직 부름."""
|
||
alias = dict(c.alias)
|
||
c.alias, c.quiet, c.last = {}, mee.raw_text(node), {}
|
||
try:
|
||
c.emit(node)
|
||
return dict(c.last)
|
||
finally:
|
||
c.alias, c.quiet = alias, ""
|
||
|
||
|
||
def _build(c: _Conv, row: dict, given: dict, lone_steps: dict) -> dict:
|
||
variables: list[dict] = []
|
||
for spec in row.get("입력") or []:
|
||
name = spec["이름"]
|
||
look = c.table.get(name, {})
|
||
value = given.get(name)
|
||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||
value = Decimal(str(value))
|
||
if "고르기" in spec:
|
||
link = {"갈래": "선택", "목록": list(spec["고르기"]), "값": value}
|
||
else:
|
||
link = {"갈래": "입력", "값": value}
|
||
if spec.get("범위"):
|
||
link["범위"] = list(spec["범위"])
|
||
variables.append(_var_row(c.reg.id(("입력", name)), name, look, spec, link))
|
||
lines: list[dict] = []
|
||
for step in row.get("중간") or []:
|
||
name = step["이름"]
|
||
look = c.table.get(name, {})
|
||
me = c.reg.id(("중간", name))
|
||
if name in lone_steps:
|
||
variables.append(_var_row(me, name, look, step, _link_of(c, lone_steps[name])))
|
||
continue
|
||
try:
|
||
text = c.text(step["식"])
|
||
except mf.FormulaError as e:
|
||
c.notes.append(f"중간 「{name}」 식 못 읽음 — {e}")
|
||
text = str(step["식"])
|
||
if step.get("표시") is not None:
|
||
text = f"반올림({text}, {int(step['표시'])})"
|
||
variables.append(_var_row(me, name, look, step, {"갈래": "식"}))
|
||
lines.append({"기호": me, "식": text})
|
||
doc = {
|
||
"꼴": "수식문서",
|
||
"판": 1,
|
||
"키": c.key,
|
||
"이름": row.get("이름") or "",
|
||
"결과단위": row.get("결과단위") or "",
|
||
"출처": row.get("출처") or "",
|
||
}
|
||
money: list[dict] = []
|
||
if "결과" in row:
|
||
head = c.found.get(("머리", "결과", "결과")) or {}
|
||
sign = head.get("기호") if msym.is_symbol(str(head.get("기호") or "")) else ""
|
||
parts = (sign, str(head.get("아래") or ""), str(head.get("위") or "")) if sign else None
|
||
parts = parts or msym.rule(str(row.get("결과단위") or ""), "결과")[:3]
|
||
c.reg.want(("결과", "결과"), parts)
|
||
text = c.text(row["결과"]["식"])
|
||
rule = row.get("끝수")
|
||
if isinstance(rule, dict) and rule.get("대상") == "결과":
|
||
text = f"{rule['방법']}({text}, {int(rule.get('자리') or 0)})"
|
||
me = c.reg.id(("결과", "결과"))
|
||
variables.append(
|
||
{
|
||
"열쇠": "결과",
|
||
**_split(me),
|
||
"이름": "결과",
|
||
"단위": row.get("결과단위") or "",
|
||
"연결": {"갈래": "식"},
|
||
}
|
||
)
|
||
lines.append({"기호": me, "식": text})
|
||
doc["결과"] = me
|
||
else:
|
||
money = _money(c, row)
|
||
for key in c.order:
|
||
if key[0] in ("입력", "중간") or key in [("결과", "결과")]:
|
||
continue
|
||
if any(alias == key for alias in c.alias.values()):
|
||
continue
|
||
one = c.links[key]
|
||
me = c.reg.id(key)
|
||
variables.append(
|
||
{
|
||
"열쇠": me,
|
||
**_split(me),
|
||
"이름": one["이름"],
|
||
"단위": one["단위"],
|
||
"연결": one["연결"],
|
||
}
|
||
)
|
||
doc |= {"변수": variables, "식": lines, "금액": money}
|
||
cut = {k: (dict(v) if isinstance(v, dict) else v) for k, v in md.CUT.items()}
|
||
rule = row.get("끝수")
|
||
if isinstance(rule, dict) and rule.get("대상") == "계":
|
||
cut["계"] = {"방법": rule["방법"], "자리": int(rule.get("자리") or 0)}
|
||
if row.get("끝전"):
|
||
cut["끝전"] = row["끝전"]
|
||
doc["끝수"] = cut
|
||
for k in ("단계", "조정"):
|
||
if row.get(k):
|
||
doc[k] = row[k]
|
||
return doc
|
||
|
||
|
||
def _split(me: str) -> dict:
|
||
sign, low, up = md.split_id(me)
|
||
return {"기호": sign, "아래": low, "위": up}
|
||
|
||
|
||
def _var_row(me: str, name: str, look: dict, spec: dict, link: dict) -> dict:
|
||
out = {
|
||
"열쇠": name,
|
||
**_split(me),
|
||
"이름": look.get("이름") or name,
|
||
"단위": spec.get("단위") or "",
|
||
}
|
||
for k in ("설명", "출처"):
|
||
if spec.get(k):
|
||
out[k] = spec[k]
|
||
out["연결"] = link
|
||
return out
|
||
|
||
|
||
def _hand(c: _Conv, at: int, item: dict) -> str:
|
||
"""손 값 줄 — 값묶음(노 · 재 · 경) 또는 손 단가 = 숫자 연결."""
|
||
name = str(item.get("이름") or "")
|
||
given = item.get("값묶음")
|
||
if given:
|
||
if set(given) < set(md.COSTS):
|
||
c.notes.append(f"호표 「{name}」 값묶음 일부 — 빠진 비목 0")
|
||
value = {k: Decimal(str(given.get(k) or 0)) for k in md.COSTS}
|
||
else:
|
||
value = Decimal(str(item["단가"]))
|
||
link = {"갈래": "숫자", "값": value}
|
||
return c._var(("손", at), "요소", f"단가:{name}", f"{name}단가", link)
|
||
|
||
|
||
def _group_name(items: list[dict]) -> str:
|
||
kinds = []
|
||
for x in items:
|
||
k = x.get("종류") or str(x.get("이름") or "")
|
||
if k not in kinds:
|
||
kinds.append(k)
|
||
return " · ".join(kinds)
|
||
|
||
|
||
def _money(c: _Conv, row: dict) -> list[dict]:
|
||
items, adds = row.get("호표") or [], row.get("덧줄") or []
|
||
out: list[dict] = []
|
||
for at, item in enumerate(items):
|
||
name = str(item.get("이름") or "")
|
||
line = {"이름": name}
|
||
if item.get("규격"):
|
||
line["규격"] = item["규격"]
|
||
line["단위"] = item.get("단위") or ""
|
||
try:
|
||
qty = c.text(item["수량"])
|
||
except mf.FormulaError as e:
|
||
c.notes.append(f"호표 「{name}」 수량 못 읽음 — {e}")
|
||
qty = str(item["수량"])
|
||
kind = item.get("종류")
|
||
if item.get("값묶음") or item.get("단가") is not None: # M02 손 값(값묶음 · 손 단가)
|
||
price, line["비목"] = _hand(c, at, item), item.get("비목") or "묶음"
|
||
if line["비목"] != "묶음" and item.get("값묶음"):
|
||
price = f"{price}.계"
|
||
elif kind == "로직":
|
||
node = mf.parse(str(item["요소"]))
|
||
price = md.text_of(c.emit(node))
|
||
bundle = node[0] == "logic"
|
||
if "비목" in item:
|
||
line["비목"] = item["비목"]
|
||
if bundle:
|
||
price = f"{price}.계"
|
||
else:
|
||
line["비목"] = "묶음"
|
||
elif isinstance(item.get("요소"), dict):
|
||
line["비목"] = item["비목"]
|
||
price = c.material(at, item)
|
||
else:
|
||
line["비목"] = item["비목"]
|
||
ref = str(
|
||
item["요소"]
|
||
) # 품셈재료(MP)만 줄 단위로 값이 갈림 — 그 밖은 식 속 요소와 한 기호
|
||
price = c.element(
|
||
ref, None, str(item.get("단위") or "") if ref[:2] == mm.LINKED else None
|
||
)
|
||
line |= {"수량": qty, "단가": price}
|
||
for k in ("번지", "합밖", "자리", "출처"):
|
||
if k in item:
|
||
line[k] = item[k]
|
||
out.append(line)
|
||
seen: dict[str, tuple] = {}
|
||
for add in adds:
|
||
name = str(add.get("이름") or "")
|
||
try:
|
||
price = c.text(add["식"], dict(seen))
|
||
except mf.FormulaError as e:
|
||
c.notes.append(f"덧줄 「{name}」 식 못 읽음 — {e}")
|
||
price = str(add["식"])
|
||
line = {"이름": name, "단위": "식", "비목": add["비목"], "단가": price}
|
||
for k in ("번지", "합밖", "자리", "출처"):
|
||
if k in add:
|
||
line[k] = add[k]
|
||
out.append(line)
|
||
seen[name] = ("덧", name)
|
||
last = {
|
||
str(x.get("이름") or ""): i for i, x in enumerate(items)
|
||
} # 같은 이름 = 뒤 줄(옛 `줄` 묶음)
|
||
for i, line in enumerate(out):
|
||
key = ("합", line["이름"]) if i < len(items) else ("덧", line["이름"])
|
||
if key in c.used_lines and (i >= len(items) or last[line["이름"]] == i):
|
||
line["기호"] = c.reg.id(key)
|
||
_groups(out, [*items, *adds])
|
||
return out
|
||
|
||
|
||
def _groups(out: list[dict], items: list[dict]) -> None:
|
||
"""묶음 — 단가산출 부름 줄마다 한 묶음 · 그 사이 다른 줄은 이어진 것끼리 · 묶음 하나면 안 둠."""
|
||
runs: list[tuple[str, list[int]]] = []
|
||
for i, item in enumerate(items):
|
||
if item.get("종류") == "로직":
|
||
runs.append((str(item.get("이름") or ""), [i]))
|
||
elif runs and runs[-1][0] == "" and items[runs[-1][1][-1]].get("종류") != "로직":
|
||
runs[-1][1].append(i)
|
||
else:
|
||
runs.append(("", [i]))
|
||
if len(runs) < 2:
|
||
return
|
||
for name, members in runs:
|
||
label = name or _group_name([items[i] for i in members])
|
||
for i in members:
|
||
out[i]["묶음"] = label
|