/* ============================================================================= * 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; /** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */ 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 { 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>(); 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(); 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) }; }