fix(M02): 도면 양식 저장이 빈 {} 로 덮이던 흠 — getDoc 을 await · 서버는 뼈대 없는 문서를 400 으로 거절

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148XFUpPfpiuTjxF1EK9c95
This commit is contained in:
2026-09-25 12:43:03 +09:00
co-authored by Claude Sonnet 5
parent dd60b6a1cb
commit 9bf3258509
3 changed files with 28 additions and 5 deletions
@@ -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 ""):
@@ -23,7 +23,7 @@ export interface Selection {
}
interface EditorHandle {
getDoc: () => unknown;
getDoc: () => unknown | Promise<unknown>;
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;
+18 -3
View File
@@ -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