"""구조물 표 검사(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 import re from typing import Any from common_util.common_util_structure_catalog import load_parts, resolve_catalog BRANCHES = ("횡단도", "카드이름", "형상", "3D") # 화면 글(322-2 다듬기 · TS `M02_SO_Def_Reflect` 짝) — 자리꼴 · 정본 한국어 · 부품 짧은 이름 PLACE_KO = {"point": "점", "interval": "구간", "site": "터"} OWNER_KO = {"pipe_points": "관 지점", "structures": "구조물"} def short_name(full: str) -> str: """괄호 설명을 뗀 부품 이름(TS `shortName` 짝).""" return re.sub(r"\s*\(.*\)\s*$", "", full) 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']}」 형상이 「없음」이면 횡단도도 「안 만듦」") 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"]), ) labels = {o["key"]: o.get("label") or o["key"] for o in t.get("options") or []} for branch, part_id in used: part = parts[branch].get(part_id) if part is None: continue name = short_name(part["이름"]) place = PLACE_KO.get(row["placement"], row["placement"]) owner = OWNER_KO.get(row["owner"], row["owner"]) if row["placement"] not in part["자리꼴"] or row["owner"] not in part["정본"]: out.append(f"「{row['name']}」 {branch} 「{name}」 — {place} · {owner} 에 안 맞음") for key in part["읽는 칸"]: if key in keys and key in hidden: out.append( f"「{row['name']}」 {branch} 「{name}」 이 쓰는 칸 「{labels[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"] name = old.get("name") or tid t = now.get(tid) if t is None: out.append(f"종류 「{name}」 — 지울 수 없음") 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"「{name}」 {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 []): label = o.get("label") or o["key"] out.append(f"「{name}」 「{label}」 선택지 값 — 고칠 수 없음(이름만 고침)") 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 []