- 항목 키 = 마스터 SS + 6자리 · 회사 SC{회사}- + 6자리 · 개인 SP{사용자}- + 6자리(장부 key_head · 층 폴더로 가름) · 한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사 · 개인에 저장 · 새 항목은 그 층 새 키 + 본뜬 곳
- 회사 · 개인 층의 옛 SS 키는 장부를 맞출 때(처음 읽을 때 한 번) 새 모양으로 옮김(숫자 여섯 그대로 · 겹치면 다음 번호) · 옛 키는 줄 옛키 에 남김 · 옛 키로도 찾음(store.find_combo · 나무 고침 · 입력 목록 · 항목 복사 · M02_Item_Source.row_of)
- summary_item 키 검사 — SC · SP 는 그 층 · 머리 번호 = owner 일 때만 · 옛 SS 는 어느 층이든 받음
- 품번(도번 SD · 면적번 SA · 재료번 SM) 새 일련 = 붙임표 + 6자리(SD-000001) — 옛 8자리(제목행 자리) · 12장 옛 6자리(SD010101)와 모양이 달라 안 겹침 · 옛 번호는 그대로 유효 · 새 모양끼리 셈 · 지운 번호 안 씀
- 시험 test_m02_item_keys 7(층마다 키 모양 · 안 바뀜 · 다시 안 씀 · 복사 본뜬 곳 · 옛 SS 옮김 · 옛 키로 찾음 · 겹친 숫자 · 참조 키 모양 · 120 형제 깊이 6 번호 · 층마다 번호 따로 · 키대장 안 겹침) · 번호 · 키 시험 기대값을 새 모양으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
236 lines
12 KiB
Python
236 lines
12 KiB
Python
"""M02 구조물 도면(structure) — 새로 · 도번 SD+8자리 · 409 · 저장 · 지운 뒤 번호 ·
|
||
번호 매김(새 항목 = 자리 뜻 없는 일련번호 · PLAN 26-1) · 집계표 저장 때 만들기/지우기 ·
|
||
복사 가져오기 · 프로젝트 복사.
|
||
면적산출(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",
|
||
"SD-000001",
|
||
)
|
||
# 도각 없는 전용 화면 — 그린 만큼이 설계 영역 · 기본 도면층 하나(없으면 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()["문서"]["도번"] == "SD-000002"
|
||
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("-000001"),
|
||
"rv": _num("-000002"),
|
||
}
|
||
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["도번"] == "SD-000001" # 번호는 서버 것 — 화면 값을 안 받음
|
||
|
||
|
||
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("-000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠
|
||
assert numbers["ms"] == _num("-000003") # 다른 열 번호는 그대로
|
||
# 새 열 = 다음 일련번호(자리 뜻 없음) · 다시 만든 도면은 짝(면적산출) 번호를 이어 받음
|
||
assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD-000004"
|
||
assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002"
|
||
|
||
|
||
def test_번호_없는_열은_열_차례로_일련번호_한_번_옮김(client, tmp_path):
|
||
"""옛 구-NN 파일 · 글 열 파일 → 새 번호(번호 없는 열 = 열 차례 일련번호 · 자리 뜻 없음) ·
|
||
내용 그대로."""
|
||
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 = { # 구조물 열 차례 그대로 1 부터(제목행 자리 뜻 없음)
|
||
"rv": "-000001",
|
||
"c1": "-000002",
|
||
"pv_b": "-000003",
|
||
"pv_l": "-000004",
|
||
"pp_len": "-000007",
|
||
"fb": "-000009",
|
||
"be": "-000011",
|
||
"h_rip_c": "-000012",
|
||
"h_rip_m": "-000013",
|
||
}
|
||
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
|
||
# 새 열 = 열 차례로 다음 일련번호 · 지운 pv_a(5) 는 비워 둠(다른 번호를 안 당김)
|
||
assert numbers["pv_x"] == _num("-000014")
|
||
assert numbers["zz"] == _num("-000015")
|
||
assert numbers["pv_y"] == _num("-000016")
|
||
assert numbers["pv_b"] == _num("-000003") # 있던 번호는 그대로
|
||
|
||
|
||
def test_지운_열_번호는_다음_저장에도_다시_안_줌(client):
|
||
"""가장 큰 번호 열(h_rip_m 13)을 지워 저장한 뒤 새 열을 더해도 13 을 다시 안 받음."""
|
||
store.renumber()
|
||
url = "/api/m02/templates/table/구조물집계표"
|
||
got = client.get(url).json()
|
||
got["문서"]["열"] = [c for c in got["문서"]["열"] if c["id"] != "h_rip_m"]
|
||
assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200
|
||
assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["-000013"]
|
||
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("-000014")
|
||
|
||
|
||
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["열"]) == ("SD-000010", "ms") # 번호 · 열은 받는 쪽 그대로
|
||
assert client.get("/api/m02/templates/structure/rv").json()["문서"]["도번"] == "SD-000001"
|
||
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})
|