"""M02 구조물 옵션 보이기 — 구조물 종류마다 B05/B06 폼에 보일 옵션을 사용자가 체크. 정본 = 구조물 정의(`M02_SO_Definition` · PLAN 48-1) 「표시」 칸 = `{type_id: {option_key: false}}` — 없는 종류 · 키는 보임. 저장은 **숨김(false)만** 남김 — 「모두 보임」 = `{}`. 옵션 정의 · 옛 값은 그대로 — 폼이 그리는 것만 갈림(`GET /api/projects/structure-types` `visible`). 판 = 정의 판(글) · 낡은 판으로 쓰면 409 · 저장마다 정의 판 +1 · 이력. """ from __future__ import annotations import logging from typing import Any from pydantic import BaseModel, Field from B05_Profile.B05_Profile_Structures_Schema import ( StructureOptionField, StructureType, StructureTypesResponse, load_definition, load_structure_types, registry_schema_version, ) from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of logger = logging.getLogger(__name__) class OptionWithVisible(StructureOptionField): """폼 옵션 한 칸 + 보임 여부(서버가 채움).""" visible: bool = True class OptionGroup(BaseModel): """옵션 그룹 한 개(PLAN 46-2 · `M02_SO_Groups`) — 폼의 보조 컨테이너 하나 · 빈 옵션도 됨.""" id: str name: str options: list[str] = Field(default_factory=list) class TypeWithVisible(StructureType): options: list[OptionWithVisible] = Field(default_factory=list) groups: list[OptionGroup] = Field(default_factory=list) class TypesWithVisible(StructureTypesResponse): """`GET /structure-types` 응답 — 옵션마다 `visible` 칸 · 종류마다 `groups`.""" types: list[TypeWithVisible] def _load() -> tuple[dict[str, Any], str]: """(표시 표 · 판) — 정의 한 벌(`M02_SO_Definition`)의 「표시」 칸 · 판 = 정의 판 글.""" doc = load_definition() return doc.get("표시") or {}, str(doc.get("판", 1)) def _shown(doc: dict[str, Any], type_id: str, key: str) -> bool: row = doc.get(type_id) return not (isinstance(row, dict) and row.get(key) is False) def hidden_keys() -> dict[str, set[str]]: """숨긴 옵션(종류 → 키 집합) — 저장 검증이 숨긴 옵션을 필수로 안 침 · 깨진 표는 모두 보임.""" try: doc = _load()[0] except store.StoreError as e: logger.warning("구조물 옵션 표시 표를 못 읽어 모두 보임으로 봄 — %s", e.detail) return {} return { type_id: {key for key, shown in row.items() if shown is False} for type_id, row in doc.items() if isinstance(row, dict) } def with_visible() -> TypesWithVisible: """레지스트리 정본 + 보임 여부 — 파일이 깨졌으면 모두 보임(폼이 죽지 않게 · 로그만).""" try: doc = _load()[0] except store.StoreError as e: logger.warning("구조물 옵션 표시 표를 못 읽어 모두 보임으로 냄 — %s", e.detail) doc = {} groups = groups_of() types = [ { **t.model_dump(), "options": [ {**o.model_dump(), "visible": _shown(doc, t.type_id, o.key)} for o in t.options ], "groups": groups.get(t.type_id, []), } for t in load_structure_types() ] return TypesWithVisible.model_validate( {"schema_version": registry_schema_version(), "types": types} ) def listing() -> dict[str, Any]: """`{판, 표시, 종류}` — 표시 = 숨긴 것만 · 종류 = 옵션이 있는 종류와 그 옵션.""" doc, version = _load() hidden: dict[str, dict[str, bool]] = {} kinds = [] for t in load_structure_types(): if not t.options: continue off = {o.key: False for o in t.options if not _shown(doc, t.type_id, o.key)} if off: hidden[t.type_id] = off kinds.append( { "type_id": t.type_id, "group": t.group, "name": t.name, "enabled": t.enabled, "options": [ {"key": o.key, "label": o.label, "phase": o.phase, "required": o.required} for o in t.options ], } ) return {"판": version, "표시": hidden, "종류": kinds} def _clean(shown: Any, keys: dict[str, set[str]] | None = None) -> dict[str, dict[str, bool]]: """받은 표 검사 → 숨김만 남김. 없는 종류 · 키 · 참거짓 아닌 값은 422.""" if not isinstance(shown, dict): raise store.StoreError(400, "표시는 종류별 묶음이어야 함") if keys is None: keys = {t.type_id: {o.key for o in t.options} for t in load_structure_types()} errors: list[str] = [] out: dict[str, dict[str, bool]] = {} for type_id, row in shown.items(): if type_id not in keys: errors.append(f"없는 구조물 종류 「{type_id}」") elif not isinstance(row, dict): errors.append(f"「{type_id}」 는 옵션별 묶음이어야 함") else: for key, value in row.items(): if key not in keys[type_id]: errors.append(f"「{type_id}」 에 없는 옵션 「{key}」") elif not isinstance(value, bool): errors.append(f"「{type_id}.{key}」 는 참 · 거짓이어야 함") elif not value: out.setdefault(type_id, {})[key] = False if errors: raise store.StoreError(422, {"errors": errors}) return out def save(shown: Any, version: str) -> dict[str, Any]: """고쳐 씀 — 판이 같을 때만. 돌려주는 것 = 새 `listing()`.""" from M02_MasterTemplete.StructOptions.M02_SO_Definition import save_sections save_sections({"표시": _clean(shown)}, version) return listing()