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
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Def_Options.ts
|
||||
* 구조물 정의 편집 화면(PLAN 48-2) — 고른 종류의 **옵션 목록**
|
||||
* (이름 · 꼴 · 단위 · 기본값 · 보이기 · 들어갈 자리 · 안 씀 · 순서 · [옵션 더함]) +
|
||||
* 고르기 꼴의 **선택지 목록**(이름 바꿈 · 빼기 · 순서 · [선택지 더함]).
|
||||
* 옵션 키는 더할 때 자동 · 안 바뀜 · 선택지는 글이 곧 값(이름 바꿈 = 기본값 · 규칙도 같이) ·
|
||||
* 저장은 화면 [저장](`M02_SO_Def.ts`).
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createTag, el, showConfirmDialog } from "@ui/ui_template_elements";
|
||||
import {
|
||||
dropChoice,
|
||||
newKey,
|
||||
renameChoice,
|
||||
type OptionDef,
|
||||
type OptionForm,
|
||||
type StructDef,
|
||||
type TypeDef,
|
||||
} from "./M02_SO_Def_Model";
|
||||
import { checkBox, head, moveButtons, pickBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { buildPlaceSelect } from "./M02_SO_Groups";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
export interface OptionsBlockOptions {
|
||||
def: StructDef;
|
||||
type: TypeDef;
|
||||
edit: boolean;
|
||||
/** 선택지를 펼친 옵션 키 — 다시 그려도 남음 */
|
||||
open: Set<string>;
|
||||
changed: () => void;
|
||||
}
|
||||
|
||||
const FORMS: readonly OptionForm[] = ["select", "number", "text", "bool"];
|
||||
const FORM_TEXT = {
|
||||
select: "FormSelect",
|
||||
number: "FormNumber",
|
||||
text: "FormText",
|
||||
bool: "FormBool",
|
||||
} as const;
|
||||
|
||||
/** 기본값 칸 — 꼴에 맞춤(고르기 = 선택지 · 예/아니오 · 숫자 · 글) · 비우면 기본값 없음 */
|
||||
function defaultBox(o: OptionDef, edit: boolean, changed: () => void): HTMLElement {
|
||||
const attr = "data-sod-default";
|
||||
const now = o.default == null ? "" : String(o.default);
|
||||
if (o.input === "select" || o.input === "bool") {
|
||||
const pairs: [string, string][] =
|
||||
o.input === "select"
|
||||
? (o.choices ?? []).map((c) => [c, c])
|
||||
: [
|
||||
["true", to("Yes")],
|
||||
["false", to("No")],
|
||||
];
|
||||
return pickBox(
|
||||
[["", to("NoDefault")], ...pairs],
|
||||
now,
|
||||
(v) => {
|
||||
o.default = v === "" ? null : o.input === "bool" ? v === "true" : v;
|
||||
changed();
|
||||
},
|
||||
{ edit, attr, cls: "m02-sod__default" },
|
||||
);
|
||||
}
|
||||
return textBox(
|
||||
now,
|
||||
(v) => {
|
||||
const n = Number(v);
|
||||
o.default = v.trim() === "" ? null : o.input === "number" && Number.isFinite(n) ? n : v;
|
||||
},
|
||||
changed,
|
||||
{ edit, attr, cls: "m02-sod__default", place: to("NoDefault") },
|
||||
);
|
||||
}
|
||||
|
||||
/** 선택지 목록 — 이름(떠날 때 바꿈) · 빼기 · 순서 · [선택지 더함] */
|
||||
function choicesBlock(opt: OptionsBlockOptions, o: OptionDef): HTMLElement {
|
||||
const { def, type, edit, changed } = opt;
|
||||
const list = (o.choices ??= []);
|
||||
const rows = list.map((c) => {
|
||||
let next = c;
|
||||
const del = createButton({ label: to("ChoiceDrop"), variant: "danger", size: "sm" });
|
||||
del.addEventListener("click", async () => {
|
||||
if (!(await showConfirmDialog(to("ChoiceDropAsk", c), to("ChoiceDrop")))) return;
|
||||
dropChoice(def, type.type_id, o, c);
|
||||
changed();
|
||||
});
|
||||
return el("div", {
|
||||
className: "m02-sod__choice",
|
||||
attrs: { "data-sod-choice": c },
|
||||
children: [
|
||||
textBox(
|
||||
c,
|
||||
(v) => (next = v.trim()),
|
||||
() => {
|
||||
if (next && next !== c && !list.includes(next))
|
||||
renameChoice(def, type.type_id, o, c, next);
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: "data-sod-choice-name" },
|
||||
),
|
||||
...moveButtons(list, c, changed, edit),
|
||||
...(edit ? [del] : []),
|
||||
],
|
||||
});
|
||||
});
|
||||
const add = createButton({ label: to("ChoiceAdd"), variant: "ghost", size: "sm" });
|
||||
add.setAttribute("data-sod-choice-add", "");
|
||||
add.addEventListener("click", () => {
|
||||
let n = list.length + 1;
|
||||
while (list.includes(to("ChoiceNew", String(n)))) n += 1;
|
||||
list.push(to("ChoiceNew", String(n)));
|
||||
changed();
|
||||
});
|
||||
return el("div", {
|
||||
className: "m02-sod__choices",
|
||||
children: [
|
||||
el("p", { className: "m02-side__empty", text: to("ChoiceNote") }),
|
||||
...(rows.length ? rows : [el("p", { className: "m02-sod__bad", text: to("ChoiceNone") })]),
|
||||
...(edit ? [add] : []),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 옵션 한 줄(+ 펼친 선택지) */
|
||||
function optionRow(opt: OptionsBlockOptions, o: OptionDef): HTMLElement {
|
||||
const { def, type, edit, open, changed } = opt;
|
||||
const off = o.enabled === false;
|
||||
const hidden = def.표시[type.type_id]?.[o.key] === false;
|
||||
const form = pickBox(
|
||||
FORMS.map((f) => [f, to(FORM_TEXT[f])] as const),
|
||||
o.input,
|
||||
(v) => {
|
||||
o.input = v as OptionForm;
|
||||
o.default = null; // 꼴이 바뀌면 옛 기본값은 뜻이 없음
|
||||
if (o.input === "select") open.add(o.key);
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: "data-sod-form", cls: "m02-sod__form" },
|
||||
);
|
||||
const choices =
|
||||
o.input === "select"
|
||||
? createButton({
|
||||
label: to("Choices", String(o.choices?.length ?? 0)),
|
||||
variant: open.has(o.key) ? "filled" : "ghost",
|
||||
size: "sm",
|
||||
})
|
||||
: null;
|
||||
choices?.setAttribute("data-sod-choices", o.key);
|
||||
choices?.addEventListener("click", () => {
|
||||
if (open.has(o.key)) open.delete(o.key);
|
||||
else open.add(o.key);
|
||||
changed();
|
||||
});
|
||||
const show = checkBox(
|
||||
to("Shown"),
|
||||
!hidden,
|
||||
(v) => {
|
||||
const row = (def.표시[type.type_id] ??= {});
|
||||
if (v) delete row[o.key];
|
||||
else row[o.key] = false;
|
||||
if (!Object.keys(row).length) delete def.표시[type.type_id];
|
||||
changed();
|
||||
},
|
||||
{ edit, attr: "data-sod-shown" },
|
||||
);
|
||||
const row = el("div", {
|
||||
className: `m02-sod__option${off ? " is-off" : ""}`,
|
||||
attrs: { "data-sod-option": o.key },
|
||||
children: [
|
||||
textBox(o.label, (v) => (o.label = v), changed, {
|
||||
edit,
|
||||
attr: "data-sod-name",
|
||||
cls: "m02-sod__name",
|
||||
}),
|
||||
el("small", { className: "m02-opts__key", text: o.key }),
|
||||
...(o.phase === "detail" ? [createTag(to("Detail"), "accent")] : []),
|
||||
form,
|
||||
textBox(o.unit ?? "", (v) => (o.unit = v.trim() || null), changed, {
|
||||
edit,
|
||||
attr: "data-sod-unit",
|
||||
cls: "m02-sod__unit",
|
||||
place: to("Unit"),
|
||||
max: 12,
|
||||
}),
|
||||
defaultBox(o, edit, changed),
|
||||
show,
|
||||
buildPlaceSelect((def.그룹[type.type_id] ??= []), o.key, edit, changed),
|
||||
checkBox(to("Unused"), off, (v) => ((o.enabled = !v), changed()), {
|
||||
edit,
|
||||
attr: "data-sod-off",
|
||||
}),
|
||||
...moveButtons(type.options, o, changed, edit),
|
||||
...(choices ? [choices] : []),
|
||||
],
|
||||
});
|
||||
return o.input === "select" && open.has(o.key)
|
||||
? el("div", { className: "m02-sod__optwrap", children: [row, choicesBlock(opt, o)] })
|
||||
: row;
|
||||
}
|
||||
|
||||
/** 옵션 목록 칸 — 제목 · 안내 · 옵션 줄 · [옵션 더함] */
|
||||
export function buildOptionsBlock(opt: OptionsBlockOptions): HTMLElement {
|
||||
const { type, edit, changed } = opt;
|
||||
const add = createButton({ label: to("OptionAdd"), variant: "ghost", size: "sm" });
|
||||
add.setAttribute("data-sod-option-add", "");
|
||||
add.addEventListener("click", () => {
|
||||
const n = type.options.length + 1;
|
||||
const o: OptionDef = {
|
||||
key: newKey(
|
||||
"opt_",
|
||||
type.options.map((x) => x.key),
|
||||
),
|
||||
label: to("OptionNew", String(n)),
|
||||
input: "select",
|
||||
choices: [to("ChoiceNew", "1")],
|
||||
default: null,
|
||||
};
|
||||
type.options.push(o);
|
||||
opt.open.add(o.key);
|
||||
changed();
|
||||
});
|
||||
return el("div", {
|
||||
className: "m02-sod__block",
|
||||
children: [
|
||||
...head(to("OptionHead"), to("OptionNote")),
|
||||
...(type.options.length
|
||||
? type.options.map((o) => optionRow(opt, o))
|
||||
: [el("p", { className: "m02-side__empty", text: to("NoOptionKind") })]),
|
||||
...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
|
||||
],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user