feat(M02): 집계 항목 폴더 구조 서버 — 경로 · 일련번호 · 나무 고침 길(PLAN 26-2a)

- 항목 경로 = 폴더 이름 배열(깊이 자유) · 집계표 열 머리(빈 칸 건너뜀)가 곧 경로 · 열에 경로를 같이 적음 · 머리 줄 수(층)는 가장 깊은 경로에 맞춤(넷 밑으로는 안 줄임) · 빈 폴더는 집계표 폴더 칸(M02_MasterTemplete_Tree)
- 표 저장과 나무 고침 한 줄기 — 나무 고침은 집계표 문서를 고쳐 같은 집계표 저장(store.write)을 탐(번호 · SS 장부 · 세 파일 · 입력 연결 파일 지우기가 함께)
- 나무 고침 길 POST /api/m02/layers/{layer}/tree(add-folder · add-item · rename · move · delete · 판 409 · 422 · 404 · 층 권한) · 읽기 GET 같은 길 {판, 폴더, items}
- 번호 = 새 항목은 자리 뜻 없는 일련번호(있던 번호 · 지운 번호 가장 큰 수 + 1) · 있던 번호 그대로
- SS 장부 = M02_MasterTemplete_Book 로 떼어 냄(Store 543 → 478) · 줄에 경로 · 이름 · 옛 종류 · 분류1~3 = 경로 앞 넷 · 경로 없는 옛 줄은 읽을 때 채움 · 옮기는 함수 migrate_paths
- 목록 길 /api/m02/items 줄에 경로 · 이름(경로 이음) · 자재 조합 · 갈라 내기 이름도 경로 이음
- 마스터 집계표 · 장부를 경로 모양으로 옮김(51 항목 · 키 · 번호 그대로 · 빈 글 머리는 null)
- 시험 test_m02_tree 5(옛 51 항목 · 옮기기 두 번 같음 · 깊이 1 · 2 · 5 · 이름 · 옮기기 · 지우기 · 마지막 항목 · 409 · 403 · 표 저장 한 줄기) · 번호 시험을 일련번호 규칙으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
This commit is contained in:
2026-09-30 07:50:20 +09:00
co-authored by Claude Opus 5.5
parent bf21676ef5
commit b5114ec17f
13 changed files with 1395 additions and 293 deletions
@@ -24,6 +24,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
from M02_MasterTemplete import M02_MasterTemplete_Matset as matset
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MasterTemplete_Tree as tree
from M02_MasterTemplete import M02_Template_Layers as layers
from M02_MasterTemplete.M02_MasterTemplete_Router import (
BranchBody,
@@ -44,6 +45,18 @@ class InputLinksBody(BaseModel):
줄: list[Any] = []
class TreeBody(BaseModel):
"""26-1 나무 고침 — `{op, 경로, 이름?, 새경로?, 키?, 판}` (+ 항목 더하기 때 `단위?`)."""
op: str
경로: list[str] | None = None
이름: str | None = None
새경로: list[str] | None = None
키: str | None = None
단위: str | None = None
판: str
class CopyItemBody(BaseModel):
from_layer: Literal["system", "company"]
from_owner: str | None = None
@@ -195,6 +208,50 @@ def get_item_pills(
return _call(_root(session, layer, owner), inputs_store.pills, key, place)
# ── 폴더 구조(PLAN 26-1 · 26-2a) — 읽기 = 층 읽기 권한 · 고침 = 층 쓰기 권한 ──
def _tree_view(folder: Path) -> dict[str, Any]:
"""나무 — `{판(집계표), 폴더: [경로 …], items: [항목 …]}` · 폴더 = 빈 폴더 + 항목 앞마디."""
with store.at(folder):
got = store.read("table", store.STRUCTURE_TABLE)
doc = tree.normalize_table(got["문서"]) # 옛 파일도 경로로 읽음
return {"판": got["판"], "폴더": tree.folders_of(doc), "items": items_store.items(folder)}
@router.get("/layers/{layer}/tree")
def get_tree(layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)):
folder = _root(session, layer, owner)
return _call(folder, _tree_view, folder)
def _edit_tree(folder: Path, body: dict[str, Any]) -> dict[str, Any]:
"""집계표를 고쳐 집계표 저장 한 줄기로 — 번호 · SS 장부 · 지운 항목의 세 파일 · 입력 연결."""
got = store.read("table", store.STRUCTURE_TABLE)
if got["판"] != body.get("판"):
raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": got["판"]})
ids = {str(r.get("키")): str(r.get("열")) for r in store.read_combos().get("줄", [])}
try:
doc, note = tree.apply(got["문서"], body, ids)
except tree.TreeError as e:
raise store.StoreError(e.status, e.detail) from e
store.write("table", store.STRUCTURE_TABLE, got["판"], doc)
return {**_tree_view(folder), "알림": note}
@router.post("/layers/{layer}/tree")
def post_tree(
layer: Layer,
body: TreeBody,
owner: str | None = None,
session: Session = Depends(verify_session),
):
"""폴더 더하기 · 항목 더하기 · 이름 바꾸기 · 옮기기 · 지우기 → 새 나무 · 판 다르면 409.
쓰이는 항목을 지워도 프로젝트엔 영향 없음(프로젝트는 복사본을 씀 · 알림의 `지운항목`)."""
folder = _root(session, layer, owner, write=True)
return _call(folder, _edit_tree, folder, body.model_dump())
# ── 층 길 — 옛 시스템 길과 같은 모양 ─────────────────────