const EstReport = (() => { let catalog = []; function setting() { const project = EstApp.state.project, settings = project.printSettings || {}; return { form: "current", paper: "A4", orientation: "landscape", margin: 12, font: 9, company: "", startPage: 1, rowsPerPage: 30, notes: "", PrintGongMethod: settings.PrintGongMethod ?? 0, PrintApplyField: settings.PrintApplyField ?? 0, PrintSumBottom: settings.PrintSumBottom ?? 0, ...settings, marginTop: settings.marginTop ?? settings.margin ?? 12, marginBottom: settings.marginBottom ?? settings.margin ?? 12, marginLeft: settings.marginLeft ?? settings.margin ?? 12, marginRight: settings.marginRight ?? settings.margin ?? 12, }; } function preview(all = false) { const { state, esc } = EstApp; const settings = setting(), form = catalog.find((row) => String(row.Index) === settings.form), output = form ? EstForms.data(form.Index, state.project, state) : null; const number = (value) => new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 12 }).format(Number(value) || 0); const root = document.createElement("div"); if (output) root.innerHTML = EstForms.grid(output); else if (form) { const total = state.project.calculation?.total || 0; root.innerHTML = `
${esc(settings.notes)}
${[8, 9].includes(form.Index) ? `

공사금액 ${number(total)}원

${esc(settings.company)}

` : ""}`; } else if (state.project.tables[state.view]) { const rows = all ? state.project.tables[state.view].map((row, index) => ({ row, index })) : EstApp.visibleRows(); const common = ["Code", "Name", "Size", "Unite"]; const columns = { EstMainDat: [...common, "GN", "TU", "TC", "NC", "GC", "JC", "SC"], HeadMainDat: [...common, "GN", "NU", "GU", "JU", "SU", "TU", "NC", "GC", "JC", "SC", "TC"], CostMainDat: ["LineData"], MachMainDat: [...common, "GN", "TU", "NC", "GC", "JC", "TC"], Wonga: ["Gubun", "Name", "Youl", "VSusic", "Cost"], }; root.innerHTML = EstViews.grid( state.view, rows, columns[state.view] || EstModel.columns(state.view).filter( (key) => ![ "ValueIndex", "SelectField", "Ratio", "NachalRatio", "Diget", "DigetIndex", ].includes(key), ), ); } else root.innerHTML = EstApp.byId("work-content").innerHTML; root .querySelectorAll("button,.row-number,.business-tools,select#quantity-sheet") .forEach((node) => node.remove()); root.querySelectorAll("input,textarea,select").forEach((node) => { const value = node.tagName === "SELECT" ? node.selectedOptions[0]?.textContent || "" : node.type === "number" && node.value !== "" ? number(node.value) : EstText.clean(EstText.encode(node.value)); if (node.tagName === "TEXTAREA") node.parentElement.classList.add("col-LineData"); if (node.type === "number") node.parentElement.classList.add("numeric"); node.replaceWith(document.createTextNode(value)); }); root .querySelectorAll("[contenteditable]") .forEach((node) => node.removeAttribute("contenteditable")); const title = form?.Name2 || EstApp.titles[state.view] || state.view; const detail = output?.detail || (state.view === "quantity" ? state.project.quantitySheets?.find((sheet) => sheet.Code === state.quantitySheet)?.Name : state.view === "progress" ? state.project.progress.find((round) => round.id === state.roundId)?.name : state.group ? EstNative.groups(state.project.tables[state.view] || []).get(state.group)?.rows[0] ?.Name : ""); const totals = [ "progress-summary", "progress-costs", "escalation-summary", "escalation-costs", ].includes(form?.Index) ? null : state.project.calculation; const errors = [...(totals?.errors || []), ...(output?.errors || [])]; const source = `${esc(state.project.name)} · ${esc(title)}

${esc(state.project.name)}

${esc(title)}

${detail ? `

${esc(detail)}

` : ""}${totals ? `
순공사비 ${number(totals.direct?.TC)}원 · 도급액 ${number(totals.total)}원
` : ""} ${paginate(root, settings)}${output && settings.notes ? `
${esc(settings.notes)}
` : ""}${errors.length ? `

계산 오류 ${errors.length}건: ${esc(errors.join(" / "))}

` : ""}`; EstApp.byId("print-frame").srcdoc = source; } function paginate(root, settings) { const table = root.querySelector("table"); if (!table || root.querySelectorAll("table").length !== 1) return root.innerHTML; const rows = [...table.querySelectorAll("tbody>tr")], pages = []; for (let at = 0; at < rows.length; at += settings.rowsPerPage) { const copy = table.cloneNode(false); copy.innerHTML = table.querySelector("thead")?.outerHTML || ""; const body = document.createElement("tbody"); rows.slice(at, at + settings.rowsPerPage).forEach((row) => body.append(row.cloneNode(true))); copy.append(body); pages.push( `
${copy.outerHTML}
`, ); } return pages.join(""); } async function open() { const defaults = await EstApp.api("/api/print-catalog"); const original = new Map( (EstApp.state.project.printCatalog || []).map((row) => [row.Index, row]), ); catalog = defaults.map((row) => ({ ...row, ...original.get(row.Index), })); catalog.push( { Index: "progress-summary", Name: "기성 내역서", Name2: "기성 내역서" }, { Index: "progress-costs", Name: "기성 원가계산서", Name2: "기성 원가계산서" }, { Index: "escalation-summary", Name: "물가변동 내역서", Name2: "물가변동 내역서" }, { Index: "escalation-costs", Name: "물가변동 원가계산서", Name2: "물가변동 원가계산서" }, { Index: "annual-summary", Name: "연차 배분표", Name2: "연차 배분표" }, { Index: "annual-items", Name: "연차별 내역서", Name2: "연차별 내역서" }, { Index: "change-costs", Name: "설계변경 원가 증감표", Name2: "설계변경 원가 증감표" }, { Index: "standard-usage", Name: "표준시장단가 사용 내역", Name2: "표준시장단가 사용 내역" }, { Index: "standard-change", Name: "표준시장단가 설계변경", Name2: "표준시장단가 설계변경" }, { Index: "estimate-usage", Name: "견적단가 사용 내역", Name2: "견적단가 사용 내역" }, { Index: "estimate-change", Name: "견적단가 설계변경", Name2: "견적단가 설계변경" }, { Index: "material-usage", Name: "자재단가 사용 내역", Name2: "자재단가 사용 내역" }, { Index: "material-change", Name: "자재단가 설계변경", Name2: "자재단가 설계변경" }, { Index: "labor-usage", Name: "노임단가 사용 내역", Name2: "노임단가 사용 내역" }, { Index: "labor-change", Name: "노임단가 설계변경", Name2: "노임단가 설계변경" }, { Index: "machine-usage", Name: "기계경비 사용 내역", Name2: "기계경비 사용 내역" }, { Index: "machine-change", Name: "기계경비 설계변경", Name2: "기계경비 설계변경" }, ); const table = EstApp.state.project.tables[EstApp.state.view]; const s = setting(), { esc } = EstApp; EstApp.showDialog( "인쇄 미리보기", `
${[ ["marginTop", "위 여백(mm)", 0, 50], ["marginBottom", "아래 여백(mm)", 0, 50], ["marginLeft", "왼쪽 여백(mm)", 0, 50], ["marginRight", "오른쪽 여백(mm)", 0, 50], ["font", "글자(pt)", 6, 20], ["rowsPerPage", "페이지당 행", 1, 100], ["startPage", "시작 쪽", 1, 99999], ] .map( ([key, label, min, max]) => ``, ) .join("")} ${table ? '' : ""}
`, ); EstApp.byId("dialog-content") .querySelectorAll("[data-print],#print-paper,#print-orientation,#print-form,#print-gong") .forEach((node) => node.addEventListener("change", () => { const key = node.id === "print-gong" ? "PrintGongMethod" : node.dataset.print || node.id.slice(6), value = node.type === "number" || node.id === "print-gong" ? Number(node.value) : node.value; try { if ( node.type === "number" && (!Number.isFinite(value) || value < Number(node.min) || value > Number(node.max) || (["rowsPerPage", "startPage"].includes(key) && !Number.isInteger(value))) ) throw Error("인쇄 설정 범위 오류"); EstApp.change(() => { const next = { ...setting(), [key]: value }; if (key === "form") { const found = catalog.find((row) => String(row.Index) === value); const original = found && { ...found, ...EstApp.state.project.nativePrintChanges?.[value], }; if (original) { next.rowsPerPage = Math.max( 1, Math.min(100, Number(original.PrintLineCount) || 30), ); for (const side of ["Top", "Bottom", "Left", "Right"]) if (Number.isFinite(original[`Print${side}`])) next[`margin${side}`] = Math.max(0, Math.min(50, original[`Print${side}`])); if (Number.isFinite(original.PrintGongMethod)) next.PrintGongMethod = Number(original.PrintGongMethod); if (Number.isFinite(original.PrintApplyField)) next.PrintApplyField = Number(original.PrintApplyField); if (Number.isFinite(original.PrintSumBottom)) next.PrintSumBottom = Number(original.PrintSumBottom); } } if (key === "notes") { EstApp.state.project.documents ||= {}; EstApp.state.project.documents[next.form] = value; } if ( [ "marginTop", "marginBottom", "marginLeft", "marginRight", "rowsPerPage", "PrintGongMethod", "PrintApplyField", "PrintSumBottom", ].includes(key) && next.form !== "current" && EstApp.state.project.printCatalog?.some((row) => String(row.Index) === next.form) ) { EstApp.state.project.nativePrintChanges ||= {}; const changes = (EstApp.state.project.nativePrintChanges[next.form] ||= {}); if (key.startsWith("margin")) changes[`Print${key.slice(6)}`] = value; else if (key === "rowsPerPage") changes.PrintLineCount = value; else changes[key] = value; } if (key === "form") next.notes = EstApp.state.project.documents?.[value] || ""; EstApp.state.project.printSettings = next; }, false); if (key === "form") { const content = EstApp.byId("dialog-content"), current = EstApp.state.project.printSettings; for (const name of [ "notes", "marginTop", "marginBottom", "marginLeft", "marginRight", "rowsPerPage", ]) content.querySelector(`[data-print=${name}]`).value = current[name]; const gongWrap = content.querySelector("#print-gong-wrap"); if (gongWrap) { gongWrap.hidden = !["115", "116", "117", "118"].includes(String(current.form)); content.querySelector("#print-gong").value = String(current.PrintGongMethod ?? 0); } } preview(EstApp.byId("print-scope")?.value === "all"); EstApp.byId("print-error").textContent = ""; } catch (error) { EstApp.byId("print-error").textContent = error.message; node.value = setting()[key]; } }), ); EstApp.byId("print-scope")?.addEventListener("change", (event) => preview(event.target.value === "all"), ); preview(); } function print() { const frame = EstApp.byId("print-frame"); if (!frame?.contentDocument?.body) throw Error("인쇄 미리보기가 준비되지 않았습니다."); frame.contentWindow.focus(); frame.contentWindow.print(); } async function exportExcelReport() { const { state } = EstApp; const form = catalog.find((row) => String(row.Index) === setting().form); const output = form && EstForms.data(form.Index, state.project, state); if (!output) throw Error("표 형식의 인쇄 서식을 선택하세요."); if (!output.rows.length) throw Error("출력할 행이 없습니다."); const used = new Set(); const columns = output.columns.map((key) => { const base = EstForms.label(key); let name = base, suffix = 2; while (used.has(name)) name = `${base} (${suffix++})`; used.add(name); return name; }); const rows = output.rows.map((row) => output.columns.map((key) => key === "LineData" ? EstText.clean(row[key] || "") : (row[key] ?? ""), ), ); const response = await fetch("/api/export-report-xlsx", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: state.project.name, columns, rows, detail: [output.detail, setting().notes].filter(Boolean).join("\n"), errors: output.errors || [], }), }); if (!response.ok) throw Error((await response.json()).error); EstApp.download( `${state.project.name} - ${form.Name}.xlsx`, await response.blob(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ); } async function exportExcel() { try { await exportExcelReport(); EstApp.byId("print-error").textContent = ""; } catch (error) { EstApp.byId("print-error").textContent = error.message; } } return { open, preview, print, exportExcel }; })();