- 표 resources/master_template/구조물옵션표시.json(숨김만 저장 · 없으면 모두 보임) - GET /api/projects/structure-types 옵션마다 visible · GET/PUT /api/m02/structure-options - M02 좌측 「구조물 옵션 보이기」 줄 → 메인 종류(군별) → 옵션 체크 · [저장] · [모두 보임] Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6KXAabDTenEU7hrDQmCKY
221 lines
7.8 KiB
TypeScript
221 lines
7.8 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_Options.ts
|
|
* 좌측 「구조물 옵션 보이기」 줄(구조물 집계 양식 아래 별도) — 누르면 메인 칸에
|
|
* 종류 목록(군별) → 고른 종류의 옵션 체크 목록(라벨 · 키 작은 글 · B06 전용 · 필수 표시) ·
|
|
* [저장] · [모두 보임]. 체크 안 한 옵션은 B05 · B06 구조물 폼에서 숨음(정의 · 값은 그대로).
|
|
* 서버 = `M02_MasterTemplete_Options.py`(마스터 템플릿 `구조물옵션표시.json` · 숨긴 것만 저장).
|
|
* ========================================================================== */
|
|
|
|
import { createButton, createTag, el, showToast } from "@ui/ui_template_elements";
|
|
import {
|
|
fetchStructureOptions,
|
|
saveStructureOptions,
|
|
StaleError,
|
|
type OptionKind,
|
|
type OptionsDoc,
|
|
} from "./M02_MasterTemplete_Api_Fetch";
|
|
import { to } from "./M02_MasterTemplete_Options_Text";
|
|
|
|
export interface OptionsSectionOptions {
|
|
isAdmin: boolean;
|
|
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
|
confirmLeave: () => Promise<boolean>;
|
|
/** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */
|
|
show: (label: string, node: HTMLElement) => (dirty: boolean) => void;
|
|
/** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */
|
|
onOpen: () => void;
|
|
}
|
|
|
|
export interface OptionsSection {
|
|
title: string;
|
|
body: HTMLElement;
|
|
/** 다른 화면을 열었음 — 고름 표시만 지움 */
|
|
clear: () => void;
|
|
}
|
|
|
|
const why = (error: unknown): string => (error instanceof Error ? error.message : "");
|
|
|
|
export function buildOptionsSection(opt: OptionsSectionOptions): OptionsSection {
|
|
const row = el("button", {
|
|
className: "m02-side__item ui-sidebar-row",
|
|
text: to("Row"),
|
|
attrs: { type: "button", "data-options-row": "" },
|
|
});
|
|
const body = el("div", { className: "m02-side__items ui-sidebar-list", children: [row] });
|
|
row.addEventListener("click", () => void open());
|
|
|
|
/** 이 화면이 열려 있는 동안 참 — 다른 것이 열리면 `clear` 가 내림 */
|
|
let opened = false;
|
|
const mark = (on: boolean): void => {
|
|
opened = on;
|
|
row.classList.toggle("is-active", on);
|
|
};
|
|
|
|
async function open(): Promise<void> {
|
|
if (opened || !(await opt.confirmLeave())) return;
|
|
let doc: OptionsDoc;
|
|
try {
|
|
doc = await fetchStructureOptions();
|
|
} catch (error) {
|
|
return showToast(to("Load_Failed", why(error)), "error");
|
|
}
|
|
draw(doc);
|
|
}
|
|
|
|
/** 메인 칸 화면 — `doc.표시` 를 고칠 사본(저장 전까지 서버에 안 감)으로 쥠 */
|
|
function draw(doc: OptionsDoc): void {
|
|
let version = doc.판;
|
|
const edit = opt.isAdmin;
|
|
/** 숨긴 옵션 — 종류 → 키 집합 */
|
|
const hidden = new Map<string, Set<string>>();
|
|
const load = (src: OptionsDoc["표시"]): void => {
|
|
hidden.clear();
|
|
for (const [id, keys] of Object.entries(src)) hidden.set(id, new Set(Object.keys(keys)));
|
|
};
|
|
load(doc.표시);
|
|
let kinds = doc.종류;
|
|
let current: OptionKind | null = null;
|
|
|
|
const kindList = el("div", { className: "m02-opts__kinds" });
|
|
const optionList = el("div", { className: "m02-opts__list" });
|
|
const off = (id: string): number => hidden.get(id)?.size ?? 0;
|
|
|
|
const paintKinds = (): void => {
|
|
const parts: HTMLElement[] = [];
|
|
let group = "";
|
|
for (const k of kinds) {
|
|
if (k.group !== group) {
|
|
group = k.group;
|
|
parts.push(
|
|
el("h4", {
|
|
className: "m02-opts__group",
|
|
text: group.length === 1 ? to("Group", group) : group,
|
|
}),
|
|
);
|
|
}
|
|
const n = off(k.type_id);
|
|
const b = el("button", {
|
|
className: `m02-opts__kind${k.type_id === current?.type_id ? " is-active" : ""}`,
|
|
attrs: { type: "button", "data-options-kind": k.type_id },
|
|
children: [
|
|
el("span", { text: k.name }),
|
|
...(n ? [createTag(to("Hidden", String(n)), "warning")] : []),
|
|
...(k.enabled ? [] : [createTag(to("Off"), "neutral")]),
|
|
],
|
|
});
|
|
b.addEventListener("click", () => {
|
|
current = k;
|
|
paintKinds();
|
|
paintOptions();
|
|
});
|
|
parts.push(b);
|
|
}
|
|
kindList.replaceChildren(...parts);
|
|
};
|
|
|
|
const paintOptions = (): void => {
|
|
if (!current) {
|
|
optionList.replaceChildren(el("p", { className: "m02-side__empty", text: to("Pick") }));
|
|
return;
|
|
}
|
|
const kind = current;
|
|
optionList.replaceChildren(
|
|
el("h3", { className: "m02-matset__head", text: kind.name }),
|
|
...kind.options.map((o) => {
|
|
const box = el("input", {
|
|
attrs: { type: "checkbox", "data-options-key": o.key },
|
|
}) as HTMLInputElement;
|
|
box.checked = !hidden.get(kind.type_id)?.has(o.key);
|
|
box.disabled = !edit;
|
|
box.addEventListener("change", () => {
|
|
const set = hidden.get(kind.type_id) ?? new Set<string>();
|
|
if (box.checked) set.delete(o.key);
|
|
else set.add(o.key);
|
|
hidden.set(kind.type_id, set);
|
|
setDirty(true);
|
|
paintKinds();
|
|
});
|
|
const tags: HTMLElement[] = [];
|
|
if (o.phase === "detail") tags.push(createTag(to("Detail"), "accent"));
|
|
if (o.required && o.phase === "b05") {
|
|
const tag = createTag(to("Required"), "danger");
|
|
tag.title = to("RequiredTip");
|
|
tags.push(tag);
|
|
}
|
|
return el("label", {
|
|
className: "m02-opts__option",
|
|
children: [
|
|
box,
|
|
el("span", { text: o.label }),
|
|
el("small", { className: "m02-opts__key", text: o.key }),
|
|
...tags,
|
|
],
|
|
});
|
|
}),
|
|
);
|
|
};
|
|
|
|
const save = createButton({ label: to("Save"), variant: "filled" });
|
|
const all = createButton({ label: to("ShowAll"), variant: "ghost" });
|
|
all.addEventListener("click", () => {
|
|
if (![...hidden.values()].some((s) => s.size)) return;
|
|
hidden.clear();
|
|
setDirty(true);
|
|
paintKinds();
|
|
paintOptions();
|
|
});
|
|
save.addEventListener("click", async () => {
|
|
const 표시: OptionsDoc["표시"] = {};
|
|
for (const [id, keys] of hidden)
|
|
if (keys.size) 표시[id] = Object.fromEntries([...keys].map((k) => [k, false as const]));
|
|
save.disabled = true; // 연달아 눌러도 한 번만
|
|
try {
|
|
const done = await saveStructureOptions(version, 표시);
|
|
version = done.판;
|
|
load(done.표시);
|
|
setDirty(false);
|
|
showToast(to("Saved"), "success");
|
|
paintKinds();
|
|
paintOptions();
|
|
} catch (error) {
|
|
if (error instanceof StaleError) {
|
|
showToast(to("Stale"), "error");
|
|
try {
|
|
const fresh = await fetchStructureOptions();
|
|
version = fresh.판;
|
|
kinds = fresh.종류;
|
|
load(fresh.표시);
|
|
setDirty(false);
|
|
current = kinds.find((k) => k.type_id === current?.type_id) ?? null;
|
|
paintKinds();
|
|
paintOptions();
|
|
} catch (again) {
|
|
showToast(to("Load_Failed", why(again)), "error");
|
|
}
|
|
} else showToast(to("Failed", why(error)), "error");
|
|
} finally {
|
|
save.disabled = false;
|
|
}
|
|
});
|
|
|
|
const node = el("div", {
|
|
className: "m02-opts",
|
|
children: [
|
|
el("p", { className: "m02-side__empty", text: to("Note") }),
|
|
el("div", {
|
|
className: "m02-opts__panes",
|
|
children: [kindList, optionList],
|
|
}),
|
|
...(edit ? [el("div", { className: "m02-side__row", children: [save, all] })] : []),
|
|
],
|
|
});
|
|
const setDirty = opt.show(to("Screen"), node);
|
|
paintKinds();
|
|
paintOptions();
|
|
opt.onOpen();
|
|
mark(true);
|
|
}
|
|
|
|
return { title: to("Title"), body, clear: () => mark(false) };
|
|
}
|