Files
Aislo/M02_MasterTemplete/M02_Hopyo_Forget.py
T
eomsangdonandClaude Opus 5.5 82037faeaa feat(743-1): 불러오기 엑셀 보관 안 함 — 그 불러오기 동안만 · 끝내기 · 화면 떠남 · 하루 지남 지움 · 같은 엑셀 다시 올림 · 만든 문서 엑셀 사본 걷음
- 끝냄 한 길 M02_Hopyo_Forget.finish — HP · 격자 · 맞춤셈 캐시 지움 · 만든 SC · SK 본뜬 = 출처(파일 · 때 · 누가 + 전국자재 · 복사원본)만 · SC 줄 메모 「실무 …」 걷음 · 실무수량 = 저장 수량.손 과 같을 때만 걷음(742 전 저장본은 남김 = 금액 그대로) · SK 또 = 그 HP 몫만
- 같은 엑셀 다시 올림 409 걷음(끝낸 기록 지문 안 씀 · 키 번은 그대로 이어감)
- 하루 지난 HP = 목록 숨김 · 올림 때 끝냄 · 붙여넣기 만듦 뒤 실무자료 끝냄
- 마법사 떠남(다른 화면 · 페이지 닫음) = 이 화면이 쥔 파일 지움(keepalive DELETE)
- 뒤 셈이 끝낸 HP 캐시를 다시 쓰지 않음(Fit._put) · 끝냄은 HP 먼저 지우고 캐시
- 옛 파일 정리 tidy(미리 보기 표 · --apply) — 옛 HP · 지문 기록 · HP 없는 문서 사본
- 본뜬 곳 칸 = 끝낸 실무자료는 「실무자료 파일」(originText)
- 시험: 718-1 「끝낸 같은 엑셀 409」 → 「다시 올림 새 키」 · 705-2 붙여넣기 실무자료 「남음」 → 「지움」(요구 584-2 로 뜻 바뀜) · 372-2 화면 시험 onDeleted 꼴 바뀜

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

206 lines
9.3 KiB
Python

"""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:]))