/* ============================================================================= * M01_MasterData_UI_LogicLab_Detail.ts * 「단가산출 로직」 상세 화면 — 컨테이너 넷을 위에서 아래로 * ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 시험 계산(입력값 + 결과값 — `Logic_Calc` 그대로) * ③ 단가산출 상세(공용 부품 `_LogicLab_Ho.ts` — 하위 박스 · 계산식 · 박스 옵션 · 그 자리 고침) ④ 텍스트 수식(`_LogicLab_Formula.ts`) * 그 자리 고침(722-2) = 시험 값만(`_LogicLab_Cover.ts` trial — 줄 사본으로 셈 · 정본 · 요소 · 캐시 안 바뀜) * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import type { LogicRow } from "./M01_MasterData_UI_Logic_Api"; import { buildEditor } from "./M01_MasterData_UI_Logic_Edit"; import { openPicker } from "./M01_MasterData_UI_Logic_Pick"; import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text"; import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula"; import { buildEst } from "./M01_MasterData_UI_LogicLab_Est"; import { buildHo, type HoContext, type HoEdit } from "./M01_MasterData_UI_LogicLab_Ho"; import { trial } from "./M01_MasterData_UI_LogicLab_Cover"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Style.css"; import "./M01_MasterData_UI_LogicLab_Pill.css"; import { block, nowInputs } from "./M01_MasterData_UI_LogicLab_View"; export { trial }; export interface DetailContext extends HoContext { file: string; reasons: string[]; /** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */ ownHost: HTMLElement; onChange: () => void; /** 시험 계산 칸(`buildCalc` 가 채움) — 둘째 컨테이너 안에 놓음 */ calcHost: HTMLElement; /** 흐름이 있는 로직 — 이 칸에 왼쪽 「지금 내용」(단가산출 상세 · 텍스트 수식 = `buildFlowView`)을 그림 · * 있으면 호표 · 읽는 식 두 묶음 대신 이 칸을 놓음(기본정보 · 시험 계산은 그대로) */ flowHost?: HTMLElement; /** 시스템 관리자 — 기본 로직 안내 글이 다름(흐름도에서 고침 / 보기만) */ admin?: boolean; /** 722-2 시험 고침 뒤 — 시험 계산 칸을 고친 줄로 다시 그림(있으면 그 자리 고침 켜짐 · 셈은 여기서 누름) */ onTrial?: () => void; /** 717-2b M02 단가산출 — 단가산출 상세 · 인력·재료·경비 덩이를 이 칸으로(SK 셈 답 · 고침 표) */ cover?: { ho: HTMLElement; est: HTMLElement }; } const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement => el("section", { className: "m01-logic__section m01lab__box", children: [ el("div", { className: "m01-logic__section-head", children: [ el("h3", { text: title }), ...(hint ? [el("span", { className: "ui-text ui-text--muted", text: hint })] : []), ], }), ...body, ], }); function infoBox(ctx: DetailContext): HTMLElement[] { const row = ctx.row; const own = isOwnLogic(row); const field = (label: string, key: "이름" | "원문번호" | "결과단위" | "출처"): HTMLElement => { if (!own) return cell(label, row[key] ?? ""); const box = el("input", { className: "ui-input ui-input--sm m01-logic__input", attrs: { "data-own": key }, }); box.value = row[key] ?? ""; box.addEventListener("input", () => { row[key] = box.value; ctx.onChange(); }); return el("label", { className: "m01lab__cell", children: [el("span", { className: "ui-text ui-text--muted", text: label }), box], }); }; const cell = (label: string, value: string): HTMLElement => el("div", { className: "m01lab__cell", children: [ el("span", { className: "ui-text ui-text--muted", text: label }), el("span", { text: value || "—" }), ], }); const note = el("textarea", { className: "ui-input ui-input--sm m01-logic__input m01-logic__note", }); note.value = row.비고 ?? ""; note.addEventListener("input", () => { if (note.value.trim()) row.비고 = note.value; else delete row.비고; ctx.onChange(); }); const reasons = ctx.reasons.length ? [ el("div", { className: "ui-banner ui-banner--danger m01-logic__reasons", children: [ el("strong", { text: tx("Head_Blocked") }), ...ctx.reasons.map((r) => el("div", { text: safeReason(r) })), ], }), ] : []; const grid = el("div", { className: "m01lab__info", children: [ cell(tx("Head_File"), ctx.file), cell(tx("Head_Key"), row.키), field(tx("Head_Number"), "원문번호"), field(tx("Head_Name"), "이름"), field(tx("Head_Unit"), "결과단위"), field(tx("Head_Source"), "출처"), cell(tx("Head_Owner"), row.소유 ?? ""), el("label", { className: "m01lab__cell m01lab__cell--wide", children: [ el("span", { className: "ui-text ui-text--muted", text: tx("Head_Note") }), note, ], }), ], }); // 늘 맨 위 접힌 한 줄 — 펼침은 이 로직 안에서 기억(다시 그려도) · 막힘 까닭은 접어도 보임 const head = [row.키, row.원문번호, row.이름, row.결과단위].filter(Boolean).join(" · "); const fold = el("details", { className: "m01lab__box m01lab__infofold", attrs: ctx.ownHost.dataset.info ? { open: "" } : {}, children: [ el("summary", { children: [ el("strong", { text: tl("Info_Title") }), el("span", { className: "ui-text ui-text--muted", text: head }), ], }), el("p", { className: "ui-text ui-text--muted", text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"), }), grid, ], }); fold.addEventListener("toggle", () => (ctx.ownHost.dataset.info = fold.open ? "1" : "")); return [fold, ...reasons]; } /** 자체 로직(키 GX…) — 정본은 아님 */ export const isOwnLogic = (row: LogicRow): boolean => row.키.startsWith("GX"); /** 자체 로직은 정본 화면과 같은 편집기를 호표 아래에 폄(머리 칸은 기본정보에서 고치므로 숨김) */ function ownEditor(ctx: DetailContext): HTMLElement { if (!ctx.ownHost.childElementCount) buildEditor(ctx.ownHost, { row: ctx.row, file: ctx.file, isNew: false, files: [], reasons: [], prices: ctx.prices, lines: ctx.lines, onChange: ctx.onChange, onFile: () => undefined, onPick: openPicker, }); return el("details", { className: "m01lab__edit", attrs: { open: "" }, children: [el("summary", { text: tl("Edit_Title") }), ctx.ownHost], }); } /** M01 그 자리 고침 고리 — 시험 값(줄 객체 열쇠) · 고치면 시험 계산을 다시 그리고 셈 */ function trialEdit(ctx: DetailContext): HoEdit | undefined { const redraw = ctx.onTrial; if (!redraw) return undefined; const covers = trial.of(ctx.row); return { covers, scope: "m01", set: (idx, cover) => { if (cover) covers[idx] = cover; else delete covers[idx]; redraw(); ctx.calcHost.querySelector("[data-calc-run]")?.click(); }, }; } function hoBox(ctx: DetailContext): HTMLElement { const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); if (!money) { return block( section(tl("Ho_Title"), [ el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }), el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), ]), "ho", ); } const body = buildHo({ ...ctx, edit: trialEdit(ctx) }); const totalLine = body.pop()!; // 계 줄 — 자체 로직은 접힌 표 밖에 둠 // 자체 로직 — 고치는 자리는 편집기 하나 · 읽기용 박스는 접어 둠(펼치면 소계가 보임) const fold = el("details", { className: "m01lab__fold", attrs: ctx.ownHost.dataset.fold ? { open: "" } : {}, children: [el("summary", { text: tl("Ho_Fold") }), ...body], }); fold.addEventListener("toggle", () => (ctx.ownHost.dataset.fold = fold.open ? "1" : "")); const blocks = isOwnLogic(ctx.row) ? [ownEditor(ctx), fold, totalLine] : [...body, totalLine]; const calculated = ctx.lines !== null; const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; return block(section(tl("Ho_Title"), [nowInputs(ctx.values), ...blocks], hint), "ho"); } /** 상세 화면 넷을 `host` 에 쌓음 */ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { const formula = el("div", { className: "m01lab__formula" }); const fx = { inputs: ctx.row.입력, values: ctx.values, onInput: ctx.onInput }; buildFormula(formula, { text: ctx.text, ...fx }); // 다시 그리면 포커스가 빠짐 — 고치던 칸(편집기 · 기본정보)을 기억해 되돌림 const active = document.activeElement; const kept = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? { node: active, from: active.selectionStart, to: active.selectionEnd } : null; // 보기 고르기(`_View.ts`) — 덩이마다 이름표 · 고른 덩이만 보임 · 시험 계산은 숨어도 그려 둠 host.replaceChildren( ...infoBox(ctx), block(section(tl("Calc_Title"), [ctx.calcHost]), "calc"), block( section(tl("View_Est"), [ ctx.cover?.est ?? buildEst({ text: ctx.text, unit: ctx.row.결과단위, ...fx }), ]), "est", ), ...(ctx.flowHost && !ctx.cover ? [block(nowInputs(ctx.values), "ho"), ctx.flowHost] : [ ctx.cover ? block(section(tl("Ho_Title"), [nowInputs(ctx.values), ctx.cover.ho]), "ho") : hoBox(ctx), block(section(tl("Formula_Title"), [formula]), "formula"), ]), ); const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음 const back = own ? host.querySelector(`[data-own="${own}"]`) : kept?.node; if (kept && back && (own || ctx.ownHost.contains(back))) { back.focus(); if (kept.from !== null) back.setSelectionRange(kept.from, kept.to); } }