/* ============================================================================= * M01_MasterData_UI_LogicLab_Modal.ts * 자료 모달 둘 — * ① 줄 모달(`openMaterialModal`) — 줄을 누르면 그 줄의 단가(재료·인력·기계 또는 다른 로직)를 * 바로 보임(알약 목록 없이) · 그 아래 한 줄 풀이 + 원문 수량 식 * ② 표 모달(`openTableModal`) — 수량 칸 안 알약(찾기(...) 표 참조)을 누르면 그 표만 보임 * 표는 표 그대로 + 지금 넣은 값으로 걸린 줄 강조 · 단일 값(단가)은 값과 출처 * 표 읽기 = `/elements` → `/table`(`Logic_Note.loadTable`) · 단가 = 로직 화면이 이미 받은 `prices` * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; import type { ElementBrief, NamedFormula } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { explain, loadTable, matchRow, type TableRow } from "./M01_MasterData_UI_Logic_Note"; import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; /** 원 단위 소수점 0 — 돈 자리만(수량·비율은 `formatNumber` 그대로) */ const formatMoney = (value: unknown): string => typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 }) : formatNumber(value); export interface ModalTarget { title: string; /** 수량 식(호표 줄) · 덧줄은 그 식 */ expr: string; /** 호표 줄의 단가 요소 키 — 덧줄은 없음 */ element?: string; prices: Record; middles: NamedFormula[]; /** 시험 계산 칸에 넣은 값 — 걸린 줄을 맞히는 데 씀 */ values: Record; } /** 수량 칸 표 알약 하나가 여는 자료 — 조각 계약의 「표 찾기」 자리 */ export interface TableTarget { title: string; table: string; conds: [string, string][]; values: Record; } function tableView( table: TableRow, conds: [string, string][], values: Record, ): HTMLElement { // 표 줄의 빈 칸은 「무엇이든」 — 서버 찾기()와 같게 그 칸은 조건에서 뺌(줄마다 따로 봄) const hit = (table.줄 ?? []).find((line) => matchRow( { ...table, 줄: [line] }, conds.filter(([col]) => line[col] !== undefined && line[col] !== null && line[col] !== ""), { values, middle: {} }, ), ) ?? null; const cols = [...Object.keys(table.조건 ?? {}), ...Object.keys(table.값칸 ?? {})]; const rows = (table.줄 ?? []).map((line) => el("tr", { className: hit === line ? "m01lab__hit" : "", attrs: hit === line ? { "data-hit": "1" } : {}, children: cols.map((c) => el("td", { text: formatNumber(line[c]) })), }), ); const use = table.용도 ? [table.용도.공종, (table.용도.대상 ?? []).join("·")].filter(Boolean).join(" · ") : ""; return el("div", { className: "m01lab__data", attrs: { "data-table": table.키 }, children: [ el("strong", { text: `${table.원문번호 ?? ""} ${table.이름 ?? table.키} · ${table.키}` }), el("p", { className: "m01-logic__muted", text: [`${tl("Modal_Source")} ${table.출처 ?? ""}`, use].filter(Boolean).join(" · "), }), el("p", { className: "m01-logic__muted", text: hit ? tl("Modal_Hit") : tl("Modal_NoHit"), }), goButton({ kind: "table", key: table.키 }), el("div", { className: "m01-logic__scroll", children: [ el("table", { className: "m01-logic__grid m01lab__table", children: [ el("thead", { children: [el("tr", { children: cols.map((c) => el("th", { text: c })) })], }), el("tbody", { children: rows }), ], }), ], }), ], }); } const groupOf = (ref: string): string => ref.startsWith("LB") ? "인력" : ref.startsWith("M") ? "재료" : ref.startsWith("EQ") ? "기계" : ""; function goButton(target: MasterJump): HTMLElement { return createButton({ label: tl("Pill_Go"), variant: "ghost", onClick: () => jumpToMaster(target), }); } function priceView(ref: string, brief: ElementBrief): HTMLElement { const cols = Object.entries(brief.값칸 ?? {}); return el("div", { className: "m01lab__data", attrs: { "data-price": ref }, children: [ el("strong", { text: `${brief.이름 ?? ref} · ${ref}` }), el("dl", { className: "m01-logic__sums", children: [ ...(brief.규격 ? [el("dt", { text: tl("Modal_Spec") }), el("dd", { text: brief.규격 })] : []), el("dt", { text: tl("Modal_Value") }), el("dd", { className: "m01-logic__money", text: formatMoney(brief.값) }), ...(brief.file ? [el("dt", { text: tl("Modal_Source") }), el("dd", { text: brief.file })] : []), ...cols.flatMap(([k, v]) => [el("dt", { text: k }), el("dd", { text: String(v) })]), ], }), goButton({ kind: "element", group: groupOf(ref), ref: brief.ref ?? ref, file: brief.file }), ], }); } /** 모달 겉틀(배경·닫기·포커스) — 몸은 `mount` 이 채움 */ function openDialog(title: string, mount: (body: HTMLElement) => void): void { const body = el("div", { className: "m01lab__modal-body" }); const close = (): void => backdrop.remove(); const dialog = el("div", { className: "m01-logic__pick m01lab__modal", attrs: { role: "dialog", "aria-label": title }, children: [ el("div", { className: "m01lab__modal-top", children: [ el("h3", { text: title }), createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }), ], }), body, ], }); const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] }); backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); mount(body); document.body.append(backdrop); dialog.tabIndex = -1; dialog.focus(); } /** 줄을 눌렀을 때 — 그 줄의 단가(재료·인력·기계 또는 다른 로직)를 바로 보임(알약 없이) */ export function openMaterialModal(target: ModalTarget): void { openDialog(target.title, (body) => { const base = target.element?.split(".")[0] ?? ""; const price = target.element ? (target.prices[target.element] ?? target.prices[base]) : undefined; const logicRef = target.element?.startsWith("로직(") ? target.element : ""; const rateParts: HTMLElement[] = []; if (price && target.element) { rateParts.push(priceView(target.element, price)); } else if (logicRef) { rateParts.push(el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` })); } else if (target.element) { rateParts.push( el("p", { className: "m01-logic__muted", text: `${target.element} — ${tl(target.element.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`, }), ); } const unit = price ? `${tl("Modal_UnitPrice")} ${formatMoney(price.값)} × ` : ""; body.replaceChildren( ...rateParts, el("p", { className: "m01-logic__muted", text: tl("Modal_Sum") }), el("p", { className: "m01lab__sum", text: `${target.title} = ${unit}${explain(target.expr)}`, }), el("details", { className: "m01lab__raw", children: [ el("summary", { text: tl("Modal_Qty") }), el("pre", { className: "m01lab__expr", text: target.expr }), ], }), ); }); } /** 수량 칸 표 알약을 눌렀을 때 — 그 표만 보임(걸린 줄 강조) */ export function openTableModal(target: TableTarget): void { openDialog(target.title, (body) => { body.replaceChildren(el("p", { className: "m01-logic__muted", text: "…" })); void loadTable(target.table).then((table) => { body.replaceChildren( table ? tableView(table, target.conds, target.values) : el("p", { className: "m01-logic__bad", text: `${target.table} ?` }), ); }); }); }