"""M02 일위대가 사전조합 나무 = 자유 폴더(PLAN 158-1) — 구조물 조합 나무와 같은 꼴(깊이 자유). 자리 = 층 뿌리 `precombo_폴더.json` `{폴더: [경로 …], 자리: {키: 경로}}` — 층(마스터 · 회사 · 개인)마다 따로. 문서 파일(`{층}/precombo/SC-######.json`)은 안 옮김 · 폴더 자리만 적음 · 자리 없는 문서 = 뿌리. 고침(폴더 더하기 · 이름 바꾸기 · 옮기기 · 지우기 · 문서 옮기기)은 판이 다르면 409 · 지우기는 빈 폴더만(안에 문서가 있으면 409). 이름 · 경로 검사 · 같은 이름 「(1)」 은 `M02_MasterTemplete_Tree` 한 벌. """ from __future__ import annotations import json from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree FILE = "precombo_폴더.json" OPS = ("add-folder", "rename", "move", "delete") def _keys() -> set[str]: """지금 층에 있는 사전조합 키(파일 이름).""" return {p.stem for p in (store.root() / "precombo").glob("[!._]*.json")} def _read() -> tuple[dict[str, Any], str]: path = store.root() / FILE if not path.is_file(): return {"폴더": [], "자리": {}}, "" raw = path.read_bytes() return json.loads(raw), store.version_of(raw) def _folders(doc: dict[str, Any], keys: set[str]) -> list[list[str]]: """폴더 전부 — 적어 둔 폴더 + 있는 문서 자리의 앞마디(빈 경로 뺌 · 겹침 뺌).""" out: list[list[str]] = [] paths = [tree._clean(p) for p in doc.get("폴더") or []] paths += [tree._clean(p) for k, p in (doc.get("자리") or {}).items() if k in keys] for path in paths: for n in range(1, len(path) + 1): if path[:n] not in out: out.append(path[:n]) return out def view_of(doc: dict[str, Any], 판: str, keys: set[str]) -> dict[str, Any]: places = {k: tree._clean(p) for k, p in (doc.get("자리") or {}).items() if k in keys} return {"판": 판, "폴더": _folders(doc, keys), "자리": {k: p for k, p in places.items() if p}} def view() -> dict[str, Any]: doc, 판 = _read() return view_of(doc, 판, _keys()) def _shift(path: list[str], old: list[str], new: list[str]) -> list[str]: return [*new, *path[len(old) :]] if tree._under(path, old) else path def apply( doc: dict[str, Any], body: dict[str, Any], keys: set[str] ) -> tuple[dict[str, Any], dict[str, Any]]: """고침 하나 → (새 자리 문서, 알림) — 순수 함수(저장은 `edit`).""" op = body.get("op") if op not in OPS: raise tree.TreeError(422, f"모르는 고침 「{op}」 — {' · '.join(OPS)}") folders = _folders(doc, keys) places = {k: tree._clean(p) for k, p in (doc.get("자리") or {}).items() if k in keys} where = tree._path(body.get("경로")) key = str(body.get("키") or "") note: dict[str, Any] = {"op": op} def need(path: list[str], what: str) -> None: if path and path not in folders: raise tree.TreeError(404, f"없는 폴더 「{' / '.join(path)}」") if not path and what: raise tree.TreeError(422, f"{what} 폴더 「경로」 가 없음") if op == "add-folder": need(where, "") made = [*where, tree._name(body.get("이름"))] if made in folders: raise tree.TreeError(409, f"이미 있는 폴더 「{' / '.join(made)}」") folders.append(made) note["경로"] = made elif op == "move" and key: if key not in keys: raise tree.TreeError(404, f"없는 사전조합 「{key}」") to = tree._path(body.get("새경로"), "새경로") need(to, "") places[key] = to note["경로"] = to elif op in ("rename", "move"): need(where, "바꿀" if op == "rename" else "옮길") if op == "rename": new = [*where[:-1], tree._name(body.get("이름"))] if new != where and new in folders: raise tree.TreeError(409, f"이미 있는 폴더 「{' / '.join(new)}」") else: to = tree._path(body.get("새경로"), "새경로") need(to, "") if tree._under(to, where): raise tree.TreeError(422, "폴더를 제 안으로 옮길 수 없음") new = tree.unique_path([*to, where[-1]], lambda p: p != where and p in folders) if new[-1] != where[-1]: note["바뀐이름"] = [new[-1]] folders = [_shift(f, where, new) for f in folders] places = {k: _shift(p, where, new) for k, p in places.items()} note["경로"] = new else: # delete — 빈 폴더만 need(where, "지울") inside = sorted(k for k, p in places.items() if tree._under(p, where)) if inside: raise tree.TreeError(409, f"폴더 안에 사전조합 {len(inside)}개 — 먼저 옮기거나 지움") folders = [f for f in folders if not tree._under(f, where)] note["경로"] = where out = {"폴더": folders, "자리": {k: p for k, p in sorted(places.items()) if p}} return out, note # ponytail: 지운 문서 자리는 다음 고침 때 걷힘 — 그 사이 같은 키로 새로 만들면 옛 자리에 보임 # (키 재사용을 막을 때 함께 고침) def edit(body: dict[str, Any]) -> dict[str, Any]: """지금 층 자리 파일을 고쳐 씀 → 새 나무 + 알림 · 판 다르면 409.""" doc, 판 = _read() if 판 != str(body.get("판") or ""): raise store.StoreError(409, {"stale": [FILE], "판": 판}) keys = _keys() try: out, note = apply(doc, body, keys) except tree.TreeError as e: raise store.StoreError(e.status, e.detail) from e atomic_write_json(store.root() / FILE, out) return {**view(), "알림": note}