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(
``,
);
}
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 };
})();