- 정의 틀 = docs/raw/M02_구조물옵션정의/정의_틀.md (담는 것 · 키 규칙 · 꼴 · 배치 · 그룹 형상 · 규칙 · 동작 · 판 이력) - resources/master_template/구조물정의.json 판 1 = 옛 B05 레지스트리(37종 · 옵션 235) + 옵션표시 + 옵션그룹 키 그대로 옮김 - GET /api/m02/structure-definition(로그인) · PUT(시스템 관리자만 · 판 +1 · 이력 판_N) · 검사(꼴 · 키 겹침 · 규칙 대상) - GET /structure-types · B05 구조물 검사가 새 정의를 읽음(응답 꼴 그대로) · 옵션 보이기 · 그룹 저장도 정의 칸으로 - 프로젝트 structures.json 에 저장 때 정의 판 · 정의에 없는 키 값은 남김(오류 없음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
"""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()
|