Merge remote-tracking branch 'origin/dev' into main_desktop_1

This commit is contained in:
2026-09-27 11:22:24 +09:00
17 changed files with 421 additions and 246807 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+93
View File
@@ -0,0 +1,93 @@
"""M02 구조물 도면(structure) — 새로 · 도번 자동 · 409 · 저장 · 지운 뒤 번호 · 프로젝트 복사."""
import shutil
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
from M02_MasterTemplete.M02_MasterTemplete_Router import router
REAL = store.FOLDER
@pytest.fixture()
def client(tmp_path, monkeypatch):
for kind in store.KINDS:
(tmp_path / kind).mkdir()
shutil.copy(REAL / "table/구조물집계표.json", tmp_path / "table")
shutil.copy(REAL / "drawing/00_template_A1.json", tmp_path / "drawing")
monkeypatch.setattr(store, "FOLDER", tmp_path)
app = FastAPI()
app.include_router(router)
return TestClient(app)
def test_새로_도번_자동_409_없는_열(client):
made = client.post("/api/m02/structures/pp_len")
assert made.status_code == 200
doc = made.json()["문서"]
assert (doc["양식"], doc["종류"], doc["열"], doc["도번"]) == (
"구조물도면",
"structure",
"pp_len",
"구-01",
)
assert (
doc["도면"]["entities"] and doc["도면"] == store.read("drawing", "00_template_A1")["문서"]
)
basis = doc["산출근거"]
assert [c["머리"][0] for c in basis["열"]] == ["공종", "규격", "산출 내역", "단위", "수량(m당)"]
assert basis["줄"] == [] and basis["종류"] == "표"
assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "구-02"
assert client.post("/api/m02/structures/pp_len").status_code == 409
assert client.post("/api/m02/structures/없는열").status_code == 404
assert client.get("/api/m02/structures/numbers").json() == {"pp_len": "구-01", "rv": "구-02"}
rows = client.get("/api/m02/templates").json()
assert {(r["종류"], r["이름"]) for r in rows if r["종류"] == "structure"} == {
("structure", "pp_len"),
("structure", "rv"),
}
def test_저장_뼈대_거름(client):
got = client.post("/api/m02/structures/pp_len").json()
doc = got["문서"]
doc["산출근거"]["줄"] = [{"id": "r1", "값": {"work": "관 부설"}}]
saved = client.put("/api/m02/templates/structure/pp_len", json={"판": got["판"], "문서": doc})
assert saved.status_code == 200 and saved.json()["판"] != got["판"]
for bad in ({}, {**doc, "도면": {}}, {**doc, "산출근거": {"열": "x"}}, {**doc, "도면": []}):
r = client.put(
"/api/m02/templates/structure/pp_len", json={"판": saved.json()["판"], "문서": bad}
)
assert r.status_code == 400
assert client.get("/api/m02/templates/structure/pp_len").json()["문서"] == doc
def test_지워도_번호를_당기지_않음(client):
for column in ("pp_len", "rv", "ms"):
client.post(f"/api/m02/structures/{column}")
assert client.delete("/api/m02/templates/structure/pp_len").status_code == 200
assert client.get("/api/m02/structures/numbers").json() == {"rv": "구-02", "ms": "구-03"}
assert client.post("/api/m02/structures/fb").json()["문서"]["도번"] == "구-04"
# 가장 큰 번호를 지우면 그 번호부터 다시(있는 것 중 가장 큰 번호 + 1)
client.delete("/api/m02/templates/structure/fb")
assert client.post("/api/m02/structures/ec").json()["문서"]["도번"] == "구-04"
def test_프로젝트_복사와_층_저장에_실림(client, tmp_path, monkeypatch):
doc = client.post("/api/m02/structures/pp_len").json()["문서"]
monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path)
project = tmp_path / "project"
copied = layers.seed_project(project)
assert "structure/pp_len" in copied["작업본"] and "structure/pp_len" in copied["초기"]
work = layers.project_dir(project)
assert layers.read_template(work, "structure", "pp_len")["문서"] == doc
assert layers.read_template(layers.initial_dir(project), "structure", "pp_len")["문서"] == doc
assert ("structure", "pp_len") in {(r["종류"], r["이름"]) for r in layers.list_templates(work)}
layers.check_skeleton("structure", doc)
with pytest.raises(ValueError):
layers.check_skeleton("structure", {**doc, "산출근거": None})
@@ -0,0 +1,64 @@
"""표 도면 줄 · 칸 고르기 신호 — `ui_template_sheet_ops.ts` 를 Node 로 바로 돌려 봄.
도면 줄 `s:drawing` = master 만 · 일위대가 줄 바로 아래(키보드 차례) · project 에는 없음.
`selectionOf` = 격자가 `onSelect` 로 넘기는 모양({row, col, colId} · 모르는 열은 col -1 · colId null).
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
OPS = ROOT / "ui_template" / "sheet" / "ui_template_sheet_ops.ts"
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
SCRIPT = """
const { navRows, selectionOf } = await import(process.argv[1]);
const doc = JSON.parse(process.argv[2]);
console.log(JSON.stringify({
master: navRows(doc, "master"),
project: navRows(doc, "project"),
pick: [selectionOf(doc, "s:drawing", "c2"), selectionOf(doc, "d:r1", "c1"), selectionOf(doc, "s:unit", "zz")],
}));
"""
DOC = {
"양식": "시험",
"종류": "표",
"판": 1,
"열": [{"id": "c1", "머리": ["가"]}, {"id": "c2", "머리": ["나"]}],
"줄": [{"id": "r1", "값": {}}],
"합계줄": [{"id": "t1", "이름": "계", "식": "SUM"}],
}
def _run() -> dict:
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module", "-e",
SCRIPT, OPS.as_uri(), json.dumps(DOC, ensure_ascii=False)],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
def test_drawing_row_under_price_master_only():
got = _run()
assert got["master"] == [
"s:unit", "s:formula", "s:desc", "s:price", "s:drawing", "d:r1", "t:t1",
] # fmt: skip
assert got["project"] == ["s:unit", "d:r1", "t:t1"]
def test_selection_shape():
assert _run()["pick"] == [
{"row": "s:drawing", "col": 1, "colId": "c2"},
{"row": "d:r1", "col": 0, "colId": "c1"},
{"row": "s:unit", "col": -1, "colId": None},
]