Files
Aislo/M02_MasterTemplete/StructOptions/M02_SO_Groups.ts
T
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

165 lines
6.8 KiB
TypeScript

/* =============================================================================
* M02_SO_Groups.ts
* 구조물 정의 화면의 **옵션 그룹** 부품(PLAN 46-2 · 48-2) — 고른 종류의 그룹 목록
* (이름 · 위로 · 아래로 · 지우기 · 형상 칸 길이 · 높이 · 전 · 후 받기 켬끔과 뜻 · [그룹 추가]) ·
* 옵션 줄마다 「들어갈 자리」 고르기(본체 · 그룹).
* 값은 화면이 쥔 고칠 사본에만 — 저장은 화면 [저장](`M02_SO_Def.ts`).
* 그룹 하나 = 프로젝트 B05 · B06 폼의 보조 컨테이너 하나 · 옵션이 없어도 됨(서버 `M02_SO_Groups.py`).
* ========================================================================== */
import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements";
import { SHAPE_KEYS, type GroupDef } from "./M02_SO_Def_Model";
import { checkBox, textBox } from "./M02_SO_Def_Ui";
import { to } from "./M02_SO_Text";
import { createText } from "@ui/ui_template_text";
import { createEmpty } from "@ui/ui_template_empty";
const NAME_MAX = 40;
/** 새 그룹 id — 서버 규칙 `g[0-9a-z]{1,16}` · 같은 종류 안에서 안 겹치게 */
export function newGroupId(list: readonly GroupDef[]): string {
const taken = new Set(list.map((g) => g.id));
for (;;) {
const id = `g${Math.random().toString(36).slice(2, 8)}`;
if (/^g[0-9a-z]{1,16}$/.test(id) && !taken.has(id)) return id;
}
}
/** 그 옵션이 든 그룹 id — 없으면 null(본체) */
export function groupOf(list: readonly GroupDef[], key: string): string | null {
return list.find((g) => g.옵션.includes(key))?.id ?? null;
}
/** 옵션을 그룹(null = 본체)으로 옮김 — 한 옵션은 한 그룹에만 */
export function moveOption(list: GroupDef[], key: string, gid: string | null): void {
for (const g of list) g.옵션 = g.옵션.filter((k) => k !== key);
const target = gid ? list.find((g) => g.id === gid) : null;
if (target) target.옵션.push(key);
}
export interface GroupBlockOptions {
/** 고른 종류의 그룹(고칠 사본 — 이 부품이 직접 고침) */
list: GroupDef[];
edit: boolean;
/** 고쳤음 — 화면이 고침 표시 · 다시 그리기 */
changed: () => void;
}
/** 그룹 형상 줄 — 칸마다 받기 체크 + 뜻(무엇을 정하는지 한 줄) */
function shapeLine(group: GroupDef, edit: boolean, changed: () => void): HTMLElement {
const shape = (group.형상 ??= {});
return el("div", {
className: "m02-sog__shape",
attrs: { "data-sog-shape": group.id },
children: SHAPE_KEYS.map((k) => {
const f = shape[k] ?? { 받기: false, 뜻: "" };
const on = checkBox(
to(`Shape_${k}` as const),
f.받기,
(v) => ((shape[k] = { ...f, 받기: v }), changed()),
{ edit, attr: `data-sog-shape-${SHAPE_KEYS.indexOf(k)}` },
);
const mean = textBox(f.뜻, (v) => (shape[k] = { ...(shape[k] ?? f), 뜻: v }), changed, {
edit: edit && f.받기,
cls: "m02-sog__mean",
place: to("ShapeMean"),
});
return el("div", { className: "m02-sog__shapecell", children: [on, mean] });
}),
});
}
/** 그룹 목록 칸 — 제목 · 안내 · 그룹 줄 · [그룹 추가] */
export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
const { list } = opt;
const rows = list.map((group, index) => {
const name = el("input", {
className: "ui-input ui-input--sm m02-sog__name",
attrs: { type: "text", maxlength: String(NAME_MAX), "data-sog-name": group.id },
}) as HTMLInputElement;
name.value = group.이름;
name.placeholder = to("GroupName");
name.disabled = !opt.edit;
// 글자마다 사본만 고침(다시 그리면 초점이 끊김) · 칸을 떠날 때 다시 그림(선택지 이름)
name.addEventListener("input", () => {
group.이름 = name.value;
name.classList.toggle("is-invalid", !name.value.trim());
});
name.addEventListener("change", () => opt.changed());
const step = (delta: number): void => {
const at = index + delta;
if (at < 0 || at >= list.length) return;
[list[index], list[at]] = [list[at], list[index]];
opt.changed();
};
const up = createButton({ label: to("GroupUp"), variant: "ghost", size: "sm" });
const down = createButton({ label: to("GroupDown"), variant: "ghost", size: "sm" });
const del = createButton({ label: to("GroupDel"), variant: "danger", size: "sm" });
up.disabled = !opt.edit || index === 0;
down.disabled = !opt.edit || index === list.length - 1;
del.disabled = !opt.edit;
up.addEventListener("click", () => step(-1));
down.addEventListener("click", () => step(1));
del.addEventListener("click", async () => {
if (!(await showConfirmDialog(to("GroupDelAsk", group.이름), to("GroupDel")))) return;
list.splice(list.indexOf(group), 1);
opt.changed();
});
const line = el("div", {
className: "m02-sog__row",
attrs: { "data-sog-group": group.id },
children: [
name,
el("small", {
className: "ui-text ui-text--muted m02-opts__key",
text: to("GroupCount", String(group.옵션.length)),
}),
...(opt.edit ? [up, down, del] : []),
],
});
return el("div", {
className: "m02-sog__item",
children: [line, shapeLine(group, opt.edit, opt.changed)],
});
});
const add = createButton({ label: to("GroupAdd"), variant: "ghost", size: "sm" });
add.setAttribute("data-sog-add", "");
add.addEventListener("click", () => {
let n = list.length + 1;
while (list.some((g) => g.이름 === to("GroupNew", String(n)))) n += 1;
list.push({ id: newGroupId(list), 이름: to("GroupNew", String(n)), 옵션: [] });
opt.changed();
});
return el("div", {
className: "ui-card m02-sog",
attrs: { "data-pad": "12" },
children: [
el("h4", { className: "m02-opts__group", text: to("GroupHead") }),
createText(to("GroupNote")),
...(rows.length ? rows : [createEmpty({ text: to("GroupNone"), small: true })]),
...(opt.edit ? [el("div", { className: "m02-side__row", children: [add] })] : []),
],
});
}
/** 옵션 줄의 「들어갈 자리」 고르기 — 본체 · 그룹 이름들 */
export function buildPlaceSelect(
list: GroupDef[],
key: string,
edit: boolean,
changed: () => void,
): HTMLSelectElement {
const select = el("select", {
className: "ui-input ui-input--sm m02-sog__place",
attrs: { "data-sog-place": key },
}) as HTMLSelectElement;
select.append(new Option(to("Main"), ""), ...list.map((g) => new Option(g.이름 || g.id, g.id)));
select.value = groupOf(list, key) ?? "";
select.disabled = !edit || !list.length;
select.addEventListener("change", () => {
moveOption(list, key, select.value || null);
changed();
});
return select;
}