"""M02 구조물 옵션 보이기(`/api/m02/structure-options` · 정본 `구조물옵션표시.json`). 못박는 것 - 파일이 없거나 `{}` 면 `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.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_Options as opt from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete.M02_MasterTemplete_Router import router as m02_router ROOT = Path(__file__).resolve().parents[2] URL = "/api/m02/structure-options" TYPES = "/api/projects/structure-types" @pytest.fixture def client(tmp_path: Path, monkeypatch) -> TestClient: monkeypatch.setattr(store, "FOLDER", tmp_path) 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: assert ( json.loads((ROOT / "resources/master_template/구조물옵션표시.json").read_text("utf-8")) == {} ) got = client.get(URL).json() assert got["판"] == "" 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={"판": "", "표시": {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={"판": "", "표시": {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((store.FOLDER / opt.FILE).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={"판": "", "표시": bad}).status_code == 422, bad assert client.put(URL, json={"판": "", "표시": []}).status_code == 422 # 묶음 아님(요청 모양) assert not (store.FOLDER / opt.FILE).exists() # 거절되면 안 씀 v1 = client.put(URL, json={"판": "", "표시": {tid: {off: False}}}).json()["판"] assert client.put(URL, json={"판": "낡음", "표시": {}}).status_code == 409 assert client.put(URL, json={"판": "", "표시": {}}).status_code == 409 # 처음 판으로 또 쓰기 assert client.get(URL).json()["판"] == v1 def test_파일이_깨져도_폼_응답은_모두_보임(client) -> None: (store.FOLDER / opt.FILE).write_text("{깨짐", encoding="utf-8") assert all(all(v.values()) for v in _visible(client).values()) assert client.get(URL).status_code == 500 # 관리 화면은 까닭을 그대로 올림 def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에서_지움() -> None: side = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts").read_text("utf-8") assert "section(options.title, options.body)" in side assert ( side.index(" summary,") < side.index("section(options.title") < side.index('section(L("M02_KindDrawing")') ) assert "onOpen: () => (options.clear(), paint())" in side # 자재 조합을 열면 이 줄 고름 지움 assert "onOpen: () => (matset.clear(), paint())" in side # 이 줄을 열면 자재 조합 고름 지움 paint = side.split("function paint(): void {")[1].split("\n }\n")[0] assert "if (opt.getOpen()) options.clear();" in paint # 양식을 열면 이 줄 고름 지움 api = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts").read_text("utf-8") assert ( 'call("/structure-options")' in api and 'call("/structure-options", { method: "PUT"' in api ) ui = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Options.ts").read_text("utf-8") assert 'o.phase === "detail"' in ui # B06 전용 표시