- M02_Sangeun_Block 신설: 끊기(닫힌 묶음 굳힘 · 금액 그대로) · 다시 잇기(고침 버림 + 원본 갈음) · 끊은 기초 수식 읽기 · 미리 셈 · 저장 검사 - Calc.sub_run 이 끊은 기초 자기 줄 + 수식 고침을 얹어 셈 · Copy.settle 이 다시 잇기 표지 처리 · [단가 갱신] · [재생성] 응답 끊은기초 한 줄 - Router: block/unlink · relink · equation 몸 블록 · 새 시험 test_796_2e_unlink_basis 6 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
213 lines
10 KiB
Python
213 lines
10 KiB
Python
"""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],
|
|
}
|