feat(M02): 공용 나무 여러 개 고르기 · 복사 · 붙여넣기 · 여러 개 옮기기 · 지우기 + 층 사이 붙여넣기 서버 · 「+」 오른쪽 맞춤 (PLAN 30-2)

- 나무 부품 — Ctrl · Shift 클릭 고름(TreePanel_Pick) · 고른 줄 배경 · 층 단추 줄 「N개 고름」 · 오른쪽 클릭 메뉴가 고른 것 전부(복사 · 옮기기 · 지우기) · Ctrl+C · Ctrl+V · Delete · 복사는 읽기 층에서도 · 붙여넣기는 쓸 수 있는 층만 · 콜백 api.many · api.paste · clipKind(일위대가 쪽이 붙일 자리)
- 서버 — POST /layers/{층}/items/paste(여러 항목 · 폴더 · 새 SC/SP 키 · 본뜬 = 원본 · 같은 이름 「(2)」 · 세 파일 · 입력 연결 · 자재 조합 · 빈 폴더까지 · M02_Layer_Access 쓰기 층만) · 나무 고침 대상 여러 개(옮기기 · 지우기 한 번 저장 · 막히면 안 씀)
- 층 단추 줄 「N개 고름」 · 「+」 오른쪽 끝
- 시험 test_m02_tree_paste 새로 · 고르기 규칙 Node 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:05:01 +09:00
co-authored by Claude Opus 5.5
parent 7ea8b75162
commit 0f510eb53b
11 changed files with 630 additions and 28 deletions
@@ -17,6 +17,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from common_util.common_util_auth import verify_session
from M02_MasterTemplete import M02_Item_Paste as paste_store
from M02_MasterTemplete import M02_Layer_Access as access
from M02_MasterTemplete import M02_MasterTemplete_Branch as branch
from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store
@@ -53,6 +54,17 @@ class TreeBody(BaseModel):
키: str | None = None
단위: str | None = None
판: str
# 여러 개 옮기기 · 지우기(PLAN 30-2) — `[{경로, 키?}]` · 있으면 이것마다 차례로 · 저장 한 번
대상: list[dict[str, Any]] | None = None
class PasteBody(BaseModel):
"""30-2 층 사이 붙여넣기 — 복사한 층(`from_layer` · `from_owner`) `대상` → 이 층 `경로` 아래."""
from_layer: Layer
from_owner: str | None = None
대상: list[dict[str, Any]]
경로: list[str] = []
class CopyItemBody(BaseModel):
@@ -192,7 +204,17 @@ def _edit_tree(folder: Path, body: dict[str, Any]) -> dict[str, Any]:
for key in [r.get("키"), *(r.get("옛키") or [])]:
ids[str(key)] = str(r.get("열"))
try:
doc, note = tree.apply(got["문서"], body, ids)
targets = body.get("대상")
if targets is None:
doc, note = tree.apply(got["문서"], body, ids)
else: # 여러 개 — 차례로 고쳐 저장은 한 번(하나라도 막히면 아무것도 안 씀)
if body.get("op") not in ("move", "delete") or not targets:
raise tree.TreeError(422, "여러 개는 옮기기 · 지우기만 — 「대상」 이 필요함")
doc, note = got["문서"], {"op": body["op"], "지운항목": []}
for one in paste_store.outer(targets):
step = {**body, "경로": one.get("경로"), "키": one.get("키")}
doc, done = tree.apply(doc, step, ids)
note["지운항목"] += done.get("지운항목", [])
except tree.TreeError as e:
raise store.StoreError(e.status, e.detail) from e
store.write("table", store.STRUCTURE_TABLE, got["판"], doc)
@@ -212,6 +234,27 @@ def post_tree(
return _call(folder, _edit_tree, folder, body.model_dump())
@router.post("/layers/{layer}/items/paste")
def post_paste(
layer: Layer,
body: PasteBody,
owner: str | None = None,
session: Session = Depends(verify_session),
):
"""여러 항목 · 폴더를 층 사이 붙여넣기(PLAN 30-2) — 복사한 층은 읽기 권한 · 이 층은 쓰기 권한 ·
새 키(SC · SP) · 본뜬 = 원본 · 같은 이름 「이름 (2)」 · 폴더는 안의 것까지 → 새 나무 + 알림."""
target = _root(session, layer, owner, write=True)
source = _root(session, body.from_layer, body.from_owner)
who = access.grant(session, body.from_layer, body.from_owner).owner
try:
note = paste_store.paste(
source, target, body.대상, body.경로, {"layer": body.from_layer, "owner": who}
)
except store.StoreError as e:
raise HTTPException(status_code=e.status, detail=e.detail) from e
return {**_call(target, _tree_view, target), "알림": note}
# ── 층 길 — 옛 시스템 길과 같은 모양 ─────────────────────