/* -*- 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 ``;
}
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);