Files
Aislo/M02_MasterTemplete/M02_Sangeun_Block.py
T
eomsangdonandClaude Sonnet 5.5 be43dad64e feat(796-2e-1): 더한 기초 블록 [연결 끊기] · [다시 잇기] 서버 — 끊음 = 이 SK 안 자기 사본 줄 · 수식 고침 · 재생성 때 줄 안 바꿈
- 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>
2026-11-07 15:38:36 +09:00

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],
}