/* -*- coding: utf-8 -*- */ /** * 보고서 출력 화면 (scr_report.js) — PM_Prt 대체 · 서식 목록(SET_REP 75종) · 미리보기 · 인쇄 · 엑셀/CSV · 조달청 XML * 근거: 지식DB STmate/old/20_분석 20 §3(SET_REP) · 30(조달청 XSD) · old/30_원자료/print_form_map.txt * 계산 = calc.js 결과(ctx.computed) · 시트 모형 = report_*.js(엑셀 대조 시험 resources/tester/test_stmate_report_*.py) */ (function (global) { "use strict"; // 보고서 짜는 모듈 — 이 끼워 넣기 파일이 불릴 때(문서 읽는 중) 함께 실음 if ( typeof document !== "undefined" && document.readyState === "loading" && !global.STMATE_REPORT_BOOK ) [ "report_core.js", "report_unit.js", "report_boq.js", "report_book.js", "report_pps.js", ].forEach((f) => document.write(``)); const P = (global.STMATE_PLUGINS = global.STMATE_PLUGINS || { screens: {}, actions: {}, menus: [], }); const esc = (s) => String(s ?? "").replace( /[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c], ); const fmtCell = (v) => typeof v === "number" ? new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 4 }).format(v) : esc(v).replace(/\n/g, "
"); const colIdx = (k) => k.split("").reduce((a, ch) => a * 26 + ch.charCodeAt(0) - 64, 0); const colName = (n) => (n > 26 ? String.fromCharCode(64 + Math.floor((n - 1) / 26)) : "") + String.fromCharCode(65 + ((n - 1) % 26)); // 차수 — 원가계산서가 저장된 차수(kanjubStages) · 2 이상이면 변경 양식 function stages(ctx) { const st = (ctx.state && ctx.state.kanjubStages) || {}; const ks = Object.keys(st).map(Number); return ks.length ? ks : [1]; } function book(ctx) { const ui = ctx.uiState; const n = Number(ui.reportStage) || 1; const key = `${ctx.currentSourcePath}|${n}|${ctx.computed && ctx.computed.kanjubComputed ? ctx.computed.kanjubComputed.grandTotal : 0}`; if (ui._reportBook && ui._reportBookKey === key) return ui._reportBook; ui._reportBook = global.STMATE_REPORT_BOOK.buildBook(ctx.dbfData, ctx.state, ctx.computed, n); ui._reportBookKey = key; return ui._reportBook; } // 시트 모형 → 표(미리보기 · 인쇄) function sheetHtml(sh) { let maxC = 1; for (const r of sh.rows) for (const k of Object.keys(r)) if (/^[A-Z]{1,2}$/.test(k)) maxC = Math.max(maxC, colIdx(k)); const cols = Array.from({ length: maxC }, (_, i) => colName(i + 1)); const body = sh.rows .map((r) => { if (r._k === "page") return ``; const tds = cols .map((c) => { const v = r[c]; const num = typeof v === "number"; return `${v == null ? "" : fmtCell(v)}`; }) .join(""); const bold = /^(title|head|T\d|sub|total|final|hopyo|parent)$/.test(r._k || ""); return `${tds}`; }) .join(""); return `${body}
`; } P.screens.PM_Prt = { render(ctx) { if (!ctx.dbfData) return `
보고서 출력

공사를 먼저 불러오십시오.

`; const ui = ctx.uiState; const B = global.STMATE_REPORT_BOOK; const bk = book(ctx); const ids = bk.sheets.map((s) => s.id || s.name); if (!ids.includes(ui.reportSheet)) ui.reportSheet = ids.includes("공사원가계산서") ? "공사원가계산서" : ids[0]; const cur = bk.sheets.find((s) => (s.id || s.name) === ui.reportSheet); const forms = (ctx.dbfData.set_rep_rows || []) .slice() .sort((a, b) => String(a.INDX).localeCompare(String(b.INDX))) .map((r) => { const id = B.FORM_SHEET[String(r.INDX).trim()]; const on = id && ids.includes(id); return `
${esc(r.INDX)} ${esc(r.NAME)}${on ? "" : " · 미구현"}
`; }) .join(""); const extra = ["〓 목 차 〓", "※※안내※※"] .filter((id) => ids.includes(id)) .map( (id) => `
${esc(id)}
`, ) .join(""); const stOpts = stages(ctx) .map( (n) => ``, ) .join(""); return `
보고서 출력 · 미리보기 (SET_REP 서식 ${forms ? (ctx.dbfData.set_rep_rows || []).length : 0}종)
${extra}${forms}

${esc(cur ? cur.title : "")}

${cur ? sheetHtml(cur) : ""}
`; }, bind(ctx, api) { const ui = ctx.uiState; const $ = (id) => document.getElementById(id); const msg = (t) => $("rp-msg") && ($("rp-msg").textContent = t); document.querySelectorAll("[data-rp-sheet]").forEach((el) => { el.onclick = () => { const id = el.getAttribute("data-rp-sheet"); if (!id) return; ui.reportSheet = id; api.renderWorkspace(); }; }); if ($("rp-stage")) $("rp-stage").onchange = (e) => { ui.reportStage = Number(e.target.value); api.renderWorkspace(); }; const pname = () => String((ctx.dbfData.prjlst && ctx.dbfData.prjlst.PNAME) || "STMATE").replace( /[\\/:*?"<>|]/g, "_", ); const post = async (url, sheets, filename, mime) => { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sheets, filename }), }); if (!res.ok) throw new Error(await res.text()); api.downloadBlob(await res.blob(), filename, mime); }; const cur = () => book(ctx).sheets.find((s) => (s.id || s.name) === ui.reportSheet); const xmime = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; if ($("rp-xlsx-all")) $("rp-xlsx-all").onclick = () => post("/api/report/xlsx", book(ctx).sheets, `${pname()}.xlsx`, xmime).then( () => msg("엑셀 저장"), (e) => msg(e.message), ); if ($("rp-xlsx-one")) $("rp-xlsx-one").onclick = () => post("/api/report/xlsx", [cur()], `${cur().name}.xlsx`, xmime).then( () => msg("엑셀 저장"), (e) => msg(e.message), ); if ($("rp-csv")) $("rp-csv").onclick = () => post("/api/report/csv", [cur()], `${cur().name}.csv`, "text/csv").then( () => msg("CSV 저장"), (e) => msg(e.message), ); if ($("rp-pps")) $("rp-pps").onclick = async () => { try { const xml = global.STMATE_REPORT_PPS.buildPpsXml( ctx.dbfData, ctx.state, ctx.computed, Number(ui.reportStage) || 1, ); const v = await api.postJson("/api/report/validate-pps", { xml }); api.downloadBlob(xml, `${pname()}_조달청.xml`, "application/xml;charset=utf-8"); msg( v.ok ? "조달청 XML 저장 · XSD 검증 통과" : `XSD 위반 ${v.errors.length}건: ${v.errors[0] || ""}`, ); } catch (e) { msg(e.message); } }; if ($("rp-print")) $("rp-print").onclick = () => { const w = window.open("", "_blank"); if (!w) return msg("팝업 차단됨"); const sh = cur(); w.document.write( `${esc(sh.title)}

${esc(sh.title)}

${sheetHtml(sh)}`, ); w.document.close(); w.focus(); w.print(); }; }, }; })(typeof window !== "undefined" ? window : globalThis);