Files
Aislo/M02_MasterTemplete/StructOptions/M02_SO_Def_Rules.ts
T
eomsangdonandClaude Opus 5.5 8ca2f1cd6e feat(M02): 48-2 구조물 옵션 정의 편집 화면 — 군 · 종류 · 옵션 · 선택지 · 배치 격자 · 그룹 형상 · 규칙 · 판 이력
- 옛 「옵션 보이기 · 그룹」 화면을 정의 편집 화면으로 넓힘(시스템 관리자 고쳐 씀 · 다른 역할 읽기만)
- 자료 손질 = 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
2026-10-05 16:15:10 +09:00

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] })] : []),
],
});
}