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,192 @@
|
||||
/* =============================================================================
|
||||
* 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";
|
||||
|
||||
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;
|
||||
}>;
|
||||
}>;
|
||||
표시?: 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}」 형상 none — 횡단도도 none 이어야 함`);
|
||||
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 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;
|
||||
if (!part.자리꼴.includes(row.placement) || !part.정본.includes(row.owner))
|
||||
out.push(
|
||||
`「${row.name}」 ${branch} 「${id}」 — 자리꼴 ${row.placement} · 정본 ${row.owner} 에 안 맞음`,
|
||||
);
|
||||
for (const key of part["읽는 칸"])
|
||||
if (keys.has(key) && hidden.includes(key))
|
||||
out.push(`「${row.name}」 ${branch} 「${id}」 이 읽는 칸 「${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);
|
||||
if (!t) {
|
||||
out.push(`종류 「${old.type_id}」 — 지울 수 없음`);
|
||||
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(`「${old.type_id}」 ${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(`「${old.type_id}.${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)] : [];
|
||||
}
|
||||
Reference in New Issue
Block a user