"""M02 단가산출(SK) 더한 기초 블록 [연결 끊기] · [다시 잇기](PLAN 796-2e · 요구 659-3 · 656-1). 더한 기초 = 더한줄 로직(`종류: 로직`). 기본 = 연결 — 이 SK 의 로직 사본(`M02_Sangeun_Copy`)이 닿는 그 기초 (원본 바뀜은 [재생성]·[다시 잇기]에서 따라옴 · 자동 아님 · 보기만). 끊음 = 더한줄 칸 `끊음` {시점, 로직 [M01 줄]} — 끊는 때 그 기초가 닿는 닫힌 묶음(하위 로직 · 찾기 표 · 요율 · 사본 줄 그대로 = 금액 그대로)을 이 SK 안 자기 사본으로 굳힘 → 그 줄만 고침: 칸 `수식고침`(796-2b 와 같은 꼴 · 같은 `eqm.apply`)을 그 기초 줄에 얹어 셈(`Calc.sub_run`). [재생성] = 끊은 기초의 줄은 안 바꿈(자원 단가만 — 단가는 SK 사본 `단가` · [단가 갱신] 한 길). 다시 잇기 = `끊음` · `수식고침` 뗌 + 표지 `다시잇기`(서버만 · 셈·저장 첫머리 `Copy.settle` 이 그 기초가 닿는 닫힌 묶음을 지금 M01 줄로 갈음하고 표지 뗌 — 고친 것 버림). 길(`M02_Sangeun_Router`): POST …/sangeun/block/{unlink|relink} {문서, 차례} → {문서, 전, 후}(저장 없음) · POST …/sangeun/equation[/preview] 몸 `블록` = 더한줄 차례 → 그 끊은 기초의 수식(고침 통째 미리 셈). 저장 = SK [저장](서버가 다시 얹어 검사 · 셈 · 브라우저 값 안 받음 — 끊음 줄은 사본과 같은 `taken` 꼴 검사). """ from __future__ import annotations from datetime import datetime from typing import Any from M02_MasterTemplete import M02_Item_Prices as prices 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 CUT, EDIT, REJOIN = "끊음", eqm.KEY, "다시잇기" MANY = 400 # 끊은 기초가 닿는 줄 상한 _OWN: dict[int, tuple[Any, Any, Any]] = {} def is_block(one: Any) -> bool: """더한 기초 = 더한줄 로직(참조 · 고정 · 자원 줄 아님).""" return isinstance(one, dict) and one.get("종류") == "로직" and bool(one.get("로직")) def clean(raw: dict, where: str) -> dict: """`sd._added` 가 부름 — 끊음 · 수식고침 꼴만(뜻 검사는 셈 · 저장 `strict`).""" cut = raw.get(CUT) if not cut: if raw.get(EDIT): raise sd.DocError(f"{where} · 수식 고침은 연결을 끊은 기초만") return {} rows = cut.get("로직") if isinstance(cut, dict) else None code = str(raw.get("로직")) if any(raw.get(k) not in (None, {}, []) for k in sd.BODY): raise sd.DocError(f"{where} · 넣은 SK 몸은 이미 이 SK 안 사본 — 연결 끊기 없음") if ( not isinstance(rows, list) or len(rows) > MANY or not all(isinstance(r, dict) and r.get("키") for r in rows) ): raise sd.DocError(f"{where} · 끊은 기초 사본 꼴이 틀림") rows = [r for r in rows if str(r["키"])[:2] not in skc.RES] # 단가 = SK 사본 한 곳 if code not in {str(r["키"]) for r in rows}: raise sd.DocError(f"{where} · 끊은 기초 사본에 「{code}」 줄 없음") return {CUT: {"시점": str(cut.get("시점") or ""), "로직": rows}} | eqm.clean(raw, {}) def own(one: dict, whole: Any) -> Any: """끊은 기초의 셈 색인 — `whole` 위에 자기 사본 줄(끊음 아님 = `whole` 그대로).""" cut = one.get(CUT) if not isinstance(cut, dict): return whole hit = _OWN.get(id(cut)) if hit and hit[0] is cut and hit[1] is whole: return hit[2] if len(_OWN) > 32: # ponytail: 통째 비움 · 끊은 기초가 많아 느려지면 LRU 로 _OWN.clear() got = skc.Copy(whole, skc._rows(cut)) _OWN[id(cut)] = (cut, whole, got) return got def row_of(whole: Any, one: dict, edits: list | None = None) -> tuple[dict, list[str], Any]: """(끊은 기초 줄 + 수식 고침, 못 얹은 까닭, 그 기초 셈 색인) — `edits` None = 그 기초 자기 고침.""" mine = own(one, whole) row = mine.logic(str(one["로직"])) use = one.get(EDIT) if edits is None else edits if not use: return row, [], mine new, why = eqm.apply(row, use, mine) return new, why, mine def strict(doc: dict, whole: Any) -> None: """저장 — 끊은 기초 고침이 못 얹히면 400.""" for at, one in enumerate(doc.get("더한줄") or [], 1): if is_block(one) and one.get(EDIT): why = row_of(whole, one)[1] if why: raise sd.DocError(f"더한줄 {at}째 수식 고침 못 얹음 — " + " · ".join(why)) def names(doc: Any) -> list[str]: """끊은 기초 이름들([단가 갱신] · [재생성] 보고).""" adds = (doc.get("더한줄") if isinstance(doc, dict) else None) or [] return [str(x.get("이름") or x.get("로직")) for x in adds if is_block(x) and x.get(CUT)] # ── 끊기 · 다시 잇기 ─────────────────────────────────────────────────── def _one(doc: Any, at: Any) -> dict: adds = (doc.get("더한줄") if isinstance(doc, dict) else None) or [] if not isinstance(at, int) or isinstance(at, bool) or not 0 <= at < len(adds): raise sd.DocError(f"더한 기초 차례 「{at}」 없음") if not is_block(adds[at]): raise sd.DocError("더한줄이 기초(로직) 아님") return adds[at] def _put(doc: dict, at: int, one: dict) -> dict: return {**doc, "더한줄": [one if k == at else x for k, x in enumerate(doc["더한줄"])]} def _closure(doc: dict, code: str) -> list[dict]: """`code` 가 닿는 닫힌 묶음(자원 단가 빼고) — 사본 있으면 사본 줄 그대로 · 없으면 지금 M01.""" over = doc[skc.KEY]["로직"] if skc.has(doc) else None got = prices.collect([code], None, None, over) got.pop("_빠진", None) whole = skc.over(doc, skc.whole_()) keys = [str(r.get("키")) for data in got.values() for r in data[prices.m01.items_key(data)]] rows = [ whole.index[k[:2]][k] for k in keys if k[:2] not in skc.RES and k in whole.index.get(k[:2], {}) ] return prices.to_json(rows) def unlink(doc: Any, at: int) -> dict: """[연결 끊기] — 그 기초가 닿는 닫힌 묶음을 이 SK 안에 굳힘(금액 그대로).""" one = _one(doc, at) if one.get(CUT): raise sd.DocError("이미 연결을 끊은 기초") if any(one.get(k) not in (None, {}, []) for k in sd.BODY): raise sd.DocError("넣은 SK 몸은 이미 이 SK 안 사본 — 연결 끊기 없음") rows = _closure(skc.settle(doc), str(one["로직"])) if not rows: raise sd.DocError(f"기초 「{one['로직']}」 줄을 못 찾음") cut = {"시점": datetime.now().isoformat(timespec="seconds"), "로직": rows} return _put(doc, at, {**one, CUT: cut}) def relink(doc: Any, at: int) -> dict: """[다시 잇기] — 끊음 · 고침 버림 + 표지(셈·저장 때 사본을 지금 M01 줄로 갈음).""" one = _one(doc, at) if not one.get(CUT): raise sd.DocError("연결된 기초") keep = {k: v for k, v in one.items() if k not in (CUT, EDIT)} return _put(doc, at, {**keep, REJOIN: True}) def change(doc: Any, at: Any, how: str) -> dict: """길 몸 — {문서, 전, 후}(전후 SK 금액 · 끊기 전후 같음).""" from M02_MasterTemplete import M02_Sangeun_Equation_Full as full new = (unlink if how == "unlink" else relink)(doc, at) money = [full.total(full.answer(d, None)) for d in (doc, new)] return {"문서": new, "전": money[0], "후": money[1]} # ── 수식 탭 ──────────────────────────────────────────────────────────── def _setup(doc: Any, at: Any) -> tuple[dict, Any, dict]: if not isinstance(doc, dict): raise sd.DocError("단가산출 문서가 묶음 아님") one = _one(doc, at) if not one.get(CUT): raise sd.DocError("연결된 기초 — 먼저 [연결 끊기]") doc = skc.settle(doc) return doc, skc.over(doc, skc.whole_()), doc["더한줄"][at] def view(whole: Any, doc: dict, one: dict, edits: list | None, inputs: dict | None) -> tuple: """(수식, 못 얹은 까닭) — 고친 줄을 그대로 그림(세부 덮개 · 뺀줄 얹기 전 = 고침 주소 그대로).""" row, why, mine = row_of(whole, one, edits) if why: return None, why code = str(one["로직"]) given = {k: v for k, v in (one.get("입력값") or {}).items() if str(v).strip()} given = {**(doc.get("입력값") or {}), **(inputs or {}), **given} return eqm.export(mine, {**row, "키": code}, given), [] def read(doc: Any, at: Any, inputs: dict | None = None) -> dict[str, Any]: """{ok, 수식 | reason} — 끊은 기초 수식(자기 고침 얹음).""" doc, whole, one = _setup(doc, at) eq, why = view(whole, doc, one, None, inputs) return {"ok": False, "reason": " · ".join(why)} if why else {"ok": True, "수식": eq} def preview(doc: Any, at: Any, edits: list, inputs: dict | None = None) -> dict[str, Any]: """고침 통째 미리 셈(저장 없음) — {되나, 까닭, 줄, 수식, 전(기초 고침 없음), 후, 금액전, 금액후(SK)}.""" from M02_MasterTemplete import M02_Sangeun_Equation_Full as full doc, whole, one = _setup(doc, at) eq, why = view(whole, doc, one, edits, inputs) if why: return {"되나": False, "까닭": why} bare = {k: v for k, v in one.items() if k != EDIT} mine = {**bare, EDIT: edits} if edits else bare plain = view(whole, doc, bare, None, inputs)[0] if edits else eq row = row_of(whole, bare, edits)[0] money = [full.total(full.answer(d, inputs)) for d in (doc, _put(doc, at, mine))] return { "되나": True, "까닭": [], "줄": {**row, "입력": []}, # 입력 칸 = SK 쪽 것만(끊은 기초 입력은 더한 기초 자리에서) "수식": eq, "전": plain["합계"], "후": eq["합계"], "금액전": money[0], "금액후": money[1], }