Files
Aislo/M02_MasterTemplete/StructOptions/M02_SO_Def_Check.ts
T
eomsangdonandClaude Opus 5.5 0da2fa0767 fix(구조물표): 322-2 다듬기(브레인 화면 확인) — 화면에 영어 키 안 보임 · 검사 글 한국어(화면 · 서버 짝) · 부품 탭 읽는 칸 = 정의 이름 · 줄바꿈 · 한눈 표 폭 줄임 · 바꿨다 되돌리면 [저장] 꺼짐
- 자리꼴 점 · 구간 · 터 · 정본 관 지점 · 구조물(키는 툴팁) · 고르기 글 = 부품 짧은 이름(괄호 설명 · 키는 툴팁) · 횡단도 없음 = 「안 만듦」 · 「벽 스윕」 → 「벽」 · 종류 키는 툴팁
- 검사 글: 「형상이 「없음」이면 횡단도도 「안 만듦」」 · 부품 짧은 이름 · 칸 이름 · 종류 이름(M02_SO_Def_Check.ts = M02_SO_Check.py)
- 부품 탭: 읽는 칸 = 그 부품을 쓰는 종류의 칸 이름(단위) · 표 줄바꿈(table-layout fixed)
- 한눈 표 형상칸 「기본 형태」 = 그림 형태 고르기(REVET_FORMS)
- [저장] 켬 = 저장할 꼴(tidy · 목록 짝 enabled)끼리 지금 판과 다를 때만(M02_SO_Def_Reflect.differs)
- 시험: 322-2 검사 글 · 되돌림 견본

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-14 16:29:30 +09:00

200 lines
8.6 KiB
TypeScript

/* =============================================================================
* M02_SO_Def_Check.ts
* 구조물 표 검사(PLAN 322-2 · 1-3 규칙) — 정의 `반영` · `부품` 꼴 · 표 줄과 부품이 맞음 · 만든 뒤 고정.
*
* · 반영 = `{종류: {목록 · 횡단도 · 카드이름 · 형상 · 형상칸 · 3D}}` · 값 = 부품 상자에 있는 것
* · 부품 = `{"갈래.부품": false}`(끈 것만)
* · 표 줄(정의 ⊕ 처음값) — 형상 none 이면 횡단도도 none(218) · 부품 자리꼴 · 정본 = 그 종류 줄 ·
* 부품이 읽는 칸 중 기본값 없는 칸(비워 두는 뜻 칸 빼고)은 새로 숨기지 못함(정의 「표시」 · 숨기면
* 그림이 늘 기본값을 씀 · 앞 판에서 이미 숨긴 칸은 그대로 둠)
* · 앞 판이 없으면(옛 판 읽기) 꼴만 검사
* · 앞 판과 비교 — 종류 지우기 없음 · 자리꼴 · 정본 · 군 글자 고정 · 선택지 값 고정(이름은 이름 표로)
* 서버 짝 = `M02_SO_Check.py`(같은 글) · 거울 시험 = `resources/tester/test_322_2_check_mirror.py`.
* ========================================================================== */
import PARTS from "../../common_util/common_util_structure_parts.json" with { type: "json" };
import {
resolveCatalog,
type CatalogDefinition,
} from "../../common_util/common_util_structure_catalog";
import { OWNER_KO, PLACE_KO, shortName } from "./M02_SO_Def_Reflect";
type Branch = "횡단도" | "카드이름" | "형상" | "3D";
interface Part {
이름: string;
"읽는 칸": string[];
자리꼴: string[];
정본: string[];
}
const PARTS_BY = (PARTS as unknown as { parts: Record<Branch, Record<string, Part>> }).parts;
export const BRANCHES: readonly Branch[] = ["횡단도", "카드이름", "형상", "3D"];
/** 갈래마다 「없음」 값 — 카드이름은 종류 이름(name). */
export const OFF_VALUE: Record<Branch, string> = {
횡단도: "none",
카드이름: "name",
형상: "none",
"3D": "none",
};
export const LISTS = ["both", "b06", "hidden"] as const;
const FIELDS = new Set(["목록", "형상칸", ...BRANCHES]);
/** 검사가 읽는 정의 칸. */
export interface CheckDef extends CatalogDefinition {
types?: ReadonlyArray<{
type_id: string;
name?: string;
group?: string;
placement?: string;
managed_by?: string | null;
options?: ReadonlyArray<{
key: string;
input?: string;
choices?: readonly string[];
default?: unknown;
empty_means?: string | null;
label?: string;
}>;
}>;
표시?: Record<string, Record<string, boolean>>;
}
const isObject = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
/** 부품 상자에 있는 부품 — 제 칸만(물려받은 이름 아님). */
const partOf = (branch: string, id: string): Part | undefined => {
const box = (PARTS_BY as Record<string, Record<string, Part>>)[branch];
return box && Object.hasOwn(box, id) ? box[id] : undefined;
};
/** 반영 · 부품 꼴. */
function shapeErrors(doc: CheckDef, known: Set<string>): string[] {
const out: string[] = [];
const reflect: unknown = doc.반영 ?? {};
if (!isObject(reflect)) out.push("반영은 {종류: 묶음} 이어야 함");
else
for (const [tid, row] of Object.entries(reflect)) {
if (!known.has(tid) || !isObject(row)) {
out.push(`반영 「${tid}」 — 없는 종류이거나 묶음 아님`);
continue;
}
for (const [field, value] of Object.entries(row)) {
const where = `반영 「${tid}.${field}」`;
if (!FIELDS.has(field)) out.push(`${where} — 모르는 칸`);
else if (field === "목록") {
if (!(LISTS as readonly unknown[]).includes(value))
out.push(`${where} — both · b06 · hidden 중 하나`);
} else if (field === "형상칸") {
const ok =
isObject(value) &&
Object.entries(value).every(([k, v]) =>
k === "form"
? typeof v === "string" && !!v.trim()
: k === "weep" && typeof v === "boolean",
);
if (!ok) out.push(`${where} — form 글 · weep 참거짓`);
} else {
const branch = field as Branch;
if (typeof value !== "string" || (value !== OFF_VALUE[branch] && !partOf(branch, value)))
out.push(`${where} — 없는 부품 「${String(value)}」`);
}
}
}
const parts: unknown = doc.부품 ?? {};
if (!isObject(parts)) out.push("부품은 {갈래.부품: 참거짓} 이어야 함");
else
for (const [key, value] of Object.entries(parts)) {
const at = key.indexOf(".");
if (at < 0 || !partOf(key.slice(0, at), key.slice(at + 1)))
out.push(`부품 「${key}」 — 없는 부품`);
else if (typeof value !== "boolean") out.push(`부품 「${key}」 — 참거짓`);
}
return out;
}
const hiddenIn = (doc: CheckDef | null, tid: string): string[] =>
Object.entries(doc?.표시?.[tid] ?? {})
.filter(([, shown]) => shown === false)
.map(([key]) => key);
/** 표 줄 — 218 규칙 · 부품 자리꼴 · 정본 · 읽는 칸 보임(새로 숨김만). */
function rowErrors(doc: CheckDef, before: CheckDef | null): string[] {
const out: string[] = [];
const rows = resolveCatalog(doc);
for (const t of doc.types ?? []) {
const row = rows.get(t.type_id);
if (!row) continue;
if (row.shape === "none" && row.station !== "none")
out.push(`「${row.name}」 형상이 「없음」이면 횡단도도 「안 만듦」`);
const kept = hiddenIn(before ?? doc, t.type_id); // 앞 판 없음 = 새로 숨긴 것 없음
const hidden = hiddenIn(doc, t.type_id).filter((key) => !kept.includes(key));
// 숨기면 그림이 기본값을 씀 — 기본값도 비워 두는 뜻도 없는 칸만 못 숨김
const keys = new Set(
(t.options ?? [])
.filter((o) => (o.default === null || o.default === undefined) && !o.empty_means)
.map((o) => o.key),
);
const labelOf = (key: string): string =>
(t.options ?? []).find((o) => o.key === key)?.label ?? key;
const used: Array<[Branch, string]> = [
["횡단도", row.station],
["카드이름", row.title],
["형상", row.shape],
["3D", row.model3d],
];
for (const [branch, id] of used) {
const part = partOf(branch, id);
if (!part) continue;
const name = shortName(part.이름);
const place = PLACE_KO[row.placement] ?? row.placement;
const owner = OWNER_KO[row.owner] ?? row.owner;
if (!part.자리꼴.includes(row.placement) || !part.정본.includes(row.owner))
out.push(`「${row.name}」 ${branch} 「${name}」 — ${place} · ${owner} 에 안 맞음`);
for (const key of part["읽는 칸"])
if (keys.has(key) && hidden.includes(key))
out.push(
`「${row.name}」 ${branch} 「${name}」 이 쓰는 칸 「${labelOf(key)}」 — 숨길 수 없음`,
);
}
}
return out;
}
/** 앞 판과 비교 — 만든 뒤 고정. */
function fixedErrors(doc: CheckDef, before: CheckDef): string[] {
const out: string[] = [];
const now = new Map((doc.types ?? []).map((t) => [t.type_id, t]));
for (const old of before.types ?? []) {
const t = now.get(old.type_id);
const name = old.name ?? old.type_id;
if (!t) {
out.push(`종류 「${name}」 — 지울 수 없음`);
continue;
}
const fixed: Array<[string, unknown, unknown]> = [
["자리꼴", old.placement, t.placement],
["정본", old.managed_by ?? null, t.managed_by ?? null],
["군 글자", old.group, t.group],
];
for (const [what, a, b] of fixed) if (a !== b) out.push(`「${name}」 ${what} — 만든 뒤 고정`);
const options = new Map((t.options ?? []).map((o) => [o.key, o]));
for (const o of old.options ?? []) {
const n = options.get(o.key);
if (o.input !== "select" || !n) continue;
const a = [...(o.choices ?? [])].sort().join("\u0000");
const b = [...(n.choices ?? [])].sort().join("\u0000");
if (a !== b)
out.push(`「${name}」 「${o.label ?? o.key}」 선택지 값 — 고칠 수 없음(이름만 고침)`);
}
}
return out;
}
/** 구조물 표 검사 — 틀린 것 글들(빈 목록 = 통과). `before` = 지금 서버 판(없으면 고정 검사 안 함). */
export function catalogProblems(doc: CheckDef, before: CheckDef | null): string[] {
const known = new Set((doc.types ?? []).map((t) => t.type_id));
const shape = shapeErrors(doc, known);
if (shape.length) return shape;
return before ? [...rowErrors(doc, before), ...fixedErrors(doc, before)] : [];
}