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:
@@ -1,19 +1,21 @@
|
||||
/* =============================================================================
|
||||
* M02_SO_Groups.ts
|
||||
* 「구조물 옵션 보이기」 화면의 **옵션 그룹** 부품(PLAN 46-2) — 고른 종류의 그룹 목록
|
||||
* (이름 · 위로 · 아래로 · 지우기 · [그룹 추가]) · 옵션 줄마다 「들어갈 자리」 고르기(본체 · 그룹).
|
||||
* 값은 화면이 쥔 고칠 사본(종류 → 그룹[])에만 — 저장은 화면 [저장](`M02_SO_Options.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 type { OptionGroupDef } from "./M02_SO_Api";
|
||||
import { SHAPE_KEYS, type GroupDef } from "./M02_SO_Def_Model";
|
||||
import { checkBox, textBox } from "./M02_SO_Def_Ui";
|
||||
import { to } from "./M02_SO_Text";
|
||||
|
||||
const NAME_MAX = 40;
|
||||
|
||||
/** 새 그룹 id — 서버 규칙 `g[0-9a-z]{1,16}` · 같은 종류 안에서 안 겹치게 */
|
||||
export function newGroupId(list: readonly OptionGroupDef[]): string {
|
||||
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)}`;
|
||||
@@ -22,35 +24,49 @@ export function newGroupId(list: readonly OptionGroupDef[]): string {
|
||||
}
|
||||
|
||||
/** 그 옵션이 든 그룹 id — 없으면 null(본체) */
|
||||
export function groupOf(list: readonly OptionGroupDef[], key: string): string | 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: OptionGroupDef[], key: string, gid: string | null): void {
|
||||
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);
|
||||
}
|
||||
|
||||
/** 이름이 다 괜찮은가 — 1~40자 · 같은 종류 안에서 안 겹침(서버도 같은 검사) */
|
||||
export function namesOk(groups: Map<string, OptionGroupDef[]>): boolean {
|
||||
for (const list of groups.values()) {
|
||||
const names = list.map((g) => g.이름.trim());
|
||||
if (names.some((n) => !n || n.length > NAME_MAX)) return false;
|
||||
if (new Set(names).size !== names.length) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface GroupBlockOptions {
|
||||
/** 고른 종류의 그룹(고칠 사본 — 이 부품이 직접 고침) */
|
||||
list: OptionGroupDef[];
|
||||
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;
|
||||
@@ -87,7 +103,7 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
|
||||
list.splice(list.indexOf(group), 1);
|
||||
opt.changed();
|
||||
});
|
||||
return el("div", {
|
||||
const line = el("div", {
|
||||
className: "m02-sog__row",
|
||||
attrs: { "data-sog-group": group.id },
|
||||
children: [
|
||||
@@ -99,6 +115,10 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
|
||||
...(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", "");
|
||||
@@ -121,7 +141,7 @@ export function buildGroupBlock(opt: GroupBlockOptions): HTMLElement {
|
||||
|
||||
/** 옵션 줄의 「들어갈 자리」 고르기 — 본체 · 그룹 이름들 */
|
||||
export function buildPlaceSelect(
|
||||
list: OptionGroupDef[],
|
||||
list: GroupDef[],
|
||||
key: string,
|
||||
edit: boolean,
|
||||
changed: () => void,
|
||||
|
||||
Reference in New Issue
Block a user