refactor(M02): 일위대가 조합 나무 서버 길 지움 — /tree · /tree-many · /paste(39-2)
- Combo_Store 나무 고침 함수(_apply · edit_tree(_many) · commit · _write_master_folders · _set_path) 지움 · 목록 폴더는 읽기만 - Combo_Copy.py(층 사이 붙여넣기) 지움 · Labor_Api 나무 · 붙여넣기 함수 지움 - 옛 나무 · 붙여넣기 시험을 「길 없음」 검사로 갈음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -7,13 +7,12 @@
|
||||
개인 `storage/{회사}/{사용자}/templates/일위대가조합.json` · 키 `UP{사용자}-######`
|
||||
회사 · 개인 파일 = 마스터와 같은 머리 · 열 한 벌 + 머리 `다음`(키 장부 · 지운 키는 다시 안 씀).
|
||||
지금 층 = `store.at(층 폴더)`(`store.root()`) — 키 머리도 그 자리에서 셈 · 권한은 라우터 몫.
|
||||
나무 = 폴더 [구분] · [구분, 상세구분] 깊이 2 · 머리 `폴더` = 폴더 전부(빈 폴더 포함).
|
||||
목록 `폴더` = [구분] · [구분, 상세구분](읽기만 · 나무 고침 · 붙여넣기 길은 39-2 에서 지움).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
@@ -127,18 +126,13 @@ def _find(doc: dict, key: str) -> dict[str, Any]:
|
||||
return row
|
||||
|
||||
|
||||
# ── 나무 폴더 ─────────────────────────────────────────────────────────
|
||||
# ── 폴더(읽기만) ───────────────────────────────────────────────────────
|
||||
def path_of(row: dict) -> list[str]:
|
||||
"""조합의 폴더 — [구분] · [구분, 상세구분] · 구분이 없으면 뿌리."""
|
||||
a, b = (str(row.get(k) or "").strip() for k in ("구분", "상세구분"))
|
||||
return [a, b] if a and b else [a] if a else []
|
||||
|
||||
|
||||
def _set_path(row: dict, path: list[str]) -> None:
|
||||
row["구분"] = path[0] if path else None
|
||||
row["상세구분"] = path[1] if len(path) > 1 else None
|
||||
|
||||
|
||||
def folders_of(doc: dict) -> list[list[str]]:
|
||||
"""폴더 전부 — 적어 둔 것 + 조합 자리 · 부모까지."""
|
||||
out: list[list[str]] = []
|
||||
@@ -312,128 +306,3 @@ def delete(key: str, version: str) -> dict[str, Any]:
|
||||
_find(doc, key)
|
||||
_refuse_used([key])
|
||||
return {"판": _save({**doc, "줄": [r for r in doc["줄"] if r.get("키") != key]}, version)}
|
||||
|
||||
|
||||
# ── 나무 고침 ─────────────────────────────────────────────────────────
|
||||
def _moved(path: list[str], old: list[str], new: list[str]) -> list[str]:
|
||||
return new + path[len(old) :] if path[: len(old)] == old else path
|
||||
|
||||
|
||||
def _folder(folders: list[list[str]], path: list[str] | None, what: str) -> list[str]:
|
||||
path = [str(x) for x in path or []]
|
||||
if not path or path not in folders:
|
||||
raise store.StoreError(404, f"{what} 폴더 「{' › '.join(path)}」 없음")
|
||||
return path
|
||||
|
||||
|
||||
def _fresh(name: Any) -> str:
|
||||
text = str(name or "").strip()
|
||||
if not text or "›" in text or len(text) > mcb.MAX_이름:
|
||||
raise store.StoreError(422, "폴더 이름 — 빈 이름 · 「›」 · 너무 긴 이름은 못 씀")
|
||||
return text
|
||||
|
||||
|
||||
def _apply(doc: dict, body: dict) -> tuple[dict, list[str]]:
|
||||
"""나무 고침 한 번 → (새 표, 지운 조합 키) · 조합 더하기 · 담은 로직은 `create` · `edit` 몫."""
|
||||
op, key = body.get("op"), body.get("키")
|
||||
folders, rows = folders_of(doc), [dict(r) for r in doc.get("줄") or []]
|
||||
gone: list[str] = []
|
||||
target = [str(x) for x in body.get("새경로") or []]
|
||||
if op in ("move", "add-folder") and target and target not in folders:
|
||||
raise store.StoreError(404, f"없는 폴더 「{' › '.join(target)}」")
|
||||
if op == "add-folder":
|
||||
made = [*[str(x) for x in body.get("경로") or []], _fresh(body.get("이름"))]
|
||||
if len(made) > DEPTH or (made[:-1] and made[:-1] not in folders):
|
||||
raise store.StoreError(422, f"폴더는 {DEPTH} 단까지 · 있는 폴더 아래에만")
|
||||
if made in folders:
|
||||
raise store.StoreError(409, f"이미 있는 폴더 「{' › '.join(made)}」")
|
||||
folders.append(made)
|
||||
elif op == "rename" and key:
|
||||
_find({"줄": rows}, str(key))["이름"] = _fresh(body.get("이름"))
|
||||
elif op == "move" and key:
|
||||
_set_path(_find({"줄": rows}, str(key)), target)
|
||||
elif op == "delete" and key:
|
||||
_find({"줄": rows}, str(key))
|
||||
gone = [str(key)]
|
||||
elif op in ("rename", "move", "delete"):
|
||||
old = _folder(folders, body.get("경로"), "고칠")
|
||||
if op == "delete":
|
||||
gone = [str(r["키"]) for r in rows if path_of(r)[: len(old)] == old]
|
||||
folders = [f for f in folders if f[: len(old)] != old]
|
||||
else:
|
||||
new = [*old[:-1], _fresh(body.get("이름"))] if op == "rename" else [*target, old[-1]]
|
||||
deepest = max(len(f) for f in folders if f[: len(old)] == old)
|
||||
if new[: len(old)] == old or deepest - len(old) + len(new) > DEPTH:
|
||||
raise store.StoreError(422, f"폴더는 {DEPTH} 단까지 · 제 안으로 못 옮김")
|
||||
if new in folders:
|
||||
raise store.StoreError(409, f"이미 있는 폴더 「{' › '.join(new)}」")
|
||||
folders = [_moved(f, old, new) for f in folders]
|
||||
for r in rows:
|
||||
if path_of(r)[: len(old)] == old:
|
||||
_set_path(r, _moved(path_of(r), old, new))
|
||||
else:
|
||||
raise store.StoreError(
|
||||
422, f"모르는 나무 고침 「{op}」 — add-folder · rename · move · delete"
|
||||
)
|
||||
rows = [r for r in rows if str(r.get("키")) not in gone]
|
||||
return {**doc, "폴더": folders, "줄": rows}, gone
|
||||
|
||||
|
||||
def edit_tree(body: dict) -> dict[str, Any]:
|
||||
"""폴더 더하기 · 이름 바꾸기(폴더 · 조합) · 옮기기(폴더 · 조합) · 지우기(폴더 = 안 조합까지 ·
|
||||
조합) → 새 목록 · 쓰이는 조합이 들면 409 · 판 다르면 409 · 조합 이름 겹침 422."""
|
||||
return edit_tree_many({"판": body.get("판"), "ops": [body]})
|
||||
|
||||
|
||||
def edit_tree_many(body: dict) -> dict[str, Any]:
|
||||
"""나무 고침 여럿을 한 번에(PLAN 30-3 · 여러 개 옮기기 · 지우기) — `{판, ops: [고침 …]}` ·
|
||||
차례대로 적용 · 하나라도 막히면 아무것도 안 씀 · 저장 한 번."""
|
||||
doc, have = _load()
|
||||
if have != (body.get("판") or ""):
|
||||
raise store.StoreError(409, {"stale": [FILE], "판": have})
|
||||
ops = body.get("ops")
|
||||
if not isinstance(ops, list) or not ops:
|
||||
raise store.StoreError(422, "나무 고침 목록(ops)이 비었음")
|
||||
new, gone = doc, []
|
||||
for op in ops:
|
||||
new, got = _apply(new, op if isinstance(op, dict) else {})
|
||||
gone += got
|
||||
_refuse_used(gone)
|
||||
renamed = [str(op["키"]) for op in ops if op.get("op") == "rename" and op.get("키")]
|
||||
commit(doc, have, new, gone, renamed)
|
||||
return {**listing(), "지운조합": gone}
|
||||
|
||||
|
||||
def commit(doc: dict, have: str, new: dict, gone: list[str], checked: list[str] = ()) -> None:
|
||||
"""고친 표 한 벌 저장 — 회사 · 개인 = 파일 한 번(`checked` 조합은 이름 검사) ·
|
||||
마스터 = 줄은 M01 저장 길 · 머리 `폴더` 따로."""
|
||||
if heads()[-1] != MASTER:
|
||||
for key in checked:
|
||||
if key in {str(r.get("키")) for r in new["줄"]}:
|
||||
if errors := _name_errors(new, _find(new, key)):
|
||||
raise store.StoreError(422, {"errors": errors})
|
||||
_save(new, have)
|
||||
return
|
||||
was = {str(r["키"]): r for r in doc["줄"]}
|
||||
changes = [{"op": "delete", "key": k} for k in gone] + [
|
||||
{"op": "edit", "key": str(r["키"]), "row": r} for r in new["줄"] if was[str(r["키"])] != r
|
||||
]
|
||||
version = _m01(combo_store._write, changes, have)["version"] if changes else have
|
||||
_write_master_folders(new["폴더"], version)
|
||||
|
||||
|
||||
def _write_master_folders(folders: list[list[str]], version: str) -> None:
|
||||
"""마스터 조합 파일 머리 `폴더` — 줄은 안 건드림(줄 고침은 M01 저장 길)."""
|
||||
path = Path(m01.FOLDER) / FILE
|
||||
with m01._LOCK:
|
||||
doc, have = m01.read(FILE)
|
||||
if have != version:
|
||||
raise store.StoreError(409, {"stale": [FILE], "판": have})
|
||||
if doc.get("폴더") == folders:
|
||||
return
|
||||
head = {k: v for k, v in doc.items() if k != "줄"}
|
||||
text = m01.dump({**head, "폴더": folders, "줄": doc.get("줄") or []})
|
||||
temporary = path.with_name(f".{FILE}.tmp")
|
||||
temporary.write_text(text, encoding="utf-8", newline="\n")
|
||||
os.replace(temporary, path)
|
||||
m01.cache.stale([FILE])
|
||||
|
||||
Reference in New Issue
Block a user