Files
Aislo/resources/tester/test_m02_item_inputs.py
T
eomsangdonandClaude Opus 5.5 2a3051fbca feat(M02): 마스터 구조물집계표 초기화 · 시험 견본화(PLAN 26-5a)
- 마스터 층 구조물 항목 51개 지움 — 나무 지우기(tree.apply delete) → 집계표 저장(store.write) 한 줄기 · 집계표 구조물 열 · SS 장부 줄 · structure/area/material 파일 153개 · 남김 = no · sta · 비고 열 · 변수 · 쪽줄 · 합계줄 · 바인딩규칙 · 자재조합 · 구조물옵션표시 · drawing · M01 · 회사 · 개인 층 · 프로젝트 복사본 · 지운 열의 열너비만 뺌
- 키 · 번호 다시 안 씀 — 장부 다음 52(새 항목 SS000052) · 지운번호 51개 기록(새 번호는 SD-000001 모양이라 옛 8자리와 안 겹침)
- 지우기 전 사본 tmp/마스터_집계표_초기화전_0930/(git 밖 · 156 파일 = 지우기 전과 같음)
- 시험 안 견본 fixtures/m02_master_sample(옛 집계표 · 장부 · 자재조합 · rv · c4 · pp_len · ms 세 파일 · 도면 줄임) + helper_m02_master_sample — 실제 마스터를 읽던 시험 일곱(table_fill · template_layers · structures_summary_snapshot · item_copy_complete · m02_tree · m02_item_inputs · spreadsheet_saved_values)을 견본으로 · M01 쓰는 곳 찾기(users_of) 시험은 이미 임시 표(combo_inputs)라 그대로
- 새 시험 test_m02_empty_master — 빈 마스터에서 항목 0 · 빈 나무 · 첫 화면 길 · 새 폴더 · 새 항목(SS000052 · SD-000001) · 지우기 · 고르기 모달 마스터 0 · 프로젝트 복사본 calc 그대로 · 원본없음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
2026-09-30 09:41:04 +09:00

241 lines
12 KiB
Python

"""M02 항목 선택 입력(PLAN 25-1b · 25-2a) — 입력 목록 · 항목 연결 파일 · 알약 · 권한.
① 입력 목록은 시험 안 마스터 견본(`fixtures/m02_master_sample` 사본 · PLAN 26-5a)으로 —
일위대가 있는 항목 셋에서 로직 수 · 입력 수 · 자동값이 M01 조합 미리 보기가 쓰는 것(로직 줄
`입력` · `master_auto`)과 같은지 · 로직마다 갈라 합치지 않는지 · 일위대가 없는 항목은 빈 목록.
② 연결 파일 쓰기는 임시 storage(회사 · 개인 층) + 견본 시스템 집계표로만 — 판 409 · 없는 키 422 ·
사라짐 표시 · 알약 · 층 권한 · 열 지우기 · 항목 복사 · 분류 더하기 복사가 파일을 같이 다룸.
"""
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 M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store
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.helper_m02_master_sample import copy_master_sample
from resources.tester.test_m02_structure import write_sample_table
TABLE = "table/구조물집계표"
UA = "UA000004" # 견본 항목 셋(rv · c4 · c1)이 쓰는 조합 — 담은 로직 여럿
OTHER_UA = "UA000007"
def _app(session: dict[str, Any]) -> TestClient:
app = FastAPI()
app.include_router(layers_router.router)
app.include_router(items_router.router)
app.dependency_overrides[verify_session] = lambda: session
return TestClient(app)
def _m01_view(ua: str) -> list[tuple[str, list[str], dict]]:
"""M01 조합 미리 보기가 그리는 것 — 담은 로직마다 (키, 입력 이름들, 자동값)."""
row = next(r for r in m01.read(combo_store.FILE)[0]["줄"] if r["키"] == ua)
whole = m01.loaded()[1]
out = []
for one in row["담은로직"]:
try:
logic = whole.logic(one["로직"])
except combo_store.cm.mf.FormulaError:
continue # 화면도 못 읽은 로직은 건너뜀
auto = inputs_store._num(m01.auto(one["로직"])["값"])
out.append((one["로직"], [s["이름"] for s in logic.get("입력") or []], auto))
return out
# ── ① 입력 목록 — 마스터 견본 사본 ─────────────────────
def test_입력_목록은_M01_조합_미리_보기와_같음(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(store, "FOLDER", copy_master_sample(tmp_path / "master"))
client = _app({"user_id": 1, "company_id": 1, "role": "USER", "is_master": False})
rows = [r for r in json.loads((store.FOLDER / store.COMBO_FILE).read_bytes())["줄"]]
with_ua = [r for r in rows if r.get("일위대가")]
picked, seen = [], set()
for r in with_ua: # 서로 다른 조합 셋
if r["일위대가"] not in seen:
picked.append(r)
seen.add(r["일위대가"])
if len(picked) == 3:
break
assert len(picked) == 3
for r in picked:
res = client.get(f"/api/m02/layers/system/items/{r['키']}/inputs")
assert res.status_code == 200, res.text # 일반 사용자 읽기
got = res.json()
want = _m01_view(r["일위대가"])
assert got["일위대가"] == r["일위대가"] and got["이름"]
assert [lg["로직"] for lg in got["로직"]] == [k for k, _, _ in want]
for logic, (_, names, auto) in zip(got["로직"], want):
assert [i["이름"] for i in logic["입력"]] == names # 로직마다 · 합치지 않음
assert {i["이름"]: i["자동값"] for i in logic["입력"]} == auto
assert all({"이름", "설명", "단위", "자동값"} <= set(i) for i in logic["입력"])
# 이름이 같은 입력이 두 로직에 있어도 두 번 나옴
names = [
i["이름"]
for r in with_ua
for lg in client.get(f"/api/m02/layers/system/items/{r['키']}/inputs").json()["로직"]
for i in lg["입력"]
]
assert len(names) > len(set(names))
no_ua = next(r for r in rows if not r.get("일위대가"))
empty = client.get(f"/api/m02/layers/system/items/{no_ua['키']}/inputs").json()
assert empty == {"일위대가": None, "이름": None, "로직": [], "빠진로직": []}
assert client.get("/api/m02/layers/system/items/SS999999/inputs").status_code == 404
# ── ② 연결 파일 — 임시 storage ─────────────────────────
@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}
return {"client": _app(session), "session": session, "storage": storage}
def _as(world: dict, role: str = "USER", user: int = 42) -> TestClient:
world["session"].update(role=role, user_id=user)
return world["client"]
def _put_table(client: TestClient, layer: str, edit) -> None:
url = f"/api/m02/layers/{layer}/templates/{TABLE}"
got = client.get(url).json()
edit(got["문서"]["열"])
res = client.put(url, json={"판": got["판"], "문서": got["문서"]})
assert res.status_code == 200, res.text
def _col(cid: str, ua: str | None = UA) -> dict[str, Any]:
return {
"id": cid,
"머리": ["sub4 돌쌓기", cid, None, None],
"단위": "㎡",
"꼴": "수",
"일위대가": ua,
}
def _key(client: TestClient, layer: str, cid: str) -> str:
items = client.get(f"/api/m02/items?layer={layer}").json()
return next(r["key"] for r in items if r["열"] == cid)
def test_연결_저장_다시_읽기_판_422_사라짐_알약(world):
client = _as(world)
_put_table(client, "personal", lambda cols: cols.append(_col("sub4_a")))
key = _key(client, "personal", "sub4_a")
base = f"/api/m02/layers/personal/items/{key}"
listing = client.get(f"{base}/inputs").json()
first, second = listing["로직"][0], listing["로직"][1]
assert client.get(f"{base}/input-links").json() == {"판": None, "줄": []}
rows = [
{"로직": first["로직"], "입력": first["입력"][0]["이름"], "자리": "면적"},
{"로직": second["로직"], "입력": second["입력"][0]["이름"], "자리": "재료"},
{"로직": first["로직"], "입력": first["입력"][1]["이름"], "자리": "선택"},
]
saved = client.put(f"{base}/input-links", json={"판": None, "줄": rows})
assert saved.status_code == 200, saved.text
assert saved.json()["판"] == 1
again = client.get(f"{base}/input-links").json()
assert again == saved.json()
assert again["줄"][0]["알약"] == f"입력|{first['로직']}|{first['입력'][0]['이름']}"
assert "알약" not in again["줄"][2] # 선택은 시트 알약이 아님
file = world["storage"] / "7" / "42" / "templates" / "inputs" / "sub4_a.json"
assert json.loads(file.read_text("utf-8")) == saved.json()
# 판 다르면 409 · 없는 키 · 모르는 자리 · 두 번은 422
assert client.put(f"{base}/input-links", json={"판": None, "줄": rows}).status_code == 409
for bad in (
[{"로직": "GF999999", "입력": "높이", "자리": "면적"}],
[{"로직": first["로직"], "입력": "없는입력", "자리": "면적"}],
[{"로직": first["로직"], "입력": first["입력"][0]["이름"], "자리": "옆"}],
[rows[0], rows[0]],
):
res = client.put(f"{base}/input-links", json={"판": 1, "줄": bad})
assert res.status_code == 422, res.text
# 알약 — 면적 = 조합 물량 + 면적으로 이은 입력 · 재료 = 재료로 이은 입력만
area = client.get(f"{base}/pills?place=면적").json()
assert area["물량"] and [p["키"] for p in area["입력"]] == [again["줄"][0]["알약"]]
mat = client.get(f"{base}/pills?place=재료").json()
assert mat["물량"] == [] and [p["입력"] for p in mat["입력"]] == [second["입력"][0]["이름"]]
assert " · " in mat["입력"][0]["이름"] # 입력 · 로직 이름(합치지 않음)
# 일위대가가 바뀌면 옛 로직 연결은 읽을 때 사라짐 · 알약에서 빠짐
_put_table(client, "personal", lambda cols: cols[-1].update(일위대가=OTHER_UA))
gone = client.get(f"{base}/input-links").json()["줄"]
assert all(r.get("사라짐") for r in gone)
assert client.get(f"{base}/pills?place=면적").json()["입력"] == []
def test_권한_일반_사용자_읽기_쓰기(world):
admin = _as(world, "SYSTEM_ADMIN")
_put_table(admin, "system", lambda cols: cols.append(_col("sub4_sys")))
key = _key(admin, "system", "sub4_sys")
base = f"/api/m02/layers/system/items/{key}"
user = _as(world)
assert user.get(f"{base}/inputs").status_code == 200
assert user.get(f"{base}/input-links").status_code == 200
assert user.put(f"{base}/input-links", json={"판": None, "줄": []}).status_code == 403
assert _as(world, "SYSTEM_ADMIN").put(f"{base}/input-links", json={"줄": []}).status_code == 200
# 회사 층 — 같은 회사 일반 사용자 읽기 · 쓰기 403 · 회사 관리자 쓰기
_put_table(_as(world, "ADMIN"), "company", lambda cols: cols.append(_col("sub4_c")))
ckey = _key(_as(world), "company", "sub4_c")
cbase = f"/api/m02/layers/company/items/{ckey}"
assert _as(world).get(f"{cbase}/inputs").status_code == 200
assert _as(world).put(f"{cbase}/input-links", json={"줄": []}).status_code == 403
assert _as(world, "ADMIN").put(f"{cbase}/input-links", json={"줄": []}).status_code == 200
# 남의 개인 층은 못 읽음
assert _as(world).get(f"{cbase}/inputs?owner=9").status_code == 403
def test_열_지우기_항목_복사_분류_복사가_연결을_같이(world):
admin = _as(world, "ADMIN")
_put_table(admin, "company", lambda cols: cols.append(_col("sub4_c")))
key = _key(admin, "company", "sub4_c")
base = f"/api/m02/layers/company/items/{key}"
logic = admin.get(f"{base}/inputs").json()["로직"][0]
rows = [{"로직": logic["로직"], "입력": logic["입력"][0]["이름"], "자리": "면적"}]
assert admin.put(f"{base}/input-links", json={"판": None, "줄": rows}).status_code == 200
# 항목 개인 복사 — 새 열에 연결이 판 1 로
user = _as(world)
made = user.post(
"/api/m02/layers/personal/items/copy", json={"from_layer": "company", "key": key}
).json()
mine = user.get(f"/api/m02/layers/personal/items/{made['key']}/input-links").json()
assert mine["판"] == 1 and [r["입력"] for r in mine["줄"]] == [rows[0]["입력"]]
# 분류 더하기(본뜬열) — 새 열이 원본 열 연결을 받음
admin = _as(world, "ADMIN")
twin = {**_col("sub4_c2", None), "본뜬열": "sub4_c"}
_put_table(admin, "company", lambda cols: cols.append(twin))
folder = world["storage"] / "7" / "templates"
with store.at(folder):
assert inputs_store.read_file("sub4_c2")["줄"] == inputs_store.read_file("sub4_c")["줄"]
# 열 지우기 — 연결 파일도 같이 지움
_put_table(
admin, "company", lambda cols: cols.remove(next(c for c in cols if c["id"] == "sub4_c"))
)
assert not (folder / "inputs" / "sub4_c.json").is_file()
assert (folder / "inputs" / "sub4_c2.json").is_file()