Files
Aislo/resources/tester/spreadsheet/test_spreadsheet_store.py
T
eomsangdonandClaude Opus 5.5 760e43c8df feat(M02): 항목 키 체계 · 품번 새 모양(PLAN 26-4a)
- 항목 키 = 마스터 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
2026-09-30 09:04:39 +09:00

145 lines
7.2 KiB
Python

"""F — 면적산출(area) · 재료산출(material) 저장 · 옛 산출근거(basis) 옮김 · [저장] 때 서버 계산값 ·
알약 연결(`연결`)은 저장 · 복사 때 문서와 같이.
서버 Node 풀이는 `test_spreadsheet_node.py` 가 따로 봄 — 여기는 `with_server_values` 를 바꿔 끼워
「브라우저 값은 버리고 서버 값만 적는다」 · 「못 풀면 저장을 막는다」 만 잼.
"""
import json
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
from resources.tester.test_m02_structure import write_sample_table
SERVER = {"s1": {"A1": {"수": "42"}}}
LINKS = {"p1": {"시트": "s1", "칸": "B2"}} # 알약 연결 — 서버는 속을 안 봄(sub7 계약)
@pytest.fixture()
def client(tmp_path, monkeypatch):
write_sample_table(tmp_path) # 실제 마스터 양식은 안 읽음
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()["문서"]
# 새 항목 번호 = 자리 뜻 없는 일련번호(PLAN 26-1)
assert made1["도번"] == "SD-000001" and made2["도번"] == "SD-000002"
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}-000002"
assert doc["시트"][0]["칸"] == {} and doc["서식"] == [{}] # 머리 줄 없음
numbers = client.get("/api/m02/structures/numbers").json()
assert numbers["pv_b"] == {"도번": "SD-000002", "면적번": "SA-000002", "재료번": "SM-000002"}
@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}-000010", "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["초기"]