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,327 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Table.ts
|
||||
* 구조물 정의 「한눈 표」 탭(PLAN 322-2) — 종류마다 B05 · B06 반영 한 줄.
|
||||
* 군 · 종류(▲▼) · 이름 · 목록 · 자리꼴 · 정본(읽기만 「고정」) · 횡단도 · 카드 이름 · 형상(+ 형상칸) ·
|
||||
* 알약 색 · 3D · 검사. 흐린 칸 = 처음 값(「기본」) · 진한 칸 = 바꾼 값 · [처음 값으로] = 그 종류 반영 지움.
|
||||
* 칸 손질 = `M02_SO_Def_Reflect` · 검사 글 = `M02_SO_Def_Check`(서버 짝) · 「종류별」 탭이 같은 칸(`reflectCells`)을 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createTag, el } from "@ui/ui_template_elements";
|
||||
import type { CatalogRow } from "../../common_util/common_util_structure_catalog";
|
||||
import { stepFamily, stepType, type StructDef, type TypeDef } from "./M02_SO_Def_Model";
|
||||
import {
|
||||
isSet,
|
||||
PART_BOX,
|
||||
resetRow,
|
||||
rowsOf,
|
||||
setReflect,
|
||||
setShapeOpt,
|
||||
valueOf,
|
||||
type ReflectField,
|
||||
} from "./M02_SO_Def_Reflect";
|
||||
import { checkBox, pickBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
type Pairs = ReadonlyArray<readonly [string, string]>;
|
||||
const LIST_PAIRS = (): Pairs => [
|
||||
["both", to("ListBoth")],
|
||||
["b06", to("ListB06")],
|
||||
["hidden", to("ListHidden")],
|
||||
];
|
||||
/** 갈래 고르기 — 없음(카드 이름은 종류 이름) + 부품 상자 차례. */
|
||||
const partPairs = (branch: Exclude<ReflectField, "목록">): Pairs => [
|
||||
[
|
||||
branch === "카드이름" ? "name" : "none",
|
||||
branch === "카드이름" ? to("TitleName") : to("OffNone"),
|
||||
],
|
||||
...Object.entries(PART_BOX[branch]).map(([id, part]) => [id, `${id} · ${part.이름}`] as const),
|
||||
];
|
||||
|
||||
interface CellCtx {
|
||||
def: StructDef;
|
||||
type: TypeDef;
|
||||
row: CatalogRow;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
}
|
||||
|
||||
/** 반영 칸 하나 — 처음 값이면 흐리게 「기본」 · 바꿨으면 진하게. */
|
||||
function reflectCell(ctx: CellCtx, field: ReflectField): HTMLElement {
|
||||
const { def, type, row, edit, changed } = ctx;
|
||||
const pairs = field === "목록" ? LIST_PAIRS() : partPairs(field);
|
||||
const box = pickBox(
|
||||
pairs,
|
||||
valueOf(row, field),
|
||||
(v) => {
|
||||
setReflect(def, type.type_id, field, v);
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: `data-sod-reflect-${field}` },
|
||||
);
|
||||
return mark(isSet(def, type.type_id, field), box);
|
||||
}
|
||||
|
||||
/** 흐림(기본) · 진함(바꿈) 꼴. */
|
||||
function mark(changed: boolean, ...children: HTMLElement[]): HTMLElement {
|
||||
return el("div", {
|
||||
className: `m02-sod__cell ${changed ? "is-changed" : "is-default"}`,
|
||||
children: [...children, ...(changed ? [] : [createTag(to("Default"), "neutral")])],
|
||||
});
|
||||
}
|
||||
|
||||
/** 형상칸 — 벽(형상 wall)만: 기본 형태 글 · 물빼기. */
|
||||
function shapeOptsCell(ctx: CellCtx): HTMLElement {
|
||||
const { def, type, row, edit, changed } = ctx;
|
||||
if (row.shape !== "wall") return el("span", { className: "m02-side__empty", text: "—" });
|
||||
const form = String(row.shapeOpts.form ?? "");
|
||||
return mark(
|
||||
isSet(def, type.type_id, "형상칸"),
|
||||
textBox(
|
||||
form,
|
||||
() => undefined,
|
||||
() => undefined,
|
||||
{
|
||||
edit,
|
||||
attr: "data-sod-shape-form",
|
||||
place: to("WallForm"),
|
||||
max: 20,
|
||||
},
|
||||
),
|
||||
checkBox(
|
||||
to("Weep"),
|
||||
row.shapeOpts.weep === true,
|
||||
(v) => (setShapeOpt(def, type.type_id, "weep", v), changed()),
|
||||
{ edit, attr: "data-sod-shape-weep" },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** 형상칸 글은 떠날 때 씀(초점 안 끊김). */
|
||||
function bindForm(node: HTMLElement, ctx: CellCtx): void {
|
||||
const box = node.querySelector<HTMLInputElement>("[data-sod-shape-form]");
|
||||
box?.addEventListener("change", () => {
|
||||
setShapeOpt(ctx.def, ctx.type.type_id, "form", box.value.trim());
|
||||
ctx.changed();
|
||||
});
|
||||
}
|
||||
|
||||
/** 알약 색(style.color). */
|
||||
function colorCell(ctx: CellCtx): HTMLElement {
|
||||
const box = el("input", {
|
||||
className: "m02-sod__color",
|
||||
attrs: { type: "color", "data-sod-color": "" },
|
||||
}) as HTMLInputElement;
|
||||
const style = (ctx.type.style ?? {}) as { color?: string };
|
||||
box.value = style.color ?? "#888888";
|
||||
box.disabled = !ctx.edit;
|
||||
box.addEventListener("change", () => {
|
||||
ctx.type.style = { ...style, color: box.value };
|
||||
ctx.changed();
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
const fixed = (text: string): HTMLElement =>
|
||||
el("span", {
|
||||
className: "m02-sod__fixed",
|
||||
children: [el("span", { text }), createTag(to("Fixed"), "neutral")],
|
||||
});
|
||||
|
||||
/** 「종류별」 탭이 쓰는 반영 칸들 — [글, 칸] 짝. */
|
||||
export function reflectCells(
|
||||
def: StructDef,
|
||||
type: TypeDef,
|
||||
edit: boolean,
|
||||
changed: () => void,
|
||||
): Array<[string, HTMLElement]> {
|
||||
const row = rowsOf(def).get(type.type_id);
|
||||
if (!row) return [];
|
||||
const ctx = { def, type, row, edit, changed };
|
||||
const opts = shapeOptsCell(ctx);
|
||||
bindForm(opts, ctx);
|
||||
return [
|
||||
[to("ColList"), reflectCell(ctx, "목록")],
|
||||
[to("ColPlacement"), fixed(row.placement)],
|
||||
[to("ColOwner"), fixed(row.owner)],
|
||||
[to("ColStation"), reflectCell(ctx, "횡단도")],
|
||||
[to("ColTitle"), reflectCell(ctx, "카드이름")],
|
||||
[to("ColShape"), reflectCell(ctx, "형상")],
|
||||
[to("ColShapeOpts"), opts],
|
||||
[to("ColColor"), colorCell(ctx)],
|
||||
[to("Col3D"), reflectCell(ctx, "3D")],
|
||||
];
|
||||
}
|
||||
|
||||
function moveButton(label: string, run: () => boolean, off: boolean, changed: () => void) {
|
||||
const b = createButton({ label, variant: "ghost", size: "sm" });
|
||||
b.disabled = off;
|
||||
b.addEventListener("click", () => run() && changed());
|
||||
return b;
|
||||
}
|
||||
|
||||
/** 그 종류 검사 글 — 이름 · 키가 든 줄. */
|
||||
const issuesOf = (issues: readonly string[], type: TypeDef): string[] =>
|
||||
issues.filter((t) => t.includes(`「${type.name}」`) || t.includes(`「${type.type_id}`));
|
||||
|
||||
export function buildTable(opt: {
|
||||
def: StructDef;
|
||||
edit: boolean;
|
||||
changed: () => void;
|
||||
issues: readonly string[];
|
||||
}): HTMLElement {
|
||||
const { def, edit, changed, issues } = opt;
|
||||
const heads = [
|
||||
"ColFamily",
|
||||
"ColKind",
|
||||
"ColName",
|
||||
"ColList",
|
||||
"ColPlacement",
|
||||
"ColOwner",
|
||||
"ColStation",
|
||||
"ColTitle",
|
||||
"ColShape",
|
||||
"ColShapeOpts",
|
||||
"ColColor",
|
||||
"Col3D",
|
||||
"ColCheck",
|
||||
] as const;
|
||||
const td = (child: HTMLElement | string): HTMLElement =>
|
||||
el("td", typeof child === "string" ? { text: child } : { children: [child] });
|
||||
const body: HTMLElement[] = [];
|
||||
const families = Object.keys(def.구조물군);
|
||||
families.forEach((g, gi) => {
|
||||
const name = textBox(def.구조물군[g], (v) => (def.구조물군[g] = v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-family-name",
|
||||
});
|
||||
const familyRow = el("tr", {
|
||||
className: "m02-sod__familyrow",
|
||||
attrs: { "data-sod-table-family": g },
|
||||
children: [
|
||||
el("td", {
|
||||
attrs: { colspan: String(heads.length) },
|
||||
children: [
|
||||
el("strong", { text: g }),
|
||||
name,
|
||||
...(edit
|
||||
? [
|
||||
moveButton(to("GroupUp"), () => stepFamily(def, g, -1), gi === 0, changed),
|
||||
moveButton(
|
||||
to("GroupDown"),
|
||||
() => stepFamily(def, g, 1),
|
||||
gi === families.length - 1,
|
||||
changed,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
body.push(familyRow);
|
||||
const kinds = def.types.filter((t) => t.group === g);
|
||||
kinds.forEach((type, ti) => {
|
||||
const cells = new Map(reflectCells(def, type, edit, changed));
|
||||
const bad = issuesOf(issues, type);
|
||||
const reset = createButton({ label: to("Reset"), variant: "ghost", size: "sm" });
|
||||
reset.disabled = !edit || !(def as { 반영?: Record<string, unknown> }).반영?.[type.type_id];
|
||||
reset.addEventListener("click", () => (resetRow(def, type.type_id), changed()));
|
||||
body.push(
|
||||
el("tr", {
|
||||
attrs: { "data-sod-table-kind": type.type_id },
|
||||
children: [
|
||||
td(""),
|
||||
el("td", {
|
||||
children: [
|
||||
el("small", { className: "m02-opts__key", text: type.type_id }),
|
||||
...(edit
|
||||
? [
|
||||
moveButton(
|
||||
to("GroupUp"),
|
||||
() => stepType(def, type.type_id, -1),
|
||||
ti === 0,
|
||||
changed,
|
||||
),
|
||||
moveButton(
|
||||
to("GroupDown"),
|
||||
() => stepType(def, type.type_id, 1),
|
||||
ti === kinds.length - 1,
|
||||
changed,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
td(
|
||||
textBox(type.name, (v) => (type.name = v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-kind-name",
|
||||
}),
|
||||
),
|
||||
...[
|
||||
"ColList",
|
||||
"ColPlacement",
|
||||
"ColOwner",
|
||||
"ColStation",
|
||||
"ColTitle",
|
||||
"ColShape",
|
||||
"ColShapeOpts",
|
||||
"ColColor",
|
||||
"Col3D",
|
||||
].map((key) => td(cells.get(to(key as (typeof heads)[number])) ?? "")),
|
||||
el("td", {
|
||||
attrs: { "data-sod-table-check": "" },
|
||||
children: [
|
||||
bad.length
|
||||
? el("span", { className: "m02-sod__bad", text: bad.join(" · ") })
|
||||
: createTag(to("Ok"), "accent"),
|
||||
reset,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
return el("div", {
|
||||
className: "m02-sod__tablewrap",
|
||||
children: [
|
||||
el("p", { className: "m02-side__empty", text: to("TableNote") }),
|
||||
el("table", {
|
||||
className: "m02-sod__table",
|
||||
attrs: { "data-sod-table": "" },
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((h) => el("th", { text: to(h) })) })],
|
||||
}),
|
||||
el("tbody", { children: body }),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 「종류별」 탭 머리 아래 반영 칸 묶음(한눈 표 한 줄과 같은 칸). */
|
||||
export function buildReflectBlock(
|
||||
def: StructDef,
|
||||
type: TypeDef,
|
||||
edit: boolean,
|
||||
changed: () => void,
|
||||
): HTMLElement {
|
||||
return el("div", {
|
||||
className: "m02-sod__block",
|
||||
attrs: { "data-sod-reflect-block": "" },
|
||||
children: [
|
||||
el("h4", { className: "m02-opts__group", text: to("ReflectHead") }),
|
||||
el("div", {
|
||||
className: "m02-sod__reflect",
|
||||
children: reflectCells(def, type, edit, changed).map(([label, cell]) =>
|
||||
el("label", {
|
||||
className: "m02-sod__reflectrow",
|
||||
children: [el("span", { text: label }), cell],
|
||||
}),
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user