Files
Aislo/M02_MasterTemplete/M02_Sangeun_Equation.py
T
eomsangdonandClaude Sonnet 5.5 137cfb701d feat(796-2c): M02 수식 탭이 SK 셈 답 전체를 그림 — 더한 기초 · 뺀줄 · 덮개 · 율 줄 · 합밖 소계표 · 합계 = SK 금액 · 고친 칸 표지
- 서버 M02_Sangeun_Equation_Full(새 파일) — 합계 = SK 셈 답 기준 · 소계 = 셈 답 줄마다 한 행(바탕 · 덮음 · 더함 · 뺌 · 합밖) + 끝수 · 더한 기초 수식 나무 · 줄 표지(식 · 상수 · 표값 · 품 · 단가 · 변수줄 · 뺌 + 원래 → 지금)
- M02_Sangeun_Equation — read · preview 가 얹음(전 = 고침 없는 SK 금액) · 수식 입력 = 자동값(SK 셈과 같은 입력)
- 부품 createEqSum(tree, open) 상태 알약 · 원래 · 접힘 기초 수식 · 줄 표지 · 흐림 CSS
- 시험 test_796_2c(견본 넷) · 낡은 시험 정리 — test_792_1 TS 거울 시험(799-2 에 걷은 모델 파일 읽음) 지움 · test_799_1 호출 글 갱신
- SK 금액 전후 같음 · M01 수식 탭 그대로

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-07 13:24:35 +09:00

185 lines
8.8 KiB
Python

"""M02 단가산출(SK) 수식 탭 서버 몫(PLAN 796-2b · 요구 655 · 656 · 657 · 요구서_655 0 · 2 장 1).
수식 = 그 SK 바탕 줄(로직 사본 줄 `M02_Sangeun_Copy` · 가져온 식 줄 `M02_Sangeun_Imported.row_of`) +
문서 덧칸 `수식고침` = 사본 위 고침 기록 — M01 수식 탭 고침 꼴(`master_equation_edit` 머리 ·
차례대로 · 식 · 더함 · 지움 · 변수 · 표시) + 줄마다 `까닭`?(예정가격작성기준 제2조 ②) ·
사본은 찍은 그대로(= 원래 대비 · 고친 칸 = 고침 목록).
셈 = `M02_Sangeun_Calc._base_of` 가 바탕 줄에 `on` 으로 얹음(엔진 · 검사 = M01 `edit_row` 한 벌) →
모든 셈 길(화면 · 저장 원값 · B09 · 펼침) 같은 줄 · 못 얹은 고침(재생성 뒤 자리 없음 등) =
고침 없는 바탕 + 짝잃음 cid `수식` · 저장(`strict`) = 400 · [재생성] = 새 사본에 같은 고침
다시 얹음(못 얹음 = 795-1 `못얹은`).
길(`M02_Sangeun_Router` · 저장 없음):
POST …/sangeun/equation {문서, inputs?} → {ok, 수식 | reason}(층 읽기)
POST …/sangeun/equation/preview {문서, inputs?, 고침} → {되나, 까닭, 줄, 수식, 전, 후}
(고침 = `수식고침` 통째 · 전 = 고침 없는 사본 합계 · 층 쓰기)
저장 = SK [저장](화면 캐시 `수식고침` → `clean_doc` 이 다시 얹어 검사 · 셈 ·
브라우저 값 안 받음).
바탕 참조 SK(`바탕.참조`) = 수식 없음(바탕 SK 의 고침은 그 SK 가 얹음).
합계 · 소계(796-2c) = SK 셈 답 기준(`M02_Sangeun_Equation_Full` — 더한 기초 · 뺀줄 · 덮개 · 율 줄 ·
합밖) · 줄 값 = 식 덮개 · 상수 · 표값 · 변수줄을 얹은 식(고침 주소는 바탕 차례 그대로) ·
전 = 고침 없는 SK 금액.
"""
from __future__ import annotations
import hashlib
import json
from typing import Any
from M02_MasterTemplete import M02_Sangeun_Doc as sd
KEY = "수식고침"
MANY = 200
LONG = 500 # 까닭 길이
LOST = "수식" # 짝잃음 cid
STAND = "GX999997" # 가져온 식(SK 키) 줄을 엔진에 얹을 잠깐 키(GX999998 = 견본 · 999999 = 초안)
_MEMO: dict[tuple[str, str], tuple[dict, list[str]]] = {}
def _eqn() -> Any:
from M01_MasterData import M01_MasterData_Equation as eqn # M02 가 M01 을 부름 — 늦게
return eqn
def _hash(v: Any) -> str:
raw = json.dumps(v, ensure_ascii=False, sort_keys=True, default=str)
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
def clean(raw: Any, out: dict) -> dict:
"""`sd.clean` 끝 — 꼴만(목록 · 묶음 · 까닭 길이) · 뜻 검사는 셈 · 저장 때 `on`."""
edits = raw.get(KEY) if isinstance(raw, dict) else None
if not edits:
return out
if not isinstance(edits, list) or len(edits) > MANY:
raise sd.DocError(f"수식고침 · {MANY} 개까지 목록")
for at, one in enumerate(edits, 1):
why = one.get("까닭") if isinstance(one, dict) else None
if not isinstance(one, dict) or (why is not None and len(str(why)) > LONG):
raise sd.DocError(f"수식고침 {at}째 · 묶음 · 까닭 {LONG} 자까지")
out[KEY] = json.loads(json.dumps(edits, ensure_ascii=False, default=str))
return out
def added_inputs(doc: Any) -> set[str]:
"""고침으로 더한 입력 이름 — `sd.clean` 입력값 거름에서 남김."""
edits = (doc.get(KEY) if isinstance(doc, dict) else None) or []
return {
str(e.get("열쇠"))
for e in edits
if isinstance(e, dict) and e.get("할일") == "더함" and e.get("갈래") == "입력"
}
def _stand(row: dict) -> tuple[dict, str]:
"""(엔진이 로직으로 부를 줄, 본 키) — M01 로직 키 아님(가져온 식 SK) = 잠깐 키."""
key = str(row.get("키"))
return (row, key) if key[:2] in ("GC", "GF", "GX") else ({**row, "키": STAND}, key)
def export(whole: Any, row: dict, inputs: dict | None) -> dict:
"""수식 읽기 — 입력 = 받은 값 › 자동값(SK 셈 `run_line` 과 같음 · 기본 제안값이 안 덮음)."""
stand, key = _stand(row)
eqn = _eqn()
auto = eqn.meq.ma.values(eqn.me.swap(whole, stand), stand)
given = {**{k: v for k, v in auto.items() if v is not None}, **(inputs or {})}
return eqn.export_row(whole, stand, given) | {"열쇠": key}
def apply(row: dict, edits: list, whole: Any) -> tuple[dict, list[str]]:
"""바탕 줄 + 고침 → (새 줄, 까닭) — 같은 줄 · 같은 고침 = 메모(셈 길마다 다시 검사 안 함)."""
tag = (_hash(row), _hash(edits))
got = _MEMO.get(tag)
if got is None:
if len(_MEMO) > 64: # ponytail: 통째 비움 · 쓰는 SK 가 많아 느려지면 LRU 로
_MEMO.clear()
stand, key = _stand(row)
new, why = _eqn().edit_row(whole, stand, edits)
got = _MEMO[tag] = ({**new, "키": key}, why)
return got
def on(row: dict, doc: Any, whole: Any) -> tuple[dict, list[dict]]:
"""셈 길 — 고침 얹은 줄 · 못 얹음 = 바탕 그대로 + 짝잃음 `수식`."""
edits = (doc.get(KEY) if isinstance(doc, dict) else None) or []
if not edits:
return row, []
new, why = apply(row, edits, whole)
if why:
return row, [{"cid": LOST, "까닭": "수식 고침 못 얹음 — " + " · ".join(why)}]
return new, []
def strict(base: dict) -> None:
"""저장 — 이 SK 고침이 못 얹히면 400(바탕 SK 사슬 것 = 알림만)."""
for x in base.get("짝잃음") or []:
if x.get("cid") == LOST and "바탕" not in x:
raise sd.DocError(x["까닭"])
# ── 길 ──────────────────────────────────────────────────────────────
def _plain(doc: Any) -> tuple[dict, Any, dict]:
"""(정한 문서, 사본 색인, 고침 없는 바탕 줄) — 바탕 참조 · 빈 SK = DocError."""
from M02_MasterTemplete import M02_Sangeun_Calc as calc
if not isinstance(doc, dict):
raise sd.DocError("단가산출 문서가 묶음 아님")
base = doc.get("바탕") or {}
if "참조" in base:
raise sd.DocError("바탕 참조 단가산출 — 수식은 바탕 단가산출에서")
doc = calc.skc.settle(doc)
whole = calc.skc.over(doc, calc.labor.m01.loaded()[1])
bare = {k: v for k, v in doc.items() if k != KEY}
row = calc._base_of(bare, whole, calc.refs.read_ref, ())["줄"]
if not row.get("중간") and not row.get("호표") and not row.get("결과"):
raise sd.DocError("수식 없음 — 바탕 로직 · 가져온 식이 없는 단가산출")
return doc, whole, row
def _tree(doc: dict, whole: Any, base: dict, inputs: dict | None, ans: dict | None) -> dict:
"""수식 = 바탕 줄(+ 고침 · 식 덮개 · 상수 · 표값 · 변수줄) + SK 셈 답 합계 · 소계(796-2c)."""
from M02_MasterTemplete import M02_Sangeun_Const as const
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
try:
shown = const.formulas(base, doc)
except Exception: # noqa: BLE001 — 틀린 식 덮개 = 바탕 그대로(짝잃음은 셈 길이 알림)
shown = base
return full.attach(export(whole, shown, inputs), doc, whole, base, inputs, ans)
def read(doc: Any, inputs: dict | None = None) -> dict[str, Any]:
"""{ok, 수식} — 저장본 · 화면 캐시 고침 얹은 수식(못 얹음 = 바탕 그대로 + 까닭)."""
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
eqn = _eqn()
try:
doc, whole, row = _plain(doc)
now, lost = on(row, doc, whole)
out = {"ok": True, "수식": _tree(doc, whole, now, inputs, full.answer(doc, inputs))}
except (sd.DocError, *eqn._ERR) as e:
return {"ok": False, "reason": str(e) or type(e).__name__}
return out | ({"reason": lost[0]["까닭"]} if lost else {})
def preview(doc: Any, edits: list, inputs: dict | None = None) -> dict[str, Any]:
"""고침 통째 미리 셈(저장 없음) — {되나, 까닭, 줄, 수식, 전(고침 없는 사본 SK 금액), 후}."""
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
eqn = _eqn()
try:
doc, whole, row = _plain(doc)
new, why = apply(row, edits, whole) if edits else (row, [])
if why:
return {"되나": False, "까닭": why}
bare = {k: v for k, v in doc.items() if k != KEY}
mine = {**bare, KEY: edits} if edits else bare
ans = full.answer(mine, inputs)
eq = _tree(mine, whole, new, inputs, ans)
before = full.total(full.answer(bare, inputs) if edits else ans)
before = before or export(whole, row, inputs)["합계"]
except (sd.DocError, *eqn._ERR) as e:
return {"되나": False, "까닭": [f"셈 막힘 — {e}"]}
return {"되나": True, "까닭": [], "줄": new, "수식": eq, "전": before, "후": eq["합계"]}