- 새 M02_Sk_AutoRenew — 프로젝트 층 밖 SK 사본 줄 = 지금 M01(+ 내 기계) · 부름 문서 = 지금 M01 수식 문서 - 고침 판정 = 내용 대조(끊음 대 부름 · 따로 문서 대 처음) · 바꿨다 되돌림 = 안 고침 - M01 에서 바뀐 항목의 고침 = 지움 + 다시 복사 + 열기 답 사본.갱신 알림 · 사본.자동 표지 - 파일은 [저장] · 알림 확인(저장) 때만 씀 · 읽기 길 안 씀 · 프로젝트 층 그대로 - 옛 시험 8 개 = 손 갱신 길(프로젝트 층 꼴 · 자동 끔) 고정 — 개인 층 「금액 그대로」 단언이 낡음 - 새 시험 test_836_2_sk_auto_renew(㉠ ~ ㉦) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
8.6 KiB
Python
191 lines
8.6 KiB
Python
"""M02 단가산출(SK) M01 자동 따라옴(PLAN 836-2 · 요구 854) — 서버 단독.
|
||
|
||
프로젝트 층 밖 SK(마스터 · 회사 · 개인) = 셈 · 열기 · 목록 · 일위대가 펼침이 모두
|
||
사본 줄 = 지금 M01(+ 층 내 기계) 줄(`rows` · 지운 키 = 사본 마지막 줄) ·
|
||
부름 문서 = 지금 M01 수식 문서(`form`)로 봄 — 묻지 않음 · [단가 갱신] · [재생성] 없음.
|
||
파일(작업본)은 [저장] · 알림 [확인](= 저장) 때만 새 것으로 씀 — 읽기 길은 안 씀 ·
|
||
저장 파일의 사본 = 마지막 저장 때 받은 M01(= 고침 판정의 「복사 당시 원본」).
|
||
고침 판정 = 내용 대조(표지 아님 · 바꿨다 되돌림 = 안 고침) — 셈에 닿는 칸(`LOGIC`)만 견줌:
|
||
끊음[키] 대 부름[키](복사 당시) · 따로 문서 대 `처음`(따로가 된 때의 문서 · 없으면 고친 것).
|
||
M01 에서 바뀐 항목에 고침이 얹혀 있으면 그 고침을 지우고 다시 복사 + 알림 `갱신` 한 줄
|
||
{키, 이름, 갈래, M01 [전 → 후], 내고침 [지워진 값]} · 고침 없는 항목 = 조용히 따라옴 ·
|
||
안 바뀐 항목의 고침 = 그대로. 따로 문서를 지우면 파생(옛 칸 + 새 M01)으로 돌아감.
|
||
가져온 식 SK = 값 줄만 따라옴(가져온 줄 문서 · 식 안 건드림 · 단가기준 「엑셀」 은 엑셀 값 그대로).
|
||
프로젝트 층 · B09 사본 = 그대로(사용자가 고르는 [단가 갱신] · [재생성]).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import copy as cp
|
||
import json
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||
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_Imported as imp
|
||
|
||
FIRST = "처음" # `수식` 칸 — 따로가 된 때의 문서(고침 판정 원본)
|
||
LOGIC = (
|
||
"변수",
|
||
"식",
|
||
"금액",
|
||
"끝수",
|
||
"결과단위",
|
||
) # 셈에 닿는 칸 — 쌓기(차림) · 이름 · 판은 안 견줌
|
||
MAX_LINES = 8
|
||
_MEMO: dict[tuple, dict] = {}
|
||
_LIVE: list[Any] = [None]
|
||
|
||
|
||
def on() -> bool:
|
||
"""자동 따라옴 — 프로젝트 층 밖."""
|
||
from M02_MasterTemplete import M02_Labor_Ref as refs
|
||
|
||
return refs.here()[0] != "project"
|
||
|
||
|
||
def is_sk(doc: Any) -> bool:
|
||
return isinstance(doc, dict) and doc.get("종류") == sd.TITLE
|
||
|
||
|
||
# ── 값 줄 ──────────────────────────────────────────────────────────────
|
||
def _fresh(row: dict, live: Any) -> dict:
|
||
key = str(row.get("키"))
|
||
got = live.index.get(key[:2], {}).get(key)
|
||
if got is None:
|
||
return row # M01 에서 지운 키 = 마지막 값
|
||
new = prices.to_json(got)
|
||
return row if skc._fp(new) == skc._fp(row) else new
|
||
|
||
|
||
def rows(copy: Any) -> Any:
|
||
"""값 사본(`skc` 꼴) → 줄마다 지금 M01(+ 층 내 기계) · 바뀐 몫만 판 다시 · 프로젝트 층 = 그대로."""
|
||
if not (isinstance(copy, dict) and isinstance(copy.get("로직"), list)) or not on():
|
||
return copy
|
||
live = skc.whole_()
|
||
if live is not _LIVE[0]:
|
||
_MEMO.clear()
|
||
_LIVE[0] = live
|
||
tag = (copy.get("판"), copy.get("단가판"), len(copy["로직"]), len(copy.get("단가") or []))
|
||
hit = _MEMO.get(tag)
|
||
if hit is not None:
|
||
return {**copy, **hit}
|
||
got: dict = {}
|
||
for part, mark in (("로직", "판"), ("단가", "단가판")):
|
||
old = copy.get(part) or []
|
||
new = [_fresh(r, live) if isinstance(r, dict) else r for r in old]
|
||
if any(a is not b for a, b in zip(old, new)):
|
||
got |= {part: new, mark: skc._fp(new)}
|
||
if len(_MEMO) > 64: # ponytail: 통째 비움 · SK 가 많아 느려지면 LRU 로
|
||
_MEMO.clear()
|
||
_MEMO[tag] = got
|
||
return {**copy, **got}
|
||
|
||
|
||
# ── 수식 사본(부름 · 끊음 · 따로 문서) ──────────────────────────────────
|
||
def _logic(doc: Any) -> str:
|
||
from M02_MasterTemplete import M02_Doc_Sk as dsk
|
||
|
||
doc = doc if isinstance(doc, dict) else {}
|
||
return dc.fp(dsk._strip({k: doc.get(k) for k in LOGIC}))
|
||
|
||
|
||
def _say(v: dict) -> str:
|
||
link = v.get("연결") or {}
|
||
for slot in ("값", "식", "키"):
|
||
if link.get(slot) not in (None, ""):
|
||
return str(link[slot])
|
||
return json.dumps(link, ensure_ascii=False, default=str)[:60]
|
||
|
||
|
||
def _name(v: dict) -> str:
|
||
return str(v.get("이름") or f"{v.get('기호') or ''}{v.get('아래') or ''}")
|
||
|
||
|
||
def diff(a: Any, b: Any) -> list[str]:
|
||
"""두 문서의 셈 칸 다른 곳 — 「이름 전 → 후」(변수 · 금액 줄) · 많으면 앞 몇 줄."""
|
||
a, b = (x if isinstance(x, dict) else {} for x in (a, b))
|
||
out: list[str] = []
|
||
va = {dc.md.var_id(v): v for v in a.get("변수") or []}
|
||
vb = {dc.md.var_id(v): v for v in b.get("변수") or []}
|
||
for vid in [*va, *(k for k in vb if k not in va)]:
|
||
x, y = va.get(vid), vb.get(vid)
|
||
if x and y and _say(x) == _say(y):
|
||
continue
|
||
name = _name(y or x)
|
||
out.append(f"{name} {_say(x) if x else '—'} → {_say(y) if y else '—'}")
|
||
la, lb = a.get("금액") or [], b.get("금액") or []
|
||
for n in range(max(len(la), len(lb))):
|
||
x, y = (la[n] if n < len(la) else None), (lb[n] if n < len(lb) else None)
|
||
if dc.fp(x) == dc.fp(y):
|
||
continue
|
||
name = str((y or x or {}).get("이름") or f"금액 {n + 1}")
|
||
show = lambda z: "—" if not z else f"{z.get('수량') or ''} × {z.get('단가') or ''}" # noqa: E731
|
||
out.append(f"{name} {show(x)} → {show(y)}")
|
||
for slot in ("식", "끝수", "결과단위"):
|
||
if dc.fp(a.get(slot)) != dc.fp(b.get(slot)):
|
||
out.append(slot)
|
||
return out[:MAX_LINES] + ([f"… {len(out) - MAX_LINES}"] if len(out) > MAX_LINES else [])
|
||
|
||
|
||
def form(sk: Any, got: Any, notes: list | None = None) -> Any:
|
||
"""수식 사본 → 지금 M01 으로 본 것(값 줄 · 부름) · M01 에서 바뀐 항목의 고침(끊음 · 따로 문서)은
|
||
지우고 `notes` 에 한 줄 · 안 바뀐 항목 고침 · 프로젝트 층 = 그대로."""
|
||
if not isinstance(got, dict) or not on() or not is_sk(sk):
|
||
return got
|
||
out = rows(got) if "로직" in got else dict(got)
|
||
if imp.is_imported(sk):
|
||
return out # 가져온 식 = 값만(식 · 가져온 줄 그대로)
|
||
live = dc.folder()
|
||
calls = dict(out.get("부름") or {})
|
||
changed = {k for k, d in calls.items() if k in live and dc.fp(d) != dc.fp(live[k])}
|
||
if not changed and not out.get("따로"):
|
||
return out
|
||
for k in changed:
|
||
calls[k] = cp.deepcopy(live[k])
|
||
cut = dict(out.get("끊음") or {})
|
||
for k in sorted(changed & set(cut)):
|
||
mine, was = cut.pop(k), (got.get("부름") or {}).get(k)
|
||
if _logic(mine) != _logic(was) and notes is not None:
|
||
notes.append(_note(k, was, live[k], diff(was, mine), "부름"))
|
||
out |= {"부름": calls, "끊음": cut}
|
||
base = out.get("원본") or {}
|
||
key = str(base.get("키") or "")
|
||
if out.get("따로") and key in live and dc.fp(live[key]) != base.get("판"):
|
||
first = out.get(FIRST)
|
||
mine = out.get("문서") or {}
|
||
if first is None or _logic(mine) != _logic(first):
|
||
mark = (
|
||
diff(first, mine)
|
||
if first is not None
|
||
else ["수식 문서 고침(옛 저장본 — 원본 없음)"]
|
||
)
|
||
if notes is not None:
|
||
notes.append(_note(key, first, live[key], mark, "문서", mine.get("이름")))
|
||
out = {k: v for k, v in out.items() if k not in ("따로", "문서", FIRST)}
|
||
if changed and "문서" in out: # 새 문서가 부르는 키 = 닫힌 묶음
|
||
keys = [*dc.calls(out["문서"]), *(c for d in cut.values() for c in dc.calls(d))]
|
||
out["부름"] = dc.closure(dc.over(out), keys, calls)
|
||
return out
|
||
|
||
|
||
def _note(key: str, was: Any, now: dict, mine: list[str], kind: str, name: Any = None) -> dict:
|
||
return {
|
||
"키": key,
|
||
"이름": str(name or now.get("이름") or key),
|
||
"갈래": kind,
|
||
"M01": diff(was, now) or ["바뀜"],
|
||
"내고침": mine,
|
||
}
|
||
|
||
|
||
def notes(sk: Any) -> list[dict]:
|
||
"""열 때 알림 — 이 SK 저장본에서 지울 고침(없으면 []) · 가져온 식 · 프로젝트 층 = []."""
|
||
if not is_sk(sk) or not dc.has(sk) or imp.is_imported(sk):
|
||
return []
|
||
out: list[dict] = []
|
||
form(sk, sk[dc.KEY], out)
|
||
return json.loads(json.dumps(dc.ml._norm(out), ensure_ascii=False, default=str))
|