- 자리꼴 점 · 구간 · 터 · 정본 관 지점 · 구조물(키는 툴팁) · 고르기 글 = 부품 짧은 이름(괄호 설명 · 키는 툴팁) · 횡단도 없음 = 「안 만듦」 · 「벽 스윕」 → 「벽」 · 종류 키는 툴팁 - 검사 글: 「형상이 「없음」이면 횡단도도 「안 만듦」」 · 부품 짧은 이름 · 칸 이름 · 종류 이름(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
215 lines
8.8 KiB
TypeScript
215 lines
8.8 KiB
TypeScript
/* =============================================================================
|
|
* 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 { clone, 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> }).처음값;
|
|
|
|
/** 화면 글(322-2 다듬기 · 영어 키 안 보임) — 자리꼴 · 정본 · 부품 짧은 이름(괄호 설명은 툴팁). */
|
|
export const PLACE_KO: Record<string, string> = { point: "점", interval: "구간", site: "터" };
|
|
export const OWNER_KO: Record<string, string> = { pipe_points: "관 지점", structures: "구조물" };
|
|
export const shortName = (full: string): string => full.replace(/\s*\(.*\)\s*$/, "");
|
|
|
|
/** 칸 → 표 줄 열쇠. */
|
|
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];
|
|
}
|
|
|
|
/** 저장할 꼴 — 목록 + enabled 함께(322-2) · 빈 묶음 걷어냄(그룹 목록 · 배치 · 종류 덧칸 · 표시 · 반영) */
|
|
export function tidy(def: StructDef): StructDef {
|
|
const out = clone(def);
|
|
syncEnabled(out);
|
|
const reflect = (out.반영 ?? {}) as Record<string, unknown>;
|
|
for (const part of [out.그룹, out.배치, out.종류, out.표시, reflect] as Record<string, unknown>[])
|
|
for (const [k, v] of Object.entries(part))
|
|
if (Array.isArray(v) ? !v.length : !Object.keys(v as object).length) delete part[k];
|
|
return out;
|
|
}
|
|
|
|
/** 열쇠 차례 무관 글 — 같은 값이면 같은 글. */
|
|
const stable = (value: unknown): string =>
|
|
Array.isArray(value)
|
|
? `[${value.map(stable).join(",")}]`
|
|
: value && typeof value === "object"
|
|
? `{${Object.keys(value)
|
|
.sort()
|
|
.map((k) => `${JSON.stringify(k)}:${stable((value as Record<string, unknown>)[k])}`)
|
|
.join(",")}}`
|
|
: JSON.stringify(value ?? null);
|
|
|
|
/** 저장할 것이 있나 — 저장할 꼴(tidy)끼리 견줌 · 빈 반영 · 부품 칸은 없는 셈(바꿨다 되돌리면 꺼짐 · 322-2). */
|
|
export function differs(work: StructDef, base: StructDef): boolean {
|
|
const canon = (def: StructDef): string => {
|
|
const out = tidy(def) as Record<string, unknown>;
|
|
for (const key of ["반영", "부품"])
|
|
if (out[key] && !Object.keys(out[key] as object).length) delete out[key];
|
|
return stable(out);
|
|
};
|
|
return canon(work) !== canon(base);
|
|
}
|