/* ============================================================================= * M02_SO_Def_Rules.ts * 구조물 정의 편집 화면(PLAN 48-2) — 고른 종류의 **규칙 표**(정의 `규칙` 중 그 종류 것만) * 「만약 상위 옵션 = 값들 중 하나 → 하위 옵션의 선택지 · 보이기 · 기본값」 한 줄씩 · [규칙 더함] · 지우기. * 「그대로」 칸은 정의에 안 남김 · 셋 다 그대로면 틀린 줄(빨강) · 적용은 B05 · B06(48-4). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; import { live, ruleBad, type OptionDef, type RuleDef, type StructDef, type TypeDef, } from "./M02_SO_Def_Model"; import { head, pickBox } from "./M02_SO_Def_Ui"; import { to } from "./M02_SO_Text"; export interface RulesBlockOptions { def: StructDef; type: TypeDef; edit: boolean; changed: () => void; } const bool = (): [string, string][] => [ ["true", to("Yes")], ["false", to("No")], ]; /** 여럿 고르기 — 체크한 것만 차례대로 */ function multiPick( all: readonly string[], picked: readonly string[], set: (v: string[]) => void, edit: boolean, attr: string, ): HTMLElement { const on = new Set(picked); return el("div", { className: "m02-sod__rulechoices", attrs: { [attr]: "" }, children: all.map((c) => { const box = el("input", { attrs: { type: "checkbox", value: c } }) as HTMLInputElement; box.checked = on.has(c); box.disabled = !edit; box.addEventListener("change", () => { if (box.checked) on.add(c); else on.delete(c); set(all.filter((x) => on.has(x))); }); return el("label", { className: "m02-sod__check", children: [box, el("span", { text: c })] }); }), }); } /** 글 칸 — 쉼표로 여럿 */ function listText( value: string[], set: (v: string[]) => void, edit: boolean, attr: string, ): HTMLElement { const box = el("input", { className: "ui-input ui-input--sm m02-sod__rulepick", attrs: { type: "text", [attr]: "", placeholder: to("RuleValues") }, }) as HTMLInputElement; box.value = value.join(", "); box.disabled = !edit; box.addEventListener("change", () => set( box.value .split(",") .map((s) => s.trim()) .filter(Boolean), ), ); return box; } /** 만약 값들 — 고르기 = 선택지 체크 · 예/아니오 = 하나 · 그 밖 = 쉼표 글 */ function whenValues( src: OptionDef | undefined, rule: RuleDef, edit: boolean, changed: () => void, ): HTMLElement { const set = (v: string[]): void => ((rule.만약.값들 = v), changed()); if (src?.input === "select") return multiPick(src.choices ?? [], rule.만약.값들, set, edit, "data-sod-rule-value"); if (src?.input === "bool") return pickBox(bool(), rule.만약.값들[0] ?? "true", (v) => set([v]), { edit, attr: "data-sod-rule-value", cls: "m02-sod__rulepick", }); return listText(rule.만약.값들, set, edit, "data-sod-rule-value"); } /** 그러면 기본값 — 「그대로」 + (좁힌) 선택지 · 예/아니오 · 글 */ function thenDefault( dst: OptionDef | undefined, rule: RuleDef, edit: boolean, changed: () => void, ): HTMLElement { const then = rule.그러면; const now = then.기본값 === undefined ? "" : String(then.기본값); const set = (v: string): void => { if (v === "") delete then.기본값; else then.기본값 = dst?.input === "bool" ? v === "true" : dst?.input === "number" && Number.isFinite(Number(v)) ? Number(v) : v; changed(); }; const pairs: [string, string][] = dst?.input === "select" ? (then.선택지 ?? dst.choices ?? []).map((c) => [c, c]) : dst?.input === "bool" ? bool() : []; if (pairs.length || (dst?.input !== "number" && dst?.input !== "text")) return pickBox([["", to("KeepDefault")], ...pairs], now, set, { edit, attr: "data-sod-rule-default", cls: "m02-sod__rulepick", }); const box = el("input", { className: "ui-input ui-input--sm m02-sod__rulepick", attrs: { type: "text", "data-sod-rule-default": "", placeholder: to("KeepDefault") }, }) as HTMLInputElement; box.value = now; box.disabled = !edit; box.addEventListener("change", () => set(box.value.trim())); return box; } function ruleRow(opt: RulesBlockOptions, rule: RuleDef, index: number): HTMLElement { const { def, type, edit, changed } = opt; const byKey = new Map(type.options.map((o) => [o.key, o])); const names = live(type.options).map((o) => [o.key, o.label] as [string, string]); const src = byKey.get(rule.만약.옵션); const dst = byKey.get(rule.그러면.옵션); const then = rule.그러면; const del = createButton({ label: to("GroupDel"), variant: "danger", size: "sm" }); del.addEventListener("click", () => (def.규칙.splice(def.규칙.indexOf(rule), 1), changed())); const show = then.보이기 === undefined ? "" : then.보이기 ? "1" : "0"; return el("div", { className: `m02-sod__rule${ruleBad(def, rule) ? " is-bad" : ""}`, attrs: { "data-sod-rule": String(index) }, children: [ el("small", { text: to("RuleIf") }), pickBox(names, rule.만약.옵션, (v) => ((rule.만약 = { 옵션: v, 값들: [] }), changed()), { edit, attr: "data-sod-rule-top", cls: "m02-sod__rulepick", }), el("small", { text: "=" }), whenValues(src, rule, edit, changed), el("small", { text: to("RuleThen") }), pickBox(names, then.옵션, (v) => ((rule.그러면 = { 옵션: v }), changed()), { edit, attr: "data-sod-rule-target", cls: "m02-sod__rulepick", }), ...(dst?.input === "select" ? [ el("small", { text: to("RuleChoices") }), multiPick( dst.choices ?? [], then.선택지 ?? [], (v) => { if (v.length) then.선택지 = v; else delete then.선택지; changed(); }, edit, "data-sod-rule-choice", ), ] : []), pickBox( [ ["", to("Keep")], ["1", to("Show")], ["0", to("Hide")], ], show, (v) => { if (v === "") delete then.보이기; else then.보이기 = v === "1"; changed(); }, { edit, attr: "data-sod-rule-show", cls: "m02-sod__rulepick" }, ), thenDefault(dst, rule, edit, changed), ...(edit ? [del] : []), ], }); } export function buildRulesBlock(opt: RulesBlockOptions): HTMLElement { const { def, type, edit, changed } = opt; const mine = def.규칙.filter((r) => r.종류 === type.type_id); const opts = live(type.options); const add = createButton({ label: to("RuleAdd"), variant: "ghost", size: "sm" }); add.setAttribute("data-sod-rule-add", ""); add.disabled = opts.length < 2; add.addEventListener("click", () => { const top = opts.find((o) => o.input === "select") ?? opts[0]; const target = opts.find((o) => o.key !== top.key)!; def.규칙.push({ 종류: type.type_id, 만약: { 옵션: top.key, 값들: top.choices?.length ? [top.choices[0]] : [] }, 그러면: { 옵션: target.key, 보이기: false }, }); changed(); }); return el("div", { className: "m02-sod__block", children: [ ...head(to("RuleHead"), to("RuleNote")), ...(mine.length ? mine.map((r) => ruleRow(opt, r, def.규칙.indexOf(r))) : [el("p", { className: "m02-side__empty", text: to("RuleNone") })]), ...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []), ], }); }