const { pack, unpack } = require("./archive.cjs");
const xml = (value) =>
String(value ?? "").replace(
/[&<>"']/g,
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c],
);
const column = (index) => {
let name = "";
for (index++; index; index = Math.floor((index - 1) / 26))
name = String.fromCharCode(65 + ((index - 1) % 26)) + name;
return name;
};
function sheet(rows) {
if (rows.length > 1048576 || rows.some((r) => r.length > 16384))
throw Error("Excel 행/열 한도 초과");
return `${rows
.map(
(r, i) =>
`${r
.map((v, j) => {
const ref = column(j) + (i + 1);
if (v == null) return "";
if (typeof v === "number") {
if (!Number.isFinite(v)) throw Error("유효하지 않은 Excel 숫자");
return `${v}`;
}
if (typeof v === "boolean") return `${v ? 1 : 0}`;
const text = typeof v === "object" ? JSON.stringify(v) : String(v);
if (text.length > 32767) throw Error("Excel 셀 문자 한도 초과: " + ref);
return `${xml(text)}`;
})
.join("")}
`,
)
.join("")}`;
}
function workbook(project) {
const files = new Map(),
sheets = [];
for (const [name, rows] of Object.entries(project.tables)) {
if (!Array.isArray(rows) || name.startsWith("Cad") || name.length > 31) continue;
// Binary originals stay in JSON/ESTX; Excel edits scalar table fields only.
const keys = [
...new Set(
rows.flatMap((r) =>
Object.keys(r).filter(
(k) => !k.startsWith("_") && r[k] != null && typeof r[k] !== "object",
),
),
),
];
if (!keys.length) continue;
sheets.push({
name,
rows: [
keys,
...rows.map((r) =>
keys.map((k) => (typeof r[k] === "object" && r[k] != null ? null : r[k])),
),
],
});
}
sheets.unshift({
name: "공사정보",
rows: [
["항목", "값"],
["공사명", project.name],
["형식", "est-web-xlsx-1"],
["설명", "표의 첫 행은 필드명입니다. 바이너리는 포함하지 않습니다."],
],
});
sheets.forEach((s, i) => files.set(`xl/worksheets/sheet${i + 1}.xml`, sheet(s.rows)));
const ns = "http://schemas.openxmlformats.org/";
files.set(
"[Content_Types].xml",
`${sheets.map((s, i) => ``).join("")}`,
);
files.set(
"_rels/.rels",
``,
);
files.set(
"xl/workbook.xml",
`${sheets.map((s, i) => ``).join("")}`,
);
files.set(
"xl/_rels/workbook.xml.rels",
`${sheets.map((s, i) => ``).join("")}`,
);
return pack(files);
}
function readWorkbook(bytes) {
const files = unpack(bytes),
selected = {};
if (!files.has("xl/workbook.xml") || !files.has("xl/_rels/workbook.xml.rels"))
throw Error("Excel 통합문서 구조 없음");
for (const [name, data] of files)
if (
name === "xl/workbook.xml" ||
name === "xl/_rels/workbook.xml.rels" ||
name === "xl/sharedStrings.xml" ||
/^xl\/worksheets\/[^/]+\.xml$/.test(name)
)
selected[name] = data.toString("utf8");
return selected;
}
module.exports = { workbook, readWorkbook, xml };