From 2a41b989a23e69d965c5f67e3619227e89412024 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 7 Nov 2026 17:26:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(800-5):=20=ED=98=B8=ED=91=9C=20=EC=A4=84?= =?UTF-8?q?=20=EA=B8=88=EC=95=A1=20=EC=B0=B8=EC=A1=B0=20=EA=B8=B0=ED=98=B8?= =?UTF-8?q?=20=E2=80=94=20=EC=A0=95=EC=9D=98=20=EC=98=81=EC=97=AD=EB=8F=84?= =?UTF-8?q?=20=EC=94=8C=EC=9A=B4=20=EA=B8=B0=ED=98=B8(C=5F=ED=95=A9?= =?UTF-8?q?=ED=8C=90)=20=C2=B7=20M02=20=EC=98=9B=20=EC=82=AC=EB=B3=B8=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=ED=91=9C=20=EC=83=88=EB=A1=9C=20=EC=93=B0?= =?UTF-8?q?=EB=8A=94=20=ED=95=9C=20=EB=B2=88=20=EB=8F=84=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정의 영역: 줄 참조 기호를 이름으로 덮던 줄 뺌 → 수식 영역과 같은 글(master_symbol 한 곳) - M02_Copy_Symbols: 옛 사본 줄 변수표가 비었거나 열쇠 그대로면 지금 M01 줄 변수표로 채움 · 고친 기호 그대로 · 금액 대조 같을 때만 씀 · --backup - test_800_5 새 4: 줄 참조 · M01 정의 한글(하위 로직 칩 빼고) 0 · 옛 사본 새로 씀 · 사용자 고침 보존 · 백업 되돌림 Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_Copy_Symbols.py | 115 ++++++++++++++ .../scripts/master_equation_view.py | 2 +- resources/tester/test_800_5_copy_symbols.py | 140 ++++++++++++++++++ 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 M02_MasterTemplete/M02_Copy_Symbols.py create mode 100644 resources/tester/test_800_5_copy_symbols.py diff --git a/M02_MasterTemplete/M02_Copy_Symbols.py b/M02_MasterTemplete/M02_Copy_Symbols.py new file mode 100644 index 000000000..d93d7b995 --- /dev/null +++ b/M02_MasterTemplete/M02_Copy_Symbols.py @@ -0,0 +1,115 @@ +"""옛 로직 사본 변수표 새로 쓰기 — 한 번 돌리는 도구(PLAN 800-5 · 요구 660-1 · 659-1). + +옛 사본(795-5 이전에 찍힌 것) 줄은 변수표(이름 · 기호 · 아래 · 위)가 비었거나 한글 그대로 → +수식 영역에 한글 기호가 보임. 지금 M01 줄의 변수표로 그 줄 변수만 채움: + · 사본 줄에 이미 정한 기호(영문 · 그리스 · 사용자가 고친 것)가 있는 칸 = 그대로 + · 기호가 비었거나 열쇠 그대로(한글)인 칸 = M01 칸(이름 · 기호 · 아래 · 위)으로 + · M01 에 그 줄 · 그 변수가 없으면 건드리지 않음(= 목록 「못닿음」) +금액 · 셈 = 변수표는 보이기만 → 전후 금액 대조(같을 때만 씀) · 사본 `판`은 줄 지문으로 다시 셈 +(M01 줄과 같아지면 `M01새판` 표지도 걷힘). +되돌릴 길 = 쓰기 전 파일 그대로 `--backup` 폴더에(`M02_Copy_Migrate --restore` 로 덮음). +프로젝트 층 문서는 건드리지 않음(굳힌 사본). + + ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Symbols # 보기만 + ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Symbols --apply --backup tmp/800_5 +""" + +from __future__ import annotations + +import json +import shutil +import sys +from pathlib import Path +from typing import Any + +from common_util.common_util_json import atomic_write_json +from M02_MasterTemplete import M02_Copy_Migrate as mig +from M02_MasterTemplete import M02_Sangeun_Copy as skc + +FIELD = "변수표" # master_equation.FIELD 와 같은 칸 +LOOK = ("이름", "기호", "아래", "위") + + +def _open(entry: Any, name: str) -> bool: + """사본 칸이 채울 자리 — 없음 · 기호 없음 · 열쇠 그대로(한글 기본).""" + sign = str((entry or {}).get("기호") or "") + return not sign or sign == name + + +def fill(copy: dict, whole: Any) -> tuple[dict, int, int]: + """(새 사본, 채운 칸 수, 못 닿은 변수 수) — 줄 `변수표`만 고치고 `판` 다시 셈.""" + rows, done, lost = [], 0, 0 + for row in copy["로직"]: + key = str(row.get("키")) + live = whole.index.get(key[:2], {}).get(key) or {} + src = live.get(FIELD) if isinstance(live.get(FIELD), dict) else {} + mine = dict(row.get(FIELD) or {}) + for part in ("입력", "중간"): + for spec in row.get(part) or []: + name = str(spec.get("이름")) + new = (src or {}).get(name) + if not _open(mine.get(name), name): + continue + if not isinstance(new, dict) or not new.get("기호"): + lost += 1 + continue + mine[name] = {**(mine.get(name) or {}), **{k: new[k] for k in LOOK if k in new}} + done += 1 + rows.append({**row, FIELD: mine} if mine != (row.get(FIELD) or {}) else row) + return {**copy, "로직": rows, "판": skc._fp(rows)}, done, lost + + +def check(kind: str, path: Path) -> dict[str, Any]: + """문서 하나 — {상태: 있음 | 채울것없음 | 같음 | 다름 | 못셈, 채움, 못닿음, 사본?}.""" + from M02_MasterTemplete import M02_MasterTemplete_Store as store + + doc = json.loads(path.read_bytes()) + if not skc.has(doc): + return {"상태": "사본없음"} + try: + with store.at(path.parent.parent): + live = skc.whole_() + new, done, lost = fill(doc[skc.KEY], live) + if not done: + return {"상태": "채울것없음", "못닿음": lost} + clean = mig._clean(kind, doc, path.stem) + before = mig._money(kind, clean, skc.over({skc.KEY: doc[skc.KEY]}, live)) + after = mig._money(kind, clean, skc.over({skc.KEY: new}, live)) + except Exception as e: # noqa: BLE001 — 못 셈 = 목록(통째로 멈추지 않음) + return {"상태": "못셈", "까닭": str(e) or type(e).__name__} + row = {"상태": "같음" if before == after else "다름", "채움": done, "못닿음": lost} + return {**row, "사본": new} if before == after else {**row, "전": before, "후": after} + + +def run(apply: bool = False, backup: Path | None = None) -> list[dict[str, Any]]: + """마스터 · 회사 · 개인 층 문서마다 {어디, 종류, 키, 상태, 씀, …} · `apply` = 같음만 씀.""" + if apply and backup is None: + raise ValueError("--apply 는 --backup 폴더가 있어야 함") + out = [] + for where_, kind, path, project in mig._files(): + if project: + continue + got = check(kind, path) + copy = got.pop("사본", None) + row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False} + if apply and copy: + keep = mig._backup_path(path, backup) + keep.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, keep) + atomic_write_json(path, {**json.loads(path.read_bytes()), skc.KEY: copy}) + row["씀"] = True + out.append(row) + return out + + +if __name__ == "__main__": # 브레인 지시 뒤만 --apply + args = sys.argv[1:] + at = Path(args[args.index("--backup") + 1]) if "--backup" in args else None + rows = run("--apply" in args, at) + for one in rows: + if one["상태"] not in ("사본없음", "채울것없음", "같음"): + print(json.dumps(one, ensure_ascii=False, default=str)) + count: dict[str, int] = {} + for one in rows: + count[one["상태"]] = count.get(one["상태"], 0) + 1 + print(json.dumps(count, ensure_ascii=False), "채움", sum(r.get("채움", 0) for r in rows)) diff --git a/resources/master_data/scripts/master_equation_view.py b/resources/master_data/scripts/master_equation_view.py index 8cbdc64cc..3027062f3 100644 --- a/resources/master_data/scripts/master_equation_view.py +++ b/resources/master_data/scripts/master_equation_view.py @@ -100,7 +100,7 @@ def definitions(lines: list[dict], row: dict, rows: list[dict], unused) -> list[ name = node.get("칸") or name if kind == "찾기" else name one = { "열쇠": key if mine else "", - "기호": (look.get("기호") or name) if not key.startswith("줄.") else name, + "기호": look.get("기호") or name, # 줄 참조도 씌운 기호(master_symbol 한 곳) "위": look.get("위") or "", "아래": look.get("아래") or "", "이름": name, diff --git a/resources/tester/test_800_5_copy_symbols.py b/resources/tester/test_800_5_copy_symbols.py new file mode 100644 index 000000000..49bf5d468 --- /dev/null +++ b/resources/tester/test_800_5_copy_symbols.py @@ -0,0 +1,140 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""800-5 기호 남은 한글 — 임시 storage 에서만. + +① 호표 줄 금액 참조(`줄.`) 기호 = 수식 영역 · 정의 영역 같은 글(한글 0) ② 새 사본 = 이미 새 기호 +③ 옛 사본 변수표 새로 씀 = 사용자 고침 칸 그대로 · 금액 같음 · 백업 되돌림 ④ M01 전수 한글 기호(하위 로직 칩 제외) 0. +""" + +from __future__ import annotations + +import copy +import importlib +import json +import re + +from M02_MasterTemplete import M02_Copy_Migrate as mig +from M02_MasterTemplete import M02_Copy_Symbols as sym +from M02_MasterTemplete import M02_Sangeun_Copy as skc +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 + +LINE = {"로직": "GC000211", "수량": {"손": 1}} # 변수 많은 합판거푸집 + + +def _make(me): + with store.at(me): + sk = refs.create( + "sangeun", + {"이름": "sub2임시", "단위": "㎡", "바탕": {"로직": "GC000211"}, "더한줄": []}, + )["이름"] + path = me / "sangeun" / f"{sk}.json" + raw = {k: v for k, v in json.loads(path.read_bytes()).items() if k != skc.KEY} + path.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8") + return {path: raw} + + +def _mine(rows, olds): + keys = {p.stem for p in olds} + return {r["키"]: r for r in rows if r["어디"].startswith("개인") and r["키"] in keys} + + +meq = importlib.import_module("master_equation") +HAN = re.compile(r"[가-힣]") + + +def _walk(tree, out: list) -> None: + if isinstance(tree, dict): + if tree.get("꼴") == "변수": + out.append(tree) + for v in tree.values(): + _walk(v, out) + elif isinstance(tree, list): + for v in tree: + _walk(v, out) + + +def test_줄_참조_기호_같은_글_한글_0(): + eq = meq.export(m01.loaded()[1], "GC000211") + refs = [] + _walk(eq["줄"], refs) + refs = [v for v in refs if str(v["열쇠"]).startswith("줄.")] + assert refs and all(not HAN.search(v["기호"]) and v["기호"] == "C" for v in refs) + defs = {(d["기호"], d["아래"]): d for d in eq["정의"]} + for v in refs: + d = defs[(v["기호"], v["아래"])] + assert d["갈래"] == "요소" and not HAN.search(d["기호"]) + assert any("호표 줄 금액" in c for d in defs.values() for c in d["연결"]) + + +def test_M01_전수_정의_한글_기호는_하위_로직_칩뿐(): + whole = m01.loaded()[1] + keys = [r["키"] for d in m01.loaded()[0].values() if d.get("그룹") == "로직" for r in d["줄"]] + for code in keys[::25]: + for d in meq.export(whole, str(code))["정의"]: + assert d["갈래"] == "블록" or not HAN.search(d["기호"]), (code, d["이름"]) + + +def _old(path): + """사본 변수표를 걷어 옛 사본 흉내 → (옛 문서, 걷은 줄 수).""" + doc = json.loads(path.read_bytes()) + cut = 0 + for r in doc[skc.KEY]["로직"]: + cut += r.pop(sym.FIELD, None) is not None + doc[skc.KEY]["판"] = skc._fp(doc[skc.KEY]["로직"]) + path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8") + return doc, cut + + +def test_새_사본은_이미_새_기호_옛_사본은_새로_씀(world, tmp_path): + olds = _make(world["personal"]) + back = tmp_path / "stamp" + mig.migrate(True, back) # 새 사본 찍음 + paths = list(olds) + for p in paths: + rows = json.loads(p.read_bytes())[skc.KEY]["로직"] + assert any(r.get(sym.FIELD) for r in rows) # ② 새 사본 = 변수표 있음 + assert {r["상태"] for r in _mine(sym.run(), olds).values()} == {"채울것없음"} + stripped = {p: _old(p) for p in paths} + assert all(cut for _, cut in stripped.values()) + # 사용자 고침 칸 하나 — 영문 기호 Z 는 그대로 + sk = next(p for p in paths if p.parent.name == "sangeun") + doc = json.loads(sk.read_bytes()) + row = next(r for r in doc[skc.KEY]["로직"] if r.get("입력")) + name = str(row["입력"][0]["이름"]) + row[sym.FIELD] = {name: {"기호": "Z", "아래": "내것"}} + sk.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8") + before = {p: p.read_bytes() for p in paths} + rows = _mine(sym.run(), olds) # 보기만 = 안 씀 + assert {r["상태"] for r in rows.values()} == {"같음"} and not any( + r["씀"] for r in rows.values() + ) + assert all(p.read_bytes() == b for p, b in before.items()) + back = tmp_path / "back" + rows = _mine(sym.run(True, back), olds) + assert all(r["씀"] and r["채움"] > 0 and r["못닿음"] == 0 for r in rows.values()) + for p in paths: + for r in json.loads(p.read_bytes())[skc.KEY]["로직"]: + for part in ("입력", "중간"): + for spec in r.get(part) or []: + look = (r.get(sym.FIELD) or {})[str(spec["이름"])] + assert look.get("기호") + if not (r is row and spec["이름"] == name): + assert not HAN.search(look["기호"]), (r["키"], spec["이름"]) + now = next(r for r in json.loads(sk.read_bytes())[skc.KEY]["로직"] if r["키"] == row["키"]) + assert now[sym.FIELD][name] == {"기호": "Z", "아래": "내것"} + assert {r["상태"] for r in _mine(sym.run(), olds).values()} == {"채울것없음"} # 다시 돌림 + mig.restore(back) + assert all(p.read_bytes() == before[p] for p in paths) + + +def test_채운_줄은_M01_줄_변수표_그대로(): + live = m01.loaded()[1] + row = copy.deepcopy(live.logic("GC000211")) + src = row[sym.FIELD] + stripped = {k: v for k, v in row.items() if k != sym.FIELD} + got, done, lost = sym.fill({"로직": [json.loads(json.dumps(stripped, default=str))]}, live) + names = {str(s["이름"]) for k in ("입력", "중간") for s in row[k]} + assert done == len(names & set(src)) and lost == len(names - set(src)) + assert all(got["로직"][0][sym.FIELD][n]["기호"] == src[n]["기호"] for n in names & set(src))