/* ============================================================================= * M01_MasterData_UI_LogicLab_Detail.ts * 「로직 개선 시험」 상세 화면 — 컨테이너 넷을 위에서 아래로 * ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 일위대가 호표(인력·자재·경비·그 밖 묶음 + 소계) * ③ 텍스트 수식(자리 — `_LogicLab_Formula.ts`) ④ 시험 계산(입력값 + 결과값 — `Logic_Calc` 그대로) * 호표 줄을 누르면 그 줄이 쓰는 자료 모달(`_LogicLab_Modal.ts`) * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import type { CalcLine, ElementBrief, HoLine, LogicRow, NamedFormula, TextAnswer, } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber, qtyKind } from "./M01_MasterData_UI_Logic_Edit"; import { tx } from "./M01_MasterData_UI_Logic_Text"; import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula"; import { openMaterialModal } from "./M01_MasterData_UI_LogicLab_Modal"; import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Style.css"; export interface DetailContext { row: LogicRow; file: string; reasons: string[]; prices: Record; /** 마지막 시험 계산의 줄 — 호표 차례 뒤에 덧줄 */ lines: CalcLine[] | null; /** 마지막 시험 계산의 읽는 식 줄(서버가 줌) */ text: TextAnswer | null; values: Record; onChange: () => void; /** 시험 계산 칸(`buildCalc` 가 채움) — 넷째 컨테이너 안에 놓음 */ calcHost: HTMLElement; } type Group = "labor" | "material" | "cost" | "other"; const GROUPS: Group[] = ["labor", "material", "cost", "other"]; const OF_KIND: Record = { 인력: "labor", 재료: "material", 기계: "cost" }; const OF_COST: Record = { 노무비: "labor", 재료비: "material", 경비: "cost" }; /** 줄 종류가 셋 중 어디에도 안 들어가면 「그 밖」 — 다른 로직을 부르는 줄은 비목으로 */ const groupOfHo = (item: HoLine): Group => OF_KIND[item.종류] ?? (item.종류 === "로직" ? OF_COST[item.비목 ?? ""] : undefined) ?? "other"; interface Entry { group: Group; name: string; spec: string; unit: string; qty: string; qtyTag: string; price: string; amount: number | null; /** 덧줄이면 true */ extra: boolean; open: () => void; } const groupName = (group: Group): string => tl(`G_${group}` as LabTextKey); 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: "m01-logic__muted", text: hint })] : []), ], }), ...body, ], }); function infoBox(ctx: DetailContext): HTMLElement { const row = ctx.row; const cell = (label: string, value: string): HTMLElement => el("div", { className: "m01lab__cell", children: [ el("span", { className: "m01-logic__muted", text: label }), el("span", { text: value || "—" }), ], }); const note = el("textarea", { className: "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: "m01-logic__reasons", children: [ el("strong", { text: tx("Head_Blocked") }), ...ctx.reasons.map((r) => el("div", { text: r })), ], }), ] : []; return section(tl("Info_Title"), [ el("p", { className: "m01-logic__muted", text: tl("Info_ReadOnly") }), ...reasons, el("div", { className: "m01lab__info", children: [ cell(tx("Head_File"), ctx.file), cell(tx("Head_Key"), row.키), cell(tx("Head_Number"), row.원문번호), cell(tx("Head_Name"), row.이름), cell(tx("Head_Unit"), row.결과단위), cell(tx("Head_Source"), row.출처), cell(tx("Head_Owner"), row.소유 ?? ""), el("label", { className: "m01lab__cell m01lab__cell--wide", children: [el("span", { className: "m01-logic__muted", text: tx("Head_Note") }), note], }), ], }), ]); } function entries(ctx: DetailContext): Entry[] { const middles: NamedFormula[] = ctx.row.중간 ?? []; const ho = ctx.row.호표 ?? []; const out: Entry[] = ho.map((item, i) => { const line = ctx.lines?.[i]; const brief = ctx.prices[item.요소]; const price = line ? line.단가 : brief?.값; return { group: groupOfHo(item), name: item.이름 ?? item.요소, spec: item.규격 ?? brief?.규격 ?? "", unit: item.단위 ?? "", qty: item.수량, qtyTag: qtyKind(item.수량), price: price === undefined || price === null ? "" : formatNumber(price), amount: line ? line.금액 : null, extra: false, open: () => openMaterialModal({ title: item.이름 ?? item.요소, expr: item.수량, element: item.요소, prices: ctx.prices, middles, values: ctx.values, }), }; }); (ctx.row.덧줄 ?? []).forEach((extra, i) => { const line = ctx.lines?.[ho.length + i]; out.push({ group: OF_COST[extra.비목 ?? ""] ?? "other", name: extra.이름, spec: "", unit: "", qty: extra.식, qtyTag: tl("Extra"), price: "", amount: line ? line.금액 : null, extra: true, open: () => openMaterialModal({ title: extra.이름, expr: extra.식, prices: ctx.prices, middles, values: ctx.values, }), }); }); return out; } const cellOf = (text: string, className = ""): HTMLElement => el("td", { className, text }); function lineRow(e: Entry): HTMLElement { const tr = el("tr", { className: `m01lab__line${e.extra ? " m01lab__line--extra" : ""}`, attrs: { tabindex: "0", "data-line": e.name }, children: [ el("td", { children: [ el("div", { text: e.name }), ...(e.spec ? [el("div", { className: "m01-logic__muted", text: e.spec })] : []), ], }), cellOf(e.unit), el("td", { children: [ el("span", { className: "m01-logic__tag", text: e.qtyTag }), el("span", { className: "m01lab__qty", text: ` ${e.qty}` }), ], }), cellOf(e.price, "m01-logic__money"), cellOf(e.amount === null ? "" : formatNumber(e.amount), "m01-logic__money"), ], }); tr.addEventListener("click", e.open); tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open()); return tr; } function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLElement[] { const sub = mine.reduce((s, e) => s + (e.amount ?? 0), 0); const empty = el("tr", { children: [ el("td", { attrs: { colspan: "5" }, className: "m01-logic__muted", text: tl("Empty_Group"), }), ], }); const subtotal = el("tr", { className: "m01lab__subtotal", attrs: { "data-group": group, "data-subtotal": calculated ? String(sub) : "" }, children: [ el("td", { attrs: { colspan: "4" }, text: `${groupName(group)} ${tl("Subtotal")}` }), cellOf(calculated ? formatNumber(sub) : "—", "m01-logic__money"), ], }); const headers = [ tl("Col_Name"), tl("Col_Unit"), tl("Col_Qty"), tl("Col_Price"), tl("Col_Amount"), ]; return [ el("h4", { className: "m01lab__group", text: groupName(group) }), el("div", { className: "m01-logic__scroll", children: [ el("table", { className: "m01-logic__grid m01lab__ho", children: [ el("thead", { children: [el("tr", { children: headers.map((h) => el("th", { text: h })) })], }), el("tbody", { children: [...(mine.length ? mine.map(lineRow) : [empty]), subtotal] }), ], }), ], }), ]; } function hoBox(ctx: DetailContext): HTMLElement { const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); if (!money) { return section(tl("Ho_Title"), [ el("p", { className: "m01-logic__muted", text: tl("Not_Money") }), el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), ]); } const all = entries(ctx); const calculated = ctx.lines !== null; const blocks = GROUPS.flatMap((group) => { const mine = all.filter((e) => e.group === group); return group === "other" && !mine.length ? [] : groupTable(group, mine, calculated); // 그 밖은 있을 때만 }); const total = all.reduce((s, e) => s + (e.amount ?? 0), 0); blocks.push( el("p", { className: "m01lab__total", attrs: { "data-sum": calculated ? String(total) : "" }, text: `${tl("Total")} ${calculated ? formatNumber(total) : "—"}`, }), ); const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; return section(tl("Ho_Title"), blocks, hint); } /** 상세 화면 넷을 `host` 에 쌓음 */ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { const formula = el("div", { className: "m01lab__formula" }); buildFormula(formula, { text: ctx.text }); host.replaceChildren( infoBox(ctx), hoBox(ctx), section(tl("Formula_Title"), [formula]), section(tl("Calc_Title"), [ctx.calcHost]), ); }