- 옛 「옵션 보이기 · 그룹」 화면을 정의 편집 화면으로 넓힘(시스템 관리자 고쳐 씀 · 다른 역할 읽기만) - 자료 손질 = M02_SO_Def_Model.ts(정의 틀 48-1 꼴) · 시험 test_m02_so_def_model.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
235 lines
7.7 KiB
TypeScript
235 lines
7.7 KiB
TypeScript
/* =============================================================================
|
|
* 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] })] : []),
|
|
],
|
|
});
|
|
}
|