Files
Aislo/resources/tester/test_m02_structure_options.py
T
eomsangdonandClaude Sonnet 5.5 e1b17a523c feat(732-1): M02 실무문서 칸 · 복사 탭 · 서버 길 걷음
- 왼쪽 「실무문서」 칸(Practice.ts) · 일위대가 가져오기 「실무문서」 탭 · 복사 원본 고르기 · 낱 CSS · locale 열쇠 걷음
- 서버 `from_layer=practice`(_copy_practice) · `DELETE /api/m02/practice`(drop_practice) · 조합 목록 · 키 하나 읽기 길 걷음 · 조합 길 시스템 관리자 문 걷음(담을 로직 읽기는 그대로)
- 옛 마스터 조합 파일 `일위대가조합.json`(UA 37) = 쓰는 곳 있음(구조물집계표 일위대가 칸 · labor 본뜬키 · 프로젝트 초기값 사본 · 불러오기 맞춤 씨앗) → 파일 · 읽기 길 유지 · 지우지 않음
- 낡은 시험: test_m02_practice 지움(화면 · 길 없음) · 조합 길 읽던 시험(combo · combo_inputs · combo_layers)은 저장소 함수로 · 실무문서 복사 시험(labor_copy · pair_copy) 줄 지움 · 탭 · 차례 단정(layer_screen · structure_options · structure_panel · 720 · precombo_scope · ui_loading) 고침

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 13:17:24 +09:00

159 lines
7.5 KiB
Python

"""M02 구조물 옵션 보이기(`/api/m02/structure-options` · 정의 「표시」 칸 · PLAN 48-1).
못박는 것
- 「표시」 가 `{}` 면 `GET /api/projects/structure-types` 옵션이 모두 visible.
- 저장한 숨김이 그 응답 visible 에 반영 · 다른 종류 · 옵션은 그대로 · 옵션 정의는 안 바뀜.
- 저장은 숨김(false)만 남김 · [모두 보임] = `{}` · 없는 종류 · 키 · 참거짓 아님 422 · 낡은 판 409.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from B05_Profile import B05_Profile_Structures_Schema as schema
from B05_Profile.B05_Profile_Structures_Router import router as b05_router
from B05_Profile.B05_Profile_Structures_Schema import load_structure_types
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.StructOptions.M02_SO_Router import router as m02_router
ROOT = Path(__file__).resolve().parents[2]
URL = "/api/m02/structure-options"
TYPES = "/api/projects/structure-types"
def structure_type(type_id):
return next(t for t in load_structure_types() if t.type_id == type_id)
@pytest.fixture
def client(tmp_path: Path, monkeypatch) -> TestClient:
"""정의 사본(표시 · 그룹 빈 칸 · 판 1)으로 — 마스터 정본은 안 씀(PLAN 48-1)."""
monkeypatch.setattr(store, "FOLDER", tmp_path)
doc = json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))
copy = tmp_path / "구조물정의.json"
copy.write_text(json.dumps({**doc, "판": 1, "표시": {}, "그룹": {}}), "utf-8")
monkeypatch.setattr(schema, "DEFINITION_PATH", str(copy))
app = FastAPI()
app.include_router(m02_router)
app.include_router(b05_router)
return TestClient(app)
def _pick() -> tuple[str, str, str]:
"""옵션이 둘 이상인 종류 하나 — (type_id, 숨길 키, 남길 키)."""
t = next(t for t in load_structure_types() if len(t.options) >= 2)
return t.type_id, t.options[0].key, t.options[1].key
def _visible(client: TestClient) -> dict[str, dict[str, bool]]:
types = client.get(TYPES).json()["types"]
return {t["type_id"]: {o["key"]: o["visible"] for o in t["options"]} for t in types}
def test_빈_표시는_모두_보임(client) -> None:
got = client.get(URL).json()
assert got["판"] == "1" and got["표시"] == {}
assert all(all(v.values()) for v in _visible(client).values())
assert client.get(TYPES).json()["schema_version"] == 1
def test_숨김을_저장하면_폼_응답에_반영_정의는_그대로(client) -> None:
tid, off, on = _pick()
before = client.get(TYPES).json()["types"]
res = client.put(URL, json={"판": "1", "표시": {tid: {off: False, on: True}}})
assert res.status_code == 200, res.text
assert res.json()["표시"] == {tid: {off: False}} # 숨김만 남김
seen = _visible(client)
assert seen[tid][off] is False and seen[tid][on] is True
assert sum(not v for row in seen.values() for v in row.values()) == 1 # 다른 것은 그대로
after = client.get(TYPES).json()["types"]
strip = lambda ts: [ # noqa: E731
{**t, "options": [{k: v for k, v in o.items() if k != "visible"} for o in t["options"]]}
for t in ts
]
assert strip(before) == strip(after) # 옵션 정의(라벨 · 필수 · 선택지 …)는 안 바뀜
kind = next(k for k in client.get(URL).json()["종류"] if k["type_id"] == tid)
assert [o["key"] for o in kind["options"]][:2] == [off, on] and "phase" in kind["options"][0]
def test_모두_보임은_빈_표_판은_저장마다_바뀜(client) -> None:
tid, off, _ = _pick()
v1 = client.put(URL, json={"판": "1", "표시": {tid: {off: False}}}).json()["판"]
res = client.put(URL, json={"판": v1, "표시": {}})
assert res.status_code == 200 and res.json()["표시"] == {} and res.json()["판"] != v1
assert json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))["표시"] == {}
assert all(all(v.values()) for v in _visible(client).values())
def test_없는_종류_키_참거짓_아님은_422_낡은_판은_409(client) -> None:
tid, off, _ = _pick()
for bad in (
{"없는종류": {off: False}},
{tid: {"없는키": False}},
{tid: {off: "no"}},
{tid: [off]},
):
assert client.put(URL, json={"판": "1", "표시": bad}).status_code == 422, bad
assert client.put(URL, json={"판": "1", "표시": []}).status_code == 422 # 묶음 아님(요청 모양)
assert json.loads(Path(schema.DEFINITION_PATH).read_text("utf-8"))["판"] == 1 # 거절되면 안 씀
v1 = client.put(URL, json={"판": "1", "표시": {tid: {off: False}}}).json()["판"]
assert client.put(URL, json={"판": "낡음", "표시": {}}).status_code == 409
assert client.put(URL, json={"판": "1", "표시": {}}).status_code == 409 # 처음 판으로 또 쓰기
assert client.get(URL).json()["판"] == v1
def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에서_지움() -> None:
side = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts").read_text("utf-8")
# 716-3 — 옵션 줄은 M01 로 옮겨 좌측 칸에서 뺌(차례 = 일위대가 → 구조물 조합 → 도면 양식)
assert "options.title" not in side and "options.clear" not in side
order = side.split('className: "m02-side__lists ui-sidebar-panel",')[1].split("});")[0]
assert order.index("summary") < order.index("drawing,")
# 실무문서 칸은 걷음(732-1)
assert "practice" not in side
assert "matset" not in side
# 화면은 정의 한 벌로 읽고 씀(PLAN 48-2) · 보이기 체크는 정의 화면 옵션 줄
api = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Api.ts").read_text("utf-8")
assert 'call("/structure-definition")' in api
ui = (ROOT / "M02_MasterTemplete/StructOptions/M02_SO_Def_Options.ts").read_text("utf-8")
assert 'o.phase === "detail"' in ui # B06 전용 표시
def test_숨긴_옵션은_필수_아님_저장이_안_막힘(client, tmp_path, monkeypatch) -> None:
"""B05 필수(기본값 없음) 옵션 — 보이면 막고 숨기면 저장됨(다른 필수는 그대로)."""
from B05_Profile import B05_Profile_Structures_Repository as repo
from B05_Profile.B05_Profile_Structures_Schema import (
StructureInstance,
StructureOptionField,
StructureType,
)
def must(key):
return StructureOptionField(key=key, label=key, input="number", required=True, phase="b05")
fake = StructureType(
type_id="fake_hide",
group="Z",
name="가짜",
placement="point",
options=[must("a"), must("b")],
)
real = repo.structure_type_map()
monkeypatch.setattr(repo, "structure_type_map", lambda: {**real, "fake_hide": fake})
item = lambda opts: StructureInstance.model_validate( # noqa: E731
{"type_id": "fake_hide", "placement": "point", "chainage_m": 10.0, "options": opts}
)
root = str(tmp_path / "project")
with pytest.raises(ValueError, match="필수"):
repo.save_structures(root, [item({})], base_revision=0) # 보임 = 필수 그대로
monkeypatch.setattr(repo, "hidden_keys", lambda: {"fake_hide": {"a"}})
with pytest.raises(ValueError, match="필수"): # a 는 숨겨 통과 · b 는 여전히 필수
repo.save_structures(root, [item({})], base_revision=0)
assert (
repo.save_structures(root, [item({"b": 3})], base_revision=0) == 1
) # 숨긴 a 는 빈 채 저장