Files
eomsangdonandClaude Opus 5.5 09134bab84 refactor(M02): 띠 알림 · 빈 상태 · 보조 글 · 배지 · 탭 · 표 · 목록 줄 · 도구줄 · 체크를 부품으로 (PLAN 553-5)
- DataFlow: 표 넷 = 데이터표 · 함수 판 알림 · 선/알약 경고 · 끊긴 곳 = 띠 알림 · 알약 · 칸 종류 = 배지 · 고르는 줄 = 목록 줄 · 입력 넷 = 입력 · 도구줄 여섯 · 날 색 → 공통 값
- 구조물 옵션 · 구조물 · 도면 · 면적 · 품목 고르기: 판 띠 = 띠 알림 · 한눈/종류별/부품 = 탭 · 표 둘 = 데이터표 · 체크 둘 = 체크 · 카드 · 번호 = 카드 · 배지 · 도면 도구줄
- 일위대가 · 호표 · 실무자료 · 본 화면 · 서고: 알림 = 띠 알림 · 빈 상태 · 보조 글 · 도구줄 여섯 · 옛 보라 별칭 0
- M01 로직 입력칸 = 입력 부품(작은 칸) · 일위대가 상세 줄 수 고정 시험 693 · 지킴 시험 M02 몫 더함

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 23:44:19 +09:00

240 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";
import { createEmpty } from "@ui/ui_template_empty";
import { createCheckbox } from "@ui/ui_template_checkbox";
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 { root } = createCheckbox(c, {
checked: on.has(c),
disabled: !edit,
value: c,
onChange: (checked) => {
if (checked) on.add(c);
else on.delete(c);
set(all.filter((x) => on.has(x)));
},
});
root.classList.add("m02-sod__check");
return root;
}),
});
}
/** 글 칸 — 쉼표로 여럿 */
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)))
: [createEmpty({ text: to("RuleNone"), small: true })]),
...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
}