/* ============================================================================= * ui_template_help.ts * [?] 설명서(54-2) — 좌측 패널 아래 단추 묶음 맨 왼쪽 단추 + 모달(왼쪽 차례 · 오른쪽 글). B02~B09 공용. * 글 = `resources/help/<페이지>/*.md` — 서버가 열 때마다 읽음(파일만 고치면 다음에 열 때 바뀜). * 페이지 = 주소(#/b05-…)의 B0N · 먼저 여는 항목 = 좌측 패널에서 보이는 `[data-help-topic]` 중 마지막 * (지금 연 상자 · 구조물 종류) · 없으면 첫 파일(00_설명서). * 단추 자리 = `.ui-sidebar-actions` 안 `[data-help-row]`(없으면 그 줄 자신)의 맨 앞. * ========================================================================== */ import "./ui_template_help.css"; import { API_BASE_URL } from "@config/config_frontend"; import { createButton, el } from "./ui_template_elements"; import { openModal } from "./ui_template_modal"; interface HelpItem { key: string; title: string; text: string; } const TEXT = { button: "?", tip: "설명서", title: "설명서", close: "닫기", empty: "설명 글 없음" }; function currentPage(): string { const n = /^#\/b0(\d)/i.exec(location.hash)?.[1]; return n ? `B0${n}` : ""; } function escapeHtml(text: string): string { return text.replace( /[&<>"]/g, (c) => `&${{ "&": "amp", "<": "lt", ">": "gt", '"': "quot" }[c]};`, ); } function inline(text: string): string { return escapeHtml(text) .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/`(.+?)`/g, "$1"); } /** 메모장 글에 쓸 만큼만 — 제목(#) · 목록(- · 1.) · 문단 · **굵게** · `코드` */ export function renderHelpMarkdown(text: string): string { const out: string[] = []; let list: "ul" | "ol" | null = null; let para: string[] = []; const flush = (): void => { if (para.length) out.push(`

${para.map(inline).join("
")}

`); para = []; if (list) out.push(``); list = null; }; for (const raw of text.split(/\r?\n/)) { const line = raw.trim(); const head = /^(#{1,4})\s+(.*)$/.exec(line); const item = /^(?:([-*])|\d+\.)\s+(.*)$/.exec(line); if (!line) flush(); else if (head) { flush(); out.push(`${inline(head[2])}`); } else if (item) { const kind = item[1] ? "ul" : "ol"; if (para.length || list !== kind) flush(); if (!list) out.push(`<${kind}>`); list = kind; out.push(`
  • ${inline(item[2])}
  • `); } else { if (list) flush(); para.push(line); } } flush(); return out.join(""); } function visibleTopics(): string[] { const panel = document.querySelector(".ui-workflow-overlay__panel--title") ?? document; return [...panel.querySelectorAll("[data-help-topic]")] .filter((node) => node.offsetParent !== null) .map((node) => node.dataset.helpTopic ?? ""); } async function fetchItems(page: string): Promise { const response = await fetch(`${API_BASE_URL}/help/${page}`, { credentials: "include", cache: "no-store", }); return response.ok ? ((await response.json()) as { items: HelpItem[] }).items : []; } /** B05 · B06 = 한 페이지(구조물 폼 공유) — 제 폴더 뒤에 짝 폴더 항목(짝의 설명서는 뺌)도 보임 */ const PAIR: Record = { B05: "B06", B06: "B05" }; async function openHelp(): Promise { const page = currentPage(); const pair = PAIR[page]; const [own, other] = await Promise.all([fetchItems(page), pair ? fetchItems(pair) : []]); const items = [...own, ...other.filter((item) => !own.some((o) => o.key === item.key))]; const topics = visibleTopics(); const first = [...topics].reverse().find((key) => items.some((item) => item.key === key)); openModal({ title: `${TEXT.title} · ${page}`, closeLabel: TEXT.close, dialogClass: "ui-help", mount: (body) => { const article = el("article", { className: "ui-help__text" }); const toc = el("nav", { className: "ui-help__toc" }); const show = (key: string): void => { const item = items.find((entry) => entry.key === key); article.innerHTML = item ? renderHelpMarkdown(item.text) : `

    ${TEXT.empty}

    `; article.dataset.helpKey = key; toc .querySelectorAll("button") .forEach((b) => b.classList.toggle("is-active", b.dataset.helpKey === key)); }; for (const item of items) { const link = el("button", { className: "ui-help__link", text: item.title, attrs: { type: "button" }, }); link.dataset.helpKey = item.key; link.addEventListener("click", () => show(item.key)); toc.append(link); } body.append(el("div", { className: "ui-help__grid", children: [toc, article] })); show(first ?? items[0]?.key ?? ""); }, }); } export function createHelpButton(): HTMLButtonElement { const button = createButton({ label: TEXT.button, variant: "ghost", onClick: () => void openHelp(), }); button.classList.add("ui-help-btn"); button.title = button.ariaLabel = TEXT.tip; return button; } /** 아래 단추 묶음에 [?] 를 한 번 — `[data-help-row]` 가 있으면 그 줄 · 없으면 묶음 자신의 맨 앞 */ export function placeHelpButton(actions: HTMLElement): void { if (actions.querySelector(".ui-help-btn")) return; (actions.querySelector("[data-help-row]") ?? actions).prepend(createHelpButton()); }