feat(M02): 48-1 구조물 정의 한 벌 — 정의 틀 문서 · 판 1 옮김 · 읽기 길

- 정의 틀 = 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
This commit is contained in:
2026-10-05 16:09:33 +09:00
co-authored by Claude Opus 5.5
parent 4600b9970c
commit bf1fa50817
22 changed files with 1194 additions and 285 deletions
@@ -1,15 +1,14 @@
"""M02 구조물 옵션 보이기 — 구조물 종류마다 B05/B06 폼에 보일 옵션을 사용자가 체크.
정본 = `resources/master_template/구조물옵션표시.json` = `{type_id: {option_key: false}}`
— 없는 종류 · 키는 보임(파일이 없어도 모두 보임).
정본 = 구조물 정의(`M02_SO_Definition` · PLAN 48-1) 「표시」 칸 = `{type_id: {option_key: false}}`
— 없는 종류 · 키는 보임.
저장은 **숨김(false)만** 남김 — 「모두 보임」 = `{}`.
옵션 정의 · 옛 값은 그대로 — 폼이 그리는 것만 갈림(`GET /api/projects/structure-types` `visible`).
판 = 파일 sha256 앞 16자(`store.version_of`) · 낡은 판으로 쓰면 409.
판 = 정의 판(글) · 낡은 판으로 쓰면 409 · 저장마다 정의 판 +1 · 이력.
"""
from __future__ import annotations
import json
import logging
from typing import Any
@@ -19,14 +18,13 @@ from B05_Profile.B05_Profile_Structures_Schema import (
StructureOptionField,
StructureType,
StructureTypesResponse,
load_definition,
load_structure_types,
registry_schema_version,
)
from common_util.common_util_json import atomic_write_json
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.StructOptions.M02_SO_Groups import groups_of
FILE = "구조물옵션표시.json"
logger = logging.getLogger(__name__)
@@ -56,18 +54,9 @@ class TypesWithVisible(StructureTypesResponse):
def _load() -> tuple[dict[str, Any], str]:
"""(숨김 표 · 판) — 파일이 없으면 빈 표 · 판 빈 글."""
path = store.FOLDER / FILE
if not path.is_file():
return {}, ""
raw = path.read_bytes()
try:
doc = json.loads(raw)
except ValueError as e:
raise store.StoreError(500, f"{FILE} 을 못 읽음 — {e}") from e
if not isinstance(doc, dict):
raise store.StoreError(500, f"{FILE} 은 종류별 묶음이어야 함")
return doc, store.version_of(raw)
"""(표시 표 · 판) — 정의 한 벌(`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:
@@ -138,11 +127,12 @@ def listing() -> dict[str, Any]:
return {"판": version, "표시": hidden, "종류": kinds}
def _clean(shown: Any) -> dict[str, dict[str, bool]]:
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, "표시는 종류별 묶음이어야 함")
keys = {t.type_id: {o.key for o in t.options} for t in load_structure_types()}
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():
@@ -165,11 +155,7 @@ def _clean(shown: Any) -> dict[str, dict[str, bool]]:
def save(shown: Any, version: str) -> dict[str, Any]:
"""고쳐 씀 — 판이 같을 때만. 돌려주는 것 = 새 `listing()`."""
fixed = _clean(shown)
path = store.FOLDER / FILE
with store._LOCK:
have = store.version_of(path.read_bytes()) if path.is_file() else ""
if have != (version or ""):
raise store.StoreError(409, {"stale": [FILE], "판": have})
atomic_write_json(path, fixed)
from M02_MasterTemplete.StructOptions.M02_SO_Definition import save_sections
save_sections({"표시": _clean(shown)}, version)
return listing()