feat(826-2): 내 기계 = 층 폴더 내기계/ 한 대 한 파일

- 층 내기계.json {줄, 다음} → 내기계/<키>.json + _다음.json · 옛 파일은 읽을 때 한 번 옮기고 지움(내용 · 다음 번호 같음)
- 만들기 = 번호 먼저 · 대 파일 하나 · 고침 = 그 대 파일만 · 지움 = 그 대 파일 지움 · 셈 색인 기억 = 폴더 자국

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 19:45:11 +09:00
co-authored by Claude Opus 5.5
parent c7cbef86a0
commit bb63ce1742
+54 -16
View File
@@ -1,6 +1,7 @@
"""M02 내 기계(PLAN 821-1 · 요구 795 · 요구서_796 3 · 7 장) — 개인 층 기계 줄 목록 · 서버 단독.
저장 = 층 뿌리 `내기계.json` {줄: [줄], 다음: 수} — 줄 = M01 기계 줄 꼴
저장 = 층 뿌리 폴더 `내기계/` — 한 대 한 파일 `<키>.json`(줄) + `_다음.json` {다음: 수}(826-2 ·
옛 `내기계.json` {줄, 다음} 은 읽을 때 한 번 옮기고 지움) — 줄 = M01 기계 줄 꼴
{키 `EQP{사용자}-####`, 원문번호 「개인 {키}」, 원문 · 구분 · 출처 = 개인, 상세구분 · 이름, 규격,
단위, 값, 비고, 방식 바로|품셈|직접, 시간당?(직접 {노무비, 재료비, 경비}), 넣은날, 고친날, 본뜬?}
값 칸 이름 = M01 기계 줄과 같음 · 바로 = 손료계수 한 칸 · 품셈 = 비율 다섯 → 계수 넷(저장 때 서버가
@@ -20,6 +21,7 @@ GC000275) · 연료만 GC000272(손료계수 한 칸 GC001198) · 조종원만 G
from __future__ import annotations
import json
import os
import re
from datetime import datetime
from decimal import Decimal
@@ -30,7 +32,7 @@ from common_util.common_util_json import atomic_write_json
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_MasterTemplete_Store as store
FILE = "내기계.json"
FILE, DIR, NEXT = "내기계.json", "내기계", "_다음.json" # FILE = 옛 한 파일(옮기기만)
PREFIX, NO = "EQP", "개인 "
FIND = re.compile(rf"{NO}({PREFIX}\d+-\d+)") # 문서 안 기계 입력 「개인 EQP…」
EASY, BOOK, DIRECT = "바로", "품셈", "직접"
@@ -63,17 +65,51 @@ def _bad(text: str, status: int = 400) -> store.StoreError:
# ── 저장 ──────────────────────────────────────────────────────────────
def _path(folder: Path | None = None) -> Path:
return Path(folder or store.root()) / FILE
def _dir(folder: Path | None = None) -> Path:
return Path(folder or store.root()) / DIR
def _load(path: Path) -> Any:
try:
return json.loads(path.read_bytes())
except (OSError, ValueError):
return {}
def _order(path: Path) -> tuple:
"""넣은 차례 = 키 끝 번호 차례(「-0009」 다음 「-0010」)."""
head, _, n = path.stem.rpartition("-")
return (head, int(n) if n.isdigit() else 0, path.stem)
def _move(folder: Path | None = None) -> None:
"""옛 한 파일 `내기계.json` → 폴더(한 번 · 내용 같음 · 옛 파일 지움)."""
old = Path(folder or store.root()) / FILE
if not old.is_file():
return
got = _load(old)
base = _dir(folder)
for row in got.get("줄") or []:
if isinstance(row, dict) and mine(row.get("키")):
atomic_write_json(base / f"{row['키']}.json", row)
if not (base / NEXT).is_file():
atomic_write_json(base / NEXT, {"다음": int(got.get("다음") or 1)})
old.unlink()
def _put(row: dict) -> None:
atomic_write_json(_dir() / f"{row['키']}.json", row)
def _read(folder: Path | None = None) -> dict[str, Any]:
try:
got = json.loads(_path(folder).read_bytes())
except (OSError, ValueError):
got = {}
rows = [r for r in got.get("줄") or [] if isinstance(r, dict) and mine(r.get("키"))]
return {"줄": rows, "다음": int(got.get("다음") or 1)}
_move(folder)
base = _dir(folder)
rows = []
for path in sorted(base.glob(f"{PREFIX}*.json"), key=_order):
row = _load(path)
if isinstance(row, dict) and mine(row.get("키")) and path.stem == row["키"]:
rows.append(row)
return {"줄": rows, "다음": int(_load(base / NEXT).get("다음") or 1)}
def rows(folder: Path | None = None) -> list[dict]:
@@ -103,12 +139,13 @@ def whole(base: Any = None) -> Any:
from M02_MasterTemplete import M02_Sangeun_Copy as skc
base = m01.loaded()[1] if base is None else base
path = _path()
_move()
path = _dir()
try:
st = path.stat()
with os.scandir(path) as it: # 폴더 자국 — 대마다 (이름, 시각, 크기)
sig = tuple(sorted((e.name, e.stat().st_mtime_ns, e.stat().st_size) for e in it))
except OSError:
return base
sig = (st.st_mtime_ns, st.st_size)
got = _MEMO.get(str(path))
if got and got[0] == sig and got[1] is base:
return got[2]
@@ -309,7 +346,8 @@ def create(raw: Any = None, base: str | None = None) -> dict[str, Any]:
when = _now()
row = {"키": key, "원문번호": f"{NO}{key}", "원문": "개인", "구분": "개인", "상세구분": body["이름"],
**body, "넣은날": when, "고친날": when, **({"본뜬": base} if base else {})} # fmt: skip
atomic_write_json(_path(), {"줄": [*data["줄"], row], "다음": data["다음"] + 1})
atomic_write_json(_dir() / NEXT, {"다음": data["다음"] + 1}) # 번호 먼저 — 겹쳐 쓰지 않게
_put(row)
return answer(row)
@@ -322,7 +360,7 @@ def update(key: str, raw: Any) -> dict[str, Any]:
body = clean(raw)
keep = {k: old[k] for k in ("키", "원문번호", "원문", "구분", "넣은날", "본뜬") if k in old}
row = {**keep, "상세구분": body["이름"], **body, "고친날": _now()}
atomic_write_json(_path(), {**data, "줄": [row if r["키"] == key else r for r in data["줄"]]})
_put(row)
return answer(row)
@@ -333,7 +371,7 @@ def delete(key: str) -> dict[str, Any]:
if not any(r["키"] == key for r in data["줄"]):
raise _bad(f"없는 내 기계 「{key}」", 404)
used = uses().get(key, [])
atomic_write_json(_path(), {**data, "줄": [r for r in data["줄"] if r["키"] != key]})
(_dir() / f"{key}.json").unlink(missing_ok=True)
return {"지움": key, "쓰는곳": used}