Files
Aislo/resources/tester/spreadsheet/test_spreadsheet_store.py
T
eomsangdonandClaude Opus 5.5 89fc686962 feat(M02): 구조물 번호 8자리(분류3) · 면적산출 SA · 재료산출 SM 서버
- 번호 [종류][분류1][분류2][분류3] · 없는 층 00 · 12장 6자리는 끝에 00 을 붙여 읽음
- 양식 종류 basis → area(면적산출) · material(재료산출) 새로 · 집계표 저장 때 세 파일 자동 만들기/지우기
- 복사 길 POST /structures · /area · /material/{받는 열}/copy?from= · 알약 연결은 문서와 같이 복사
- 번호 목록 {열 id: {도번, 면적번, 재료번}} · 저장 때 번호 칸은 서버 것 그대로
- renumber: basis/ → area/ 옮김(표번 → 면적번 · 시트 이름 면적산출) · 빠진 재료산출 만듦

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

147 lines
7.1 KiB
Python

"""F — 면적산출(area) · 재료산출(material) 저장 · 옛 산출근거(basis) 옮김 · [저장] 때 서버 계산값 ·
알약 연결(`연결`)은 저장 · 복사 때 문서와 같이.
서버 Node 풀이는 `test_spreadsheet_node.py` 가 따로 봄 — 여기는 `with_server_values` 를 바꿔 끼워
「브라우저 값은 버리고 서버 값만 적는다」 · 「못 풀면 저장을 막는다」 만 잼.
"""
import json
import shutil
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from common_util import common_util_spreadsheet as spreadsheet
from M02_MasterTemplete import M02_MasterTemplete_Router as router_module
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
REAL = store.FOLDER
SERVER = {"s1": {"A1": {"수": "42"}}}
LINKS = {"p1": {"시트": "s1", "칸": "B2"}} # 알약 연결 — 서버는 속을 안 봄(sub7 계약)
@pytest.fixture()
def client(tmp_path, monkeypatch):
for kind in store.KINDS:
(tmp_path / kind).mkdir()
shutil.copy(REAL / "table/구조물집계표.json", tmp_path / "table")
monkeypatch.setattr(store, "FOLDER", tmp_path)
def fake(book):
clean = {k: v for k, v in book.items() if k != "계산값"}
return {**clean, "계산값": SERVER}
monkeypatch.setattr(spreadsheet, "with_server_values", fake)
app = FastAPI()
app.include_router(router_module.router)
return TestClient(app)
def _write(folder, kind, column, doc):
(folder / kind).mkdir(exist_ok=True)
(folder / kind / f"{column}.json").write_text(json.dumps(doc, ensure_ascii=False), "utf-8")
def test_옛_산출근거를_면적산출로_옮기고_번호_8자리(client, tmp_path):
"""12장 파일(SD · SC 6자리 · basis/) → SA 로 옮김 · 끝에 00 · 재료산출 새로 · 내용 그대로."""
structure = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": "rv", "도번": "SD010101"}
_write(tmp_path, "structure", "rv", {**structure, "도면": {"entities": [{"type": "line"}]}})
old = {**store._blank("area", "rv"), "표번": "SC010101", "연결": LINKS}
old["시트"][0].update({"이름": "산출근거", "칸": {"A2": {"값": "터파기"}}})
_write(tmp_path, "basis", "rv", old)
done = store.renumber()
assert done["옮김"] == ["rv"] and not (tmp_path / "basis").exists()
area = store.read("area", "rv")["문서"]
assert (area["면적번"], "표번" in area, area["연결"]) == ("SA01010100", False, LINKS)
assert area["시트"][0]["이름"] == "면적산출" and area["시트"][0]["칸"] == {
"A2": {"값": "터파기"}
}
assert store.read("structure", "rv")["문서"]["도번"] == "SD01010100"
material = store.read("material", "rv")["문서"]
assert (material["재료번"], material["시트"][0]["이름"]) == ("SM01010100", "재료산출")
assert material["시트"][0]["칸"] == {} and material["서식"] == [{}] # 진짜 빈 시트
before = {p: p.read_bytes() for p in tmp_path.rglob("*.json")}
assert store.renumber() == {"옮김": [], "지움": [], "바꿈": [], "새로": []}
assert before == {p: p.read_bytes() for p in tmp_path.rglob("*.json")} # 두 번 돌려도 같음
def test_새_구조물_도면은_면적_재료_진짜_빈_시트_같은_8자리(client):
made1 = client.post("/api/m02/structures/rv").json()["문서"]
made2 = client.post("/api/m02/structures/pv_b").json()["문서"]
assert made1["도번"] == "SD01010100" and made2["도번"] == "SD02010100"
for kind, key, head in (("area", "면적번", "SA"), ("material", "재료번", "SM")):
doc = client.get(f"/api/m02/templates/{kind}/pv_b").json()["문서"]
assert doc[key] == f"{head}02010100"
assert doc["시트"][0]["칸"] == {} and doc["서식"] == [{}] # 머리 줄 없음
numbers = client.get("/api/m02/structures/numbers").json()
assert numbers["pv_b"] == {"도번": "SD02010100", "면적번": "SA02010100", "재료번": "SM02010100"}
@pytest.mark.parametrize(
("kind", "key", "head"), [("area", "면적번", "SA"), ("material", "재료번", "SM")]
)
def test_다른_열_내용을_복사해_가져옴_연결도_같이(client, kind, key, head):
store.renumber()
got = client.get(f"/api/m02/templates/{kind}/rv").json()
got["문서"]["시트"][0]["칸"] = {"A1": {"식": "1+1"}}
got["문서"]["연결"] = LINKS
client.put(f"/api/m02/templates/{kind}/rv", json=got)
r = client.post(f"/api/m02/{kind}/ms/copy", params={"from": "rv"})
assert r.status_code == 200
ms = client.get(f"/api/m02/templates/{kind}/ms").json()["문서"]
assert (ms["시트"][0]["칸"], ms["연결"]) == ({"A1": {"식": "1+1"}}, LINKS)
assert (ms[key], ms["열"]) == (f"{head}10010100", "ms") # 번호 · 열은 받는 쪽 그대로
assert ms["계산값"] == SERVER # 서버가 다시 풂
assert client.post(f"/api/m02/{kind}/ms/copy", params={"from": "없는열"}).status_code == 404
def test_저장은_서버_계산값만_적음_연결_두고_번호는_서버_것(client):
store._ensure("area", "rv")
got = client.get("/api/m02/templates/area/rv").json()
number = got["문서"]["면적번"]
doc = {
**got["문서"],
"계산값": {"s1": {"A1": {"수": "브라우저"}}},
"연결": LINKS,
"면적번": "x",
}
saved = client.put("/api/m02/templates/area/rv", json={"판": got["판"], "문서": doc})
assert saved.status_code == 200
now = client.get("/api/m02/templates/area/rv").json()["문서"]
assert (now["계산값"], now["연결"], now["면적번"]) == (SERVER, LINKS, number)
# 뼈대 없는 문서 · 판이 다르면 막음
bad = client.put("/api/m02/templates/area/rv", json={"판": saved.json()["판"], "문서": {}})
assert bad.status_code == 400
stale = client.put("/api/m02/templates/area/rv", json={"판": got["판"], "문서": doc})
assert stale.status_code == 409
def test_못_풀면_저장을_막음(client, monkeypatch):
store._ensure("material", "rv")
got = client.get("/api/m02/templates/material/rv").json()
def broken(_book):
raise spreadsheet.RecalcError("스프레드시트 계산 실패 — 시험")
monkeypatch.setattr(spreadsheet, "with_server_values", broken)
r = client.put("/api/m02/templates/material/rv", json={"판": got["판"], "문서": got["문서"]})
assert r.status_code == 503
assert client.get("/api/m02/templates/material/rv").json()["판"] == got["판"]
def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch):
client.post("/api/m02/structures/rv")
for kind in ("area", "material"):
doc = store.read(kind, "rv")["문서"]
layers.check_skeleton(kind, doc)
for bad in ({}, {**doc, "시트": {}}):
with pytest.raises(ValueError):
layers.check_skeleton(kind, bad)
layers.check_skeleton("structure", {"도면": {"entities": []}}) # 산출근거 없어도 됨
monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path)
copied = layers.seed_project(tmp_path / "project")
for kind in ("area", "material"):
assert f"{kind}/rv" in copied["작업본"] and f"{kind}/rv" in copied["초기"]