"""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["초기"]