- 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
99 lines
4.9 KiB
Python
99 lines
4.9 KiB
Python
"""일위대가 조합 층 사이 복사(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}
|