feat(M02): 구조물 옵션 보이기 — 종류마다 B05/B06 폼에 보일 옵션 체크 · 표 · 서버 길 · 좌측 줄 · 화면
- 표 resources/master_template/구조물옵션표시.json(숨김만 저장 · 없으면 모두 보임) - GET /api/projects/structure-types 옵션마다 visible · GET/PUT /api/m02/structure-options - M02 좌측 「구조물 옵션 보이기」 줄 → 메인 종류(군별) → 옵션 체크 · [저장] · [모두 보임] Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""M02 구조물 옵션 보이기 — 구조물 종류마다 B05/B06 폼에 보일 옵션을 사용자가 체크.
|
||||
|
||||
정본 = `resources/master_template/구조물옵션표시.json` = `{type_id: {option_key: false}}`
|
||||
— 없는 종류 · 키는 보임(파일이 없어도 모두 보임).
|
||||
저장은 **숨김(false)만** 남김 — 「모두 보임」 = `{}`.
|
||||
옵션 정의 · 옛 값은 그대로 — 폼이 그리는 것만 갈림(`GET /api/projects/structure-types` `visible`).
|
||||
판 = 파일 sha256 앞 16자(`store.version_of`) · 낡은 판으로 쓰면 409.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from B05_Profile.B05_Profile_Structures_Schema import (
|
||||
StructureOptionField,
|
||||
StructureType,
|
||||
StructureTypesResponse,
|
||||
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
|
||||
|
||||
FILE = "구조물옵션표시.json"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OptionWithVisible(StructureOptionField):
|
||||
"""폼 옵션 한 칸 + 보임 여부(서버가 채움)."""
|
||||
|
||||
visible: bool = True
|
||||
|
||||
|
||||
class TypeWithVisible(StructureType):
|
||||
options: list[OptionWithVisible] = Field(default_factory=list)
|
||||
|
||||
|
||||
class TypesWithVisible(StructureTypesResponse):
|
||||
"""`GET /structure-types` 응답 — 옵션마다 `visible` 칸."""
|
||||
|
||||
types: list[TypeWithVisible]
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 with_visible() -> TypesWithVisible:
|
||||
"""레지스트리 정본 + 보임 여부 — 파일이 깨졌으면 모두 보임(폼이 죽지 않게 · 로그만)."""
|
||||
try:
|
||||
doc = _load()[0]
|
||||
except store.StoreError as e:
|
||||
logger.warning("구조물 옵션 표시 표를 못 읽어 모두 보임으로 냄 — %s", e.detail)
|
||||
doc = {}
|
||||
types = [
|
||||
{
|
||||
**t.model_dump(),
|
||||
"options": [
|
||||
{**o.model_dump(), "visible": _shown(doc, t.type_id, o.key)} for o in t.options
|
||||
],
|
||||
}
|
||||
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) -> 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()}
|
||||
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()`."""
|
||||
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)
|
||||
return listing()
|
||||
Reference in New Issue
Block a user