Files
Aislo/resources/master_data/scripts/master_doc_convert.py
eomsangdonandClaude Opus 5.5 abe722f378 feat(804-7): 줄 쌓기 「수식」 탭 수정 2차(요구 687 · 688)
- 로직 삭제 = 서버 정본에서 바로(로직 줄 + 수식 문서 · `/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>
2026-11-08 13:33:36 +09:00

630 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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