feat(구조물표): 322-2 M02 표 화면 — 탭 셋(한눈 표 · 종류별 · 부품) · 반영 · 부품 · 알약 색 저장 · 목록 + enabled 함께 · 검사 짝(화면 = 서버 · 1-3 규칙 · 만든 뒤 고정) · 명세 폼 보임 따름 · 판 알림
- 한눈 표(M02_SO_Def_Table): 군 · 종류 ▲▼ · 이름 · 목록 · 자리꼴 · 정본(고정) · 횡단도 · 카드 이름 · 형상(+ 벽 기본 형태 · 물빼기) · 알약 색 · 3D · 검사 · 처음 값 = 흐림 「기본」 · 바꿈 = 진함 · [처음 값으로] - 종류별: 반영 칸 + 명세 이름 나무 줄마다 보임(컨테이너 = 그 안 칸) · 못 숨김 · 미리보기 숨김 따름 · 「안 씀」 = 목록 B06 만 - 부품 탭(M02_SO_Def_Parts): 부품 · 읽는 칸 · 자리꼴 · 정본 · 쓰는 종류 · 켬/끔(정의 부품 = 끈 것만) - 편집 손질(M02_SO_Def_Reflect): 처음 값이면 반영에서 지움 · [저장] 때 enabled = 목록 both 일 때만 참 - 검사 짝 M02_SO_Def_Check.ts ↔ M02_SO_Check.py(글까지 같음): 반영 · 부품 꼴 · 형상 none → 횡단도 none · 부품 자리꼴 · 정본 · 기본값 없는 읽는 칸 새로 숨김 금지(앞 판 숨김 그대로) · 종류 지우기 · 자리꼴 · 정본 · 군 글자 · 선택지 값 고정 · 서버 check(doc, 지금 판) · 앞 판 없으면 꼴만 - 명세 폼: 쓰는 옵션 키가 모두 숨긴 칸 · 상자는 안 그림(값 그대로) · B05 · B06 시설 · 구조물 명세 폼이 정의 「표시」를 따름 - 판 알림: GET 구조물 목록에 definition_version · structures.json 판 ≠ 지금 판이면 「구조물 정의가 판 N 으로 바뀜 — 저장하면 반영」 한 번 - 시험: test_322_2_check_mirror(거울 · 규칙 · 반영 손질 · 서버 저장 · 422) · 판 알림 · 옛 정의 화면 시험 = 선택지 값 대신 이름 표 · 못 숨김 아닌 칸 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""구조물 표 검사(PLAN 322-2 · 1-3 규칙) — 반영 · 부품 꼴 · 표 줄과 부품이 맞음 · 만든 뒤 고정.
|
||||
|
||||
- 반영 = `{종류: {목록 · 횡단도 · 카드이름 · 형상 · 형상칸 · 3D}}` · 값 = 부품 상자에 있는 것
|
||||
- 부품 = `{"갈래.부품": false}`(끈 것만)
|
||||
- 표 줄(정의 ⊕ 처음값) — 형상 none 이면 횡단도도 none(218) · 부품 자리꼴 · 정본 = 그 종류 줄 ·
|
||||
부품이 읽는 칸 중 기본값 없는 칸(비워 두는 뜻 칸 빼고)은 새로 숨기지 못함(정의 「표시」 · 숨기면
|
||||
그림이 늘 기본값을 씀 · 앞 판에서 이미 숨긴 칸은 그대로 둠)
|
||||
- 앞 판이 없으면(옛 판 읽기) 꼴만 검사
|
||||
- 앞 판과 비교 — 종류 지우기 없음 · 자리꼴 · 정본 · 군 글자 고정 · 선택지 값 고정(이름은 이름 표로)
|
||||
화면 짝 = `M02_SO_Def_Check.ts`(같은 글)
|
||||
거울 시험 = `resources/tester/test_322_2_check_mirror.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_structure_catalog import load_parts, resolve_catalog
|
||||
|
||||
BRANCHES = ("횡단도", "카드이름", "형상", "3D")
|
||||
OFF_VALUE = {"횡단도": "none", "카드이름": "name", "형상": "none", "3D": "none"}
|
||||
LISTS = ("both", "b06", "hidden")
|
||||
_FIELDS = {"목록", "형상칸", *BRANCHES}
|
||||
|
||||
|
||||
def _shape_errors(doc: dict[str, Any], known: set[str]) -> list[str]:
|
||||
"""반영 · 부품 꼴."""
|
||||
parts = load_parts()["parts"]
|
||||
out: list[str] = []
|
||||
reflect = doc.get("반영", {})
|
||||
if not isinstance(reflect, dict):
|
||||
out.append("반영은 {종류: 묶음} 이어야 함")
|
||||
else:
|
||||
for tid, row in reflect.items():
|
||||
if tid not in known or not isinstance(row, dict):
|
||||
out.append(f"반영 「{tid}」 — 없는 종류이거나 묶음 아님")
|
||||
continue
|
||||
for field, value in row.items():
|
||||
where = f"반영 「{tid}.{field}」"
|
||||
if field not in _FIELDS:
|
||||
out.append(f"{where} — 모르는 칸")
|
||||
elif field == "목록":
|
||||
if value not in LISTS:
|
||||
out.append(f"{where} — both · b06 · hidden 중 하나")
|
||||
elif field == "형상칸":
|
||||
ok = isinstance(value, dict) and all(
|
||||
(isinstance(v, str) and bool(v.strip()))
|
||||
if k == "form"
|
||||
else (k == "weep" and isinstance(v, bool))
|
||||
for k, v in value.items()
|
||||
)
|
||||
if not ok:
|
||||
out.append(f"{where} — form 글 · weep 참거짓")
|
||||
elif not isinstance(value, str) or (
|
||||
value != OFF_VALUE[field] and value not in parts[field]
|
||||
):
|
||||
shown = value if isinstance(value, str) else _js(value)
|
||||
out.append(f"{where} — 없는 부품 「{shown}」")
|
||||
off = doc.get("부품", {})
|
||||
if not isinstance(off, dict):
|
||||
out.append("부품은 {갈래.부품: 참거짓} 이어야 함")
|
||||
else:
|
||||
for key, value in off.items():
|
||||
branch, _, part = key.partition(".")
|
||||
if part not in parts.get(branch, {}):
|
||||
out.append(f"부품 「{key}」 — 없는 부품")
|
||||
elif not isinstance(value, bool):
|
||||
out.append(f"부품 「{key}」 — 참거짓")
|
||||
return out
|
||||
|
||||
|
||||
def _js(value: Any) -> str:
|
||||
"""TS `String(value)` 와 같은 글(검사 글 짝)."""
|
||||
if value is None:
|
||||
return "null"
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (list, tuple)):
|
||||
return ",".join(_js(v) for v in value)
|
||||
if isinstance(value, dict):
|
||||
return "[object Object]"
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return str(int(value))
|
||||
return str(value)
|
||||
|
||||
|
||||
def _hidden_in(doc: dict[str, Any] | None, tid: str) -> list[str]:
|
||||
row = ((doc or {}).get("표시") or {}).get(tid) or {}
|
||||
return [key for key, shown in row.items() if shown is False]
|
||||
|
||||
|
||||
def _row_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]:
|
||||
"""표 줄 — 218 규칙 · 부품 자리꼴 · 정본 · 읽는 칸 보임(새로 숨김만)."""
|
||||
parts = load_parts()["parts"]
|
||||
rows = resolve_catalog(doc)
|
||||
out: list[str] = []
|
||||
for t in doc.get("types") or []:
|
||||
row = rows.get(t["type_id"])
|
||||
if row is None:
|
||||
continue
|
||||
if row["shape"] == "none" and row["station"] != "none":
|
||||
out.append(f"「{row['name']}」 형상 none — 횡단도도 none 이어야 함")
|
||||
kept = _hidden_in(before or doc, t["type_id"]) # 앞 판 없음 = 새로 숨긴 것 없음
|
||||
hidden = [key for key in _hidden_in(doc, t["type_id"]) if key not in kept]
|
||||
# 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
|
||||
keys = {
|
||||
o["key"]
|
||||
for o in t.get("options") or []
|
||||
if o.get("default") is None and not o.get("empty_means")
|
||||
}
|
||||
used = (
|
||||
("횡단도", row["station"]),
|
||||
("카드이름", row["title"]),
|
||||
("형상", row["shape"]),
|
||||
("3D", row["model3d"]),
|
||||
)
|
||||
for branch, part_id in used:
|
||||
part = parts[branch].get(part_id)
|
||||
if part is None:
|
||||
continue
|
||||
if row["placement"] not in part["자리꼴"] or row["owner"] not in part["정본"]:
|
||||
out.append(
|
||||
f"「{row['name']}」 {branch} 「{part_id}」 — 자리꼴 {row['placement']} · "
|
||||
f"정본 {row['owner']} 에 안 맞음"
|
||||
)
|
||||
for key in part["읽는 칸"]:
|
||||
if key in keys and key in hidden:
|
||||
out.append(
|
||||
f"「{row['name']}」 {branch} 「{part_id}」 이 읽는 칸 「{key}」 — "
|
||||
"숨길 수 없음"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _fixed_errors(doc: dict[str, Any], before: dict[str, Any]) -> list[str]:
|
||||
"""앞 판과 비교 — 만든 뒤 고정."""
|
||||
out: list[str] = []
|
||||
now = {t["type_id"]: t for t in doc.get("types") or []}
|
||||
for old in before.get("types") or []:
|
||||
tid = old["type_id"]
|
||||
t = now.get(tid)
|
||||
if t is None:
|
||||
out.append(f"종류 「{tid}」 — 지울 수 없음")
|
||||
continue
|
||||
fixed = (
|
||||
("자리꼴", old.get("placement"), t.get("placement")),
|
||||
("정본", old.get("managed_by"), t.get("managed_by")),
|
||||
("군 글자", old.get("group"), t.get("group")),
|
||||
)
|
||||
for what, a, b in fixed:
|
||||
if a != b:
|
||||
out.append(f"「{tid}」 {what} — 만든 뒤 고정")
|
||||
options = {o["key"]: o for o in t.get("options") or []}
|
||||
for o in old.get("options") or []:
|
||||
n = options.get(o["key"])
|
||||
if o.get("input") != "select" or n is None:
|
||||
continue
|
||||
if sorted(o.get("choices") or []) != sorted(n.get("choices") or []):
|
||||
out.append(f"「{tid}.{o['key']}」 선택지 값 — 고칠 수 없음(이름은 이름 표로)")
|
||||
return out
|
||||
|
||||
|
||||
def catalog_errors(doc: dict[str, Any], before: dict[str, Any] | None) -> list[str]:
|
||||
"""구조물 표 검사 — 틀린 것 글들(빈 목록 = 통과). `before` = 지금 판(없으면 고정 검사 안 함)."""
|
||||
known = {t["type_id"] for t in doc.get("types") or [] if isinstance(t, dict)}
|
||||
shape = _shape_errors(doc, known)
|
||||
if shape:
|
||||
return shape
|
||||
return _row_errors(doc, before) + _fixed_errors(doc, before) if before else []
|
||||
Reference in New Issue
Block a user