Files
Aislo/resources/master_data/scripts/master_doc_link.py
T
eomsangdonandClaude Opus 5.5 a53fae747c feat(846-1): 기계 줄 시간당 작업량 — 기계 값 칸 작업량 · 덮음 일당 | 시간당 · 계산기 짝(금액 불변)
- 기계 자료: M01 기계 221 대 값 칸 `작업량` = [{식 · 단위 · 변수(규격에서 정해지는 칸)}] (건설 공통 8-2 장 작업량 틀 · 빌더 build_기계_작업량_846_1.py · --check 0 · 다른 칸 · 시간당 비용 그대로)
- 계산기 짝 master_doc_work.py ↔ common_util_formula_doc_work.ts: 기계 줄 수량에서 시간당 작업량 자리 읽기(일당 = 시공량 ÷ (8 × 대수) · 식 = …/hr 나눔 변수) · 셈 답 줄 칸 `작업량` · 덮음 `작업량: {방식 시간당, 식, 변수?}` = 그 자리를 1 ÷ Q(기계 작업량 식)로 바꿔 셈 · 부착 기계(브레이커 · 집게) 줄 따로 · 식 없는 기계 · 단위 안 맞음 = 시간당 거절
- 덮음 짝(master_doc_cover · common_util_formula_doc_cover): 작업량은 기계 식 문서에 안 얹음 · prune 이 작업량 정리(일당 · 기본과 같은 변수 = 지움) · 몸 검사 · CoverEntry · CoverBase 꼴
- 저장 길: 사본 부름 묶음에 시간당 식 키(M02_Doc_Copy.calls) · 내 기계 clean 이 작업량 받음 · 연결 읽기 작업량 칸(master_doc_link)
- 기계 보기: 작업량 칸 = 제어 구역 몫(ownedCols — 바탕 칸 숫자 입력으로 안 그림 · 그리기는 846-2)
- 지킴: 1,520 문서 · 기계 655 대 · 견본 SK 금액 전후 다름 0 · 빌더 802-2 --check 1,520 같음(옵션 표본 18,873) · 둘 다 가능 줄 전수 시간당 셈 멈춤 0
- 시험: test_846_1_machine_work(일당 · 시간당 손셈 · E · K 덮음 · 정리 · 거절 · 부착 · 거울 · 저장 길 · 내 기계) 신설 · test_816_1 기대값 고침(까닭 — 4륜 덤프 1톤 기계 줄에 작업량 칸이 생겨 값 칸 비교에서 뺌)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-16 00:00:06 +09:00

240 lines
9.8 KiB
Python

# -*- coding: utf-8 -*-
"""수식 문서 연결 풀이 — M01 마스터(PLAN 802-1 · 꼴 `_수식문서_틀.md` 3 장).
`master_doc_calc.calc(doc, Links(...))` 가 부름. 셈 엔진(`master_formula.run`
· `evaluate`)은 안 부름 —
마스터 색인(`Master`) · 요소 값(준용 · 자재 고르기 · 단위 환산 = `master_material`
· `master_pick`)만 씀.
표 찾기(범위 규칙 · 여러 줄 가름)는 여기
· TS 짝 = `common_util/common_util_formula_doc_calc.ts` `findRow`.
수식 문서 저장 자리 = `resources/master_data/수식문서/수식_<옛 로직 파일 꼬리>.json`
(로직 파일마다 한 벌).
"""
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
import master_doc as md
import master_doc_calc as mc
import master_formula as mf
import master_machine_crew as mcrew
import master_material as mm
import master_pick as mp
FOLDER = Path(__file__).resolve().parents[1] / "수식문서"
_ARITH_OK = ("num", "bin", "neg")
def _arith(text: str):
"""표 칸의 분수 · 식 글(「1/1.30」) → 수 · 아니면 글 그대로."""
try:
node = md.parse(text)
except md.DocError:
return text
if any(n[0] not in _ARITH_OK for n in md.walk(node)):
return text
def ev(n):
if n[0] == "num":
return n[1]
if n[0] == "neg":
return -ev(n[1])
a, b = ev(n[2]), ev(n[3])
return {"+": a + b, "-": a - b, "*": a * b}.get(n[1]) if n[1] != "/" else a / b
try:
return ev(node)
except ArithmeticError:
return text
def _cell(value, col: str):
if value is None:
raise mc.Gap(f"칸 「{col}」 값 없음 — 관리자가 채울 값")
if isinstance(value, list) and col in (
*mcrew.LISTS,
"작업량",
): # 기계 인력 · 잡품 줄(842-3) · 작업량(846-1)
return value
if isinstance(value, list):
raise mc.Broken(f"칸 「{col}」 값이 범위 — 입력으로 받아야 함")
if isinstance(value, str):
return _arith(value)
return value
# ── 표 찾기 ───────────────────────────────────────────────────────────
def _match(table: dict, row: dict, cond: str, want) -> bool:
if cond not in row: # 빈 조건 칸 = 무엇이든 맞음
return True
have = row[cond]
if table.get("조건", {}).get(cond) != "범위":
return have == want
low, high = have
rule, *more = str(table.get("범위규칙") or "초과·이하").split(" · ")
rule = rule.split(" (")[0]
if rule in ("이상·이하",):
return (low is None or want >= low) and (high is None or want <= high)
if rule in ("초과·이하", "이하"):
return (low is None or want > low) and (high is None or want <= high)
if rule == "이상·미만":
top = max(r[cond][1] for r in table["줄"] if r.get(cond) and r[cond][1] is not None)
closed = any("이하 포함" in x for x in more) and high == top
return (low is None or want >= low) and (
high is None or want < high or (closed and want == high)
)
raise mc.Broken(f"모르는 범위규칙 「{table.get('범위규칙')}」")
def find(table: dict, conds: dict, want: str | None = None) -> dict:
"""조건에 맞는 한 줄 — 「이하」 · 「이상·이하」 는 위 끝이 가장 작은 줄.
여럿이면 찾는 칸 있는 줄."""
for cond in conds:
if cond not in table.get("조건", {}):
raise mc.Broken(f"표 {table.get('키')} 에 없는 조건 「{cond}」")
try:
hits = [r for r in table["줄"] if all(_match(table, r, c, v) for c, v in conds.items())]
except TypeError:
raise mc.Broken(f"표 {table.get('키')} 범위 조건에 수 아닌 값") from None
if len(hits) > 1 and str(table.get("범위규칙") or "").split(" · ")[0] in ("이하", "이상·이하"):
for c in conds:
ups = [r[c][1] for r in hits if table["조건"].get(c) == "범위" and r.get(c)]
if ups and None not in ups:
hits = [r for r in hits if not r.get(c) or r[c][1] == min(ups)]
if len(hits) > 1 and want:
hits = [r for r in hits if want in r]
if len(hits) != 1:
what = " · ".join(f"{c} {mc.plain_text(v)}" for c, v in conds.items())
where = f"표 {table.get('키')}({table.get('원문번호')})"
raise mc.Gap(f"{where}에 {what} 인 줄이 " + ("없음" if not hits else f"{len(hits)}개"))
return hits[0]
# ── 연결 ──────────────────────────────────────────────────────────────
class Links:
"""M01 마스터 연결 — 표 · 요소 · 단가산출 문서(`docs` = {키: 문서} · 없으면 저장 폴더)."""
def __init__(self, master: mf.Master, docs: dict[str, dict] | None = None):
self.master = master
self.docs = load_all() if docs is None else docs
def _get(self, ref: str) -> dict:
try:
return self.master.get(ref)
except mf.FormulaError as e:
raise mc.Broken(str(e)) from None
def table(self, p: dict):
if not p.get("표") or not p.get("칸"): # 고르는 중 — 표지(예외 아님)
raise mc.Broken("표 · 칸 고르지 않음")
table = self._get(p["표"])
if "줄" not in table or "조건" not in table:
raise mc.Broken(f"「{p['표']}」 표 아님")
row = find(table, p.get("조건") or {}, p.get("칸"))
if p["칸"] not in row: # 원문 「-」 = 그 줄에 값 칸 없음
if p["칸"] not in (table.get("값칸") or {}) and not any(
p["칸"] in r for r in table["줄"]
):
raise mc.Broken(f"표 {table.get('키')} 에 칸 「{p['칸']}」 없음")
raise mc.Gap(f"표 {table.get('키')} 고른 줄에 「{p['칸']}」 값 없음")
return _cell(row[p["칸"]], p["칸"])
def element(self, p: dict):
if not p.get("키") and p.get("재료") is None: # 고르는 중 — 표지(예외 아님)
raise mc.Broken("요소 고르지 않음")
env = {}
if p.get("지역") is not None:
env[mm.REGION] = p["지역"]
if p.get("계약종별") is not None:
env[mm.TARIFF] = p["계약종별"]
if p.get("재료") is not None or p.get(
"단위"
): # 금액 줄 단가 — 고르기 · 단위 환산 · 값 없음 = 빈 줄
ref = p.get("재료") if p.get("재료") is not None else p["키"]
price, _, why, _ = mp.priced(self.master, {"단위": p.get("단위")}, ref, env, None)
if price is None:
raise mc.Gap(why or "단가 없음")
if isinstance(price, (dict, list)):
raise mc.Broken("단가가 묶음 · 범위")
return price
self._get(p["키"])
try:
value, _ = mm.element(self.master, p["키"], env)
except mf.FormulaError as e:
raise mc.Broken(str(e)) from None
col = p.get("칸")
if col:
if not isinstance(value, dict):
raise mc.Broken(f"「{p['키']}」 값에 칸 없음")
if col not in value:
raise mc.Broken(f"「{p['키']}」 칸 「{col}」 없음")
return _cell(value[col], col)
if isinstance(value, dict):
raise mc.Broken(f"「{p['키']}」 값이 묶음 — 칸 필요")
return _cell(value, self.master.label(p["키"]))
def doc(self, key: str) -> dict | None:
if key in self.docs:
return self.docs[key]
try:
row = self.master.get(key)
except mf.FormulaError:
return None
return self.docs.get(str(row.get("키")))
# ── 저장 ──────────────────────────────────────────────────────────────
def file_of(logic_file: str) -> str:
"""옛 로직 파일 이름 → 수식 문서 파일 이름(`로직_X.json` → `수식_X.json`)."""
return "수식_" + logic_file.split("_", 1)[1]
def load_file(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"), parse_float=Decimal, parse_int=Decimal)
def load_all(folder: Path = FOLDER) -> dict[str, dict]:
"""저장 폴더 문서 전부 — {키: 문서}."""
out: dict[str, dict] = {}
for path in sorted(folder.glob("수식_*.json")):
for doc in load_file(path).get("줄") or []:
out[str(doc.get("키"))] = doc
return out
def _norm(v):
if isinstance(v, Decimal):
if v == v.to_integral_value() and "E" not in str(v) and "." not in str(v):
return int(v)
return float(v)
if isinstance(v, dict):
return {k: _norm(x) for k, x in v.items()}
if isinstance(v, list):
return [_norm(x) for x in v]
return v
def _enc(v, depth: int, pad: str) -> str:
"""깊이 4(변수 · 식 · 금액 줄 하나)부터 한 줄."""
one = json.dumps(v, ensure_ascii=False, separators=(", ", ": "))
if depth >= 4 or not isinstance(v, (dict, list)) or not v:
return one
inner, cut = pad + " ", ",\n"
if isinstance(v, list):
body = cut.join(inner + _enc(x, depth + 1, inner) for x in v)
return "[\n" + body + "\n" + pad + "]"
body = cut.join(
inner + json.dumps(k, ensure_ascii=False) + ": " + _enc(x, depth + 1, inner)
for k, x in v.items()
)
return "{\n" + body + "\n" + pad + "}"
def dump(data) -> str:
"""저장 글 — UTF-8 · LF · 들여쓰기 2칸 · 변수 · 식 · 금액 줄 하나 = 한 줄.
수는 군더더기 없이."""
return _enc(_norm(data), 0, "") + "\n"