"""M02 일위대가 불러오기 끝냄(PLAN 743-1 · 요구 582-3 · 584-2) — 올린 엑셀은 그 불러오기 동안만. 끝냄 한 길 `finish` = [끝내기] · 화면 떠남 · 하루 지남(`Store.EXPIRE`) · 붙여넣기 만듦 뒤 · 엑셀 보기 [삭제]: HP · 격자 · 맞춤셈 캐시 지움 · 그 HP 로 만든 이 층 일위대가(SC) · 단가산출(SK) 엑셀 사본 걷음 (`본뜬` → 출처 `{층 「실무자료」, 파일, 때, 누가}` + 전국자재 · 복사원본 · SC 줄 메모 「실무 …」 걷음 · `본뜬.실무수량` = 모든 값이 그 줄 저장 `수량.손` 과 같을 때만 걷음(742-1 · 다르면 남김 = 금액 그대로)) · SK `본뜬.또` = 그 HP 몫만 뺌 · 문서 몸(줄 · 덮개 · 더한줄 · 별칭 · 구분)과 별칭 장부는 그대로 · 끝낸 기록 없음(같은 엑셀 다시 올림 됨 · 키 번은 `불러옴.json` 번으로 다시 안 씀). 옛 파일 정리 `tidy`(한 번 · 층 폴더마다 · 미리 보기 = 표만) — 옛 HP(지문 없음) · 하루 지난 HP 끝냄 · 끝낸 기록 지문 비움 · HP 가 없는 SC · SK 엑셀 사본 걷음. `python -m M02_MasterTemplete.M02_Hopyo_Forget <층 폴더>… [--apply]`(없으면 미리 보기). """ from __future__ import annotations import json import re import sys from collections.abc import Callable from datetime import datetime from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Ref as refs ORIGIN = "실무자료" KEEP = ("층", "파일", "전국자재", "복사원본") _MEMO = re.compile(r"^실무 ") _HP = re.compile(r"^(HP-\d{6}) ") # ── 사본 걷음(순수) ─────────────────────────────────────────────────── def _slim(got: dict[str, Any]) -> dict[str, Any]: """출처만 — 파일 · 때 · 누가(+ 전국자재 · 복사원본).""" sure = got.get("확인") if isinstance(got.get("확인"), dict) else {} out = {k: got[k] for k in KEEP if got.get(k) is not None} return {**out, "층": ORIGIN, "때": sure.get("때"), "누가": sure.get("누가")} def _hand(one: Any) -> Any: qty = one.get("수량") if isinstance(one, dict) else None return qty.get("손") if isinstance(qty, dict) else None def _carried(doc: dict[str, Any], hands: dict[str, Any]) -> bool: """실무수량 값이 모두 그 줄 · 자재 저장 수량과 같음(742-1 저장 뒤).""" lines = doc.get("담은로직") or [] mats = {m.get("코드"): m for m in doc.get("자재") or [] if isinstance(m, dict)} for at, value in (hands.get("줄") or {}).items(): i = int(at) if str(at).isdigit() else -1 if not 0 <= i < len(lines) or _hand(lines[i]) != value: return False return all(_hand(mats.get(code)) == value for code, value in (hands.get("자재") or {}).items()) def strip_sc(doc: dict[str, Any]) -> dict[str, Any]: """일위대가(SC) 엑셀 사본 걷음 — 본뜬 출처만 · 줄 메모 「실무 …」 뺌.""" got = doc.get("본뜬") or {} lines = [ {k: v for k, v in one.items() if k != "메모"} if isinstance(one, dict) and _MEMO.match(str(one.get("메모") or "")) else one for one in doc.get("담은로직") or [] ] out = {**doc, "담은로직": lines, "본뜬": _slim(got)} hands = got.get("실무수량") if isinstance(hands, dict) and hands and not _carried(out, hands): out["본뜬"]["실무수량"] = hands # 742 전 저장본 — 셈이 기대므로 남김 return out def strip_sk(doc: dict[str, Any], gone: Callable[[dict], bool]) -> dict[str, Any]: """단가산출(SK) — 끝낸 출처(`gone(본뜬 · 또 한 칸)`) 몫만 걷음 · 다른 불러오기 몫은 그대로.""" got = doc.get("본뜬") or {} also = [x for x in got.get("또") or [] if isinstance(x, dict) and not gone(x)] own = _slim(got) if gone(got) else {k: v for k, v in got.items() if k != "또"} return {**doc, "본뜬": {**own, "또": also} if also else own} # ── 끝냄 ───────────────────────────────────────────────────────────── def _docs(folder: Path, kind: str): prefix = "SC" if kind == "precombo" else "SK" for path in sorted((folder / kind).glob(f"{prefix}-*.json")): try: doc = json.loads(path.read_bytes()) except (OSError, ValueError): continue got = doc.get("본뜬") if isinstance(doc, dict) else None if isinstance(got, dict) and got.get("층") == ORIGIN: yield path, doc def _here(got: dict[str, Any], here: refs.Here) -> bool: """출처 원본이 이 층(원본 없는 옛 것 = 이 층).""" return not got.get("원본") or tuple(got["원본"]) == tuple(here) def forget(folder: str | Path, gone: Callable[[str], bool], apply: bool = True) -> list[str]: """`gone(본뜬 키)` 인 이 층 SC · SK 엑셀 사본 걷음 → 걷은 문서 키(`apply` 거짓 = 셈만).""" folder, out = Path(folder), [] here = refs.where(folder) def hit(one: dict[str, Any]) -> bool: mark = str(one.get("키") or "") return bool(mark) and _here(one, here) and gone(mark) for kind in ("precombo", "sangeun"): for path, doc in _docs(folder, kind): got = doc["본뜬"] if not any(hit(x) for x in [got, *(got.get("또") or [])] if isinstance(x, dict)): continue new = strip_sc(doc) if kind == "precombo" else strip_sk(doc, hit) if new != doc: out.append(path.stem) if apply: atomic_write_json(path, new) return out def _caches(folder: Path, key: str) -> list[Path]: from M02_MasterTemplete import M02_Hopyo_Fit as fit try: got = fit.cache_path(folder, key) except Exception: # noqa: BLE001 — 캐시 자리 못 찾음 = 지울 것 없음 return [] return list(got.parent.glob(f"{'_'.join(got.name.split('_')[:2])}_*.json")) def finish(folder: str | Path, key: str) -> dict[str, Any]: """불러오기 끝냄 — 만든 문서 사본 걷음 · HP · 격자 · 맞춤셈 캐시 지움 → `{지움, 만든}`.""" folder = Path(folder) hopyo_store.read(folder, key) # 없으면 404 head = f"{key} " made = forget(folder, lambda mark: mark.startswith(head)) caches = _caches(folder, key) # HP 지우기 전에 자리 셈(HP 지문) hopyo_store.delete(folder, key) # 먼저 지움 — 뒤 셈은 HP 없으면 캐시 안 씀(`Fit._put`) # ponytail: 뒤 셈이 HP 확인 직후 · 쓰기 직전이면 한 벌 남을 수 있음(ms 틈) — tmp 캐시라 둠 for old in caches: old.unlink(missing_ok=True) return {"지움": key, "만든": len(made)} def expired(folder: str | Path, now: datetime | None = None) -> list[str]: """하루 지난 작업 중 HP 키(`M02_Hopyo_Store.stale`).""" out = [] for path in sorted((Path(folder) / hopyo_store.DIRNAME).glob("HP-*.json")): doc = hopyo_store._load(path) if isinstance(doc, dict) and doc.get("지문") and hopyo_store.stale(doc, now): out.append(path.stem) return out def sweep(folder: str | Path, now: datetime | None = None) -> list[str]: """하루 지난 HP 끝냄 → 끝낸 키.""" for key in (got := expired(folder, now)): finish(folder, key) return got # ── 옛 파일 정리(한 번) ─────────────────────────────────────────────── def tidy(folder: str | Path, apply: bool = False) -> list[dict[str, str]]: """옛 실무자료 정리 표 `[{할일, 대상}]` — `apply` 거짓 = 미리 보기(쓰기 없음).""" folder = Path(folder) home = folder / hopyo_store.DIRNAME rows: list[dict[str, str]] = [] every = sorted(p.stem for p in home.glob("HP-*.json")) old = [k for k in every if not (hopyo_store._load(home / f"{k}.json") or {}).get("지문")] ended = [*old, *expired(folder)] live = set(every) - set(ended) for key in ended: rows.append({"할일": "HP 끝냄(옛 · 하루 지남)", "대상": f"{hopyo_store.DIRNAME}/{key}"}) if apply: finish(folder, key) log = hopyo_store.done(folder) if log["지문"]: rows.append({"할일": f"끝낸 기록 지문 {len(log['지문'])} 비움", "대상": "불러옴.json"}) if apply: hopyo_store._put_done(folder, {"번": log["번"]}) def gone(mark: str) -> bool: m = _HP.match(mark) return bool(m) and m.group(1) not in live for key in forget(folder, gone, apply): rows.append({"할일": "엑셀 사본 걷음(출처만)", "대상": key}) return rows def main(argv: list[str]) -> int: apply = "--apply" in argv for one in [a for a in argv if a != "--apply"]: rows = tidy(one, apply) print(f"# {one} — {'고침' if apply else '미리 보기'} {len(rows)}") for row in rows: print(f"{row['할일']}\t{row['대상']}") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))