Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Combo_Copy.py
T
eomsangdonandClaude Opus 5.5 c0a76b844d feat(M02): 일위대가 조합 층 사이 복사 · 여러 개 옮기기 · 지우기 서버(PLAN 30-3)
- POST /api/m02/layers/{layer}/combos/paste — 원본 층(읽기) 조합 · 폴더 → 이 층(쓰기) 경로 폴더에 새 키(UA · UC · UP) · 본뜬키 = 원본(마스터는 UA 만) · 같은 이름 「이름 (2)」 · 폴더는 안 조합 · 빈 폴더까지 · 깊이 2 넘으면 422
- POST /api/m02/layers/{layer}/combos/tree-many — 나무 고침 여럿 차례대로 · 저장 한 번 · 하나라도 막히면 안 씀
- 새 M02_MasterTemplete_Combo_Copy · Combo_Store edit_tree 를 edit_tree_many · commit 한 길로
- Labor_Api 에 pasteCombos · editComboTreeMany
- 시험 test_m02_combo_layers 네 건 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
2026-09-30 18:53:57 +09:00

99 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""일위대가 조합 층 사이 복사(PLAN 30-3) — 읽을 수 있는 층의 조합 · 폴더를 쓸 수 있는 층에 붙여넣기.
정함(30장): 복사본 = 대상 층의 새 키(마스터 UA · 회사 UC · 개인 UP) · 본뜬키 = 원본 키 ·
같은 이름이면 「이름 (2)」 · 담은 로직 그대로 · 폴더 복사는 안의 조합 · 빈 폴더까지.
붙는 자리 = `경로`(대상 폴더 · 깊이 2 안) — 고른 조합은 그 폴더에 · 고른 폴더는 그 아래 한 단으로.
마스터에 붙이면 M01 저장 길(대장 UA) · 본뜬키는 UA 만(마스터 틀) — 다른 층 원본이면 비움.
권한은 라우터 몫(원본 층 읽기 · 대상 층 쓰기) · 대상 층 = `store.at(대상)`.
"""
from __future__ import annotations
from typing import Any
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
from M02_MasterTemplete import M02_MasterTemplete_Store as store
def _free_name(name: str, taken: set[str]) -> str:
out, n = name, 2
while out in taken:
out, n = f"{name} ({n})", n + 1
taken.add(out)
return out
def _paths(value: Any, what: str) -> list[list[str]]:
if not isinstance(value, list) or not all(isinstance(p, list) for p in value):
raise store.StoreError(422, f"「{what}」 이 폴더 경로 목록이 아님")
return [[str(x) for x in p] for p in value]
def picked(source: dict, keys: list[str], folders: list[list[str]], where: list[str]):
"""(붙일 조합 [(원본 줄, 새 경로)], 새 빈 폴더) — 깊이 2 넘으면 422 · 없는 키 · 폴더 404."""
have = combos.folders_of(source)
rows = {str(r.get("키")): r for r in source.get("줄") or []}
out: dict[str, tuple[dict, list[str]]] = {}
made: list[list[str]] = []
for folder in folders:
if folder not in have:
raise store.StoreError(404, f"없는 폴더 「{' › '.join(folder)}」")
cut = len(folder) - 1 # 고른 폴더부터 대상 아래로
made += [where + f[cut:] for f in have if f[: len(folder)] == folder]
for key, row in rows.items():
path = combos.path_of(row)
if path[: len(folder)] == folder:
out[key] = (row, where + path[cut:])
for key in keys:
if key not in rows:
raise store.StoreError(404, f"없는 조합 「{key}」")
out.setdefault(key, (rows[key], list(where)))
if any(len(p) > combos.DEPTH for p in [*made, *(p for _, p in out.values())]):
raise store.StoreError(422, f"폴더는 {combos.DEPTH} 단까지 — 더 얕은 폴더에 붙여 넣음")
return list(out.values()), made
def paste(source: dict, body: dict) -> dict[str, Any]:
"""`source` = 원본 층 표(부르는 쪽이 원본 층에서 읽음) · `body` = `{키?, 폴더?, 경로?, 판}` →
`{…대상 층 목록, 새키: [원본 → 새 키]}`."""
keys = [str(k) for k in body.get("키") or []]
folders = _paths(body.get("폴더") or [], "폴더")
where = [str(x) for x in body.get("경로") or []]
if not keys and not folders:
raise store.StoreError(422, "붙여 넣을 조합 · 폴더가 없음")
doc, have = combos._load()
if have != (body.get("판") or ""):
raise store.StoreError(409, {"stale": [combos.FILE], "판": have})
if where and where not in combos.folders_of(doc):
raise store.StoreError(404, f"없는 폴더 「{' › '.join(where)}」")
items, made = picked(source, keys, folders, where)
taken = {str(r.get("이름") or "").strip() for r in doc["줄"]}
master = combos.heads()[-1] == combos.MASTER
rows = []
for row, path in items:
one = {k: row.get(k) for k in combos.mcb.COLS if k != "키"}
one["이름"] = _free_name(str(row.get("이름") or "").strip(), taken)
one["본뜬키"] = str(row.get("키"))
if master and not one["본뜬키"].startswith(combos.MASTER):
one["본뜬키"] = None # 마스터 틀은 UA 계보만
combos._set_path(one, path)
rows.append(one)
folders_after = combos.folders_of({**doc, "폴더": [*combos.folders_of(doc), *made]})
if master:
new = [combos.create(one)["키"] for one in rows] # M01 저장 길(검사 · 대장) — 하나씩
_, version = combos._load()
combos._write_master_folders(folders_after, version)
else:
number = int(doc.get("다음") or 1)
fixed = []
for at, one in enumerate(rows):
fixed.append(combos._clean(one, f"{combos.heads()[-1]}{number + at:06d}"))
new_doc = {**doc, "다음": number + len(fixed), "폴더": folders_after}
new_doc["줄"] = [*doc["줄"], *fixed]
for one in fixed:
combos._check(new_doc, one)
combos._save(new_doc, have)
new = [one["키"] for one in fixed]
pairs = [{"원본": str(row.get("키")), "새키": key} for (row, _), key in zip(items, new)]
return {**combos.listing(), "새키": pairs}