/* ============================================================================= * M01_MasterData_UI_LogicLab_Source.ts * 원문 보기(품셈 본문 절) — 출처 글로 서버가 찾음(`GET /m01/logic/source`) · 늦게 온 답은 버림 * M01 로직 상세 「원문」 패널 · M02 일위대가 상세 「원문」 패널(47-4)이 같이 씀 * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { el } from "@ui/ui_template_elements"; import { API_BASE_URL } from "@config/config_frontend"; import "../M02_MasterTemplete/DataFlow/M02_DF_Panel.css"; import "./M01_MasterData_UI_LogicLab_Side.css"; const TEXT = { Loading: ["원문 찾는 중", "Looking up the source"], None: ["원문 연결 없음", "No linked source"], From: ["출처", "Source"], NoSource: ["출처 없음", "No source"], Near: ["{v} 절이 없어 위 절", "No {v} section — showing the parent"], } as const; const ts = (key: keyof typeof TEXT, v = ""): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0].replace("{v}", v); interface Block { h?: number; text?: string; p?: string; table?: string[][]; /** 출처가 가리킨 항 · 주 — 강조 */ hit?: boolean; } interface Found { number: string; matched: string; file: string; blocks: Block[]; item?: string; } function blockNode(b: Block): HTMLElement { const node = rawNode(b); if (b.hit) node.classList.add("m01lab-src__hit"); return node; } function rawNode(b: Block): HTMLElement { if (b.table) return el("div", { className: "m01-logic__scroll", children: [ el("table", { className: "m01-logic__grid m01lab-src__table", children: [ el("tbody", { children: b.table.map((r) => el("tr", { children: r.map((c) => el("td", { text: c })) }), ), }), ], }), ], }); if (b.h) return el("h4", { className: "m01lab-src__head", text: b.text ?? "" }); return el("p", { className: "m01lab-src__p", text: b.p ?? "" }); } export interface SourceView { root: HTMLElement; /** 그 출처 글의 원문을 그림 — 지금 보이는 것과 같으면 안 부름 */ show(src: string): void; } export function createSourceView(): SourceView { const body = el("div", { className: "m01lab-src__body" }); const title = el("strong", { className: "m02-dfp__title" }); let shown: string | null = null; const show = (src: string): void => { if (shown === src) return; shown = src; title.textContent = src || ts("NoSource"); body.replaceChildren(el("p", { className: "m01-logic__muted", text: ts("Loading") })); const none = (): void => body.replaceChildren( el("p", { className: "m01lab-src__none", text: ts("None") }), el("p", { className: "m01-logic__muted", text: `${ts("From")} · ${src || "—"}` }), ); if (!src.trim()) return none(); void fetch(`${API_BASE_URL}/m01/logic/source?${new URLSearchParams({ src })}`, { credentials: "include", }) .then((r) => (r.ok ? (r.json() as Promise<{ found: Found[] }>) : { found: [] })) .catch(() => ({ found: [] as Found[] })) .then(({ found }) => { if (shown !== src) return; if (!found.length) return none(); body.replaceChildren( ...found.flatMap((f) => [ el("p", { className: "m01-logic__muted m01lab-src__file", text: [ f.file, f.matched === f.number ? "" : ts("Near", f.number), f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "", ] .filter(Boolean) .join(" · "), }), ...f.blocks.map(blockNode), ]), ); requestAnimationFrame(() => body.querySelector(".m01lab-src__hit")?.scrollIntoView({ block: "center" }), ); }); }; return { root: el("div", { className: "m01lab-src", children: [title, body] }), show }; }