- 보고서: 원본 STMATE 엑셀과 셀 대조(목록표·자재단가대비·중기·일위대가·산근 목록·설계내역서·원가계산서) · 조달청 XML 공식 XSD 검증 · PM_Prt 화면 - 제잡비: 원본 계산 순서 4형식(T/S/TA/SA) · 실무 7건 저장 결과키 전수 1원 · 요율표 조회 · 선택지 계산 · 제잡비·제비율 화면 - 식언어·수량: 함수·조건식·금액변수·문법 검사·수량변수·QCODE · 산근·구성·중기공식 편집 화면 · 순환 알림 - 화면: 원본 메뉴 체계·단축키 · 설계내역서 편집(행 넣기·지우기·이동·레벨·보관함) · 초기값 · 단가목록표 - 저장: 초기값(Start8x.Ini) 바뀐 키 저장 · 노임 환산 직종 = 초기값 CONLD · 산근·구성 결과에 적용단가 칸 채우지 않음(원본 0) - DBF 글 칸 앞 빈칸 보존 · 시험 도우미 표준입력 utf8(한글 조각 깨짐 원인) - 실무 5건 재계산 후 저장 = 바뀐 칸 0 · 시험 332건 통과 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
219 lines
9.5 KiB
JavaScript
219 lines
9.5 KiB
JavaScript
/* -*- 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(`<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];
|
|
}
|
|
|
|
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 `<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);
|
|
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 `<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 = ["〓 목 차 〓", "※※안내※※"]
|
|
.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("");
|
|
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-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>
|
|
<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;
|
|
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(
|
|
`<html><head><title>${esc(sh.title)}</title><style>@page{size:A4 ${sh.rows.some((r) => r.M != null) ? "landscape" : "portrait"};margin:12mm}table{border-collapse:collapse;font-size:10pt}td{border:1px solid #888;padding:1px 4px}.rp-page{page-break-after:always}</style></head><body><h2 style="text-align:center">${esc(sh.title)}</h2>${sheetHtml(sh)}</body></html>`,
|
|
);
|
|
w.document.close();
|
|
w.focus();
|
|
w.print();
|
|
};
|
|
},
|
|
};
|
|
})(typeof window !== "undefined" ? window : globalThis);
|