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>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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:]))
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -469,8 +469,9 @@ export const saveLedger = async (
|
||||
})
|
||||
).줄;
|
||||
|
||||
export const deleteHopyo = (scope: HopyoScope, key: string): Promise<unknown> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE" });
|
||||
/** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */
|
||||
export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise<unknown> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive });
|
||||
|
||||
/** [확인하고 사전조합 만들기] — 확인한 줄만 · 직접 줄 호표 = `통째` → `target` 층 사전조합(파일 층 = `from`) */
|
||||
export const buildHopyo = (
|
||||
|
||||
@@ -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<void> {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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<ManageLayer[]> {
|
||||
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<string, HopyoScope>(); // 이 화면이 쥔 파일 「층|키」 → 층
|
||||
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<void> {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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<MatMarks> = {
|
||||
자재구분: "사급",
|
||||
@@ -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;
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 — 다시 올림 됨
|
||||
|
||||
@@ -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) == [] # 두 번째 = 할 일 없음
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user