"""F — 산출근거 저장(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"}}} @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 _old_structure(folder, column, basis): doc = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": column, "도번": "구-01"} doc.update({"도면": {"format": 6, "entities": []}, "산출근거": basis}) (folder / "structure" / f"{column}.json").write_text( json.dumps(doc, ensure_ascii=False), encoding="utf-8" ) def test_옛_산출근거를_따로_파일로_옮김(client, tmp_path): old = { "열": [ {"id": "work", "머리": ["공종"]}, {"id": "detail", "머리": ["산출", "내역"]}, {"id": "qty", "머리": ["수량(m당)"]}, ], "줄": [{"id": "1", "값": {"work": "터파기", "qty": 1.5}}, {"id": "2", "값": {}}], "보기": {"열너비": {"detail": 210}}, } _old_structure(tmp_path, "pp_len", old) done = store.migrate_basis() assert done["옮김"] == ["pp_len"] columns = store._column_ids() assert set(done["새로"]) == set(columns) - {"pp_len"} structure = json.loads((tmp_path / "structure/pp_len.json").read_text(encoding="utf-8")) assert "산출근거" not in structure and structure["도번"] == "구-01" basis = store.read("basis", "pp_len")["문서"] sheet = basis["시트"][0] assert (basis["종류"], basis["열"], sheet["id"]) == ("통합문서", "pp_len", "s1") assert sheet["칸"] == { "A1": {"값": "공종", "서식": 1}, "B1": {"값": "산출 내역", "서식": 1}, "C1": {"값": "수량(m당)", "서식": 1}, "A2": {"값": "터파기"}, "C2": {"값": 1.5}, } assert sheet["열"] == {"B": {"폭": 30.0}} assert "계산값" not in basis # 식 없는 새 문서 — 첫 [저장] 때 서버가 적음 # 두 번 돌려도 같음 before = {p.name: p.read_bytes() for p in (tmp_path / "basis").iterdir()} assert store.migrate_basis() == {"옮김": [], "새로": []} assert before == {p.name: p.read_bytes() for p in (tmp_path / "basis").iterdir()} def test_새_구조물_도면은_산출근거_따로(client): made = client.post("/api/m02/structures/rv").json()["문서"] assert "산출근거" not in made basis = client.get("/api/m02/templates/basis/rv").json()["문서"] heads = [basis["시트"][0]["칸"][f"{c}1"]["값"] for c in "ABCDE"] assert heads == ["공종", "규격", "산출 내역", "단위", "수량(m당)"] def test_표번은_도번과_같은_규칙(client): made1 = client.post("/api/m02/structures/rv").json()["문서"] made2 = client.post("/api/m02/structures/pv_b").json()["문서"] assert made1["도번"] == "구-01" and made2["도번"] == "구-02" basis1 = client.get("/api/m02/templates/basis/rv").json()["문서"] basis2 = client.get("/api/m02/templates/basis/pv_b").json()["문서"] assert basis1["표번"] == "표-01" and basis2["표번"] == "표-02" numbers = client.get("/api/m02/basis/numbers").json() assert numbers["rv"] == "표-01" and numbers["pv_b"] == "표-02" def test_표번_없는_옛_파일은_만나면_채움(client, tmp_path): store.ensure_basis("rv") doc = store.read("basis", "rv")["문서"] del doc["표번"] (tmp_path / "basis" / "rv.json").write_text( json.dumps(doc, ensure_ascii=False), encoding="utf-8" ) numbers = client.get("/api/m02/basis/numbers").json() assert numbers["rv"] == "표-01" assert store.read("basis", "rv")["문서"]["표번"] == "표-01" # 한 번 채우면 파일에 남음 def test_저장은_서버_계산값만_적음(client): store.ensure_basis("rv") got = client.get("/api/m02/templates/basis/rv").json() doc = {**got["문서"], "계산값": {"s1": {"A1": {"수": "브라우저"}}}} saved = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": doc}) assert saved.status_code == 200 assert client.get("/api/m02/templates/basis/rv").json()["문서"]["계산값"] == SERVER # 뼈대 없는 문서 · 판이 다르면 막음 bad = client.put("/api/m02/templates/basis/rv", json={"판": saved.json()["판"], "문서": {}}) assert bad.status_code == 400 stale = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": doc}) assert stale.status_code == 409 def test_못_풀면_저장을_막음(client, monkeypatch): store.ensure_basis("rv") got = client.get("/api/m02/templates/basis/rv").json() def broken(_book): raise spreadsheet.RecalcError("스프레드시트 계산 실패 — 시험") monkeypatch.setattr(spreadsheet, "with_server_values", broken) r = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": got["문서"]}) assert r.status_code == 503 assert client.get("/api/m02/templates/basis/rv").json()["판"] == got["판"] def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch): store.ensure_basis("rv") doc = store.read("basis", "rv")["문서"] layers.check_skeleton("basis", doc) for bad in ({}, {**doc, "시트": {}}): with pytest.raises(ValueError): layers.check_skeleton("basis", bad) layers.check_skeleton("structure", {"도면": {"entities": []}}) # 산출근거 없어도 됨 monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path) copied = layers.seed_project(tmp_path / "project") assert "basis/rv" in copied["작업본"] and "basis/rv" in copied["초기"]