284 lines
12 KiB
JavaScript
284 lines
12 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* 보고서 출력 화면 (scr_report.js) — PM_Prt 대체 · 서식 목록(SET_REP 75종) · 미리보기 · 인쇄 · 엑셀/CSV · 조달청 XML
|
|
* 인쇄 선택(옵션·순서·영역) = scr_report_opts.js · 양식 변형(견본 없는 서식) = report_forms.js
|
|
* 근거: 지식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_coll.js",
|
|
"report_san.js",
|
|
"report_init.js",
|
|
"report_link.js",
|
|
"report_book.js",
|
|
"report_forms.js",
|
|
"report_pps.js",
|
|
].forEach((f) => document.write(`<script src="${f}"></script>`));
|
|
|
|
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, "<br>");
|
|
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];
|
|
}
|
|
|
|
// 보고서 묶음 — 인쇄 선택(scr_report_opts)을 덮은 설정으로 짬 · 양식 변형은 extra(서식 목록에서 고름)
|
|
function book(ctx) {
|
|
const ui = ctx.uiState;
|
|
const n = Number(ui.reportStage) || 1;
|
|
const O = global.STMATE_REPORT_OPTS;
|
|
const eff = O ? O.effective(ctx) : { dbf: ctx.dbfData, opts: {}, key: "" };
|
|
const key = `${ctx.currentSourcePath}|${n}|${ctx.computed && ctx.computed.kanjubComputed ? ctx.computed.kanjubComputed.grandTotal : 0}|${eff.key}`;
|
|
if (ui._reportBook && ui._reportBookKey === key) return ui._reportBook;
|
|
const bk = global.STMATE_REPORT_BOOK.buildBook(eff.dbf, ctx.state, ctx.computed, n, eff.opts);
|
|
const Fm = global.STMATE_REPORT_FORMS;
|
|
bk.extra = {};
|
|
if (Fm) {
|
|
const u = global.STMATE_REPORT_UNIT.unitContext(eff.dbf);
|
|
try {
|
|
bk.extra = Fm.buildFormSheets(u, eff.dbf, ctx.state, ctx.computed, n, eff.opts);
|
|
} catch (e) {
|
|
bk.extraError = e.message;
|
|
}
|
|
}
|
|
bk.eff = eff;
|
|
ui._reportBook = bk;
|
|
ui._reportBookKey = key;
|
|
return bk;
|
|
}
|
|
// 묶음 시트 + 변형 시트에서 id 로 찾기
|
|
const findSheet = (bk, id) =>
|
|
bk.sheets.find((s) => (s.id || s.name) === id) || (bk.extra || {})[id];
|
|
|
|
// 인쇄 영역 끝 열 — 시트 이름표 열 앞까지(원본 인쇄 영역 A~이름표 앞 · 바로가기 → 칸은 인쇄 밖)
|
|
function printEnd(sh) {
|
|
const r2 = sh.rows[1] || {};
|
|
const tag = Object.keys(r2).find((k) => /^[A-Z]{1,2}$/.test(k) && r2[k] === sh.name);
|
|
return tag ? colIdx(tag) - 1 : 0;
|
|
}
|
|
// 시트 모형 → 표(미리보기 · 인쇄 — 인쇄면 인쇄 영역만)
|
|
function sheetHtml(sh, forPrint) {
|
|
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));
|
|
if (forPrint && printEnd(sh) > 0) maxC = Math.min(maxC, printEnd(sh));
|
|
const cols = Array.from({ length: maxC }, (_, i) => colName(i + 1));
|
|
const body = sh.rows
|
|
.map((r) => {
|
|
if (r._k === "page") return `<tr class="rp-page"><td colspan="${maxC}"></td></tr>`;
|
|
const tds = cols
|
|
.map((c) => {
|
|
const v = r[c];
|
|
const num = typeof v === "number";
|
|
return `<td style="${num ? "text-align:right" : ""}">${v == null ? "" : fmtCell(v)}</td>`;
|
|
})
|
|
.join("");
|
|
const bold = /^(title|head|T\d|sub|total|final|hopyo|parent)$/.test(r._k || "");
|
|
return `<tr style="${bold ? "font-weight:bold" : ""}${r._k === "head" ? ";background:#eef3f8" : ""}">${tds}</tr>`;
|
|
})
|
|
.join("");
|
|
return `<table class="rp-table" style="border-collapse:collapse;font-size:12px" border="1">${body}</table>`;
|
|
}
|
|
|
|
P.screens.PM_Prt = {
|
|
render(ctx) {
|
|
if (!ctx.dbfData)
|
|
return `<div class="pane"><div class="pane-title">보고서 출력</div><p>공사를 먼저 불러오십시오.</p></div>`;
|
|
const ui = ctx.uiState;
|
|
const B = global.STMATE_REPORT_BOOK;
|
|
const bk = book(ctx);
|
|
const ids = bk.sheets.map((s) => s.id || s.name).concat(Object.keys(bk.extra || {}));
|
|
if (!ids.includes(ui.reportSheet))
|
|
ui.reportSheet = ids.includes("공사원가계산서") ? "공사원가계산서" : ids[0];
|
|
const cur = findSheet(bk, ui.reportSheet);
|
|
const formMap = Object.assign(
|
|
{},
|
|
B.FORM_SHEET,
|
|
(global.STMATE_REPORT_FORMS || {}).FORM_SHEET || {},
|
|
);
|
|
const forms = (ctx.dbfData.set_rep_rows || [])
|
|
.slice()
|
|
.sort((a, b) => String(a.INDX).localeCompare(String(b.INDX)))
|
|
.map((r) => {
|
|
const id = formMap[String(r.INDX).trim()];
|
|
const on = id && ids.includes(id);
|
|
return `<div class="rp-form${on && id === ui.reportSheet ? " active" : ""}" data-rp-sheet="${on ? esc(id) : ""}" style="padding:2px 6px;cursor:${on ? "pointer" : "default"};color:${on ? "inherit" : "#999"}">${esc(r.INDX)} ${esc(r.NAME)}${on ? "" : " · 미구현"}</div>`;
|
|
})
|
|
.join("");
|
|
const extra = ["〓 목 차 〓", "※※안내※※", "〓 INITIAL 〓", "제잡비요율표"]
|
|
.filter((id) => ids.includes(id))
|
|
.map(
|
|
(id) =>
|
|
`<div class="rp-form" data-rp-sheet="${esc(id)}" style="padding:2px 6px;cursor:pointer">${esc(id)}</div>`,
|
|
)
|
|
.join("");
|
|
const stOpts = stages(ctx)
|
|
.map(
|
|
(n) =>
|
|
`<option value="${n}" ${Number(ui.reportStage || 1) === n ? "selected" : ""}>${n === 1 ? "당초(1차수)" : `변경 ${n}차수`}</option>`,
|
|
)
|
|
.join("");
|
|
const O = global.STMATE_REPORT_OPTS;
|
|
const orderIds = bk.sheets
|
|
.map((s) => s.id || s.name)
|
|
.filter((id) => !["〓 목 차 〓", "※※안내※※", "〓 INITIAL 〓"].includes(id));
|
|
const optsHtml = O ? O.html(ctx, orderIds, stages(ctx)) : "";
|
|
return `<div class="pane" id="rp-root">
|
|
<div class="pane-title"><span>보고서 출력 · 미리보기 (SET_REP 서식 ${forms ? (ctx.dbfData.set_rep_rows || []).length : 0}종)</span></div>
|
|
<div style="display:flex;gap:6px;align-items:center;margin:4px 0;flex-wrap:wrap">
|
|
<label>차수 <select id="rp-stage">${stOpts}</select></label>
|
|
<button class="win-btn" id="rp-print">인쇄</button>
|
|
<button class="win-btn" id="rp-print-all">인쇄(묶음 전체)</button>
|
|
<button class="win-btn" id="rp-xlsx-all">엑셀 내보내기(전체)</button>
|
|
<button class="win-btn" id="rp-xlsx-one">엑셀(이 시트)</button>
|
|
<button class="win-btn" id="rp-csv">CSV(이 시트)</button>
|
|
<button class="win-btn" id="rp-pps">조달청 XML</button>
|
|
<span id="rp-msg"></span>
|
|
</div>
|
|
${optsHtml}
|
|
<div style="display:flex;gap:8px;align-items:flex-start">
|
|
<div style="width:240px;max-height:70vh;overflow:auto;border:1px solid #ccc">${extra}${forms}</div>
|
|
<div id="rp-preview" style="flex:1;max-height:70vh;overflow:auto">
|
|
<h3 style="margin:2px 0">${esc(cur ? cur.title : "")}</h3>${cur ? sheetHtml(cur) : ""}
|
|
</div>
|
|
</div></div>`;
|
|
},
|
|
bind(ctx, api) {
|
|
const ui = ctx.uiState;
|
|
if (global.STMATE_REPORT_OPTS && ctx.dbfData) {
|
|
const ids = book(ctx)
|
|
.sheets.map((s) => s.id || s.name)
|
|
.filter((id) => !["〓 목 차 〓", "※※안내※※", "〓 INITIAL 〓"].includes(id));
|
|
global.STMATE_REPORT_OPTS.bind(ctx, api, ids);
|
|
}
|
|
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,
|
|
formulas: !!book(ctx).eff.formulas,
|
|
print: book(ctx).eff.print || {},
|
|
}),
|
|
});
|
|
if (!res.ok) throw new Error(await res.text());
|
|
api.downloadBlob(await res.blob(), filename, mime);
|
|
};
|
|
const cur = () => findSheet(book(ctx), 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);
|
|
}
|
|
};
|
|
const printSheets = (list) => {
|
|
const w = window.open("", "_blank");
|
|
if (!w) return msg("팝업 차단됨");
|
|
const pr = book(ctx).eff.print || {};
|
|
const wide = list.some((sh) => sh.rows.some((r) => r.M != null));
|
|
const orient = pr.orient || (wide ? "landscape" : "portrait");
|
|
const body = list
|
|
.map(
|
|
(sh, i) =>
|
|
`<section style="${i ? "page-break-before:always" : ""}"><h2 style="text-align:center">${esc(sh.title)}</h2>${sheetHtml(sh, true)}</section>`,
|
|
)
|
|
.join("");
|
|
w.document.write(
|
|
`<html><head><title>${esc(list[0].title)}</title><style>@page{size:A4 ${orient};margin:12mm}body{font-family:'${esc(pr.font || "굴림체")}'}table{border-collapse:collapse;font-size:${Number(pr.size) || 10}pt}td{border:1px solid #888;padding:1px 4px}.rp-page{page-break-after:always}</style></head><body>${body}</body></html>`,
|
|
);
|
|
w.document.close();
|
|
w.focus();
|
|
w.print();
|
|
};
|
|
if ($("rp-print")) $("rp-print").onclick = () => printSheets([cur()]);
|
|
if ($("rp-print-all"))
|
|
$("rp-print-all").onclick = () =>
|
|
printSheets(book(ctx).sheets.filter((s) => s.name !== "〓 INITIAL 〓"));
|
|
},
|
|
};
|
|
})(typeof window !== "undefined" ? window : globalThis);
|