Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
242 lines
10 KiB
Python
242 lines
10 KiB
Python
"""구조물도 양식 라이브러리 — 가져오기·박기 (2026-09-13, PLAN 4장 ①).
|
|
|
|
겨누는 것
|
|
① 가져오기 전은 프로그램 기본 · 머리 「가져오기 전」
|
|
② 로그인한 사람의 개인 단에서 가져오면 **프로젝트에 박히고** 원단위·자재총괄도 그 양식을 봄
|
|
③ ⛔ 박힌 뒤에는 라이브러리를 안 읽음 — 개인 파일을 지우거나 다른 사람이 열어도 값이 같음
|
|
④ 가져오면 그 종류의 고친 식이 비워짐
|
|
⑤ [확정] 때 안 박힌 기본 양식을 박음 · 코드 모양이 아니면 거절
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402
|
|
import B08_Quantity.B08_Quantity_Router_Material as material_module # noqa: E402
|
|
import B08_Quantity.B08_Quantity_Router_StructureSheet as router_module # noqa: E402
|
|
from B05_Profile.B05_Profile_Structures_Repository import save_structures # noqa: E402
|
|
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
|
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402
|
|
from common_util.common_util_auth import verify_session # noqa: E402
|
|
|
|
PROJECT_ID = "33333333-3333-3333-3333-333333333333"
|
|
SHEETS = f"/api/projects/{PROJECT_ID}/quantity/structure-sheets"
|
|
PERSONAL_CODE = "AX-ST-0badc0de"
|
|
|
|
|
|
@pytest.fixture()
|
|
def storage(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
base = tmp_path / "storage"
|
|
monkeypatch.setattr(library_module, "STORAGE_BASE_DIR", str(base))
|
|
# 개인 단(회사 7 · 사람 42)에 모르터 식을 두 배로 고친 찰쌓기 한 벌.
|
|
mine = load_template("masonry_wet")
|
|
for row in mine["rows"]:
|
|
if row["name"] == "모르터":
|
|
row["formula"] = "A*0.018"
|
|
mine.update(code=PERSONAL_CODE, library_tier="personal", name="돌쌓기(찰) 내 것")
|
|
folder = base / "7" / "42" / "library"
|
|
folder.mkdir(parents=True)
|
|
(folder / f"{PERSONAL_CODE}.json").write_text(json.dumps(mine), encoding="utf-8")
|
|
return base
|
|
|
|
|
|
@pytest.fixture()
|
|
def project(tmp_path: Path) -> Path:
|
|
root = tmp_path / "project"
|
|
root.mkdir()
|
|
wall = StructureInstance.model_validate(
|
|
{
|
|
"type_id": "masonry_wet",
|
|
"placement": "interval",
|
|
"start_m": 100.0,
|
|
"end_m": 110.0,
|
|
"options": {"height_m": 2.5, "back_len_cm": 45},
|
|
}
|
|
)
|
|
save_structures(str(root), [wall], base_revision=0)
|
|
return root
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(project: Path, storage: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
|
async def fake_root(project_id):
|
|
return str(project)
|
|
|
|
async def no_route(project_id):
|
|
return {}
|
|
|
|
monkeypatch.setattr(router_module, "_project_root", fake_root)
|
|
monkeypatch.setattr(material_module, "_section_modes", no_route)
|
|
monkeypatch.setattr(material_module, "_ground_types", no_route)
|
|
app = FastAPI()
|
|
app.include_router(router_module.router)
|
|
app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 42}
|
|
return TestClient(app)
|
|
|
|
|
|
def _sheet(client: TestClient) -> dict:
|
|
response = client.get(SHEETS)
|
|
assert response.status_code == 200, response.text
|
|
return response.json()["sheets"][0]
|
|
|
|
|
|
def _mortar(sheet: dict) -> dict:
|
|
return next(row for row in sheet["rows"] if row["name"] == "모르터")
|
|
|
|
|
|
def test_가져오기_전은_프로그램_기본(client: TestClient) -> None:
|
|
item = _sheet(client)["library_item"]
|
|
assert item["imported_from"] is None
|
|
assert item["code"] == load_template("masonry_wet")["code"]
|
|
|
|
|
|
def test_개인_단에서_가져오면_박히고_라이브러리를_다시_안_읽는다(
|
|
client: TestClient, project: Path, storage: Path
|
|
) -> None:
|
|
before = _mortar(_sheet(client))["unit_amount"]
|
|
listed = client.get(f"{SHEETS}/library", params={"type_id": "masonry_wet"}).json()["items"]
|
|
assert [item["tier"] for item in listed] == ["personal", "program"]
|
|
|
|
taken = client.put(
|
|
f"{SHEETS}/library/import",
|
|
json={"type_id": "masonry_wet", "tier": "personal", "code": PERSONAL_CODE},
|
|
)
|
|
assert taken.status_code == 200, taken.text
|
|
sheet = _sheet(client)
|
|
assert sheet["library_item"]["imported_from"] == "personal"
|
|
assert _mortar(sheet)["unit_amount"] == pytest.approx(before * 2)
|
|
# 가져온 식은 「사용자 식」이 아니라 그 양식의 식.
|
|
assert _mortar(sheet)["source"] == "library"
|
|
|
|
# 원단위·자재총괄 창구도 박힌 양식을 봄.
|
|
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table
|
|
|
|
structures, names, _ = material_module._collect_structures(str(project))
|
|
table = build_table(
|
|
structures,
|
|
names,
|
|
structure_templates=library_module.project_templates(project),
|
|
)
|
|
mortar = next(c for c in table["structures"][0]["components"] if c["name"] == "모르터")
|
|
assert mortar["amount"] == pytest.approx(_mortar(sheet)["unit_amount"] * 10.0)
|
|
|
|
# ⛔ 개인 파일이 사라지고 다른 사람이 열어도 값이 그대로.
|
|
(storage / "7" / "42" / "library" / f"{PERSONAL_CODE}.json").unlink()
|
|
client.app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 99}
|
|
assert _mortar(_sheet(client))["unit_amount"] == pytest.approx(before * 2)
|
|
|
|
|
|
def test_가져오면_그_종류의_고친_식이_비워진다(client: TestClient, project: Path) -> None:
|
|
sheet = _sheet(client)
|
|
seq = _mortar(sheet)["no"]
|
|
client.put(
|
|
f"{SHEETS}/formulas",
|
|
json={"sheet_key": sheet["key"], "rows": [{"seq": seq, "formula": "A*0.03"}]},
|
|
)
|
|
program_code = load_template("masonry_wet")["code"]
|
|
taken = client.put(
|
|
f"{SHEETS}/library/import",
|
|
json={"type_id": "masonry_wet", "tier": "program", "code": program_code},
|
|
)
|
|
assert taken.json()["cleared_formulas"] == 1
|
|
stored = json.loads((project / "project_settings.json").read_text(encoding="utf-8"))
|
|
assert stored["quantity"]["structure_formula_overrides"] == {}
|
|
assert _sheet(client)["library_item"]["imported_from"] == "program"
|
|
|
|
|
|
def test_없는_항목_가져오기는_404(client: TestClient) -> None:
|
|
missing = client.put(
|
|
f"{SHEETS}/library/import",
|
|
json={"type_id": "masonry_wet", "tier": "company", "code": "AX-ST-12345678"},
|
|
)
|
|
assert missing.status_code == 404
|
|
|
|
|
|
def test_내_라이브러리에_저장은_고친_식을_담고_프로젝트는_안_바꾼다(
|
|
client: TestClient, project: Path, storage: Path
|
|
) -> None:
|
|
"""PLAN 4장 ②③ — 개인 단에 한 벌(같은 종류는 덮어씀) · 지우기도 개인 단만."""
|
|
personal = storage / "7" / "42" / "library"
|
|
sheet = _sheet(client)
|
|
seq = _mortar(sheet)["no"]
|
|
client.put(
|
|
f"{SHEETS}/formulas",
|
|
json={"sheet_key": sheet["key"], "rows": [{"seq": seq, "formula": "A*0.03"}]},
|
|
)
|
|
settings_before = (project / "project_settings.json").read_text(encoding="utf-8")
|
|
|
|
saved = client.put(f"{SHEETS}/library/personal", json={"sheet_key": sheet["key"]})
|
|
assert saved.status_code == 200, saved.text
|
|
assert saved.json()["code"] == PERSONAL_CODE # 같은 종류가 있어 그 코드로 덮어씀
|
|
files = list(personal.glob("*.json"))
|
|
assert len(files) == 1
|
|
item = json.loads(files[0].read_text(encoding="utf-8"))
|
|
mortar_row = next(row for row in item["rows"] if row["name"] == "모르터")
|
|
assert mortar_row["formula"] == "A*0.03" and mortar_row["source"] == "library"
|
|
assert "default_formula" not in mortar_row
|
|
assert item["library_tier"] == "personal" and item["pum_edition"] == "2026-01-01"
|
|
# 프로젝트는 그대로 — 박힌 양식 없음 · 고친 식 그대로.
|
|
assert library_module.project_templates(project) == {}
|
|
assert (project / "project_settings.json").read_text(encoding="utf-8") == settings_before
|
|
|
|
# 가져온 뒤 지워도 프로젝트는 그대로.
|
|
client.put(
|
|
f"{SHEETS}/library/import",
|
|
json={"type_id": "masonry_wet", "tier": "personal", "code": PERSONAL_CODE},
|
|
)
|
|
amount = _mortar(_sheet(client))["unit_amount"]
|
|
deleted = client.delete(f"{SHEETS}/library/personal", params={"type_id": "masonry_wet"})
|
|
assert deleted.json()["deleted"] == 1 and not list(personal.glob("*.json"))
|
|
assert _mortar(_sheet(client))["unit_amount"] == pytest.approx(amount)
|
|
|
|
|
|
def test_회사_없는_사람은_개인_단을_못_쓴다(client: TestClient) -> None:
|
|
client.app.dependency_overrides[verify_session] = lambda: {"company_id": None, "user_id": 1}
|
|
sheet = _sheet(client)
|
|
assert (
|
|
client.put(f"{SHEETS}/library/personal", json={"sheet_key": sheet["key"]}).status_code
|
|
== 403
|
|
)
|
|
|
|
|
|
def test_확정_때_기본_양식을_박는다(project: Path) -> None:
|
|
assert library_module.pin_program_templates(project) >= 1
|
|
pinned = library_module.project_templates(project)["masonry_wet"]
|
|
assert pinned["imported_from"] == "program"
|
|
assert library_module.pin_program_templates(project) == 0 # 이미 박힌 것은 그대로
|
|
|
|
|
|
def test_항목마다_양식형_고정형_종류가_붙는다(tmp_path: Path) -> None:
|
|
"""PLAN 4장 「항목마다 종류 배지」 — 명세 13장: 칸은 같고 `formula` 유무로만 갈림."""
|
|
folder = tmp_path / "personal"
|
|
folder.mkdir()
|
|
fixed = {
|
|
"type_id": "masonry_wet",
|
|
"code": "AX-ST-0000beef",
|
|
"name": "뽑은 항목",
|
|
"rows": [{"name": "돌쌓기", "formula": "", "amount": 20.9}],
|
|
}
|
|
(folder / "AX-ST-0000beef.json").write_text(json.dumps(fixed), encoding="utf-8")
|
|
dirs = {"personal": folder, "program": library_module.TEMPLATE_DIR}
|
|
kinds = {item["tier"]: item["kind"] for item in library_module.list_items(dirs, "masonry_wet")}
|
|
assert kinds == {"personal": "fixed", "program": "form"}
|
|
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Library.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "양식형" in ui and "고정형" in ui and "item.kind" in ui
|
|
|
|
|
|
def test_코드_모양이_아니면_박지_않는다(project: Path) -> None:
|
|
with pytest.raises(ValueError):
|
|
library_module.import_item(project, {"type_id": "masonry_wet", "code": "../x"}, "personal")
|