- 구조물 · 서고 사본 단가 묶음 = M02 일위대가 · SC · SK 로직 사본 줄 먼저(`collect(over)`) · M01 에서 지워진 키 = `사본_남은줄.json`
- 단가 책 새 줄([저장] · 첫 굳힘) = 연결 사본 `단가.json` 자원 줄(구조물 지정 때 값 · `Linked.frozen_rows`) · 없는 자원만 지금 M01 + 책 `now_keys` · 입력 `now` · 알림 한 줄
- POST …/estimation/prices 기본 = 단가 갱신(값만 · 사본 재적용 없음) · job 재생성 = 옛 [업데이트] · 미리 보기 단가 갱신 = 바뀐 자원 · 총괄 · 대가 합 전 → 후
- B09 화면 단추 [업데이트] → [단가 갱신] · 미리 보기 창 [재생성] → 재생성 미리 보기 → [적용] · 안내 글 둘 이름 맞춤
- 옮기기 도구(보기만) — 프로젝트 층 못셈 14 = 「못셈(옛 SP 참조)」 갈래 · B09 구조물 · 서고 사본 대조(그대로 · 재생성 금액)
- 새 시험 test_795_4_b09_copy(8) · 낡은 시험 고침: test_203_1 · test_296 = 단추 이름 「업데이트」 · 호버 글 · 몸 `{ user }` 를 박아 둔 것 → 795-4 이름 · job 덧칸으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
223 lines
10 KiB
Python
223 lines
10 KiB
Python
"""옛 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 · 참조 · 손 단가 줄만) = 그대로.
|
||
프로젝트 층 일위대가 = 세기 · 대조만(795-4 · 쓰지 않음) — 옛 SP 참조(프로젝트 층은 굳힌
|
||
사본만 읽음 · 마스터 SP 를 바로 가리키는 옛 꼴) = 「못셈(옛 SP 참조)」 갈래.
|
||
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}」")
|
||
|
||
|
||
OLD_SP = "위 층만 가리킴" # 프로젝트 층 문서가 마스터 SP 를 바로 가리킴(굳힌 사본 아님)
|
||
LOCAL = ("입력값.json", "고른자재.json", "면적.json") # 재생성 대조 때 사본 값 그대로
|
||
|
||
|
||
def _b09_files() -> list[tuple[str, str, Path]]:
|
||
"""(어디, 종류, 폴더) — 프로젝트 구조물 사본 · B09 서고 사본."""
|
||
root = layers.storage_root()
|
||
out = []
|
||
for kind, pattern in (
|
||
("구조물사본", "*/*/*/templates/structures/*/manifest.json"),
|
||
("서고사본", "*/*/*/B09_Estimation/v1/library/*/manifest.json"),
|
||
):
|
||
for p in sorted(root.glob(pattern)):
|
||
at = p.parents[3] if kind == "구조물사본" else p.parents[4]
|
||
out.append((f"B09 {at.name}", kind, p.parent))
|
||
return out
|
||
|
||
|
||
def _copy_money(files: dict[str, Any]) -> Any:
|
||
from M02_MasterTemplete import M02_Item_Prices as prices
|
||
|
||
labor = files.get("일위대가.json")
|
||
if not isinstance(labor, dict):
|
||
return None
|
||
got = prices.calc(
|
||
labor,
|
||
files.get("단가.json") or {},
|
||
files.get("입력값.json") or {},
|
||
files.get("고른자재.json") or {},
|
||
files.get("면적.json"),
|
||
)
|
||
return _round({k: got.get(k) for k in ("기준합", "반영합")})
|
||
|
||
|
||
def b09_check(kind: str, folder: Path) -> dict[str, Any]:
|
||
"""B09 사본 하나(보기만) — {상태: 그대로 | 필요없음 | 못셈, 전, 재생성?, 재생성다름?, 까닭?}."""
|
||
from B09_Estimation import B09_Estimation_Bill_Library as lib
|
||
from M02_MasterTemplete.M02_Item_Source import build_source, read_json
|
||
|
||
own = {p.name: read_json(p) for p in folder.glob("*.json")}
|
||
manifest = own.get("manifest.json") or {}
|
||
try:
|
||
before = _copy_money(own)
|
||
except Exception as e: # noqa: BLE001 — 못 셈 = 목록
|
||
return {"상태": "못셈", "까닭": str(e) or type(e).__name__}
|
||
if before is None:
|
||
return {"상태": "필요없음"}
|
||
try:
|
||
if kind == "서고사본":
|
||
fresh, _ = lib._build(*lib._source_of(manifest))
|
||
lib._apply(fresh, str(manifest.get("종류")), manifest.get("값") or {})
|
||
else:
|
||
fresh = build_source(manifest["원본"])
|
||
after = _copy_money({**fresh, **{n: own[n] for n in LOCAL if n in own}})
|
||
except Exception as e: # noqa: BLE001 — 원본 없음 · 못 셈 = 재생성 대조만 빠짐
|
||
return {"상태": "그대로", "전": before, "까닭": f"재생성 대조 못 함 — {e}"}
|
||
return {"상태": "그대로", "전": before, "재생성": after, "재생성다름": after != before}
|
||
|
||
|
||
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)
|
||
if project and got["상태"] == "못셈" and OLD_SP in got.get("까닭", ""):
|
||
got["상태"] = "못셈(옛 SP 참조)"
|
||
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)
|
||
for where_, kind, folder in _b09_files(): # 795-4 B09 사본 = 보기만
|
||
out.append({"어디": where_, "종류": kind, "키": folder.name, **b09_check(kind, folder)})
|
||
out[-1]["씀"] = False
|
||
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 ("있음", "필요없음", "같음") or one.get("재생성다름"):
|
||
print(json.dumps(one, ensure_ascii=False, default=str))
|
||
print(json.dumps(table(rows), ensure_ascii=False, indent=1))
|