diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 519a58bf..5ee4c9e7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -65,9 +65,17 @@ def read(kind: str, name: str) -> dict[str, Any]: return {"종류": kind, "이름": name, "판": version_of(raw), "문서": json.loads(raw)} +def _check_skeleton(kind: str, doc: Any) -> None: + """빈 문서·뼈대 없는 문서는 거절 — 표는 열 목록 · 도면은 entities 목록.""" + key = "열" if kind == "table" else "entities" + if not isinstance(doc, dict) or not isinstance(doc.get(key), list): + raise StoreError(400, f"양식 문서에 「{key}」 목록이 없음 — 저장하지 않음") + + def write(kind: str, name: str, version: str, doc: Any) -> dict[str, Any]: """`version` 이 빈 글이면 새로 만듦(이미 있으면 409) · 아니면 그 판일 때만 덮어씀.""" path = _path(kind, name) + _check_skeleton(kind, doc) with _LOCK: have = version_of(path.read_bytes()) if path.is_file() else "" if have != (version or ""): diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 5418cd04..4da5b2a4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -23,7 +23,7 @@ export interface Selection { } interface EditorHandle { - getDoc: () => unknown; + getDoc: () => unknown | Promise; destroy: () => void; } @@ -162,7 +162,7 @@ export function createMain(isAdmin: boolean): MainHandle { if (!sel) return; const at = sel; try { - const doc = editor ? editor.getDoc() : loaded; + const doc = editor ? await editor.getDoc() : loaded; const info = await saveTemplate(at.layer, at.kind, at.name, at.projectId, version, doc); version = info.판; loaded = doc; diff --git a/resources/tester/test_m02_store.py b/resources/tester/test_m02_store.py index e271c248..c6090378 100644 --- a/resources/tester/test_m02_store.py +++ b/resources/tester/test_m02_store.py @@ -25,7 +25,9 @@ def test_new_save_reopen_delete_and_stale(client): assert made.status_code == 200 v1 = made.json()["판"] assert ( - client.put("/api/m02/templates/table/집계표", json={"판": "", "문서": {}}).status_code + client.put( + "/api/m02/templates/table/집계표", json={"판": "", "문서": {"열": []}} + ).status_code == 409 ) @@ -34,7 +36,7 @@ def test_new_save_reopen_delete_and_stale(client): saved = client.put("/api/m02/templates/table/집계표", json={"판": v1, "문서": {"열": [1, 2]}}) assert saved.status_code == 200 and saved.json()["판"] != v1 - stale = client.put("/api/m02/templates/table/집계표", json={"판": v1, "문서": {}}) + stale = client.put("/api/m02/templates/table/집계표", json={"판": v1, "문서": {"열": []}}) assert stale.status_code == 409 and stale.json()["detail"]["stale"] == ["집계표"] rows = client.get("/api/m02/templates").json() @@ -46,9 +48,22 @@ def test_new_save_reopen_delete_and_stale(client): def test_bad_names_and_kinds(client): assert ( - client.put("/api/m02/templates/table/a..b", json={"판": "", "문서": {}}).status_code == 200 + client.put("/api/m02/templates/table/a..b", json={"판": "", "문서": {"열": []}}).status_code + == 200 ) assert ( client.put("/api/m02/templates/table/.숨김", json={"판": "", "문서": {}}).status_code == 422 ) assert client.put("/api/m02/templates/other/x", json={"판": "", "문서": {}}).status_code == 404 + + +def test_empty_doc_rejected_and_file_untouched(client): + good = {"format": 6, "entities": [{"type": "line"}]} + made = client.put("/api/m02/templates/drawing/도", json={"판": "", "문서": good}) + v = made.json()["판"] + for bad in ({}, {"format": 6}, {"entities": "x"}, []): + r = client.put("/api/m02/templates/drawing/도", json={"판": v, "문서": bad}) + assert r.status_code == 400 + assert client.put("/api/m02/templates/table/표", json={"판": "", "문서": {}}).status_code == 400 + assert client.get("/api/m02/templates/drawing/도").json()["문서"] == good + assert client.get("/api/m02/templates/table/표").status_code == 404