Files
Aislo/M02_MasterTemplete/M02_Copy_Migrate.py
T
eomsangdonandClaude Opus 5.5 16d7eede50 feat(795-3): 옛 SK · SC · 일위대가 사본 옮기기 도구 — 전후 금액 대조 · 같음만 씀 · 백업 · 되돌림
- 새 M02_Copy_Migrate — 사본 없는 마스터 · 회사 · 개인 층 문서에 지금 M01 사본 찍어 대조(SK 기준 · SC · 일위대가 기준합 · 반영합)
- 같음 = 사본 칸만 더해 씀 · 다름 · 못 셈 = 안 씀(목록) · 프로젝트 층 일위대가 = 세기 · 대조만(795-4)
- --apply 는 --backup 폴더 필수 · --restore 로 다시 덮음 · 기본 = 보기만(표)
- 실제 storage 보기만 결과 — 다름 0 · 층 문서 같음 41 · 필요없음 6 · 프로젝트 같음 70 · 못셈 14(옛 SP 참조) · 옮기지 않음
- 시험 test_795_3_migrate 3(임시 storage)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 11:32:22 +09:00

155 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""옛 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))