"""M02 층 집계표(PLAN 21-2) — 층별 읽기 · 쓰기 · 권한 · 첫 진입 뼈대 · 항목 목록 · 항목 복사 · 시스템 회귀. 임시 storage(회사 · 개인 층) + 견본 시스템 집계표(`write_sample_table`)로만 봄 — 실제 양식은 안 읽음(시스템 층 두 자리 `store.FOLDER` · `layers.SYSTEM_ROOT` 모두 사본). 자재 코드는 실제 M01 마스터에 있는 MT 키를 씀(자재 조합 저장이 마스터 대조를 함). """ from __future__ import annotations import json from pathlib import Path from typing import Any import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from config import config_system from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router from M02_MasterTemplete import M02_MasterTemplete_Router_Layers as layers_router 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 TABLE = "table/구조물집계표" def _material() -> dict[str, Any]: for data in m01.loaded()[0].values(): for row in data.get("줄") or []: if str(row.get("키", "")).startswith("MT"): return {"코드": row["키"], "이름": row.get("이름"), "규격": None, "단위": None} raise AssertionError("마스터에 자재 키가 없음") @pytest.fixture() def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: system = tmp_path / "system" system.mkdir() write_sample_table(system) monkeypatch.setattr(store, "FOLDER", system) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) store.renumber() storage = tmp_path / "storage" storage.mkdir() monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "USER", "is_master": False} app = FastAPI() app.include_router(layers_router.router) app.include_router(items_router.router) app.dependency_overrides[verify_session] = lambda: session return {"client": TestClient(app), "session": session, "storage": storage, "system": system} def _as(world: dict, role: str = "USER", user: int = 42, company: int | None = 7) -> TestClient: world["session"].update(role=role, user_id=user, company_id=company) return world["client"] def _add_column(client: TestClient, layer: str, col: dict[str, Any]) -> None: url = f"/api/m02/layers/{layer}/templates/{TABLE}" got = client.get(url).json() got["문서"]["열"].append(col) put = client.put(url, json={"판": got["판"], "문서": got["문서"]}) assert put.status_code == 200, put.text COL = {"id": "cw", "머리": ["돌기슭막이", "찰쌓기", "회사형", None], "단위": "m", "꼴": "수"} def test_시스템_층은_그대로_항목_목록은_SS_줄(world): client = _as(world) rows = client.get("/api/m02/items").json() ss = store.read_combos()["줄"] assert [r["key"] for r in rows] == [r["키"] for r in ss] and rows[0]["key"] == "SS000001" assert set(rows[0]) == {"key", "names", *items_router.items_store.ITEM_FIELDS} assert set(rows[0]["names"]) == {"일위대가", "자재조합", "도번", "면적번", "재료번"} assert client.get("/api/m02/layers/system/structure-combos").json() == store.read_combos() # 시스템 층 쓰기는 SYSTEM_ADMIN 만 assert client.post("/api/m02/layers/system/structures/rv").status_code == 403 url = f"/api/m02/layers/system/templates/{TABLE}" got = client.get(url).json() assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 403 assert client.delete(f"{url}?판={got['판']}").status_code == 403 admin = _as(world, "SYSTEM_ADMIN") assert admin.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200 assert admin.delete(f"{url}?판={got['판']}").status_code == 409 # 쓰는 양식 def test_회사_층_첫_진입_뼈대와_쓰기_권한(world): client = _as(world) assert client.get("/api/m02/items?layer=company").json() == [] root = world["storage"] / "7" / "templates" table = json.loads((root / "table/구조물집계표.json").read_text("utf-8")) assert ( table["열"] == [] and table["층"] == json.loads((world["system"] / "table/구조물집계표.json").read_text("utf-8"))["층"] ) assert json.loads((root / "구조물집계표조합.json").read_text("utf-8"))["다음"] == 1 assert json.loads((root / "자재조합.json").read_text("utf-8"))["다음"] == 1 url = f"/api/m02/layers/company/templates/{TABLE}" got = client.get(url).json() got["문서"]["열"].append(COL) assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 403 _add_column(_as(world, "ADMIN"), "company", COL) items = _as(world).get("/api/m02/items?layer=company").json() # 같은 회사 일반 사용자 assert [(r["key"], r["열"]) for r in items] == [("SC7-000001", "cw")] # 회사 키(26-4a) assert items[0]["도번"].startswith("SD") and (root / "structure/cw.json").is_file() assert client.get("/api/m02/items?layer=company&owner=9").status_code == 403 # 남의 회사 # 일위대가 갈라 내기(27-2c) = 회사 층 쓰기 권한 — 일반 사용자 403 · 회사 관리자는 회사 조합(UC) branch = "/api/m02/layers/company/structures/cw/combo" body = {"본뜬키": "UA000001", "담은로직": [{"로직": "GF000221"}], "이름": "x"} assert _as(world).post(branch, json=body).status_code == 403 admin = _as(world, "ADMIN") made = admin.post(branch, json=body) assert made.status_code == 200 and made.json()["키"] == "UC7-000001" assert admin.delete(f"{url}?판={client.get(url).json()['판']}").status_code == 409 # 쓰는 양식 def test_개인_층은_본인만_회사_없으면_409(world): client = _as(world) _add_column(client, "personal", {**COL, "id": "p1"}) assert [r["열"] for r in client.get("/api/m02/items?layer=personal").json()] == ["p1"] assert (world["storage"] / "7" / "42" / "templates" / "area" / "p1.json").is_file() other = client.get("/api/m02/items?layer=personal&owner=7/43") assert other.status_code == 403 lonely = _as(world, company=None) groups = lonely.get("/api/m02/items?all=1").json() assert [(g["layer"], g["items"]) for g in groups][1:] == [("company", []), ("personal", [])] assert len(groups[0]["items"]) == len(store.read_combos()["줄"]) # 마스터는 보임 made = lonely.post( "/api/m02/layers/personal/material-combos", json={"줄": {"이름": "a", "담은자재": []}} ) assert made.status_code == 409 and "회사부터" in made.json()["detail"] def test_회사_항목을_개인으로_복사(world): admin = _as(world, "ADMIN") _add_column(admin, "company", COL) base = "/api/m02/layers/company" drawing = admin.get(f"{base}/templates/structure/cw").json() doc = {**drawing["문서"], "도면": {**drawing["문서"]["도면"], "entities": [{"type": "line"}]}} admin.put(f"{base}/templates/structure/cw", json={"판": drawing["판"], "문서": doc}) 판 = admin.get(f"{base}/material-combos").json()["판"] row = {"이름": "회사 자재", "담은자재": [_material()]} um = admin.post(f"{base}/material-combos", json={"판": 판, "줄": row}).json()["키"] admin.post(f"{base}/structures/cw/use-combo", json={"종류": "자재조합", "키": um}) admin.post(f"{base}/structures/cw/use-combo", json={"종류": "일위대가", "키": "UA000004"}) row = admin.get("/api/m02/items?layer=company").json()[0] ss = row["key"] ua_name = combos.find("UA000004")["이름"] assert row["names"] == { # 키 대신 화면 이름 "일위대가": ua_name, "자재조합": "회사 자재", "도번": "돌기슭막이 찰쌓기 회사형", "면적번": "돌기슭막이 찰쌓기 회사형", "재료번": "돌기슭막이 찰쌓기 회사형", } user = _as(world) body = {"from_layer": "company", "from_owner": "7", "key": ss} got = user.post("/api/m02/layers/personal/items/copy", json=body) assert got.status_code == 200, got.text made = got.json() assert made["열"] == "cw" and made["key"] == "SP42-000001" # 응답 = 개인 층 새 키(바로 고름) mine_rows = user.get("/api/m02/items?layer=personal").json() assert mine_rows[0]["key"] == made["key"] and mine_rows[0]["names"]["일위대가"] == ua_name mine = "/api/m02/layers/personal" copy = user.get(f"{mine}/templates/structure/cw").json()["문서"] assert copy["도면"]["entities"] == [{"type": "line"}] and copy["열"] == "cw" col = next(c for c in user.get(f"{mine}/templates/{TABLE}").json()["문서"]["열"]) assert col["본뜬"] == {"layer": "company", "owner": "7", "key": ss} mat = user.get(f"{mine}/material-combos/{col['자재조합']}").json() assert mat["이름"] == "회사 자재" and mat["담은자재"][0]["코드"] == _material()["코드"] # 또 복사하면 새 열 id · 새 SS 키 · 자재 조합 이름 (2) again = user.post("/api/m02/layers/personal/items/copy", json=body).json() assert (again["열"], again["key"]) == ("cw_2", "SP42-000002") names = [r["이름"] for r in user.get(f"{mine}/material-combos").json()["줄"]] assert names == ["회사 자재", "회사 자재 (2)"] # 마스터 항목도 개인으로 · 없는 키 404 master = {"from_layer": "system", "key": "SS000001"} assert user.post("/api/m02/layers/personal/items/copy", json=master).status_code == 200 missing = {**body, "key": "SS999999"} assert user.post("/api/m02/layers/personal/items/copy", json=missing).status_code == 404