Files
Aislo/resources/tester/test_m02_structure.py
T
eomsangdonandClaude Opus 5.5 16de9eed8b fix(M02): 16장 점검 — 지운 열 번호 재사용 막음 · 이름 창 커서 · Enter 만들기 · 바인딩 열 손 입력 막음
- 지운 구조물 열의 도번 8자리를 조합 장부 지운번호에 남겨 새 열이 다시 안 받음(다음 저장에서도)
- 공용 창 틀: 몸의 첫 입력 칸에 커서(붙기 전 focus 가 안 먹던 것)
- 좌측 새로 · 본떠 만들기 이름 창에서 Enter = 만들기
- 표: 바인딩 · 계산 열은 손 입력 열 단추 꺼짐(켜도 프로젝트에서 잠김)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vX53nJGC8ucBRFJns2n7B
2026-09-28 21:56:51 +09:00

233 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""M02 구조물 도면(structure) — 새로 · 도번 SD+8자리 · 409 · 저장 · 지운 뒤 번호 ·
제목행 번호 매김(분류3 까지) · 집계표 저장 때 만들기/지우기 · 복사 가져오기 · 프로젝트 복사.
면적산출(area) · 재료산출(material)은 따로 파일 —
`resources/tester/spreadsheet/test_spreadsheet_store.py`."""
import json
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
# 견본 구조물집계표 — 실제 마스터 양식은 사용자가 화면에서 고치므로 시험은 이 견본만 읽음
_SAMPLE_COLUMNS = [
("no", ["NO", None, None, None], "글"),
("sta", ["측점", None, None, None], "글"),
("rv", ["돌기슭막이", "(형태마다)", "H=(높이마다)", None], "수"),
("c1", ["돌기슭막이", "(형태마다)", "새 열", None], "수"),
("pv_b", ["콘크리트포장", "T=(두께마다)", "B", None], "수"),
("pv_l", ["콘크리트포장", "T=(두께마다)", "L", None], "수"),
("pv_a", ["콘크리트포장", "T=(두께마다)", "A", None], "수"),
("pv_jt", ["콘크리트포장", "T=(두께마다)", "줄눈", None], "수"),
("pp_len", ["관공", "(관종 · 관경마다)", "L", None], "수"),
("pp_cp", ["관공", "(관종 · 관경마다)", "커플링밴드", None], "수"),
("fb", ["세월교", "(관경 × 련수마다)", None, None], "수"),
("ms", ["돌쌓기", "(쌓기마다)", "H=(높이마다)", None], "수"),
("be", ["소단", None, None, None], "수"),
("h_rip_c", ["돌붙임", "찰붙임", None, None], "수"),
("h_rip_m", ["돌붙임", "메붙임", None, None], "수"),
("memo", ["비고", None, None, None], "글"),
]
def write_sample_table(folder) -> None:
"""임시 폴더에 종류 폴더 · 견본 구조물집계표를 만듦(`store.FOLDER` 자리)."""
for kind in store.KINDS:
(folder / kind).mkdir(exist_ok=True)
table = {
"양식": store.STRUCTURE_TABLE,
"종류": "표",
"판": 1,
"층": ["종류", "분류1", "분류2", "분류3"],
"열": [{"id": i, "머리": h, "단위": None, "꼴": k} for i, h, k in _SAMPLE_COLUMNS],
"줄": [],
"합계줄": [],
"보기": {},
}
(folder / "table" / f"{store.STRUCTURE_TABLE}.json").write_text(
json.dumps(table, ensure_ascii=False), encoding="utf-8"
)
@pytest.fixture()
def client(tmp_path, monkeypatch):
write_sample_table(tmp_path)
monkeypatch.setattr(store, "FOLDER", tmp_path)
app = FastAPI()
app.include_router(router)
return TestClient(app)
def _num(code: str) -> dict:
"""세 번호 — 같은 8자리."""
return {"도번": f"SD{code}", "면적번": f"SA{code}", "재료번": f"SM{code}"}
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",
"SD01010100",
)
# 도각 없는 전용 화면 — 그린 만큼이 설계 영역 · 기본 도면층 하나(없으면 CAD 가 못 엶)
assert doc["도면"]["format"] == 6
assert doc["도면"]["entities"] == []
assert len(doc["도면"]["layers"]) == 1
# 면적산출 · 재료산출은 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서씩
assert "산출근거" not in doc
for kind in ("area", "material"):
assert client.get(f"/api/m02/templates/{kind}/pp_len").json()["문서"]["종류"] == "통합문서"
assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD02010100"
assert client.post("/api/m02/structures/pp_len").status_code == 409
assert client.post("/api/m02/structures/없는열").status_code == 404
assert client.post("/api/m02/structures/no").status_code == 404 # 글 열은 구조물 아님
assert client.get("/api/m02/structures/numbers").json() == {
"pp_len": _num("01010100"),
"rv": _num("02010100"),
}
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["도면"]["entities"] = [{"type": "line"}]
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, "도면": {"entities": "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
now = client.get("/api/m02/templates/structure/pp_len").json()["판"]
client.put(
"/api/m02/templates/structure/pp_len", json={"판": now, "문서": {**doc, "도번": "SD9"}}
)
got = client.get("/api/m02/templates/structure/pp_len").json()["문서"]
assert got["도번"] == "SD01010100" # 번호는 서버 것 — 화면 값을 안 받음
def test_지워도_번호를_당기지_않음_짝은_같은_번호(client):
for column in ("pp_len", "rv", "ms"):
client.post(f"/api/m02/structures/{column}")
assert client.delete("/api/m02/templates/structure/rv").status_code == 200
numbers = client.get("/api/m02/structures/numbers").json()
assert numbers["rv"] == {**_num("02010100"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠
assert numbers["ms"] == _num("03010100") # 다른 열 번호는 그대로
# 같은 종류(관공) 새 열 = 그 무리 다음 번호 · 다시 만든 도면은 짝(면적산출) 번호를 이어 받음
assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD01010200"
assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD02010100"
def test_제목행으로_번호_매김_한_번_옮김(client, tmp_path):
"""옛 구-NN 파일 · 글 열 파일 → 새 번호(열 차례 = 종류 · 분류1 · 분류2 · 분류3 차례) ·
내용 그대로."""
for column in ("no", "rv", "pv_l"):
doc = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": column, "도번": "구-07"}
doc["도면"] = {"format": 6, "entities": [{"type": "line"}], "layers": []}
(tmp_path / "structure" / f"{column}.json").write_text(
json.dumps(doc, ensure_ascii=False), encoding="utf-8"
)
done = store.renumber()
assert done["지움"] == ["structure/no"] # 글 열(NO · 측점 · 비고)은 구조물 아님
numbers = client.get("/api/m02/structures/numbers").json()
assert set(numbers) == set(store._column_ids()) and "sta" not in numbers
expect = {
"rv": "01010100", # 돌기슭막이 · (형태마다) · H=(높이마다) · 분류3 없음 = 00
"c1": "01010200", # 같은 종류 · 분류1 · 다음 분류2
"pv_b": "02010100", # 콘크리트포장 · T=(두께마다) · B
"pv_l": "02010200",
"pp_len": "03010100",
"fb": "04010000", # 세월교 · (관경 × 련수마다) · 분류2 없음 = 00
"be": "06000000", # 소단 — 분류 모두 없음
"h_rip_c": "07010000", # 돌붙임 · 찰붙임
"h_rip_m": "07020000", # 돌붙임 · 메붙임 — 분류1 다음 번호
}
for column, code in expect.items():
assert numbers[column] == _num(code), column
assert store.read("structure", "rv")["문서"]["도면"]["entities"] == [{"type": "line"}]
again = {"옮김": [], "지움": [], "바꿈": [], "새로": []}
assert store.renumber() == again # 두 번 돌려도 같음
def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(client):
store.renumber()
got = client.get("/api/m02/templates/table/구조물집계표").json()
table = got["문서"]
table["열"] = [c for c in table["열"] if c["id"] != "pv_a"]
new = {"id": "pv_x", "머리": ["콘크리트포장", "T=(두께마다)", "X"], "단위": "m", "꼴": "수"}
kind = {"id": "zz", "머리": ["새 구조물", None, None], "단위": "개소", "꼴": "수"}
text = {"id": "note2", "머리": ["메모", None, None], "단위": None, "꼴": "글"}
third = {"id": "pv_y", "머리": ["콘크리트포장", "T=(두께마다)", "B", "H=1"], "꼴": "수"}
table["열"] += [new, kind, text, third]
saved = client.put(
"/api/m02/templates/table/구조물집계표", json={"판": got["판"], "문서": table}
)
assert saved.status_code == 200
numbers = client.get("/api/m02/structures/numbers").json()
assert "pv_a" not in numbers and "note2" not in numbers
for kind_ in ("structure", "area", "material"):
assert client.get(f"/api/m02/templates/{kind_}/pv_a").status_code == 404
assert numbers["pv_x"] == _num("02010500") # 지운 pv_a(03)는 비워 둠 · 무리 다음
assert numbers["zz"] == _num("08000000") # 새 종류 = 종류 다음 번호
assert numbers["pv_y"] == _num("02010101") # pv_b 무리 안 분류3 다음 번호
def test_지운_열_번호는_다음_저장에도_다시_안_줌(client):
"""무리 맨 끝 열(pv_jt 02010400)을 지워 저장한 뒤 새 열을 더해도 04 를 다시 안 받음."""
store.renumber()
url = "/api/m02/templates/table/구조물집계표"
got = client.get(url).json()
got["문서"]["열"] = [c for c in got["문서"]["열"] if c["id"] != "pv_jt"]
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["02010400"]
got = client.get(url).json()
new = {"id": "pv_z", "머리": ["콘크리트포장", "T=(두께마다)", "Z", None], "꼴": "수"}
got["문서"]["열"].append(new)
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("02010500")
def test_다른_열_도면을_복사해_가져옴(client):
store.renumber()
src = client.get("/api/m02/templates/structure/rv").json()
src["문서"]["도면"]["entities"] = [{"type": "circle"}]
client.put("/api/m02/templates/structure/rv", json=src)
r = client.post("/api/m02/structures/ms/copy", params={"from": "rv"})
assert r.status_code == 200
ms = client.get("/api/m02/templates/structure/ms").json()["문서"]
assert ms["도면"]["entities"] == [{"type": "circle"}]
assert (ms["도번"], ms["열"]) == ("SD05010100", "ms") # 번호 · 열은 받는 쪽 그대로
assert client.get("/api/m02/templates/structure/rv").json()["문서"]["도번"] == "SD01010100"
bad = client.post("/api/m02/structures/ms/copy", params={"from": "없는열"})
assert bad.status_code == 404
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})