feat(796-2b): M02 단가산출 수식 서버 길 — 사본 줄 수식 읽기 · 고침 미리 셈 · 사본 위 고침 기록(수식고침) · [저장] 정본 셈
- 새 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>
This commit is contained in:
@@ -32,12 +32,12 @@ import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor, M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Const as const, M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc, M02_Sangeun_Views as views
|
||||
from M02_MasterTemplete import M02_Sangeun_Const as const, M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd, M02_Sangeun_Equation as eqm
|
||||
from M02_MasterTemplete import M02_Sangeun_Views as views
|
||||
|
||||
COSTS = labor.COSTS
|
||||
Ref = dict[str, Any]
|
||||
@@ -120,13 +120,13 @@ def _base_of(doc: dict, whole: Any, read: Any, trail: tuple[str, ...]) -> dict[s
|
||||
"""이 SK 가 얹힐 바탕 — {줄, 고름, 자재표시, 입력값, 켜쌓기율, 짝잃음}."""
|
||||
base = doc.get("바탕")
|
||||
empty = {"고름": {}, "자재표시": {}, "입력값": {}, "켜쌓기율": None, "짝잃음": []}
|
||||
if not base:
|
||||
return {**empty, "줄": sd.empty_row(doc)}
|
||||
if "로직" in base:
|
||||
if not base or "로직" in base: # 사본(795-1) · 가져온 식 · 그 위 수식 고침(796-2b)
|
||||
try:
|
||||
return {**empty, "줄": skc.over(doc, whole).logic(base["로직"])} # 사본(795-1)
|
||||
row = skc.over(doc, whole).logic(base["로직"]) if base else sd.empty_row(doc)
|
||||
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
|
||||
raise sd.DocError(f"바탕 로직 · {e}") from e
|
||||
row, lost = eqm.on(row, doc, skc.over(doc, whole))
|
||||
return {**empty, "줄": row, "짝잃음": lost}
|
||||
ref = base["참조"]
|
||||
rid = refs.ref_id(ref)
|
||||
if rid in trail:
|
||||
@@ -533,7 +533,7 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
||||
try:
|
||||
rough = sd.clean(doc, None, whole)
|
||||
_check_refs(rough, here_)
|
||||
base = _base_of(rough, whole, refs.read_ref, ())
|
||||
eqm.strict(base := _base_of(rough, whole, refs.read_ref, ())) # 수식 고침(796-2b)
|
||||
out = sd.clean(doc, base["줄"], whole)
|
||||
out["바탕"], out["더한줄"] = rough["바탕"], rough["더한줄"]
|
||||
for one in out["더한줄"]: # 처음수량(744-1) = 넣을 때 수량 · 없으면 지금 수량
|
||||
|
||||
@@ -30,6 +30,7 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation as eqm
|
||||
from M02_MasterTemplete import M02_Sangeun_Formula as fx
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
@@ -159,8 +160,11 @@ def lost(row: dict, doc: dict) -> list[dict]:
|
||||
|
||||
def no_flat(doc: Any) -> None:
|
||||
"""펼침 막음 — 상수 · 표값 덮개 SK(펼친 줄에 못 옮김 · 옛 참조 그대로 = 값 그대로)."""
|
||||
if isinstance(doc, dict) and (doc.get("상수덮개") or doc.get("표값덮개") or fx.has(doc)):
|
||||
raise sd.DocError("상수 · 표값 덮개 · 식 칸 단가산출은 펼치지 않음(참조 그대로)")
|
||||
keys = ("상수덮개", "표값덮개", eqm.KEY) # 수식 고침(796-2b)
|
||||
if isinstance(doc, dict) and (any(doc.get(k) for k in keys) or fx.has(doc)):
|
||||
raise sd.DocError(
|
||||
"상수 · 표값 덮개 · 식 칸 · 수식 고침 단가산출은 펼치지 않음(참조 그대로)"
|
||||
)
|
||||
if imp.is_imported(doc): # 가져온 식(793-2) = M01 기초 없음
|
||||
raise sd.DocError("가져온 식 단가산출은 펼치지 않음(참조 그대로)")
|
||||
|
||||
@@ -231,6 +235,7 @@ def clean_more(raw: Any, out: dict) -> dict:
|
||||
out["까닭"] = why
|
||||
if isinstance(raw.get("사본"), dict): # 로직 사본(795-1) — 셈 · 저장 첫머리 settle 이 정함
|
||||
out["사본"] = raw["사본"]
|
||||
out = eqm.clean(raw, out) # 수식 고침(796-2b)
|
||||
return imp.clean(raw, fx.clean(raw, out)) # 식 칸(792-1) · 가져온 식(793-2)
|
||||
|
||||
|
||||
@@ -336,7 +341,7 @@ def answer(got: dict, clean: dict, raw: Any, given: dict | None, trail: tuple) -
|
||||
|
||||
|
||||
def _bare(doc: dict) -> dict:
|
||||
out = {k: v for k, v in doc.items() if k not in (*NEW, *fx.FIELDS, "뺀더한줄")}
|
||||
out = {k: v for k, v in doc.items() if k not in (*NEW, *fx.FIELDS, "뺀더한줄", eqm.KEY)}
|
||||
return out | {"덮개": {}, "뺀줄": [], "더한줄": []}
|
||||
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@ class Copy:
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._whole, name)
|
||||
|
||||
def __copy__(self) -> Copy: # 수식 고침(796-2b · `master_equation_edit.swap`) — 얕은 사본
|
||||
out = object.__new__(Copy)
|
||||
out.__dict__.update(self.__dict__)
|
||||
return out
|
||||
|
||||
|
||||
def _rows(copy: dict) -> list[dict]:
|
||||
raw = json.dumps(prices.to_json([*copy["로직"], *(copy.get("단가") or [])]), ensure_ascii=False)
|
||||
|
||||
@@ -315,7 +315,9 @@ def clean(doc: Any, base_row: dict | None = None, whole: Any = None) -> dict[str
|
||||
given = _given(doc.get("입력값"), "입력값")
|
||||
opened = [str(x) for x in doc.get("열린입력") or []]
|
||||
if base_row is not None:
|
||||
keep = names | _added_names(whole, doc.get("더한줄"))
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation as eqm # 수식 고침 더한 입력(796-2b)
|
||||
|
||||
keep = names | _added_names(whole, doc.get("더한줄")) | eqm.added_inputs(doc)
|
||||
given = {k: v for k, v in given.items() if k in keep}
|
||||
bad = [x for x in opened if x not in names or x in given]
|
||||
if bad:
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"""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["합계"]}
|
||||
@@ -7,6 +7,8 @@
|
||||
(만들 층 쓰기).
|
||||
로직 사본(795-1 · `M02_Sangeun_Copy`) `…/sangeun/copy/price` [단가 갱신] · `/regen` [재생성]
|
||||
(화면 캐시 문서에 할일 얹음 · 저장 없음 · 층 읽기).
|
||||
수식 탭(796-2b · `M02_Sangeun_Equation`) `…/sangeun/equation`(층 읽기) · `/preview`(고침 미리 셈 ·
|
||||
층 쓰기) — 저장 없음 · 고침 = 화면 캐시 `수식고침` → [저장].
|
||||
|
||||
저장 안 함 · 로그인 사용자 · 층 읽기 권한(`Router_Items._root`) · 화면은 최신 응답만.
|
||||
몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값) ·
|
||||
@@ -27,6 +29,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
from M02_MasterTemplete import M02_Sangeun_Copy as skc
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation as eqm
|
||||
from M02_MasterTemplete import M02_Sangeun_Formula as fx
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
@@ -215,3 +218,33 @@ def sangeun_copy(
|
||||
raise HTTPException(status_code=404, detail="price · regen")
|
||||
args = (body.문서, _JOBS[how], body.입력값)
|
||||
return _call(_root(session, layer, owner), _sd(skc.preview), *args)
|
||||
|
||||
|
||||
class EquationBody(BaseModel):
|
||||
문서: dict[str, Any]
|
||||
inputs: dict[str, Any] | None = None
|
||||
고침: list[Any] | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/equation")
|
||||
def sangeun_equation(
|
||||
layer: Layer,
|
||||
body: EquationBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""수식 탭 읽기(796-2b) — 사본(가져온 식) 줄 + 문서 `수식고침` → {ok, 수식 | reason}."""
|
||||
return _call(_root(session, layer, owner), _sd(eqm.read), body.문서, body.inputs)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/equation/preview")
|
||||
def sangeun_equation_preview(
|
||||
layer: Layer,
|
||||
body: EquationBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""수식 고침 미리 셈(796-2b · 층 쓰기 · 저장 없음) — 고침 = `수식고침` 통째 →
|
||||
{되나, 까닭, 줄, 수식, 전, 후}."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, _sd(eqm.preview), body.문서, body.고침 or [], body.inputs)
|
||||
|
||||
Reference in New Issue
Block a user