feat(826-2): M01 기계 = 한 대 한 파일 · M01 저장 길 줄이기

- 기계.json 한 파일 → 폴더 기계/(EQ 키마다 파일 + _머리.json · 차례 = 키 차례) · 옮긴 빌더 build_기계폴더_826_2 (655 대 머리 · 줄 · 차례 대조 뒤 옛 파일 지움)
- 읽기 도우미 master_machine_files — cm.load · Store_Cache · Store.read 가 옛 「기계.json」 한 표 꼴로 지어 줌(API · 화면 · 색인 그대로) · 쓰기 = 바뀐 대 파일만
- 기계.json 직접 열던 곳 도우미로: M02_San_Basis_Index · M02_Logic_Standard 지문 · build_용도 · master_keys.adopt · _틀 · _갱신절차
- 저장 몸 M01_MasterData_Store_Save — 전 = 기억 · 후 = 고친 파일만 얕은 사본 · 틀은 고친 줄만 · 조합 · 일위대가 문서는 로직 줄이 빠질 때만 · 재료 찾기 기억 이어받음 · 저장 뒤 쓴 파일만 기억에 갈아 끼움
- 마스터 사본 시험 22 개 = 기계 폴더도 복사 · 821_2 · 815_2 글 대조 = 대 파일 하나 대조(옛 한 파일 글 대조 낡음)
- 새 시험 test_826_2 — 폴더 = 한 표 · 한 대 한 파일 · 새 · 지움 · 409 · 옛 길과 같은 오류 스무 견본

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 19:45:00 +09:00
co-authored by Claude Opus 5.5
parent 5fb7b6c16b
commit c7cbef86a0
694 changed files with 40486 additions and 39831 deletions
@@ -0,0 +1,44 @@
"""826-2 기계 한 파일(`기계.json`) → 폴더 `기계/`(한 대 한 파일 · `master_machine_files`).
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/build_기계폴더_826_2.py [마스터 폴더]
옮긴 뒤 폴더를 다시 읽어 옛 파일과 머리 · 줄마다 · 차례까지 같은지 대조하고 같을 때만 옛 파일을 지움.
두 번 돌려도 같음(옛 파일이 없으면 할 일 없음).
"""
from __future__ import annotations
import json
import sys
from decimal import Decimal
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import master_machine_files as mmf # noqa: E402
def move(folder: Path) -> int:
old = folder / mmf.NAME
if not old.is_file():
return 0
data = json.loads(old.read_bytes(), parse_float=Decimal, parse_int=Decimal)
keys = [str(r["키"]) for r in data["줄"]]
if keys != sorted(keys):
raise SystemExit("줄 차례가 키 차례 아님 — 폴더 꼴(키 차례)로 못 옮김")
mmf.write(folder, data)
got = mmf.table(folder)[0]
if list(got) != list(data) or any(got[k] != data[k] for k in data if k != "줄"):
raise SystemExit("머리 다름 — 옛 파일 둠")
if len(got["줄"]) != len(data["줄"]):
raise SystemExit(f"줄 수 다름 {len(got['줄'])} ≠ {len(data['줄'])} — 옛 파일 둠")
for a, b in zip(got["줄"], data["줄"]):
if a != b or list(a) != list(b):
raise SystemExit(f"줄 다름 「{b.get('키')}」 — 옛 파일 둠")
old.unlink()
return len(keys)
if __name__ == "__main__":
target = Path(sys.argv[1]) if len(sys.argv) > 1 else HERE.parent
print(f"옮긴 기계 {move(target)} 대")
@@ -91,7 +91,9 @@ def _load_names(path, key="이름"):
def load_masters():
인력_이름 = _load_names(MASTER / "인력.json")
기계_d = json.load(open(MASTER / "기계.json", encoding="utf-8"))
import master_machine_files as mmf # 기계 = 폴더(826-2)
기계_d = mmf.table(MASTER)[0]
기계_이름 = set()
for r in 기계_d["줄"]:
기계_이름.add(r["이름"])
@@ -16,7 +16,6 @@
from __future__ import annotations
import json
import re
import sys
from decimal import Decimal
@@ -33,6 +32,7 @@ import master_basis as mbs # noqa: E402
import master_combo as mcb # noqa: E402
import master_formula as mf # noqa: E402
import master_keys as mk # noqa: E402
import master_machine_files as mmf # noqa: E402
# (2) 본문은 따로 모듈 — 부르는 쪽은 `cm.check_body` · `cm.DIVISIONS` 그대로
from check_master_body import DIVISIONS, check_body # noqa: E402, F401
@@ -164,14 +164,10 @@ def _detail(name: str) -> str:
def load(names: list[str] | None = None, folder: Path = MASTER) -> dict[str, dict]:
"""첫 층 JSON 모두(밑줄 파일 빼고) — 수는 Decimal. `folder` = 마스터 폴더(M01 시험은 사본)."""
out = {}
for path in sorted(Path(folder).glob("*.json")):
if path.name.startswith("_") or (
names and path.name not in names and path.stem not in names
):
for name in mmf.names(folder): # 기계는 폴더(826-2) — 옛 한 파일 꼴 「기계.json」 으로 읽음
if name.startswith("_") or (names and name not in names and Path(name).stem not in names):
continue
out[path.name] = json.loads(
path.read_text(encoding="utf-8"), parse_float=Decimal, parse_int=Decimal
)
out[name] = mmf.read(folder, name)
return out
+9 -2
View File
@@ -11,6 +11,8 @@ import re
from decimal import Decimal
from pathlib import Path
import master_machine_files as mmf # 기계 = 폴더(826-2)
MASTER = Path(__file__).resolve().parent.parent
BOOK = MASTER / "_키대장.json"
KEY = re.compile(r"[A-Z]{2}\d{6}")
@@ -399,7 +401,8 @@ def adopt(folder: Path = MASTER) -> list[str]:
if group in MERGED:
file, slot, labels = MERGED[group]
label = labels[src]
whole = _read(folder / file) if (folder / file).is_file() else {"그룹": group}
there = (folder / file).is_file() or (file == mmf.NAME and mmf.here(folder))
whole = mmf.read(folder, file) if there else {"그룹": group}
heads = whole.setdefault(slot, {})
heads[label] = {k: v for k, v in data.items() if k not in ("그룹", "줄")}
stamped = [
@@ -418,7 +421,11 @@ def adopt(folder: Path = MASTER) -> list[str]:
new = {**head, **rest, _items(data): stamped}
old_text = path.read_text(encoding="utf-8")
text = keep_text(old_text, data, new) or _store().dump(new)
(folder / target).write_text(text, encoding="utf-8", newline="\n")
if target == mmf.NAME: # 기계 = 한 대 한 파일(826-2)
mmf.write(folder, {**{k: v for k, v in whole.items() if k != "줄"}, "줄": rows_all})
(folder / target).unlink(missing_ok=True)
else:
(folder / target).write_text(text, encoding="utf-8", newline="\n")
if target != path.name:
path.unlink()
done.append(target)
@@ -0,0 +1,142 @@
"""M01 기계 = 폴더 `기계/` — 한 대가 파일 하나(826-2).
`기계/<키>.json` = 줄 하나 · `기계/_머리.json` = 머리(그룹 · 원문 · 구분) · 줄 차례 = 키 차례
(새 기계는 늘 다음 키라 끝에 붙음 · 옛 한 파일 차례와 같음).
읽는 쪽은 옛 한 파일 꼴 그대로 — `table()` 이 「기계.json」 {그룹, 원문, 구분, 줄} 로 지어 줌
(색인 · 계산식 · 화면 · API 는 그 이름을 그대로 씀). 쓰는 쪽은 바뀐 대만 — `write()`.
폴더가 없고 옛 `기계.json` 파일이 있으면 그 파일을 읽음(시험이 만든 작은 마스터).
"""
from __future__ import annotations
import hashlib
import json
import os
from decimal import Decimal
from pathlib import Path
NAME = "기계.json" # 표 꼴 이름(메모리 · API)
DIR = "기계"
HEAD = "_머리.json"
_MEMO: dict[str, tuple] = {} # 파일 경로 → (자국, 바이트, 읽은 줄)
def here(folder: Path) -> bool:
return (Path(folder) / DIR / HEAD).is_file()
def names(folder: Path) -> list[str]:
"""첫 층 표 이름 — `*.json` 파일 + 기계 폴더(「기계.json」) · 이름 차례."""
out = {p.name for p in Path(folder).glob("*.json")}
return sorted(out | {NAME}) if here(folder) else sorted(out)
def _entries(folder: Path) -> list[os.DirEntry]:
with os.scandir(Path(folder) / DIR) as it:
return sorted(
(e for e in it if e.name.endswith(".json") and not e.name.startswith(".")),
key=lambda e: e.name,
)
def stamp(folder: Path) -> tuple:
"""폴더 자국 — 파일마다 (이름, 시각, 크기)(Windows 는 목록이 자국을 같이 줌)."""
return tuple((e.name, e.stat().st_mtime_ns, e.stat().st_size) for e in _entries(folder))
def _parse(raw: bytes):
return json.loads(raw, parse_float=Decimal, parse_int=Decimal)
def _read(entry: os.DirEntry) -> tuple[bytes, object]:
"""(바이트, 읽은 것) — 자국이 같으면 기억한 것(읽은 것은 **읽기 전용**으로만 나눔)."""
st = entry.stat()
sig = (st.st_mtime_ns, st.st_size)
got = _MEMO.get(entry.path)
if got is None or got[0] != sig:
raw = Path(entry.path).read_bytes()
got = _MEMO[entry.path] = (sig, raw, _parse(raw))
return got[1], got[2]
def _parts(folder: Path, shared: bool, parse=_parse) -> tuple[list[bytes], dict, list]:
raws, head, rows = [], {}, []
for entry in _entries(folder):
raw, data = _read(entry)
data = data if shared else parse(raw)
raws.append(entry.name.encode() + b"\0" + raw)
if entry.name == HEAD:
head = data
elif not entry.name.startswith("_"):
rows.append(data)
return raws, head, rows
def table(folder: Path, shared: bool = False, exact: bool = True) -> tuple[dict, str]:
"""(옛 한 파일 꼴, 판) — 판 = 머리 · 줄 파일 내용 지문. `shared` = 기억한 줄을 그대로 나눔
(읽기 전용 기억 `Store_Cache` 만) · 아니면 새로 읽은 사본(고쳐도 됨) · `exact` 아니면 수 = int · float."""
parse = _parse if exact else json.loads
if not here(folder):
raw = (Path(folder) / NAME).read_bytes()
return parse(raw), hashlib.sha256(raw).hexdigest()[:16]
raws, head, rows = _parts(folder, shared and exact, parse)
return {**head, "줄": rows}, hashlib.sha256(b"\n".join(raws)).hexdigest()[:16]
def raw(folder: Path, name: str) -> bytes:
"""첫 층 표의 내용 바이트(지문용) — 기계 폴더는 파일들을 이은 것."""
if name == NAME and here(folder):
return b"\n".join(_parts(folder, True)[0])
return (Path(folder) / name).read_bytes()
def read(folder: Path, name: str):
"""첫 층 표 하나(새로 읽은 사본 · 수 = Decimal)."""
if name == NAME and here(folder):
return table(folder)[0]
return _parse((Path(folder) / name).read_bytes())
def text(row) -> str:
"""줄 파일 글 — 들여쓰기 2칸 · 수는 읽은 글 그대로(M01 저장 글 모양 `Store_Shape.dump`)."""
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
from M01_MasterData.M01_MasterData_Store_Shape import dump
return dump(row, 0, width=0, expand=True)
def _put(path: Path, body: str) -> bool:
data = body.encode("utf-8")
if path.is_file() and path.read_bytes() == data:
return False
tmp = path.with_name(f".{path.name}.tmp")
tmp.write_bytes(data)
os.replace(tmp, path)
return True
def write(folder: Path, data: dict, old: dict | None = None) -> list[str]:
"""표 꼴 → 폴더 — 바뀐 대만 씀 · 빠진 대는 지움 · 머리가 바뀌면 머리도. `old` = 고치기 전 표
(없으면 폴더 전부와 견줌). 돌려줌 = 쓰거나 지운 파일 이름."""
base = Path(folder) / DIR
base.mkdir(exist_ok=True)
rows = {f"{r['키']}.json": r for r in data.get("줄") or []}
if len(rows) != len(data.get("줄") or []):
raise ValueError("기계 키 겹침 — 한 대 한 파일")
head = {k: v for k, v in data.items() if k != "줄"}
was = None if old is None else {f"{r['키']}.json": r for r in old.get("줄") or []}
done = []
if (old is None or {k: v for k, v in old.items() if k != "줄"} != head) and _put(
base / HEAD, text(head)
):
done.append(HEAD)
for name, row in rows.items():
if (was is None or was.get(name) != row) and _put(base / name, text(row)):
done.append(name)
gone = set(was) - set(rows) if was is not None else {e.name for e in _entries(folder)}
for name in sorted(gone - set(rows) - {HEAD}):
(base / name).unlink(missing_ok=True)
done.append(name)
return done