diff --git a/M02_MasterTemplete/M02_Hopyo_Fit.py b/M02_MasterTemplete/M02_Hopyo_Fit.py index b4a3c16a4..a5b58c2cf 100644 --- a/M02_MasterTemplete/M02_Hopyo_Fit.py +++ b/M02_MasterTemplete/M02_Hopyo_Fit.py @@ -377,13 +377,22 @@ def run_job(folder: Path, key: str) -> dict[str, Any]: if unit not in out["블록"]: out["블록"][unit] = compute(block, line, targets) out["셈"] = [at, len(todo)] - if at % EVERY == 0: - atomic_write_json(path, out) + if at % EVERY == 0 and not _put(folder, key, path, out): + return out out["끝"] = True - atomic_write_json(path, out) + _put(folder, key, path, out) return out +def _put(folder: Path, key: str, path: Path, out: dict[str, Any]) -> bool: + """캐시 씀 — 셈 도중 HP 를 끝냈으면(743-1) 안 쓰고 지움 → 거짓.""" + if not _hp_path(folder, key).is_file(): + path.unlink(missing_ok=True) + return False + atomic_write_json(path, out) + return True + + def _work() -> None: while True: folder, key = _JOBS.get() diff --git a/M02_MasterTemplete/M02_Hopyo_Forget.py b/M02_MasterTemplete/M02_Hopyo_Forget.py new file mode 100644 index 000000000..adfca6def --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Forget.py @@ -0,0 +1,205 @@ +"""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:])) diff --git a/M02_MasterTemplete/M02_Hopyo_Paste.py b/M02_MasterTemplete/M02_Hopyo_Paste.py index 4bfc4c117..5cc8d666d 100644 --- a/M02_MasterTemplete/M02_Hopyo_Paste.py +++ b/M02_MasterTemplete/M02_Hopyo_Paste.py @@ -24,6 +24,7 @@ from typing import Any from M02_MasterTemplete import M02_Hopyo_Build as hb from M02_MasterTemplete import M02_Hopyo_Excel as hx from M02_MasterTemplete import M02_Hopyo_Fixed as hfix +from M02_MasterTemplete import M02_Hopyo_Forget as forget from M02_MasterTemplete import M02_Hopyo_Store as hstore from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -296,7 +297,8 @@ def paste( folder: Any, text: str, cols: Any, who: Any, lines_only: bool = False, taken: Any = () ) -> dict[str, Any]: """[Ctrl+V] — `갈래` 「물음」(열 짝 짐작 + 보기 · 안 씀) · 「줄」(호표 머리 없음 · `lines_only`) · - 「만듦」(호표 머리 있음 = 실무자료 「붙여넣기 …」 + 호표마다 사전조합 · 705-3 `build_all` 그대로).""" + 「만듦」(호표 머리 있음 = 실무자료 「붙여넣기 …」 + 호표마다 사전조합 · 705-3 `build_all` 그대로 + · 만든 뒤 실무자료 끝냄 743-1).""" try: parsed = read_text(text, cols) except PasteError as error: @@ -310,4 +312,6 @@ def paste( return {**head, "갈래": "줄", **fixed_lines(parsed, taken)} keep = {k: parsed[k] for k in ("꼴", "시트", "호표", "단산", "검산")} row = hstore.save(folder, f"{SHEET} {datetime.now():%m-%d %H:%M}", keep, who) - return {**head, "갈래": "만듦", "키": row["키"], **hb.build_all(folder, folder, row["키"], who)} + made = hb.build_all(folder, folder, row["키"], who) + forget.finish(folder, row["키"]) # 743-1 — 붙인 글 실무자료는 만든 뒤 지움(엑셀 사본 걷음) + return {**head, "갈래": "만듦", "키": row["키"], **made} diff --git a/M02_MasterTemplete/M02_Hopyo_Store.py b/M02_MasterTemplete/M02_Hopyo_Store.py index 8fd3e0add..53ef5ead8 100644 --- a/M02_MasterTemplete/M02_Hopyo_Store.py +++ b/M02_MasterTemplete/M02_Hopyo_Store.py @@ -4,9 +4,9 @@ 파일 = 읽은 결과만(원본 엑셀 안 둠) `{키, 파일, 올린때, 올린이, 시트, 호표, 검산}` · 키 `HP-######` (층마다 다음 번호) · 맞춤 기억 = `{줄: {줄 열쇠: {로직?|자재?|빼기?, 입력값, 환산, 확정}}}`. 층 종류(`layers.KINDS`) 밖 폴더 — 양식 목록 · 복사 · 프로젝트 씨앗에 안 섞임 · 권한은 길 몫. -한시 불러오기(718-1) — 파일에 `지문`(올린 엑셀 sha256) · 목록 = 지문 있는 것(작업 중)만 · 옛 HP 는 -파일 그대로 목록에서만 숨김 · 끝낸 기록 `실무자료/불러옴.json` -`{번, 지문 {지문: {파일, 키, 끝낸때, 만든}}}` · 번 = 낸 키 가장 큰 번(지운 뒤에도 키를 다시 안 씀). +한시 불러오기(718-1 · 743-1) — 파일에 `지문`(올린 엑셀 sha256) · 목록 = 지문 있고 하루 안 지난 것 +(작업 중)만 · 옛 HP · 하루 지난 HP 는 목록에서 숨김(지움 = `M02_Hopyo_Forget`) · +`실무자료/불러옴.json` `{번}` = 낸 키 가장 큰 번(지운 뒤에도 키를 다시 안 씀 · 옛 `지문` 안 읽음). """ from __future__ import annotations @@ -15,6 +15,7 @@ import hashlib import json import re import threading +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any @@ -27,6 +28,7 @@ MEMORY = "맞춤" GRID = "격자" DONE = "불러옴" NO_GRID = "격자 없음 — 다시 올리면 보임" +EXPIRE = timedelta(days=1) # 작업 중 HP 보관 — 올린 뒤 하루 _KEY = re.compile(r"^HP-(\d{6})$") # ponytail: 프로세스 안 잠금 — 서버를 여럿 띄우면 파일 잠금으로 _LOCK = threading.Lock() @@ -68,18 +70,27 @@ def fingerprint(data: bytes) -> str: return hashlib.sha256(data).hexdigest() +def stale(doc: dict[str, Any], now: datetime | None = None) -> bool: + """하루 지난 HP — 올린때 기준 · 못 읽는 때 = 지난 것.""" + try: + when = datetime.fromisoformat(str(doc.get("올린때") or "").replace("Z", "+00:00")) + except ValueError: + return True + return (now or datetime.now(timezone.utc)) - when > EXPIRE + + def rows(folder: str | Path) -> list[dict[str, Any]]: - """층 실무자료 목록(키 차례) — 작업 중(지문 있음)만 · 옛 HP 는 숨김.""" + """층 실무자료 목록(키 차례) — 작업 중(지문 있음 · 하루 안)만 · 옛 HP · 지난 HP 는 숨김.""" out = [] for path in sorted(_dir(folder).glob("HP-*.json")): doc = _load(path) - if isinstance(doc, dict) and _KEY.match(path.stem) and doc.get("지문"): + if isinstance(doc, dict) and _KEY.match(path.stem) and doc.get("지문") and not stale(doc): out.append({**brief(doc), "지문": doc["지문"]}) return out def done(folder: str | Path) -> dict[str, Any]: - """끝낸 기록 `{번, 지문 {지문: {파일, 키, 끝낸때, 만든}}}`.""" + """키 번 기록 `{번, 지문}`(지문 = 743 전 끝낸 기록 · 안 읽음 · 정리 `Forget.tidy`).""" doc = _load(_dir(folder) / f"{DONE}.json") doc = doc if isinstance(doc, dict) else {} marks = doc.get("지문") @@ -137,22 +148,12 @@ def grid(folder: str | Path, key: str) -> dict[str, Any]: return doc -def delete(folder: str | Path, key: str, made: list[str] | None = None) -> None: - """HP · 격자 지움 · 만든 것(`made` SC 키)이 있으면 지문을 끝낸 기록에 남김(다시 올림 막음).""" +def delete(folder: str | Path, key: str) -> None: + """HP · 격자 지움(끝낸 기록 없음 · 같은 엑셀 다시 올림 됨 · 743-1).""" path = _path(folder, key) with _LOCK: - doc = _load(path) - if not isinstance(doc, dict): + if not isinstance(_load(path), dict): raise store.StoreError(404, f"없는 실무자료 「{key}」") - if made and doc.get("지문"): - log = done(folder) - log["지문"][doc["지문"]] = { - "파일": doc.get("파일"), - "키": key, - "끝낸때": utc_now_text(), - "만든": made, - } - _put_done(folder, log) path.unlink() _grid_path(folder, key).unlink(missing_ok=True) diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index dc113d411..8705b2a01 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -8,8 +8,8 @@ 일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) · 338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 · 올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`) · -718-1 한시 불러오기 = 올림 지문(끝낸 같은 엑셀 409 · 작업 중 같은 엑셀 = 그 파일 `이어서`) · -지우기 = [끝내기](HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가가 있으면 지문 기록 · 별칭 장부 남음). +718-1 한시 불러오기 = 올림 지문(작업 중 같은 엑셀 = 그 파일 `이어서` · 끝낸 것 = 새로 · 743-1) · +지우기 = [끝내기](`M02_Hopyo_Forget.finish` · 올림 때 하루 지난 HP 도 끝냄). 층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌). ⚠ 등록은 `main.py`(로그인만). """ @@ -27,6 +27,7 @@ from pydantic import BaseModel, ConfigDict, Field from common_util.common_util_auth import verify_session from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel +from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger @@ -229,14 +230,7 @@ def _who(session: Session) -> dict[str, Any]: def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]: mark = hopyo_store.fingerprint(data) - if old := hopyo_store.done(store.root())["지문"].get(mark): - made = len(old.get("만든") or []) - when = str(old.get("끝낸때") or "")[:10] - raise store.StoreError( - 409, - f"이미 불러온 엑셀 「{old.get('파일')}」({when} · 만든 일위대가 · 단가산출 {made})" - " — 다시 만들지 않음", - ) + hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1) if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None): return {**same, "이어서": True} try: @@ -502,29 +496,10 @@ def hopyo_ledger_write( def hopyo_delete( layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) ) -> dict: - """실무자료 지우기 = 불러오기 [끝내기](718-1) — HP · 격자 · 맞춤셈 캐시 지움 · 만든 일위대가 - (이 층 확인된 본뜬 「키 제N호표」)는 그대로 · 있으면 지문 기록(같은 엑셀 다시 올림 막음) · - 별칭 장부 그대로 → `{지움, 만든}`.""" + """실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 · + 만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`.""" folder = _root(session, layer, owner, write=True) - return _call(folder, _finish, folder, key) - - -def _finish(folder: Path, key: str) -> dict[str, Any]: - head = f"{key} 제" - made = sorted( - {sc for mark, sc in hopyo_build.confirmed(folder).items() if mark.startswith(head)} - ) - made += sorted(hopyo_sangeun.made(folder, key).values()) # 단가산출(SK) - if (cache_path := _later("M02_Hopyo_Fit", "cache_path")) is not None: - try: - got = cache_path(folder, key) - except (OSError, store.StoreError): - got = None - # ponytail: 뒤 셈이 도는 중이면 끝낸 뒤 캐시를 다시 쓸 수 있음 — tmp 캐시라 둠 - for old in got.parent.glob(f"{'_'.join(got.name.split('_')[:2])}_*.json") if got else []: - old.unlink(missing_ok=True) - hopyo_store.delete(folder, key, made) - return {"지움": key, "만든": len(made)} + return _call(folder, hopyo_forget.finish, folder, key) class SangeunBody(_Strict): diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index eb3b487db..24d77cfa9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -469,8 +469,9 @@ export const saveLedger = async ( }) ).줄; -export const deleteHopyo = (scope: HopyoScope, key: string): Promise => - call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE" }); +/** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */ +export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise => + call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive }); /** [확인하고 사전조합 만들기] — 확인한 줄만 · 직접 줄 호표 = `통째` → `target` 층 사전조합(파일 층 = `from`) */ export const buildHopyo = ( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index cd670718c..fe4456135 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Side.ts * 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시) — [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) → - * 본 칸 마법사(`M02_MasterTemplete_Hopyo_Wizard` · 결과 층 고름 · 작업 중 이어서 · [끝내기] 때 올린 파일 지움) · + * 본 칸 마법사(`M02_MasterTemplete_Hopyo_Wizard` · 결과 층 고름 · 작업 중 이어서 · [끝내기] · 화면 떠남 때 올린 파일 지움) · * 옛 파일 목록 없음(옛 HP 는 서버 목록에서 숨김) · * 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층). * ========================================================================== */ @@ -40,7 +40,8 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { onClick: () => void showView(WIZARD), }); start.classList.add("m02-layers__btn"); - start.title = "다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내면 올린 파일 지움)"; + start.title = + "다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)"; start.dataset.hopyoWizardOpen = ""; const ledger = el("button", { className: "m02-side__item ui-sidebar-row", @@ -52,7 +53,7 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { className: "m02-hopyo-side", children: [el("div", { className: "m02-side__row", children: [start] }), ledger], }); - let open: { key: string; view: HTMLElement } | null = null; + let open: { key: string; view: HTMLElement; leave?: () => void } | null = null; const paintList = (): void => { ledger.classList.toggle("is-active", open?.key === LEDGER_TITLE); @@ -61,18 +62,18 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { async function showView(key: string): Promise { if (open?.key === key && open.view.isConnected) return; if (!(await opt.confirmLeave())) return; - const view = - key === WIZARD - ? buildHopyoWizard() - : buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() }); + open?.leave?.(); // 마법사 떠남 = 올린 파일 지움(743-1) + const wiz = key === WIZARD ? buildHopyoWizard() : null; + const view = wiz?.root ?? buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() }); opt.show(key === WIZARD ? title : `${title} — ${LEDGER_TITLE}`, view); opt.onOpen(); - open = { key, view }; + open = { key, view, ...(wiz ? { leave: wiz.leave } : {}) }; paintList(); } const paint = (): void => { if (open && !open.view.isConnected) { + open.leave?.(); // 다른 화면으로 = 마법사 떠남 open = null; paintList(); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Wizard.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Wizard.ts index d93bc1087..019a69dac 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Wizard.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Wizard.ts @@ -2,14 +2,15 @@ * M02_MasterTemplete_Hopyo_Wizard.ts * 일위대가 불러오기 = 한시 마법사(PLAN 718-1 · 요구서 내역12 4장 22) — 본 칸 한 화면 · 걸음 셋: * ① 결과 층(쓸 수 있는 층만 · 기본 개인) · 결과(일위대가 늘 · 단가산출 체크) · 엑셀 고르기 → 올림 - * (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 서버 409 안내) · 작업 중 파일 이어서 + * (그 층에 둠 · 같은 엑셀 작업 중 = 그 파일 이어서 · 끝낸 같은 엑셀 = 새로 올림) · 작업 중 파일 이어서 * ② 보기 · 확인 · 만들기 = 엑셀 보기 창(`Hopyo_Sheet` — [모두 고정으로 들여오기] · 호표 [수정] → * 확인 창 만들기 · [구조물 조합으로] 그대로) · 만들 층 = 고른 층 · * 단가산출 체크면 단가산출 칸(`Hopyo_WizardSk` — 기초 · SK · 고름 대기 기초 고름 · 729-3) · * 안 이은 줄 띠(`Hopyo_Left` — 안 만든 호표의 남은 줄 · [고르기] = 줄마다 이을 것 · 729-3) * ③ [끝내기] = 남은 줄 다시 셈 → 있으면 막음(띠 · 남은 수 · 표) · 없으면 물음 → 서버가 이번 HP · 격자 · 맞춤셈 캐시 지움 · - * 만든 일위대가 · 단가산출 · 별칭 장부 남음 · 지문 기록 - * 작업 중 = 서버에 보관(떠났다 와도 ① 목록에서 이어서) · 옛 HP(지문 없음)는 서버 목록에서 숨김. + * 만든 일위대가 · 단가산출(엑셀 사본 걷고 출처만) · 별칭 장부 남음 + * 작업 중 = 이 화면 동안만(743-1) — 화면 떠남(`leave` · 페이지 닫음) = 이 화면에서 올리거나 이은 파일 지움 · + * 하루 지난 것은 서버가 지움 · 엑셀 자료는 보관하지 않음(필요하면 다시 올림). * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; @@ -61,7 +62,13 @@ export async function writableLayers(): Promise { return LAYERS.map((x) => x.layer).filter((layer) => accessFor(layer, null, user).canWrite); } -export function buildHopyoWizard(): HTMLElement { +export interface HopyoWizard { + root: HTMLElement; + /** 화면 떠남 — 이 화면에서 올리거나 이은 파일 지움(743-1) */ + leave: () => void; +} + +export function buildHopyoWizard(): HopyoWizard { const steps = createSegment({ options: STEPS.map((s) => ({ ...s, disabled: true })), value: "1", @@ -73,7 +80,7 @@ export function buildHopyoWizard(): HTMLElement { attrs: { "data-hopyo-wizard": "" }, children: [ createText( - "한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내면 올린 파일 지움 · 별칭 장부는 남음", + "한시 — 다른 프로그램 일위대가 엑셀을 우리 일위대가로 옮김 · 끝내거나 화면을 떠나면 올린 파일 지움 · 별칭 장부는 남음", "hint", ), el("div", { className: "ui-toolbar", children: [steps.root] }), @@ -84,6 +91,16 @@ export function buildHopyoWizard(): HTMLElement { let pick: ManageLayer = "personal"; let wantSangeun = false; // 결과 「단가산출」 체크 const scopeOf = (layer: ManageLayer): HopyoScope => ({ layer, owner: null }); + const held = new Map(); // 이 화면이 쥔 파일 「층|키」 → 층 + const hold = (layer: ManageLayer, key: string): void => + void held.set(`${layer}|${key}`, scopeOf(layer)); + const drop = (layer: ManageLayer, key: string): void => void held.delete(`${layer}|${key}`); + const leave = (): void => { + window.removeEventListener("pagehide", leave); + for (const [id, scope] of held) void deleteHopyo(scope, id.split("|")[1], true).catch(() => {}); + held.clear(); + }; + window.addEventListener("pagehide", leave); async function first(): Promise { steps.set("1"); @@ -166,6 +183,7 @@ export function buildHopyoWizard(): HTMLElement { function second(layer: ManageLayer, file: HopyoFile): void { steps.set("2"); + hold(layer, file.키); const scope = scopeOf(layer); const view = createButton({ label: "엑셀 보기 · 확인 · 만들기", @@ -179,7 +197,10 @@ export function buildHopyoWizard(): HTMLElement { 파일: file.파일, target: scope, canWrite: true, - onDeleted: () => void first(), + onDeleted: () => { + drop(layer, file.키); + void first(); + }, }), ) .catch((error: unknown) => showToast(why(error), "error")), @@ -265,13 +286,14 @@ export function buildHopyoWizard(): HTMLElement { (x) => !x.one.호표.만든?.확인 && hasDraft(draftKey(scope, file.키, x.one.호표.번)), ).length; const ok = await showConfirmDialog( - `「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출과 별칭 장부는 남음 · 하나라도 만들었으면 같은 엑셀은 다시 올리지 못함` + + `「${file.파일}」 불러오기를 끝냄 — 올린 파일(엑셀 결과 · 격자 · 맞춤 캐시)을 지움 · 만든 일위대가 · 단가산출(출처만 남김)과 별칭 장부는 남음 · 같은 엑셀은 필요하면 다시 올림` + (loose ? ` · ${L("M02_LeftLoose").replace("{n}", String(loose))}` : ""), "끝내기", ); if (!ok) return steps.set("2"); try { const got = (await deleteHopyo(scope, file.키)) as { 만든?: number }; + drop(layer, file.키); showToast(`끝냄 — 만든 일위대가 · 단가산출 ${got.만든 ?? 0} · 올린 파일 지움`, "success"); } catch (error) { showToast(why(error), "error"); @@ -282,5 +304,5 @@ export function buildHopyoWizard(): HTMLElement { } void first(); - return root; + return { root, leave }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 6a2519765..05cb23fd5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -127,6 +127,9 @@ export type NationalMat = MatMarks & { 단위: string; 수량: { 손: number }; }; +/** 본뜬 곳 글 — 「층 키」 · 끝낸 실무자료 = 「실무자료 파일」 · 없으면 null */ +export const originText = (o: LaborDoc["본뜬"]): string | null => + o?.키 || o?.파일 ? `${o.층} ${o.키 ?? o.파일}` : null; /** 읽을 때 기본값(162-1 · 서버 `MARK_DEFAULT` 와 같음) — 저장은 비움 */ export const MAT_MARK_DEFAULT: Required = { 자재구분: "사급", @@ -163,7 +166,14 @@ export interface LaborDoc { 열: string; 일위대가번?: string; 단위: string | null; - 본뜬?: { 층: string; 주인: string | null; 키: string; 전국자재?: NationalMat[] } | null; + /** 실무자료 끝낸 뒤(743-1) = 키 없이 `파일 · 때 · 누가` 출처만 */ + 본뜬?: { + 층: string; + 주인?: string | null; + 키?: string; + 파일?: string; + 전국자재?: NationalMat[]; + } | null; 담은로직: LaborRow[]; 자재: LaborMat[]; 계산값?: unknown; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index a0315acaa..bfb4b5a62 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -44,6 +44,7 @@ import { type LaborQty, type LaborRow, type MatMarkKey, + originText, } from "./M02_MasterTemplete_Labor_Api"; import { buildLaborDiagram, @@ -567,7 +568,7 @@ export function mountLaborDetail( ); drawUnit(); const origin = createInputField({ type: "text", label: tc("Labor_Origin") }); - origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self"); + origin.input.value = originText(cur.본뜬) ?? tc("Origin_Self"); origin.input.readOnly = true; const section = (title: string, button: HTMLElement | null, body: HTMLElement): HTMLElement => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts index 7bbbb4138..158082efc 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Table.ts @@ -30,6 +30,7 @@ import { type LaborCalc, type LaborDoc, type LaborRow, + originText, } from "./M02_MasterTemplete_Labor_Api"; import type { LaborDetailHandle, LaborDetailOptions } from "./M02_MasterTemplete_Labor_Detail"; import { laborBlock } from "./M02_MasterTemplete_Labor_Grid"; @@ -429,7 +430,7 @@ export function mountLaborTable( ); drawUnit(); const origin = createInputField({ type: "text", label: tc("Labor_Origin") }); - origin.input.value = cur.본뜬?.키 ? `${cur.본뜬.층} ${cur.본뜬.키}` : tc("Origin_Self"); + origin.input.value = originText(cur.본뜬) ?? tc("Origin_Self"); origin.input.readOnly = true; host.replaceChildren( diff --git a/resources/tester/test_705_2_hopyo_paste.py b/resources/tester/test_705_2_hopyo_paste.py index 62cacc2b0..b567d7af9 100644 --- a/resources/tester/test_705_2_hopyo_paste.py +++ b/resources/tester/test_705_2_hopyo_paste.py @@ -94,10 +94,9 @@ def test_실무_xlsx_넷_붙여넣기_만듦_합계_같음(client, tag): assert got["같음"] == FILES[tag][1] and all(abs(x["차이"]) <= 1 for x in got["다름"]) assert len(got["만듦"]) == got["호표"] and got["고정"] + got["호표참조"] == got["줄"] assert master_hash() == before - # 718-1 — 붙여넣기 실무자료 = 지문 없음 → 불러오기 목록엔 숨김 · 파일은 남음 + # 743-1 — 붙여넣기 실무자료 = 만든 뒤 지움(엑셀 보관 안 함) assert client.get("/api/m02/layers/personal/hopyo").json()["줄"] == [] - doc = client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").json() - assert doc["파일"].startswith("붙여넣기") and doc["시트"] == "붙여넣기" + assert client.get(f"/api/m02/layers/personal/hopyo/{got['키']}").status_code == 404 def _block(rows, no): diff --git a/resources/tester/test_718_1_hopyo_import.py b/resources/tester/test_718_1_hopyo_import.py index ed4b93a5a..044b1efe2 100644 --- a/resources/tester/test_718_1_hopyo_import.py +++ b/resources/tester/test_718_1_hopyo_import.py @@ -1,7 +1,7 @@ -"""일위대가 불러오기 한시(PLAN 718-1) — 올림 지문 · [끝내기] 지움 · 겹침 막음. +"""일위대가 불러오기 한시(PLAN 718-1 · 743-1) — 올림 지문 · [끝내기] 지움. 같은 엑셀 작업 중 다시 올림 = 그 파일 이어서(새 키 없음) · [끝내기] = HP · 격자 · 맞춤셈 캐시 0 · -만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 409 · 만든 것 없이 지움 = 다시 올림 됨 · +만든 일위대가 · 별칭 장부 그대로 · 끝낸 같은 엑셀 다시 올림 = 새 키(743-1 · 409 걷음) · 옛 HP(지문 없음) = 파일 그대로 목록에서만 숨김 · 키 번 = 지운 뒤에도 다시 안 씀. 층 = 임시 폴더. """ @@ -44,9 +44,9 @@ def test_이어서_끝내기_지움_겹침_막음(client): assert hs.memory(folder) == {"장부 줄": {"갈래": "안 씀"}} assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before # 만든 일위대가 그대로 - block = _post(client) # 끝낸 같은 엑셀 = 막음 · 안내 - assert block.status_code == 409 and "이미 불러온 엑셀" in block.json()["detail"] - assert f"단가산출 {n}" in block.json()["detail"] and _keys(client) == [] + again = _post(client) # 743-1 — 끝낸 같은 엑셀 = 다시 올림 됨(새 키) + assert again.status_code == 200 and again.json()["키"] == "HP-000002" + assert _keys(client) == ["HP-000002"] assert sorted((folder / "precombo").glob("SC-*.json")) == sc_before @@ -88,6 +88,6 @@ def test_결과_단가산출_산근_블록_기초_SK_고름대기_겹침_없음( again = client.post(url).json() # 다시 = 그대로(겹침 없음) assert again["만듦"] == [] and again["그대로"] == len(got["만듦"]) res = client.delete(URL.format("personal") + "/HP-000001").json() - assert res["만든"] == len(got["만듦"]) # 단가산출만 만들어도 지문 기록(블록마다) + assert res["만든"] == len(docs) # 같은 내용 SK 한 벌 — 사본 걷은 문서 수 assert len(list((folder / "sangeun").glob("SK-*.json"))) == len(docs) # 남음 - assert _post(client).status_code == 409 + assert _post(client).status_code == 200 # 743-1 — 다시 올림 됨 diff --git a/resources/tester/test_743_1_hopyo_forget.py b/resources/tester/test_743_1_hopyo_forget.py new file mode 100644 index 000000000..a8b6c63a0 --- /dev/null +++ b/resources/tester/test_743_1_hopyo_forget.py @@ -0,0 +1,157 @@ +"""불러오기 엑셀 보관 안 함(PLAN 743-1 · 요구 582-3 · 584-2 · `M02_Hopyo_Forget`). + +[끝내기] = 남는 파일 0 · 만든 일위대가 · 단가산출 = 출처(파일 · 때 · 누가)만 · 금액 그대로 · +엑셀 사본(본뜬 키 · 원본 · 확인 · 품셈 · 줄 메모 「실무 …」) 걷음 · +실무수량 = 저장 수량과 같을 때만 걷음 · SK 또 = 그 HP 몫만 · +하루 지난 HP = 목록 숨김 → 올림 때 지움 · 옛 파일 정리 미리 보기(쓰기 0) → 고침. +층 = 임시 폴더. +""" + +import json +from datetime import datetime, timedelta, timezone + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Forget as forget +from M02_MasterTemplete import M02_Hopyo_Store as hs +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.test_m02_hopyo_excel import URL, _post, client # noqa: F401 + +ALL = "/api/m02/layers/personal/hopyo-build-all" +SK_URL = URL.format("personal") + "/HP-000001/sangeun" + + +def _files(folder): + return sorted(p.name for p in (folder / hs.DIRNAME).rglob("HP-*.json")) + + +def _sums(folder): + """문서마다 1 단위 기준 계 — 끝내기 전후 같아야 함.""" + whole = refs.Expand(m01.loaded()[1]) + out = {} + with store.at(folder): + for kind in ("precombo", "sangeun"): + for p in sorted((folder / kind).glob("*.json")): + out[p.stem] = refs.summary(folder, kind, p.stem, whole).get("기준금액") + return out + + +def _docs(folder, kind): + return {p.stem: json.loads(p.read_bytes()) for p in sorted((folder / kind).glob("*.json"))} + + +# ── 순수 ────────────────────────────────────────────────────────────── +SURE = {"누가": {"id": 42, "이름": "갑"}, "때": "2026-11-02T00:00:00Z"} +HEAD = {"층": "실무자료", "주인": None, "키": "HP-000003 제2호표", "파일": "가.xls"} + + +def test_SC_사본_걷음_출처만_실무수량은_저장_수량과_같을_때만(): + lines = [ + {"로직": "L1", "메모": "실무 0.3㎥ × 1 · 규격", "수량": {"손": 0.3}, "별칭": "엑셀 품"}, + {"로직": "L2", "메모": "사용자 메모", "수량": {"손": 2}}, + ] + doc = { + "이름": "가", + "담은로직": lines, + "자재": [{"코드": "MT1", "수량": {"손": 5}}], + "본뜬": {**HEAD, "원본": ["personal", "7/42"], "확인": SURE, "품셈": [0], + "전국자재": [{"이름": "모래"}], "실무수량": {"줄": {"0": 0.3, "1": 2}, + "자재": {"MT1": 5}}}, + } # fmt: skip + got = forget.strip_sc(doc) + assert got["본뜬"] == { + "층": "실무자료", + "파일": "가.xls", + "전국자재": [{"이름": "모래"}], + "때": SURE["때"], + "누가": SURE["누가"], + } + assert "메모" not in got["담은로직"][0] and got["담은로직"][1]["메모"] == "사용자 메모" + assert got["담은로직"][0]["별칭"] == "엑셀 품" and got["담은로직"][0]["수량"] == {"손": 0.3} + assert got["자재"] == doc["자재"] + # 742 전 저장본(수량 null) = 실무수량이 셈 근거 → 남김 + old = {**doc, "담은로직": [{**x, "수량": None} for x in lines]} + assert forget.strip_sc(old)["본뜬"]["실무수량"] == doc["본뜬"]["실무수량"] + + +def test_SK_또_그_HP_몫만_걷음(): + other = {"키": "HP-000004 단산1", "원본": None, "갈래": "SK", "엑셀품": {}, "이름": "나"} + doc = {"본뜬": {**HEAD, "키": "HP-000003 단산1", "확인": SURE, "갈래": "기초", "엑셀품": {}, + "못옮김": [], "또": [other]}} # fmt: skip + got = forget.strip_sk(doc, lambda x: str(x.get("키")).startswith("HP-000003 ")) + assert got["본뜬"] == {**{"층": "실무자료", "파일": "가.xls"}, **SURE, "또": [other]} + both = forget.strip_sk(doc, lambda x: True) + assert "또" not in both["본뜬"] and "키" not in both["본뜬"] + + +# ── 길 ─────────────────────────────────────────────────────────────── +def test_끝내기_남는_파일_0_금액_그대로_출처만_다시_올림(client): + folder = layers.personal_dir(7, 42) + assert _post(client).json()["키"] == "HP-000001" + fit.run_job(folder, "HP-000001") + assert client.post(SK_URL).status_code == 200 + made = client.post(ALL, json={"층": "personal", "키": "HP-000001"}).json() + assert made["만듦"] + before = _sums(folder) # 모두 고정 = 메모 없음(메모 걷음은 순수 시험) + cache = fit.cache_path(folder, "HP-000001") + assert cache.is_file() + + res = client.delete(URL.format("personal") + "/HP-000001") + assert res.status_code == 200 + assert _files(folder) == [] and not cache.exists() # HP · 격자 · 맞춤셈 캐시 0 + assert _sums(folder) == before # 금액 그대로 + for kind in ("precombo", "sangeun"): + for key, doc in _docs(folder, kind).items(): + got = doc["본뜬"] + assert got["층"] == "실무자료" and got["파일"] and got["때"], key + assert not {"키", "원본", "확인", "품셈", "갈래", "엑셀품", "못옮김", "또"} & set(got) + lines = doc.get("담은로직") or [] + assert not any(str(x.get("메모", "")).startswith("실무 ") for x in lines) + assert hs.done(folder)["지문"] == {} # 끝낸 기록 없음 + again = _post(client) # 같은 엑셀 다시 올림 = 새 키(409 없음) + assert again.status_code == 200 and again.json()["키"] == "HP-000002" + + +def test_하루_지난_HP_목록_숨김_올림_때_지움(client): + folder = layers.personal_dir(7, 42) + assert _post(client).json()["키"] == "HP-000001" + path = folder / hs.DIRNAME / "HP-000001.json" + doc = json.loads(path.read_bytes()) + old = datetime.now(timezone.utc) - timedelta(days=1, minutes=1) + doc["올린때"] = old.replace(microsecond=0).isoformat().replace("+00:00", "Z") + path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8") + assert client.get(URL.format("personal")).json()["줄"] == [] # 숨김 + assert forget.expired(folder) == ["HP-000001"] + got = _post(client).json() # 같은 엑셀이어도 지난 것 = 지우고 새로(이어서 아님) + assert got["키"] == "HP-000002" and not got.get("이어서") + assert _files(folder) == ["HP-000002.json", "HP-000002.json"] # 이번 것 + 격자 + + +def test_옛_파일_정리_미리_보기_쓰기_0_고침(client): + folder = layers.personal_dir(7, 42) + assert _post(client).json()["키"] == "HP-000001" # 작업 중 — 그대로 + home = folder / hs.DIRNAME + (home / "HP-000007.json").write_text(json.dumps({"키": "HP-000007", "호표": []}), "utf-8") + hs._put_done(folder, {"번": 7, "지문": {"ab": {"파일": "옛.xls", "만든": ["SC-000001"]}}}) + (folder / "precombo").mkdir(exist_ok=True) + gone = {"이름": "옛", "단위": "개", "담은로직": [{"로직": "L", "메모": "실무 1개 × 1"}], + "본뜬": {**HEAD, "키": "HP-000005 제1호표", "확인": SURE}} # fmt: skip + live = {**gone, "본뜬": {**HEAD, "키": "HP-000001 제1호표", "확인": SURE}} + for key, doc in (("SC-000001", gone), ("SC-000002", live)): + text = json.dumps(doc, ensure_ascii=False) + (folder / "precombo" / f"{key}.json").write_text(text, "utf-8") + snap = {p: p.read_bytes() for p in folder.rglob("*.json")} + + rows = forget.tidy(folder) + assert [r["대상"] for r in rows] == ["실무자료/HP-000007", "불러옴.json", "SC-000001"] + assert {p: p.read_bytes() for p in folder.rglob("*.json")} == snap # 미리 보기 = 쓰기 0 + + assert forget.tidy(folder, apply=True) == rows + assert _files(folder) == ["HP-000001.json", "HP-000001.json"] + assert hs.done(folder) == {"번": 7, "지문": {}} + docs = _docs(folder, "precombo") + assert "키" not in docs["SC-000001"]["본뜬"] and "메모" not in docs["SC-000001"]["담은로직"][0] + assert docs["SC-000002"] == live # 작업 중 HP 것 그대로 + assert forget.tidy(folder) == [] # 두 번째 = 할 일 없음 diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 429ecb39a..a6710daec 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -538,8 +538,12 @@ def test_명사형_단추와_별칭_장부_풀이_359(): def test_372_2_저장은_개인_층에_삭제는_화면_층_권한(): - """718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음.""" + """718-1 — 만들 층 = 마법사에서 고른 층(올린 층과 같음) · [삭제] = 끝내기 → 처음 걸음 · + 743-1 — 지운 파일은 떠남 지움 목록에서 뺌 · 떠남(화면 바뀜 · 페이지 닫음) = 쥔 파일 지움.""" side = _text("M02_MasterTemplete_Hopyo_Wizard.ts") open_ = side.split("m.openHopyoSheet({")[1].split("}),")[0] assert "target: scope," in open_ and "canWrite: true," in open_ - assert "onDeleted: () => void first()," in open_ + assert "drop(layer, file.키);" in open_ and "void first();" in open_ + assert 'addEventListener("pagehide", leave)' in side and "return { root, leave };" in side + panel = _text("M02_MasterTemplete_Hopyo_Side.ts") + assert panel.count("open?.leave?.()") == 1 and panel.count("open.leave?.()") == 1