"""여러 항목 · 폴더를 층 사이 붙여넣기(PLAN 30-2) — 구조물 라이브러리 나무의 [복사] → [붙여넣기]. 붙일 폴더(`dest`) 아래 새 항목 — 대상 층 새 키(SC · SP) · 열 `본뜬` = 원본(층 · 주인 · 키) · 같은 이름은 「이름 (2)」 · 폴더는 안의 항목 · 빈 폴더까지(폴더 이름이 겹치면 「폴더 (2)」). 파일(도면 · 면적 · 재료 · 입력 연결 · 자재 조합)은 `Items.copy_item` 이 옮김. 권한은 부르는 쪽 몫. """ from __future__ import annotations from pathlib import Path from typing import Any from M02_MasterTemplete import M02_MasterTemplete_Items as items_store from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree def _under(path: list[str], folder: list[str]) -> bool: return path[: len(folder)] == folder def outer(targets: list[dict[str, Any]]) -> list[dict[str, Any]]: """고른 것 가운데 다른 고른 폴더 안에 든 것은 뺌(폴더가 안의 것까지 다룸) — 옮기기 · 지우기 · 붙여넣기.""" folders = [list(t.get("경로") or []) for t in targets if not t.get("키")] def inside(t: dict[str, Any]) -> bool: path = list(t.get("경로") or []) return any(_under(path, f) and (t.get("키") or path != f) for f in folders if f) return [t for t in targets if not inside(t)] def _table(folder: Path) -> dict[str, Any]: with store.at(folder): return tree.normalize_table(store.read("table", store.STRUCTURE_TABLE)["문서"]) def _items(doc: dict[str, Any]) -> list[dict[str, Any]]: return [c for c in doc.get("열") or [] if tree.is_item(c)] def _unique(path: list[str], taken: list[list[str]]) -> list[str]: n, out = 2, path while out in taken: out, n = [*path[:-1], f"{path[-1]} ({n})"], n + 1 return out def paste( source: Path, target: Path, targets: list[dict[str, Any]], dest: list[str], origin: dict[str, Any], ) -> dict[str, Any]: """`targets` = `[{경로, 키?}]`(키 = 항목 · 없으면 폴더 경로) → `target` 층 `dest` 아래 · 돌려줌 알림 `{op: "paste", 경로, 열: [새 열 id]}`. `origin` = `{layer, owner}`(본뜬).""" src = _table(source) with store.at(source): keys = {str(r.get("열")): str(r.get("키")) for r in store.read_combos().get("줄", [])} src_items = [(tree.path_of(c), keys.get(c["id"])) for c in _items(src)] src_folders = tree.folders_of(src) items_store.ensure(target) dst = _table(target) taken = [tree.path_of(c) for c in _items(dst)] taken_folders = tree.folders_of(dst) plan: list[tuple[str, list[str]]] = [] # (원본 키, 새 경로) new_folders: list[list[str]] = [] def busy(path: list[str]) -> bool: paths = [*taken, *(p for _, p in plan)] return path in [*taken_folders, *new_folders] or any(_under(p, path) for p in paths) for one in outer(targets): if one.get("키"): key = str(one["키"]) found = next((p for p, k in src_items if k == key), None) if found is None: raise store.StoreError(404, f"없는 항목 「{key}」") plan.append((key, _unique([*dest, found[-1]], [*taken, *(p for _, p in plan)]))) continue folder = list(one.get("경로") or []) if not folder or ( folder not in src_folders and not any(_under(p, folder) for p, _ in src_items) ): raise store.StoreError(404, f"없는 폴더 「{' '.join(folder)}」") top, n = [*dest, folder[-1]], 2 while busy(top): top, n = [*dest, f"{folder[-1]} ({n})"], n + 1 new_folders.append(top) new_folders += [[*top, *f[len(folder) :]] for f in src_folders if _under(f, folder)] plan += [(k, [*top, *p[len(folder) :]]) for p, k in src_items if _under(p, folder) and k] made = [ items_store.copy_item(source, target, key, {**origin, "key": key}, path)["열"] for key, path in plan ] if new_folders: # 빈 폴더도 남게 — 집계표 `폴더` with store.at(target): got = store.read("table", store.STRUCTURE_TABLE) doc = got["문서"] doc["폴더"] = [*(doc.get("폴더") or []), *new_folders] store.write("table", store.STRUCTURE_TABLE, got["판"], doc) return {"op": "paste", "경로": dest, "열": made}