refactor(M02): 일위대가 조합 나무 서버 길 지움 — /tree · /tree-many · /paste(39-2)
- Combo_Store 나무 고침 함수(_apply · edit_tree(_many) · commit · _write_master_folders · _set_path) 지움 · 목록 폴더는 읽기만 - Combo_Copy.py(층 사이 붙여넣기) 지움 · Labor_Api 나무 · 붙여넣기 함수 지움 - 옛 나무 · 붙여넣기 시험을 「길 없음」 검사로 갈음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
"""일위대가 조합 층 사이 복사(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}
|
||||
@@ -7,13 +7,12 @@
|
||||
개인 `storage/{회사}/{사용자}/templates/일위대가조합.json` · 키 `UP{사용자}-######`
|
||||
회사 · 개인 파일 = 마스터와 같은 머리 · 열 한 벌 + 머리 `다음`(키 장부 · 지운 키는 다시 안 씀).
|
||||
지금 층 = `store.at(층 폴더)`(`store.root()`) — 키 머리도 그 자리에서 셈 · 권한은 라우터 몫.
|
||||
나무 = 폴더 [구분] · [구분, 상세구분] 깊이 2 · 머리 `폴더` = 폴더 전부(빈 폴더 포함).
|
||||
목록 `폴더` = [구분] · [구분, 상세구분](읽기만 · 나무 고침 · 붙여넣기 길은 39-2 에서 지움).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
@@ -127,18 +126,13 @@ def _find(doc: dict, key: str) -> dict[str, Any]:
|
||||
return row
|
||||
|
||||
|
||||
# ── 나무 폴더 ─────────────────────────────────────────────────────────
|
||||
# ── 폴더(읽기만) ───────────────────────────────────────────────────────
|
||||
def path_of(row: dict) -> list[str]:
|
||||
"""조합의 폴더 — [구분] · [구분, 상세구분] · 구분이 없으면 뿌리."""
|
||||
a, b = (str(row.get(k) or "").strip() for k in ("구분", "상세구분"))
|
||||
return [a, b] if a and b else [a] if a else []
|
||||
|
||||
|
||||
def _set_path(row: dict, path: list[str]) -> None:
|
||||
row["구분"] = path[0] if path else None
|
||||
row["상세구분"] = path[1] if len(path) > 1 else None
|
||||
|
||||
|
||||
def folders_of(doc: dict) -> list[list[str]]:
|
||||
"""폴더 전부 — 적어 둔 것 + 조합 자리 · 부모까지."""
|
||||
out: list[list[str]] = []
|
||||
@@ -312,128 +306,3 @@ def delete(key: str, version: str) -> dict[str, Any]:
|
||||
_find(doc, key)
|
||||
_refuse_used([key])
|
||||
return {"판": _save({**doc, "줄": [r for r in doc["줄"] if r.get("키") != key]}, version)}
|
||||
|
||||
|
||||
# ── 나무 고침 ─────────────────────────────────────────────────────────
|
||||
def _moved(path: list[str], old: list[str], new: list[str]) -> list[str]:
|
||||
return new + path[len(old) :] if path[: len(old)] == old else path
|
||||
|
||||
|
||||
def _folder(folders: list[list[str]], path: list[str] | None, what: str) -> list[str]:
|
||||
path = [str(x) for x in path or []]
|
||||
if not path or path not in folders:
|
||||
raise store.StoreError(404, f"{what} 폴더 「{' › '.join(path)}」 없음")
|
||||
return path
|
||||
|
||||
|
||||
def _fresh(name: Any) -> str:
|
||||
text = str(name or "").strip()
|
||||
if not text or "›" in text or len(text) > mcb.MAX_이름:
|
||||
raise store.StoreError(422, "폴더 이름 — 빈 이름 · 「›」 · 너무 긴 이름은 못 씀")
|
||||
return text
|
||||
|
||||
|
||||
def _apply(doc: dict, body: dict) -> tuple[dict, list[str]]:
|
||||
"""나무 고침 한 번 → (새 표, 지운 조합 키) · 조합 더하기 · 담은 로직은 `create` · `edit` 몫."""
|
||||
op, key = body.get("op"), body.get("키")
|
||||
folders, rows = folders_of(doc), [dict(r) for r in doc.get("줄") or []]
|
||||
gone: list[str] = []
|
||||
target = [str(x) for x in body.get("새경로") or []]
|
||||
if op in ("move", "add-folder") and target and target not in folders:
|
||||
raise store.StoreError(404, f"없는 폴더 「{' › '.join(target)}」")
|
||||
if op == "add-folder":
|
||||
made = [*[str(x) for x in body.get("경로") or []], _fresh(body.get("이름"))]
|
||||
if len(made) > DEPTH or (made[:-1] and made[:-1] not in folders):
|
||||
raise store.StoreError(422, f"폴더는 {DEPTH} 단까지 · 있는 폴더 아래에만")
|
||||
if made in folders:
|
||||
raise store.StoreError(409, f"이미 있는 폴더 「{' › '.join(made)}」")
|
||||
folders.append(made)
|
||||
elif op == "rename" and key:
|
||||
_find({"줄": rows}, str(key))["이름"] = _fresh(body.get("이름"))
|
||||
elif op == "move" and key:
|
||||
_set_path(_find({"줄": rows}, str(key)), target)
|
||||
elif op == "delete" and key:
|
||||
_find({"줄": rows}, str(key))
|
||||
gone = [str(key)]
|
||||
elif op in ("rename", "move", "delete"):
|
||||
old = _folder(folders, body.get("경로"), "고칠")
|
||||
if op == "delete":
|
||||
gone = [str(r["키"]) for r in rows if path_of(r)[: len(old)] == old]
|
||||
folders = [f for f in folders if f[: len(old)] != old]
|
||||
else:
|
||||
new = [*old[:-1], _fresh(body.get("이름"))] if op == "rename" else [*target, old[-1]]
|
||||
deepest = max(len(f) for f in folders if f[: len(old)] == old)
|
||||
if new[: len(old)] == old or deepest - len(old) + len(new) > DEPTH:
|
||||
raise store.StoreError(422, f"폴더는 {DEPTH} 단까지 · 제 안으로 못 옮김")
|
||||
if new in folders:
|
||||
raise store.StoreError(409, f"이미 있는 폴더 「{' › '.join(new)}」")
|
||||
folders = [_moved(f, old, new) for f in folders]
|
||||
for r in rows:
|
||||
if path_of(r)[: len(old)] == old:
|
||||
_set_path(r, _moved(path_of(r), old, new))
|
||||
else:
|
||||
raise store.StoreError(
|
||||
422, f"모르는 나무 고침 「{op}」 — add-folder · rename · move · delete"
|
||||
)
|
||||
rows = [r for r in rows if str(r.get("키")) not in gone]
|
||||
return {**doc, "폴더": folders, "줄": rows}, gone
|
||||
|
||||
|
||||
def edit_tree(body: dict) -> dict[str, Any]:
|
||||
"""폴더 더하기 · 이름 바꾸기(폴더 · 조합) · 옮기기(폴더 · 조합) · 지우기(폴더 = 안 조합까지 ·
|
||||
조합) → 새 목록 · 쓰이는 조합이 들면 409 · 판 다르면 409 · 조합 이름 겹침 422."""
|
||||
return edit_tree_many({"판": body.get("판"), "ops": [body]})
|
||||
|
||||
|
||||
def edit_tree_many(body: dict) -> dict[str, Any]:
|
||||
"""나무 고침 여럿을 한 번에(PLAN 30-3 · 여러 개 옮기기 · 지우기) — `{판, ops: [고침 …]}` ·
|
||||
차례대로 적용 · 하나라도 막히면 아무것도 안 씀 · 저장 한 번."""
|
||||
doc, have = _load()
|
||||
if have != (body.get("판") or ""):
|
||||
raise store.StoreError(409, {"stale": [FILE], "판": have})
|
||||
ops = body.get("ops")
|
||||
if not isinstance(ops, list) or not ops:
|
||||
raise store.StoreError(422, "나무 고침 목록(ops)이 비었음")
|
||||
new, gone = doc, []
|
||||
for op in ops:
|
||||
new, got = _apply(new, op if isinstance(op, dict) else {})
|
||||
gone += got
|
||||
_refuse_used(gone)
|
||||
renamed = [str(op["키"]) for op in ops if op.get("op") == "rename" and op.get("키")]
|
||||
commit(doc, have, new, gone, renamed)
|
||||
return {**listing(), "지운조합": gone}
|
||||
|
||||
|
||||
def commit(doc: dict, have: str, new: dict, gone: list[str], checked: list[str] = ()) -> None:
|
||||
"""고친 표 한 벌 저장 — 회사 · 개인 = 파일 한 번(`checked` 조합은 이름 검사) ·
|
||||
마스터 = 줄은 M01 저장 길 · 머리 `폴더` 따로."""
|
||||
if heads()[-1] != MASTER:
|
||||
for key in checked:
|
||||
if key in {str(r.get("키")) for r in new["줄"]}:
|
||||
if errors := _name_errors(new, _find(new, key)):
|
||||
raise store.StoreError(422, {"errors": errors})
|
||||
_save(new, have)
|
||||
return
|
||||
was = {str(r["키"]): r for r in doc["줄"]}
|
||||
changes = [{"op": "delete", "key": k} for k in gone] + [
|
||||
{"op": "edit", "key": str(r["키"]), "row": r} for r in new["줄"] if was[str(r["키"])] != r
|
||||
]
|
||||
version = _m01(combo_store._write, changes, have)["version"] if changes else have
|
||||
_write_master_folders(new["폴더"], version)
|
||||
|
||||
|
||||
def _write_master_folders(folders: list[list[str]], version: str) -> None:
|
||||
"""마스터 조합 파일 머리 `폴더` — 줄은 안 건드림(줄 고침은 M01 저장 길)."""
|
||||
path = Path(m01.FOLDER) / FILE
|
||||
with m01._LOCK:
|
||||
doc, have = m01.read(FILE)
|
||||
if have != version:
|
||||
raise store.StoreError(409, {"stale": [FILE], "판": have})
|
||||
if doc.get("폴더") == folders:
|
||||
return
|
||||
head = {k: v for k, v in doc.items() if k != "줄"}
|
||||
text = m01.dump({**head, "폴더": folders, "줄": doc.get("줄") or []})
|
||||
temporary = path.with_name(f".{FILE}.tmp")
|
||||
temporary.write_text(text, encoding="utf-8", newline="\n")
|
||||
os.replace(temporary, path)
|
||||
m01.cache.stale([FILE])
|
||||
|
||||
@@ -95,15 +95,6 @@ export interface ComboLayerList extends ComboList {
|
||||
layer: LaborScope["layer"];
|
||||
owner: string | null;
|
||||
}
|
||||
export interface ComboTreeEdit {
|
||||
op: "add-folder" | "rename" | "move" | "delete";
|
||||
경로?: string[];
|
||||
이름?: string;
|
||||
새경로?: string[];
|
||||
키?: string;
|
||||
판: string;
|
||||
}
|
||||
|
||||
async function request<T>(url: string, method = "GET", body?: unknown): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}/m02${url}`, {
|
||||
method,
|
||||
@@ -197,36 +188,6 @@ export const deleteCombo = (
|
||||
scope: LaborScope = SYSTEM_SCOPE,
|
||||
): Promise<unknown> => request(at(scope, `/${enc(key)}`, { 판: version }), "DELETE");
|
||||
|
||||
/** 나무 고침 — 폴더 더하기 · 이름 · 옮기기(폴더 · 조합) · 지우기 → 새 목록 */
|
||||
export const editComboTree = (
|
||||
body: ComboTreeEdit,
|
||||
scope: LaborScope = SYSTEM_SCOPE,
|
||||
): Promise<ComboList & { 지운조합: string[] }> => request(at(scope, "/tree"), "POST", body);
|
||||
|
||||
/** 여러 개 옮기기 · 지우기(PLAN 30-3) — 나무 고침 여럿을 차례대로 · 저장 한 번 */
|
||||
export const editComboTreeMany = (
|
||||
ops: Omit<ComboTreeEdit, "판">[],
|
||||
판: string,
|
||||
scope: LaborScope = SYSTEM_SCOPE,
|
||||
): Promise<ComboList & { 지운조합: string[] }> =>
|
||||
request(at(scope, "/tree-many"), "POST", { 판, ops });
|
||||
|
||||
/** 층 사이 복사(PLAN 30-3) — `from` 층의 조합 · 폴더를 이 층 `경로` 폴더에 새 키로 */
|
||||
export const pasteCombos = (
|
||||
from: LaborScope,
|
||||
pick: { 키?: string[]; 폴더?: string[][] },
|
||||
경로: string[],
|
||||
판: string,
|
||||
scope: LaborScope = SYSTEM_SCOPE,
|
||||
): Promise<ComboList & { 새키: { 원본: string; 새키: string }[] }> =>
|
||||
request(at(scope, "/paste"), "POST", {
|
||||
from_layer: from.layer,
|
||||
from_owner: from.owner,
|
||||
...pick,
|
||||
경로,
|
||||
판,
|
||||
});
|
||||
|
||||
/** 보기만 — 수량(inputs)은 이 부름에만 실림 · 저장 안 함 */
|
||||
export const previewCombo = (
|
||||
combo: Combo | string,
|
||||
|
||||
@@ -19,7 +19,6 @@ from pydantic import BaseModel
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Combo_Copy as combo_copy
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
|
||||
@@ -31,36 +30,6 @@ class ComboBody(BaseModel):
|
||||
판: str | None = None # 새로 만들 때 빈 글 = 지금 판 · 고칠 때 어긋나면 409
|
||||
|
||||
|
||||
class ComboTreeBody(BaseModel):
|
||||
"""`{op: add-folder|rename|move|delete, 경로?, 이름?, 새경로?, 키?, 판}` — 폴더 깊이 2."""
|
||||
|
||||
op: str
|
||||
경로: list[str] | None = None
|
||||
이름: str | None = None
|
||||
새경로: list[str] | None = None
|
||||
키: str | None = None
|
||||
판: str
|
||||
|
||||
|
||||
class TreeManyBody(BaseModel):
|
||||
"""여러 개 옮기기 · 지우기(PLAN 30-3) — `ops` = 나무 고침(`ComboTreeBody` 모양) · 저장 한 번."""
|
||||
|
||||
판: str
|
||||
ops: list[dict[str, Any]]
|
||||
|
||||
|
||||
class PasteBody(BaseModel):
|
||||
"""층 사이 복사(PLAN 30-3) — 원본 층(`from_layer` · `from_owner`)의 조합 `키` · 폴더 `폴더` →
|
||||
이 층 `경로` 폴더에 새 키로. `판` = 이 층 조합 표 판."""
|
||||
|
||||
from_layer: Layer
|
||||
from_owner: str | None = None
|
||||
키: list[str] = []
|
||||
폴더: list[list[str]] = []
|
||||
경로: list[str] = []
|
||||
판: str = ""
|
||||
|
||||
|
||||
class PreviewBody(BaseModel):
|
||||
"""저장 전 미리 보기 — `combo` 는 조합 줄 통째 또는 그 층 키 · `inputs` 는 로직마다."""
|
||||
|
||||
@@ -166,41 +135,6 @@ def delete_practice(판: str = "", session: Session = Depends(verify_session)) -
|
||||
return _call(_root(session, "system", None, write=True), combos.drop_practice, 판)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/combos/tree")
|
||||
def post_tree(
|
||||
layer: Layer,
|
||||
body: ComboTreeBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, combos.edit_tree, body.model_dump())
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/combos/tree-many")
|
||||
def post_tree_many(
|
||||
layer: Layer,
|
||||
body: TreeManyBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, combos.edit_tree_many, body.model_dump())
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/combos/paste")
|
||||
def post_paste(
|
||||
layer: Layer,
|
||||
body: PasteBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
"""원본 층 읽기 권한 · 이 층 쓰기 권한(M02_Layer_Access) — 마스터 붙이기 = 시스템 관리자."""
|
||||
source = _call(_root(session, body.from_layer, body.from_owner), lambda: combos._load()[0])
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, combo_copy.paste, source, body.model_dump())
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/combos/preview")
|
||||
def post_preview(
|
||||
layer: Layer,
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
- 키 = 회사 `UC{회사}-######` · 개인 `UP{사용자}-######` · 지운 번호 다시 안 줌 · 마스터 UA.
|
||||
- 권한 = 층 규칙(마스터 쓰기 시스템 관리자 · 회사 쓰기 회사 관리자 · 개인 쓰기 본인 · 읽기 규칙).
|
||||
- 검사 = 마스터와 같음(빈 조합 · 이름 겹침 · 없는 본뜬키) · 판 어긋남 409.
|
||||
- 나무 = 폴더 [구분, 상세구분] 깊이 2 · 더하기 · 이름 · 옮기기 · 지우기(쓰는 조합 409).
|
||||
- 마스터 나무 고침 = 줄은 M01 저장 길 · 빈 폴더는 머리 `폴더` · 틀 검사 그대로 통과.
|
||||
- 나무 고침 · 층 사이 붙여넣기 길은 없음(PLAN 39-2 · 일위대가는 구조물 열마다).
|
||||
- 고르기(use-combo) · 갈라 내기 = 세 층 조합 · 남의 층 것 403 · 갈라 낸 새 조합은 그 층에.
|
||||
- 조합 이름 · 알약 · 스냅샷 원본이 회사 · 개인 조합도 읽음.
|
||||
정본(마스터 · 마스터 템플릿)은 읽기만 · 쓰기는 임시 storage 와 마스터 사본.
|
||||
@@ -78,13 +77,6 @@ def _list(client: TestClient, layer: str, **params) -> dict:
|
||||
return res.json()
|
||||
|
||||
|
||||
def _tree(client: TestClient, layer: str, want: int = 200, **body) -> dict:
|
||||
body.setdefault("판", _list(client, layer)["판"])
|
||||
res = client.post(URL.format(layer) + "/tree", json=body)
|
||||
assert res.status_code == want, res.text
|
||||
return res.json()
|
||||
|
||||
|
||||
# ── 키 · 자리 ─────────────────────────────────────────────────────────
|
||||
def test_회사_개인_키_장부_지운_번호는_다시_안_줌(world):
|
||||
boss = _as(world, "ADMIN")
|
||||
@@ -135,60 +127,7 @@ def test_검사_빈_조합_이름_겹침_본뜬키_판(world):
|
||||
assert res.status_code == 200 and res.json()["combo"]["담은로직"][0]["로직"] == B
|
||||
|
||||
|
||||
# ── 나무 ─────────────────────────────────────────────────────────────
|
||||
def test_나무_폴더_깊이_2_이름_옮기기_지우기(world):
|
||||
me = _as(world)
|
||||
key = _new(me, "personal", _combo("돌쌓기 한 벌"))["키"]
|
||||
_tree(me, "personal", op="add-folder", 경로=[], 이름="돌쌓기")
|
||||
_tree(me, "personal", op="add-folder", 경로=["돌쌓기"], 이름="찰쌓기")
|
||||
_tree(me, "personal", 422, op="add-folder", 경로=["돌쌓기", "찰쌓기"], 이름="셋째")
|
||||
_tree(me, "personal", 409, op="add-folder", 경로=[], 이름="돌쌓기")
|
||||
_tree(me, "personal", 404, op="move", 키=key, 새경로=["없음"])
|
||||
got = _tree(me, "personal", op="move", 키=key, 새경로=["돌쌓기", "찰쌓기"])
|
||||
row = next(r for r in got["줄"] if r["키"] == key)
|
||||
assert (row["구분"], row["상세구분"]) == ("돌쌓기", "찰쌓기")
|
||||
got = _tree(me, "personal", op="rename", 경로=["돌쌓기"], 이름="석축")
|
||||
assert got["폴더"] == [["석축"], ["석축", "찰쌓기"]]
|
||||
assert next(r for r in got["줄"] if r["키"] == key)["구분"] == "석축"
|
||||
_tree(me, "personal", 422, op="move", 경로=["석축"], 새경로=["석축", "찰쌓기"])
|
||||
got = _tree(me, "personal", op="move", 경로=["석축", "찰쌓기"], 새경로=[])
|
||||
assert got["폴더"] == [["석축"], ["찰쌓기"]] # 빈 「석축」 은 남음
|
||||
row = next(r for r in got["줄"] if r["키"] == key)
|
||||
assert (row["구분"], row["상세구분"]) == ("찰쌓기", None)
|
||||
_tree(me, "personal", 409, op="add-folder", 경로=[], 이름="석축", 판="낡은판")
|
||||
_new(me, "personal", _combo("다른 것"))
|
||||
got = _tree(me, "personal", op="rename", 키=key, 이름="찰쌓기 한 벌")
|
||||
assert next(r for r in got["줄"] if r["키"] == key)["이름"] == "찰쌓기 한 벌"
|
||||
assert (
|
||||
"이미 있음"
|
||||
in _tree(me, "personal", 422, op="rename", 키=key, 이름="다른 것")["detail"]["errors"][0]
|
||||
)
|
||||
other = next(r["키"] for r in _list(me, "personal")["줄"] if r["이름"] == "다른 것")
|
||||
_tree(me, "personal", op="delete", 키=other)
|
||||
got = _tree(me, "personal", op="delete", 경로=["찰쌓기"])
|
||||
assert got["지운조합"] == [key] and got["줄"] == [] and got["폴더"] == [["석축"]]
|
||||
|
||||
|
||||
def test_쓰는_조합이_든_폴더는_못_지움_이름을_알림(world):
|
||||
me = _as(world)
|
||||
key = _new(me, "personal", _combo("쓰는 것", 구분="석축"))["키"]
|
||||
tree = me.post(
|
||||
"/api/m02/layers/personal/tree",
|
||||
json={"op": "add-item", "경로": ["옹벽"], "이름": "찰쌓기", "단위": "m", "판": _판(me)},
|
||||
).json()
|
||||
column = tree["알림"]["열"]
|
||||
use = me.post(
|
||||
f"/api/m02/layers/personal/structures/{column}/use-combo",
|
||||
json={"종류": "일위대가", "키": key},
|
||||
)
|
||||
assert use.status_code == 200, use.text
|
||||
got = _tree(me, "personal", 409, op="delete", 경로=["석축"])
|
||||
assert got["detail"]["users"] == ["옹벽 찰쌓기"]
|
||||
res = me.delete(f"{URL.format('personal')}/{key}", params={"판": _list(me, "personal")["판"]})
|
||||
assert res.status_code == 409
|
||||
assert [r["키"] for r in _list(me, "personal")["줄"]] == [key] # 그대로 남음
|
||||
|
||||
|
||||
# ── 마스터 사본 ───────────────────────────────────────────────────────
|
||||
def _판(client: TestClient) -> str:
|
||||
return client.get("/api/m02/layers/personal/tree").json()["판"]
|
||||
|
||||
@@ -208,28 +147,6 @@ def master_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
||||
return folder
|
||||
|
||||
|
||||
def test_마스터_나무는_M01_저장_길_빈_폴더는_머리_틀_검사_통과(world, master_copy):
|
||||
admin = _as(world, "SYSTEM_ADMIN")
|
||||
before = _list(admin, "system")
|
||||
first = before["줄"][0]["키"]
|
||||
got = _tree(admin, "system", op="rename", 키=first, 이름="이름 바꾼 견본")
|
||||
assert next(r for r in got["줄"] if r["키"] == first)["이름"] == "이름 바꾼 견본"
|
||||
before = _list(admin, "system")
|
||||
old = next(r["구분"] for r in before["줄"] if r["구분"]) # 정본 폴더 이름에 기대지 않음
|
||||
assert [old] in before["폴더"]
|
||||
_tree(admin, "system", op="add-folder", 경로=[], 이름="빈 폴더")
|
||||
got = _tree(admin, "system", op="rename", 경로=[old], 이름="시험 폴더")
|
||||
assert ["시험 폴더"] in got["폴더"] and ["빈 폴더"] in got["폴더"]
|
||||
assert [old] not in got["폴더"]
|
||||
doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8"))
|
||||
assert ["빈 폴더"] in doc["폴더"]
|
||||
assert sum(r["구분"] == "시험 폴더" for r in doc["줄"]) == len(
|
||||
[r for r in before["줄"] if r["구분"] == old]
|
||||
)
|
||||
assert m01.cm.check_form(combos.FILE, doc) == [] # 마스터 틀 그대로
|
||||
_tree(_as(world), "system", 403, op="add-folder", 경로=[], 이름="막힘")
|
||||
|
||||
|
||||
# ── 세 층 고르기 · 갈라 내기 · 읽는 곳 ─────────────────────────────────
|
||||
def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(world):
|
||||
boss = _as(world, "ADMIN")
|
||||
@@ -316,81 +233,10 @@ def test_담을_로직_읽기는_로그인이면_됨(world):
|
||||
assert user.get("/api/m02/logic-lib/auto", params={"key": A}).status_code == 200
|
||||
|
||||
|
||||
# ── 층 사이 복사 · 여러 개 옮기기(PLAN 30-3) ─────────────────────────────
|
||||
def _paste(client: TestClient, layer: str, body: dict, want: int = 200, **params) -> dict:
|
||||
body.setdefault("판", _list(client, layer, **params)["판"] if want != 403 else "")
|
||||
res = client.post(URL.format(layer) + "/paste", json=body, params=params)
|
||||
assert res.status_code == want, res.text
|
||||
return res.json()
|
||||
|
||||
|
||||
def test_마스터_조합_셋을_개인에_붙임_새_키_본뜬키_같은_이름은_2(world):
|
||||
user = _as(world)
|
||||
master = _list(user, "system")["줄"][:3]
|
||||
keys = [r["키"] for r in master]
|
||||
got = _paste(user, "personal", {"from_layer": "system", "키": keys})
|
||||
assert [p["새키"] for p in got["새키"]] == ["UP42-000001", "UP42-000002", "UP42-000003"]
|
||||
rows = {r["키"]: r for r in got["줄"]}
|
||||
for pair, src in zip(got["새키"], master):
|
||||
one = rows[pair["새키"]]
|
||||
assert (one["본뜬키"], one["이름"], one["count"]) == (src["키"], src["이름"], src["count"])
|
||||
assert (one["구분"], one["상세구분"]) == (None, None) # 붙인 자리 = 맨 위
|
||||
again = _paste(user, "personal", {"from_layer": "system", "키": keys[:1]})
|
||||
assert next(r for r in again["줄"] if r["키"] == "UP42-000004")["이름"].endswith(" (2)")
|
||||
_paste(user, "company", {"from_layer": "system", "키": keys}, 403) # 회사 쓰기 권한 없음
|
||||
_paste(user, "personal", {"from_layer": "personal", "from_owner": "8/9", "키": keys}, 403)
|
||||
_paste(user, "personal", {"from_layer": "system", "키": ["UA999999"]}, 404)
|
||||
stale = {"from_layer": "system", "키": keys, "판": "낡은판"}
|
||||
assert user.post(URL.format("personal") + "/paste", json=stale).status_code == 409
|
||||
|
||||
|
||||
def test_회사관리자_개인_폴더를_회사에_붙임_깊이_2_넘으면_422(world):
|
||||
admin = _as(world, "ADMIN")
|
||||
for name, where in (("가", ["석축"]), ("나", ["석축", "찰쌓기"])):
|
||||
_new(admin, "personal", _combo(name, 구분=where[0], 상세구분=(where + [None])[1]))
|
||||
_tree(admin, "personal", op="add-folder", 경로=["석축"], 이름="빈 곳")
|
||||
body = {"from_layer": "personal", "폴더": [["석축"]]}
|
||||
got = _paste(admin, "company", body)
|
||||
assert [p["새키"] for p in got["새키"]] == ["UC7-000001", "UC7-000002"]
|
||||
assert sorted(got["폴더"]) == [["석축"], ["석축", "빈 곳"], ["석축", "찰쌓기"]]
|
||||
by = {r["이름"]: (r["구분"], r["상세구분"], r["본뜬키"]) for r in got["줄"]}
|
||||
assert by["가"] == ("석축", None, "UP42-000001") and by["나"][:2] == ("석축", "찰쌓기")
|
||||
_tree(admin, "company", op="add-folder", 경로=[], 이름="다른 곳")
|
||||
deep = {"from_layer": "personal", "폴더": [["석축"]], "경로": ["다른 곳"]}
|
||||
assert "2 단까지" in _paste(admin, "company", deep, 422)["detail"]
|
||||
one = {"from_layer": "personal", "폴더": [["석축", "찰쌓기"]], "경로": ["다른 곳"]}
|
||||
got = _paste(admin, "company", one) # 한 단 폴더는 들어감
|
||||
assert ["다른 곳", "찰쌓기"] in got["폴더"]
|
||||
|
||||
|
||||
def test_여러_개_옮기기_지우기_한_번에(world):
|
||||
me = _as(world)
|
||||
keys = [_new(me, "personal", _combo(n))["키"] for n in ("가", "나", "다")]
|
||||
_tree(me, "personal", op="add-folder", 경로=[], 이름="석축")
|
||||
판 = _list(me, "personal")["판"]
|
||||
ops = [{"op": "move", "키": k, "새경로": ["석축"]} for k in keys[:2]]
|
||||
res = me.post(URL.format("personal") + "/tree-many", json={"판": 판, "ops": ops})
|
||||
assert res.status_code == 200, res.text
|
||||
assert [r["구분"] for r in res.json()["줄"]] == ["석축", "석축", None]
|
||||
bad = {"판": res.json()["판"], "ops": [*ops[:1], {"op": "move", "키": "UP42-999999"}]}
|
||||
assert me.post(URL.format("personal") + "/tree-many", json=bad).status_code == 404
|
||||
assert [r["구분"] for r in _list(me, "personal")["줄"]] == ["석축", "석축", None] # 안 씀
|
||||
판 = _list(me, "personal")["판"]
|
||||
ops = [{"op": "delete", "키": k} for k in keys[1:]]
|
||||
got = me.post(URL.format("personal") + "/tree-many", json={"판": 판, "ops": ops}).json()
|
||||
assert got["지운조합"] == keys[1:] and [r["키"] for r in got["줄"]] == keys[:1]
|
||||
stale = {"판": "낡은판", "ops": ops}
|
||||
assert me.post(URL.format("personal") + "/tree-many", json=stale).status_code == 409
|
||||
|
||||
|
||||
def test_시스템_관리자_개인_조합을_마스터에_붙임_본뜬키는_UA_만(world, master_copy):
|
||||
# ── 나무 길 없음(PLAN 39-2) ────────────────────────────────────────────
|
||||
def test_나무_고침_붙여넣기_길은_없음(world):
|
||||
"""PLAN 39-2 — 일위대가 조합 나무 길(`/tree` · `/tree-many` · `/paste`)을 지움."""
|
||||
admin = _as(world, "SYSTEM_ADMIN")
|
||||
up = _new(admin, "personal", _combo("개인 것"))["키"]
|
||||
before = len(_list(admin, "system")["줄"])
|
||||
got = _paste(admin, "system", {"from_layer": "personal", "키": [up]})
|
||||
new = got["새키"][0]["새키"]
|
||||
assert new.startswith("UA") and len(got["줄"]) == before + 1
|
||||
row = next(r for r in got["줄"] if r["키"] == new)
|
||||
assert row["본뜬키"] is None and row["이름"] == "개인 것"
|
||||
doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8"))
|
||||
assert m01.cm.check_form(combos.FILE, doc) == []
|
||||
for tail in ("/tree", "/tree-many", "/paste"):
|
||||
res = admin.post(URL.format("personal") + tail, json={"판": ""})
|
||||
assert res.status_code in (404, 405), tail
|
||||
|
||||
Reference in New Issue
Block a user