/* ============================================================================= * 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; const LIST_PAIRS = (): Pairs => [ ["both", to("ListBoth")], ["b06", to("ListB06")], ["hidden", to("ListHidden")], ]; /** 갈래 고르기 — 없음(카드 이름은 종류 이름) + 부품 상자 차례. */ const partPairs = (branch: Exclude): 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("[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 }).반영?.[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], }), ), }), ], }); }