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:
2026-10-14 15:40:38 +09:00
co-authored by Claude Opus 5.5
parent bbd6254892
commit 3abd2bdfd4
26 changed files with 1635 additions and 34 deletions
@@ -0,0 +1,176 @@
/* =============================================================================
* M02_SO_Def_Reflect.ts
* 구조물 표 편집(PLAN 322-2)의 순수 손질 — 정의 사본의 `반영` · `부품` · 목록 + enabled · 형상칸.
*
* · 지금 값 = 구조물 표 한 줄(`resolveCatalog` · 정의 반영 ⊕ 처음값 ⊕ 부품 끔)
* · 반영에는 처음 값과 다른 칸만 남김(같으면 지움 = 「기본」) · [처음 값으로] = 그 종류 반영 지움
* · 목록과 enabled 는 함께 씀 — enabled = 목록 both 일 때만 참(옛 짝 · 브레인 322-1 답 ①)
* 화면 그리기 없음 — 표 · 종류별 · 부품 탭이 이 함수들로 사본을 고침.
* ========================================================================== */
import PARTS from "../../common_util/common_util_structure_parts.json" with { type: "json" };
import {
resolveCatalog,
type CatalogDefinition,
type CatalogRow,
} from "../../common_util/common_util_structure_catalog";
import type { StructDef } from "./M02_SO_Def_Model";
export type ReflectField = "목록" | "횡단도" | "카드이름" | "형상" | "3D";
type Reflect = Partial<Record<ReflectField, string>> & { 형상칸?: Record<string, unknown> };
interface Part {
이름: string;
"읽는 칸": string[];
자리꼴: string[];
정본: string[];
}
type Branch = Exclude<ReflectField, "목록">;
export const PART_BOX = (PARTS as unknown as { parts: Record<Branch, Record<string, Part>> }).parts;
const START = (PARTS as unknown as { 처음값: Record<string, Reflect> }).처음값;
/** 칸 → 표 줄 열쇠. */
const ROW_KEY: Record<ReflectField, keyof CatalogRow> = {
목록: "list",
횡단도: "station",
카드이름: "title",
형상: "shape",
"3D": "model3d",
};
export const reflectOf = (def: StructDef): Record<string, Reflect> =>
((def as { 반영?: Record<string, Reflect> }).반영 ??= {});
export const partsOff = (def: StructDef): Record<string, boolean> =>
((def as { 부품?: Record<string, boolean> }).부품 ??= {});
/** 사본 전체 표 — 종류마다 한 줄. */
export const rowsOf = (def: StructDef): Map<string, CatalogRow> =>
resolveCatalog(def as unknown as CatalogDefinition);
/** 칸 지금 값. */
export const valueOf = (row: CatalogRow, field: ReflectField): string =>
String(row[ROW_KEY[field]]);
/** 반영이 없을 때(처음 값 · 목록은 enabled 짝) 그 칸 값. */
export function defaultOf(def: StructDef, typeId: string, field: ReflectField): string {
const copy = { ...def, 반영: { ...reflectOf(def) } } as unknown as StructDef;
const own = { ...(reflectOf(copy)[typeId] ?? {}) };
delete own[field];
reflectOf(copy)[typeId] = own;
const row = rowsOf(copy).get(typeId);
return row ? valueOf(row, field) : "";
}
/** 반영에 적힌 칸인가(아니면 「기본」). */
export const isSet = (def: StructDef, typeId: string, field: ReflectField | "형상칸"): boolean =>
field in (reflectOf(def)[typeId] ?? {});
const dropEmpty = (def: StructDef, typeId: string): void => {
const all = reflectOf(def);
if (all[typeId] && !Object.keys(all[typeId]).length) delete all[typeId];
};
/** 칸 고침 — 처음 값과 같으면 반영에서 지움 · 목록이면 enabled 도(both 일 때만 참). */
export function setReflect(
def: StructDef,
typeId: string,
field: ReflectField,
value: string,
): void {
const type = def.types.find((t) => t.type_id === typeId);
if (field === "목록" && type) type.enabled = value === "both";
const own = (reflectOf(def)[typeId] ??= {});
delete own[field];
if (defaultOf(def, typeId, field) !== value) own[field] = value;
dropEmpty(def, typeId);
}
/** 형상칸 한 칸 고침(form 글 · weep 참거짓) — 처음 값과 같으면 지움. */
export function setShapeOpt(def: StructDef, typeId: string, key: string, value: unknown): void {
const own = (reflectOf(def)[typeId] ??= {});
const opts = { ...(own.형상칸 ?? {}) };
const start = START[typeId]?.형상칸?.[key];
if (value === start || value === "" || (value === false && start === undefined)) delete opts[key];
else opts[key] = value;
if (Object.keys(opts).length) own.형상칸 = opts;
else delete own.형상칸;
dropEmpty(def, typeId);
}
/** [처음 값으로] — 그 종류 반영 지움 · enabled = 처음 목록 both 일 때만 참. */
export function resetRow(def: StructDef, typeId: string): void {
delete reflectOf(def)[typeId];
const type = def.types.find((t) => t.type_id === typeId);
if (type) type.enabled = START[typeId] ? START[typeId].목록 === "both" : type.enabled;
}
/** [저장] 전 — 모든 종류 enabled = 지금 목록 both 일 때만 참(목록 + enabled 함께 씀). */
export function syncEnabled(def: StructDef): void {
const rows = rowsOf(def);
for (const type of def.types) {
const row = rows.get(type.type_id);
if (row) type.enabled = row.list === "both";
}
}
/** 부품 켬끔 — 끔만 남김. */
export function setPartOn(def: StructDef, key: string, on: boolean): void {
const off = partsOff(def);
if (on) delete off[key];
else off[key] = false;
}
/** 못 숨김 칸 — 종류가 지금 쓰는 부품들이 읽는 칸 중 기본값도 비워 두는 뜻도 없는 옵션
* (숨기면 그림이 받을 값이 없음 · 검사 짝 `M02_SO_Def_Check`). */
export function readKeys(def: StructDef, typeId: string): Set<string> {
const row = rowsOf(def).get(typeId);
if (!row) return new Set();
const type = def.types.find((t) => t.type_id === typeId);
const bare = new Set(
(type?.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],
];
return new Set(
used
.flatMap(([branch, id]) => PART_BOX[branch][id]?.["읽는 칸"] ?? [])
.filter((key) => bare.has(key)),
);
}
/** 그 갈래 부품을 지금 쓰는 종류들. */
export function usersOf(def: StructDef, branch: Branch, id: string): CatalogRow[] {
const field = ROW_KEY[branch];
return [...rowsOf(def).values()].filter((row) => row[field] === id);
}
/** 정의 「표시」 숨김 키(그 종류). */
export const hiddenKeys = (def: StructDef, typeId: string): Set<string> =>
new Set(
Object.entries(def.표시[typeId] ?? {})
.filter(([, shown]) => shown === false)
.map(([key]) => key),
);
/** 옵션 키들 보임 켬끔 — 정의 옵션인 키만(숨김 = false 만 남김). */
export function setShown(
def: StructDef,
typeId: string,
keys: readonly string[],
on: boolean,
): void {
const type = def.types.find((t) => t.type_id === typeId);
const known = new Set((type?.options ?? []).map((o) => o.key));
const row = (def.표시[typeId] ??= {});
for (const key of keys.filter((k) => known.has(k))) {
if (on) delete row[key];
else row[key] = false;
}
if (!Object.keys(row).length) delete def.표시[typeId];
}