From ea95007a32dd33da28be18a09e86fb20fc73555f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 13:37:38 +0900 Subject: [PATCH] =?UTF-8?q?feat(836-2):=20=EA=B8=B0=EA=B3=84=20=EB=8D=AE?= =?UTF-8?q?=EC=9D=8C(836-1)=20=EB=8F=84=20=EA=B3=A0=EC=B9=9C=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=E2=80=94=20=EA=B7=B8=20=EA=B8=B0=EA=B3=84=20?= =?UTF-8?q?=EC=A4=84=20M01=20=EB=B0=94=EB=80=8C=EB=A9=B4=20=EB=8D=AE?= =?UTF-8?q?=EC=9D=8C=20=EC=A7=80=EC=9B=80=20+=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=C2=B7=20=EB=AC=B8=EC=84=9C=20=EB=8B=A4=EC=8B=9C=20=EB=B3=B5?= =?UTF-8?q?=EC=82=AC=20=EB=95=8C=20=EB=8D=AE=EC=9D=8C=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Doc_Sk.py | 2 +- M02_MasterTemplete/M02_Sangeun_Copy.py | 2 +- M02_MasterTemplete/M02_Sk_AutoRenew.py | 71 +++++++++++++++++++- resources/tester/test_836_2_sk_auto_renew.py | 35 ++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/M02_MasterTemplete/M02_Doc_Sk.py b/M02_MasterTemplete/M02_Doc_Sk.py index 0d532ecbc..d971df7f3 100644 --- a/M02_MasterTemplete/M02_Doc_Sk.py +++ b/M02_MasterTemplete/M02_Doc_Sk.py @@ -131,7 +131,7 @@ def copy_of(sk: dict, ctx: tuple | None) -> dict | None: root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(e)} return {**got, "문서": root, "부름": {}} try: - root = derive(sk, ctx) + root = renew.carry(got, derive(sk, ctx)) # 맡긴 기계 덮음(836-2) except dc._ERR as e: root = {**dc.md.blank(str(sk.get("키"))), "_틀림": str(e)} values = dc.values_of(sk[skc.KEY]) if skc.has(sk) else dc.values_of(got) # 서버가 정한 값 diff --git a/M02_MasterTemplete/M02_Sangeun_Copy.py b/M02_MasterTemplete/M02_Sangeun_Copy.py index 590cd571a..26e4c9d91 100644 --- a/M02_MasterTemplete/M02_Sangeun_Copy.py +++ b/M02_MasterTemplete/M02_Sangeun_Copy.py @@ -314,7 +314,7 @@ def _formula_job(out: dict, job: Any) -> dict: if got is None or job not in (PRICE, REGEN): return out if job == REGEN: - drop = ("문서", "끊음", "따로", "처음") + drop = ("문서", "끊음", "따로", "처음", "기계덮음") got = {k: v for k, v in got.items() if k not in drop} | {"부름": {}} elif got.get("따로"): got = dc.refresh(got)[0] diff --git a/M02_MasterTemplete/M02_Sk_AutoRenew.py b/M02_MasterTemplete/M02_Sk_AutoRenew.py index 3aed98f79..0f9d8285f 100644 --- a/M02_MasterTemplete/M02_Sk_AutoRenew.py +++ b/M02_MasterTemplete/M02_Sk_AutoRenew.py @@ -26,6 +26,9 @@ 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 +import master_doc_cover as mcv # noqa: E402 — 기계 덮음(836-1 · scripts 길 = dc) + +COVER = mcv.COVER # 문서 칸 기계덮음(836-1) — 따로 문서를 다시 복사할 때 `수식` 머리에 맡겨 둠 FIRST = "처음" # `수식` 칸 — 따로가 된 때의 문서(고침 판정 원본) LOGIC = ( "변수", @@ -136,6 +139,7 @@ def form(sk: Any, got: Any, notes: list | None = None) -> Any: if not isinstance(got, dict) or not on() or not is_sk(sk): return got out = rows(got) if "로직" in got else dict(got) + out = _covers(got, out, notes) if imp.is_imported(sk): return out # 가져온 식 = 값만(식 · 가져온 줄 그대로) live = dc.folder() @@ -164,13 +168,78 @@ def form(sk: Any, got: Any, notes: list | None = None) -> Any: ) 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)} + keep = {COVER: mine[COVER]} if mine.get(COVER) else {} # 기계 덮음 = 다른 항목(그대로) + out = {k: v for k, v in out.items() if k not in ("따로", "문서", FIRST, COVER)} | keep 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 +# ── 기계 덮음(836-1) — 그 기계 줄이 M01 에서 바뀌면 덮음 지움 ───────────── +def _eq_rows(copy: dict) -> dict[str, dict]: + """사본의 기계 줄 {원문번호 · 키: 줄}.""" + out: dict[str, dict] = {} + for r in [*(copy.get("로직") or []), *(copy.get("단가") or [])]: + if isinstance(r, dict) and str(r.get("키"))[:2] == "EQ": + out[str(r.get("키"))] = r + if r.get("원문번호"): + out[str(r["원문번호"])] = r + return out + + +def _mine(entry: dict) -> list[str]: + vals = [f"{k} {v}" for k, v in (entry.get("값") or {}).items()] + crew = [f"조종원 {x.get('키')} {x.get('인')}인" for x in entry.get("조종원") or []] + misc = [ + f"잡품 {x.get('이름') or x.get('키') or x.get('줄')} {x.get('율')}%" + for x in entry.get("잡품") or [] + ] + return [*vals, *crew, *misc] or ["덮음"] + + +def _row_diff(a: dict, b: dict) -> list[str]: + va, vb = a.get("값") or {}, b.get("값") or {} + if not isinstance(va, dict) or not isinstance(vb, dict): + return [f"{a.get('값')} → {b.get('값')}"] + keys = [*va, *(k for k in vb if k not in va)] + return [f"{k} {va.get(k, '—')} → {vb.get(k, '—')}" for k in keys if va.get(k) != vb.get(k)] + + +def _covers(got: dict, out: dict, notes: list | None) -> dict: + """문서(따로) · 맡긴 기계 덮음 가운데 그 기계 줄이 M01 에서 바뀐 것 = 지움 + 알림.""" + doc = out.get("문서") if isinstance(out.get("문서"), dict) else None + cover = dict(mcv.cover_of(doc) if doc is not None else out.get(COVER) or {}) + if not cover: + return out + was, now = _eq_rows(got), _eq_rows(out) + for text in list(cover): + a, b = was.get(text), now.get(text) + if a is None or b is None or skc._fp(a) == skc._fp(b): + continue + entry = cover.pop(text) + if notes is not None: + name = f"{b.get('이름') or ''} {b.get('규격') or ''}".strip() or text + notes.append( + { + "키": str(b.get("키")), + "이름": name, + "갈래": "기계", + "M01": _row_diff(a, b) or ["바뀜"], + "내고침": _mine(entry), + } # fmt: skip + ) + if doc is not None: + rest = {k: v for k, v in doc.items() if k != COVER} + return {**out, "문서": {**rest, COVER: cover} if cover else rest} + return {k: v for k, v in out.items() if k != COVER} | ({COVER: cover} if cover else {}) + + +def carry(got: dict, root: dict) -> dict: + """파생 문서에 맡긴 기계 덮음을 다시 얹음(따로 문서를 다시 복사한 뒤).""" + return {**root, COVER: got[COVER]} if isinstance(got.get(COVER), dict) and got[COVER] else root + + def _note(key: str, was: Any, now: dict, mine: list[str], kind: str, name: Any = None) -> dict: return { "키": key, diff --git a/resources/tester/test_836_2_sk_auto_renew.py b/resources/tester/test_836_2_sk_auto_renew.py index 6b52cf995..09f19c86f 100644 --- a/resources/tester/test_836_2_sk_auto_renew.py +++ b/resources/tester/test_836_2_sk_auto_renew.py @@ -211,3 +211,38 @@ def test_imported_values_only(world, monkeypatch): back = _at(me, lambda: imp.to_price(store.read("sangeun", key)["문서"], "엑셀")["문서"]) _at(me, lambda: store.write("sangeun", key, store.read("sangeun", key)["판"], back)) assert _sum(me, key) == excel + + +def _cover(me, key: str, cover: dict) -> None: + got = _at(me, path.read, key) + doc = {**copy.deepcopy(got["문서"]), "기계덮음": cover} + _at(me, path.save, key, {"문서": doc, "판": got["판"]}) + + +def test_machine_cover_dropped_when_machine_moves(world, monkeypatch): + """㉢ 기계 덮음(836-1) + 그 기계 줄 M01 바뀜 = 덮음 지움 + 알림(전 · 후 · 내 고침 값).""" + me = world["personal"] + key = _sk(me) + _stack(me, key, [("기계", MACHINE, "묶음")]) + no = MACHINE["입력"]["기계"].strip("'") + _cover(me, key, {no: {"값": {"가격": 150000}}}) + assert _at(me, path.read, key)["문서"]["기계덮음"] + _move(monkeypatch, DIG) + got = _at(me, path.read, key) + assert "기계덮음" not in got["문서"] + [note] = got["사본"]["갱신"] + assert note["키"] == DIG and note["갈래"] == "기계" + assert any(x.startswith("가격 ") for x in note["M01"]) and note["내고침"] == ["가격 150000"] + + +def test_machine_cover_kept_other_item(world, monkeypatch): + """㉣ 기계 덮음 + 다른 항목(바탕 문서)만 바뀜 = 덮음 그대로(문서 다시 복사 뒤에도).""" + me = world["personal"] + key = _base(me) + _stack(me, key, [("기계", MACHINE, "묶음")]) + no = MACHINE["입력"]["기계"].strip("'") + _cover(me, key, {no: {"값": {"가격": 150000}}}) + _doc_move(monkeypatch, PCT, _rate("노무비 × 4%")) + got = _at(me, path.read, key) + assert got["문서"]["기계덮음"] == {no: {"값": {"가격": 150000}}} + assert [n["갈래"] for n in got["사본"]["갱신"]] == ["문서"] # 기계 줄 붙인 문서 고침