- 새 M02_Sangeun_Equation — 문서 덧칸 `수식고침`(M01 수식 고침 꼴 + 까닭) = 사본 위 기록 · 사본은 찍은 그대로(원래 대비) - 셈 = _base_of 가 바탕 줄(사본 · 가져온 식)에 얹음 → 모든 셈 길 한 벌 · 못 얹음 = 짝잃음 「수식」 · 저장 = 400 - 길 POST …/sangeun/equation(층 읽기) · /equation/preview(층 쓰기 · 저장 없음) · 저장 = SK [저장] - 가져온 식(SK 키) = 잠깐 키 GX999997 로 엔진에 얹음 · 사본 색인 Copy 얕은 사본(__copy__) - 원래(sheet) = 수식고침 뺀 셈 · 수식고침 SK 펼치지 않음 · 더한 입력 이름 입력값에 남김 - 시험 test_796_2b_m02_equation 6 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
7.3 KiB
Python
158 lines
7.3 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 가 얹음).
|
|
"""
|
|
|
|
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:
|
|
stand, key = _stand(row)
|
|
return _eqn().export_row(whole, stand, inputs) | {"열쇠": 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 read(doc: Any, inputs: dict | None = None) -> dict[str, Any]:
|
|
"""{ok, 수식} — 저장본 · 화면 캐시 고침 얹은 수식(못 얹음 = 바탕 그대로 + 까닭)."""
|
|
eqn = _eqn()
|
|
try:
|
|
doc, whole, row = _plain(doc)
|
|
now, lost = on(row, doc, whole)
|
|
out = {"ok": True, "수식": export(whole, now, 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]:
|
|
"""고침 통째 미리 셈(저장 없음) — {되나, 까닭, 줄, 수식, 전(고침 없는 사본), 후}."""
|
|
eqn = _eqn()
|
|
try:
|
|
doc, whole, row = _plain(doc)
|
|
before = export(whole, row, inputs)["합계"]
|
|
new, why = apply(row, edits, whole) if edits else (row, [])
|
|
if why:
|
|
return {"되나": False, "까닭": why}
|
|
eq = export(whole, new, inputs)
|
|
except (sd.DocError, *eqn._ERR) as e:
|
|
return {"되나": False, "까닭": [f"셈 막힘 — {e}"]}
|
|
return {"되나": True, "까닭": [], "줄": new, "수식": eq, "전": before, "후": eq["합계"]}
|