"""옛 SK · SC · 일위대가 → 로직 사본 찍어 넣기 — 한 번 돌리는 도구(PLAN 795-3 · 요구 655 · 656). 대상 = 사본 없는 마스터 · 회사 · 개인 층 문서(`sangeun` · `precombo` · `labor`) — 지금 M01 로 사본을 찍고(`M02_Sangeun_Copy.stamp` · `M02_Labor_Copy.stamp`) 전후 금액 대조: SK = `calc_doc` 기준(노 · 재 · 경 · 계) · SC · 일위대가 = `Labor_Doc.calc` 기준합 · 반영합. 같음 = 파일에 `사본` 칸만 더해 씀(그 밖 칸 그대로) · 다름 · 못 셈 = 안 씀(목록) · 닿을 것 없음(바탕 참조 SK · 참조 · 손 단가 줄만) = 그대로. 프로젝트 층 일위대가 = 세기 · 대조만(B09 사본 규칙 795-4 와 함께 옮김). 되돌릴 길 = 쓰기 전 파일 그대로 `--backup` 폴더에(층 자리 그대로) · `--restore` = 다시 덮음. ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate # 보기만(표) ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate --apply --backup tmp/795_3 ./venv/Scripts/python.exe -m M02_MasterTemplete.M02_Copy_Migrate --restore tmp/795_3 """ from __future__ import annotations import json import shutil import sys from collections import Counter from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_Labor_Copy as lc from M02_MasterTemplete import M02_Sangeun_Copy as skc from M02_MasterTemplete import M02_Template_Layers as layers KINDS = ("sangeun", "precombo", "labor") def _anchors() -> dict[str, Path]: return {"system": layers.system_dir(), "storage": layers.storage_root()} def _files() -> list[tuple[str, str, Path, bool]]: """(어디, 종류, 파일, 프로젝트?) — 층 문서 + 프로젝트 층 일위대가.""" out = [(w, k, p, False) for k in KINDS for w, p in layers.layer_files(k)] root = layers.storage_root() for p in sorted(root.glob("*/*/*/templates/labor/[!.]*.json")): out.append((f"프로젝트 {p.parts[-4]}", "labor", p, True)) return out def _round(value: Any) -> Any: if isinstance(value, dict): return {k: _round(v) for k, v in value.items()} return None if value is None else round(float(value), 6) def _money(kind: str, doc: dict, whole: Any) -> Any: """그 문서 금액 — `whole` = 셈 색인(지금 M01 | 사본 덮개).""" from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Sangeun_Calc as calc if kind == "sangeun": return _round(calc.run(doc, whole, None, line=False).get("기준")) got = labor.calc(doc, None if kind == "labor" else {}, whole=whole) return _round({k: got.get(k) for k in ("기준합", "반영합")}) def _clean(kind: str, doc: dict, key: str) -> dict: from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Sangeun_Doc as sd plain = {k: v for k, v in doc.items() if k not in (skc.KEY, skc.JOB)} if kind == "sangeun": return sd.clean(plain, None, skc.whole_()) return labor.clean({**plain, "열": doc.get("열") or key}) 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 skc.has(doc): return {"상태": "있음"} folder = path.parent.parent try: with store.at(folder): clean = _clean(kind, doc, path.stem) copy = skc.stamp(clean) if kind == "sangeun" else lc.stamp(clean) if copy is None: return {"상태": "필요없음"} live = skc.whole_() before = _money(kind, clean, live) after = _money(kind, clean, skc.over({skc.KEY: copy}, live)) except Exception as e: # noqa: BLE001 — 못 셈 = 목록(통째로 멈추지 않음) return {"상태": "못셈", "까닭": str(e) or type(e).__name__} row = {"상태": "같음" if before == after else "다름", "전": before, "후": after} return {**row, "사본": copy} if before == after else {**row, "까닭": "전후 금액 다름 — 안 옮김"} def _backup_path(path: Path, backup: Path) -> Path: for name, anchor in _anchors().items(): if path.resolve().is_relative_to(anchor.resolve()): return backup / name / path.resolve().relative_to(anchor.resolve()) raise ValueError(f"층 밖 파일 「{path}」") def migrate(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 _files(): got = check(kind, path) copy = got.pop("사본", None) row = {"어디": where_, "종류": kind, "키": path.stem, **got, "씀": False} if apply and copy and not project: keep = _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 def restore(backup: Path) -> list[str]: """`--backup` 폴더 파일을 제자리로 다시 덮음 — 덮은 파일 목록.""" done = [] for name, anchor in _anchors().items(): for keep in sorted((backup / name).rglob("*.json")): target = anchor / keep.relative_to(backup / name) shutil.copy2(keep, target) done.append(str(target)) return done def table(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]: """층 갈래 × 종류 × 상태 개수.""" count: Counter = Counter() for r in rows: count[(r["어디"].split(" ")[0], r["종류"], r["상태"])] += 1 out: dict[str, dict[str, int]] = {} for (layer, kind, state), n in sorted(count.items()): out.setdefault(f"{layer} {kind}", {})[state] = n return out if __name__ == "__main__": # 브레인 · 사용자 승인 뒤만 --apply args = sys.argv[1:] if "--restore" in args: print("\n".join(restore(Path(args[args.index("--restore") + 1])))) raise SystemExit at = Path(args[args.index("--backup") + 1]) if "--backup" in args else None rows = migrate("--apply" in args, at) for one in rows: if one["상태"] not in ("있음", "필요없음", "같음"): print(json.dumps(one, ensure_ascii=False, default=str)) print(json.dumps(table(rows), ensure_ascii=False, indent=1))