Files
Aislo/resources/tester/spreadsheet/test_spreadsheet_store.py
T
eomsangdonandClaude Opus 5.5 cd824aca25 feat(spreadsheet): F 서버 재계산 · 산출근거 따로 저장 · M02 끼우기 · 금빛 시험
- common_util_spreadsheet_node.ts · common_util_spreadsheet.py · build:spreadsheet — [저장] 때 화면과 같은 TS 엔진을 Node 로 돌려 계산값 정본(브라우저 값 버림 · 못 풀면 503)
- M02 새 종류 basis — resources/master_template/basis/<열 id>.json(구조물집계표 열마다) · 층 복사 · 뼈대 검사 · 옛 구조물 문서 산출근거 49 벌 옮김(migrate_basis) · 새 열도 같이 만듦
- 상세 산출근거 카드 = 새 스프레드시트(import() 로 늦게 받음) · basis 읽기 · 저장
- ⚠ 임시 [스프레드시트 시험] 단추 — 빈 통합문서(시트 셋) · 저장 자리 tmp/spreadsheet_trial.json · 1단계 검증 뒤 지움
- 시험 resources/tester/spreadsheet/ — 저장 · 서버 다리 · 크기 예산 · M02 만 늦게 불러옴 · 실무 구조도 xlsx 세 벌 수식 2,699 칸 엑셀 캐시값 대조(엔진 빈 몸이면 건너뜀)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168FQuoV7vDh5nhnSowW5cp
2026-09-27 20:12:39 +09:00

139 lines
6.1 KiB
Python

"""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)
monkeypatch.setattr(router_module, "TRIAL_PATH", tmp_path / "trial.json")
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):
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["초기"]
def test_임시_시험_주소(client):
first = client.get("/api/m02/spreadsheet-trial").json()["문서"]
assert [s["이름"] for s in first["시트"]] == ["시트1", "시트2", "시트3"]
first["시트"][0]["칸"] = {"A1": {"식": "1+41"}}
saved = client.put("/api/m02/spreadsheet-trial", json={"문서": first}).json()["문서"]
assert saved["계산값"] == SERVER
assert client.get("/api/m02/spreadsheet-trial").json()["문서"] == saved