diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py index 1e342c88..a2730e68 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Layers.py @@ -279,6 +279,39 @@ async def save_layer_template( return {"종류": kind, "이름": name, "판": version, "층": layer} +@router.delete("/layers/{layer}/templates/{kind}/{name}") +async def delete_layer_template( + layer: Layer, + kind: str, + name: str, + 판: str | None = Query(None), + project_id: str | None = Query(None), + session: dict[str, Any] = Depends(verify_session), +) -> dict[str, Any]: + """개인 = 본인만 · 회사 = 회사 관리자만 · 시스템 · 프로젝트 층은 거절 · 판이 다르면 409.""" + if layer in ("system", "project"): + raise HTTPException(status_code=403, detail="개인 · 회사 층 양식만 지울 수 있습니다.") + folder = await _layer_dir(session, layer, project_id=project_id, write=True) + try: + layers.check_kind(kind) + layers.check_name(name) + except ValueError as error: + raise _bad(error) from error + if not 판: + raise HTTPException(status_code=400, detail="지울 양식의 판이 필요합니다.") + try: + found = await asyncio.to_thread( + layers.delete_template, folder, kind, name, version=판, check_version=True + ) + except layers.StaleTemplate as error: + raise HTTPException( + status_code=409, detail={"message": str(error), "판": error.current} + ) from error + if not found: + raise HTTPException(status_code=404, detail="양식을 찾을 수 없습니다.") + return {"종류": kind, "이름": name, "층": layer, "지움": True} + + # ── 프로젝트 길 ─────────────────────────────────────── diff --git a/M02_MasterTemplete/M02_Template_Layers.py b/M02_MasterTemplete/M02_Template_Layers.py index 7d48de77..03d6a52f 100644 --- a/M02_MasterTemplete/M02_Template_Layers.py +++ b/M02_MasterTemplete/M02_Template_Layers.py @@ -100,7 +100,10 @@ def check_name(name: str) -> str: text = str(name or "").strip() bad = not text or text != name or text.startswith((".", "_")) or len(text) > 80 if bad or any(ch in text for ch in '/\\:*?"<>|') or ".." in text: - raise ValueError("양식 이름이 올바르지 않습니다.") + raise ValueError( + "양식 이름이 올바르지 않습니다 — 빈 이름 · 앞뒤 빈칸 · 80자 초과 · 앞머리 . _ · " + '`/ \ .. : * ? " < > |` 는 못 씁니다.' + ) return text @@ -189,10 +192,20 @@ def write_template( return version_of(path) or "" -def delete_template(layer_dir: str | Path, kind: str, name: str) -> bool: +def delete_template( + layer_dir: str | Path, + kind: str, + name: str, + *, + version: str | None = None, + check_version: bool = False, +) -> bool: + """양식 지움 · manifest 줄도 뺌. `check_version` 이면 지금 판과 달라 `StaleTemplate`.""" path = template_path(layer_dir, kind, name) if not path.is_file(): return False + if check_version and version_of(path) != version: + raise StaleTemplate(version_of(path)) path.unlink() manifest = read_manifest(layer_dir) if manifest.pop(f"{kind}/{name}", None) is not None: diff --git a/resources/tester/test_m02_template_layers.py b/resources/tester/test_m02_template_layers.py index 4b79b0b3..c9723a67 100644 --- a/resources/tester/test_m02_template_layers.py +++ b/resources/tester/test_m02_template_layers.py @@ -354,3 +354,51 @@ def test_뼈대_없는_문서는_400으로_거절하고_파일을_안_씀(world: table = _url() assert client.put(table, json={"판": None, "문서": {}}).status_code == 400 assert client.put(url, json={"판": version, "문서": {"entities": []}}).status_code == 200 + + +def _mine(name: str = "내양식", layer: str = "personal") -> str: + return f"/api/m02/layers/{layer}/templates/table/{name}?project_id={P1}" + + +def test_개인_회사_양식_지우기_권한_판_이름_예외(world: dict[str, Any]) -> None: + client, session = world["client"], world["session"] + doc = {"열": []} + for layer in ("personal", "company"): + client.put(_mine(layer=layer), json={"판": None, "문서": doc}) + personal = layers.personal_dir(7, 42) / "table/내양식.json" + company = layers.company_dir(7) / "table/내양식.json" + assert personal.is_file() and not company.is_file() # 일반 사용자 회사 저장은 403 + version = layers.version_of(personal) + # 시스템 · 프로젝트 층은 거절 + for layer in ("system", "project"): + assert client.delete(_mine("구조물집계표", layer) + f"&판={version}").status_code == 403 + assert (_root(world, P1) / "templates/table/구조물집계표.json").is_file() + # 이름 예외 · 판 없음 · 판 다름 · 없는 양식 + for bad in ("%2E%2E", "a%2E%2Eb", "a%2Fb", "a%5Cb", "_initial", ".x", "a*b"): + got = client.delete(_mine(bad) + f"&판={version}") + assert got.status_code in (400, 404) and personal.is_file(), bad + assert client.delete(_mine("a%2Fb") + f"&판={version}").status_code in (400, 404) + assert client.delete(_mine()).status_code == 400 + stale = client.delete(_mine() + "&판=0000000000000000") + assert stale.status_code == 409 and stale.json()["detail"]["판"] == version + assert client.delete(_mine("없는것") + f"&판={version}").status_code == 404 + # 다른 회사 사람은 못 지움 · 회사 층은 관리자만 + assert client.delete(_mine("내양식", "company") + f"&판={version}").status_code == 403 + # 지움 · 연달아 누르면 둘째는 404 · manifest 줄도 없어짐 + assert client.delete(_mine() + f"&판={version}").status_code == 200 + assert not personal.exists() + assert client.delete(_mine() + f"&판={version}").status_code == 404 + session["role"] = "ADMIN" + client.put(_mine(layer="company"), json={"판": None, "문서": doc}) + assert company.is_file() + session.update(role="USER") + assert ( + client.delete(_mine(layer="company") + f"&판={layers.version_of(company)}").status_code + == 403 + ) + session.update(role="ADMIN") + assert ( + client.delete(_mine(layer="company") + f"&판={layers.version_of(company)}").status_code + == 200 + ) + assert "table/내양식" not in layers.read_manifest(layers.company_dir(7))