feat(M02): 항목 하나 비우기 길 — 도면 · 일위대가 · 면적산출을 새 열과 같은 빈 문서로 (40-4)
- POST /api/m02/layers/{층}/{structures|labor|area}/{열}/clear(owner) · 번호 · 열 그대로 · 받는 층 쓰기 권한(_root)
- Store.clear · 면적을 비우면 그 열 일위대가 계산값 다시 · 장부 맞춤
- 화면 함수 Api_Fetch clearPart
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
This commit is contained in:
@@ -226,6 +226,19 @@ export const copyPartFrom = async (
|
||||
});
|
||||
};
|
||||
|
||||
/** 항목 하나 비우기(40-4 · 카드 [삭제]) — 새 열과 같은 빈 문서 · 번호 · 열 그대로 · 지금 층 */
|
||||
export const clearPart = (
|
||||
kind: "structure" | "labor" | "area",
|
||||
column: string,
|
||||
): Promise<TemplateInfo> =>
|
||||
call(
|
||||
owned(
|
||||
activeLayer,
|
||||
`/layers/${activeLayer}/${kind === "structure" ? "structures" : kind}/${enc(column)}/clear`,
|
||||
),
|
||||
{ method: "POST" },
|
||||
);
|
||||
|
||||
/** 다른 열(`from`) 구조물 도면 내용을 받는 열(`column`)로 가져옴 — 도번 · 열은 받는 쪽 그대로 */
|
||||
export const copyStructure = async (column: string, from: string): Promise<void> => {
|
||||
await scoped(`/structures/${enc(column)}/copy?from=${enc(from)}`, { method: "POST" });
|
||||
|
||||
@@ -317,6 +317,20 @@ def _copy_practice(column: str, key: str) -> dict[str, Any]:
|
||||
return store.copy_from("labor", column, key, body=labor.from_combo(key))
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/{kind}/{column}/clear")
|
||||
def post_clear(
|
||||
layer: Layer,
|
||||
kind: Literal["structures", "labor", "area"],
|
||||
column: str,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
):
|
||||
"""항목 하나 비우기(40-4 · 카드 [삭제]) — 빈 문서 · 번호 · 열 그대로 · 받는 층 쓰기 권한."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
part = "structure" if kind == "structures" else kind
|
||||
return _call(folder, store.clear, part, column)
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/{kind}/{column}/copy")
|
||||
def post_copy(
|
||||
layer: Layer,
|
||||
|
||||
@@ -351,6 +351,21 @@ def create_structure(column: str) -> dict[str, Any]:
|
||||
return read("structure", column)
|
||||
|
||||
|
||||
def clear(kind: str, column: str) -> dict[str, Any]:
|
||||
"""항목 하나 비우기(40-4) — 새 열 만들 때와 같은 빈 문서로 다시 씀 · 번호 · 열 그대로 ·
|
||||
없는 파일은 404 · 면적산출을 비우면 그 열 일위대가 계산값도 다시 · 장부 맞춤."""
|
||||
if kind not in _KEY:
|
||||
raise StoreError(404, f"없는 종류 「{kind}」")
|
||||
number = read(kind, column)["문서"].get(_KEY[kind])
|
||||
with _LOCK:
|
||||
atomic_write_json(_path(kind, column), {**_blank(kind, column), _KEY[kind]: number})
|
||||
info = _info(kind, _path(kind, column))
|
||||
if kind == "area":
|
||||
_refresh_labor(column)
|
||||
sync_combos()
|
||||
return info
|
||||
|
||||
|
||||
def copy_from(
|
||||
kind: str,
|
||||
column: str,
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""항목 하나 비우기(PLAN 40-4 · 카드 [삭제]) — `POST /api/m02/layers/{층}/{종류}/{열}/clear`.
|
||||
|
||||
새 열과 같은 빈 문서 · 번호 · 열 그대로 · 권한 = 받는 층 쓰기(`_root`).
|
||||
시스템 층 = 견본 표(임시 폴더) · 개인 층 = 임시 storage · 로직은 M01 마스터(읽기만).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from config import config_system
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
from resources.tester.test_m02_structure import write_sample_table
|
||||
|
||||
TEN = "GC000421"
|
||||
URL = "/api/m02/layers/{}/{}/{}/clear"
|
||||
NUMBER = {
|
||||
"structures": ("structure", "도번"),
|
||||
"labor": ("labor", "일위대가번"),
|
||||
"area": ("area", "면적번"),
|
||||
}
|
||||
|
||||
|
||||
def _fill(folder: Path, cid: str) -> None:
|
||||
"""세 문서를 채움 — 도면 선 · 일위대가 로직 · 면적 칸 + 알약 연결."""
|
||||
with store.at(folder):
|
||||
for kind in ("structure", "labor", "area"):
|
||||
store._ensure(kind, cid)
|
||||
got = store.read("structure", cid)
|
||||
doc = got["문서"]
|
||||
doc["도면"]["entities"] = [{"type": "line"}]
|
||||
store.write("structure", cid, got["판"], doc)
|
||||
got = store.read("labor", cid)
|
||||
store.write("labor", cid, got["판"], {**got["문서"], "담은로직": [{"로직": TEN}]})
|
||||
got = store.read("area", cid)
|
||||
sheet = got["문서"]["시트"][0]
|
||||
sheet["칸"] = {"B6": {"값": "5"}}
|
||||
sheet["연결"] = [{"키": "사석 수상고르기|㎡", "이름": "x", "칸": "B6", "단위": "㎡"}]
|
||||
store.write("area", cid, got["판"], got["문서"])
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
|
||||
system = tmp_path / "system"
|
||||
system.mkdir()
|
||||
write_sample_table(system)
|
||||
monkeypatch.setattr(store, "FOLDER", system)
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||
(tmp_path / "storage").mkdir()
|
||||
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
||||
with store.at(system):
|
||||
store.create_structure("rv")
|
||||
_fill(system, "rv")
|
||||
personal = layers.personal_dir(7, 42)
|
||||
items_store.ensure(personal)
|
||||
with store.at(personal):
|
||||
got = store.read("table", store.STRUCTURE_TABLE)
|
||||
got["문서"]["열"] = [{"id": "a", "머리": ["돌", "가"], "꼴": "수"}]
|
||||
store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"])
|
||||
_fill(personal, "a")
|
||||
session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False}
|
||||
app = FastAPI()
|
||||
app.include_router(items_router.router)
|
||||
app.dependency_overrides[verify_session] = lambda: session
|
||||
return {"client": TestClient(app), "session": session, "system": system, "personal": personal}
|
||||
|
||||
|
||||
def _as(world: dict, role: str = "USER", user: int = 42) -> TestClient:
|
||||
world["session"].update(role=role, user_id=user)
|
||||
return world["client"]
|
||||
|
||||
|
||||
def _doc(folder: Path, kind: str, cid: str) -> dict:
|
||||
return json.loads((folder / kind / f"{cid}.json").read_bytes())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["structures", "labor", "area"])
|
||||
def test_세_종류_빈_문서_번호_열_그대로(world, kind):
|
||||
system = world["system"]
|
||||
part, key = NUMBER[kind]
|
||||
before = _doc(system, part, "rv")
|
||||
got = _as(world, "SYSTEM_ADMIN").post(URL.format("system", kind, "rv"))
|
||||
assert got.status_code == 200, got.text
|
||||
after = _doc(system, part, "rv")
|
||||
with store.at(system):
|
||||
blank = store._blank(part, "rv")
|
||||
assert after == {**blank, key: before[key]} # 새 열과 같은 빈 문서 + 번호
|
||||
assert after["열"] == "rv" and after[key]
|
||||
with store.at(system): # 장부 번호 그대로
|
||||
row = next(r for r in store.read_combos()["줄"] if r["열"] == "rv")
|
||||
assert row[key] == before[key]
|
||||
|
||||
|
||||
def test_면적_비우면_일위대가_계산값_수량_없음으로(world):
|
||||
system = world["system"]
|
||||
assert _doc(system, "labor", "rv")["계산값"]["줄"][0]["수량출처"] == "칸"
|
||||
_as(world, "SYSTEM_ADMIN").post(URL.format("system", "area", "rv"))
|
||||
assert _doc(system, "labor", "rv")["계산값"]["줄"][0]["수량출처"] == "없음"
|
||||
assert _doc(system, "labor", "rv")["담은로직"][0]["로직"] == TEN # 일위대가는 그대로
|
||||
|
||||
|
||||
def test_권한_일반_마스터_403_남의_개인_403_내_개인_됨(world):
|
||||
system, personal = world["system"], world["personal"]
|
||||
client = _as(world)
|
||||
assert client.post(URL.format("system", "labor", "rv")).status_code == 403
|
||||
assert _doc(system, "labor", "rv")["담은로직"] # 안 지워짐
|
||||
other = URL.format("personal", "labor", "a") + "?owner=7/99"
|
||||
assert client.post(other).status_code == 403
|
||||
assert client.post(URL.format("personal", "labor", "a")).status_code == 200
|
||||
assert _doc(personal, "labor", "a")["담은로직"] == []
|
||||
|
||||
|
||||
def test_회사_관리자는_동료_개인_층_비움(world):
|
||||
personal = world["personal"]
|
||||
client = _as(world, "ADMIN", user=41)
|
||||
got = client.post(URL.format("personal", "structures", "a") + "?owner=7/42")
|
||||
assert got.status_code == 200, got.text
|
||||
assert _doc(personal, "structure", "a")["도면"]["entities"] == []
|
||||
|
||||
|
||||
def test_없는_열_404_모르는_종류_422(world):
|
||||
client = _as(world, "SYSTEM_ADMIN")
|
||||
assert client.post(URL.format("system", "labor", "없는열")).status_code == 404
|
||||
assert client.post(URL.format("system", "material", "rv")).status_code == 422
|
||||
Reference in New Issue
Block a user