Files
Aislo/tools/est-test/native-excel-to-est.js
T

371 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 원본 「엑셀데이타를 EST로 변환」(mnuExcelToEst → TfrmExcelToEst btnOkClick 0x109c3bc — spec_pc §1) 7개 탭:
// 노임 → 자재 → 견적 → 표준시장 → 단가산출 → 일위대가 → 내역서 차례로 한 Excel 파일의 시트들을 새 공사로 옮긴다(금액은 Excel 값 그대로,
// 다시 셈 끔). 셀 규칙: 문자 Trim·줄바꿈 제거·품명/규격 앞 「.」 제거·단위 M2→㎡ M3→㎥·산출근거 「*」→「×」, 수치는 쉼표 빼고,
// 노무비율 ≤ 1 이면 ×100, 열 「B:D」 는 이어 붙임. 단가산출·일위대가는 머리 줄 꼴(#.산근 · #| · 제n호표 · 산근 n 호표 : … (… 당) ·
// (산근 · No n 호표 · No.n)을 찾아 호표를 만든다(원본의 글자 굵기 판정은 웹 Excel 읽기가 굵기를 몰라 하지 않음).
const EstExcelToEst = (() => {
const TABS = [
["LaborcDat", "노임단가", ["Code", "Name", "Size", "Unite", "Cost1", "Bigo"]],
[
"StuffcDat",
"자재단가",
[
"Code",
"Name",
"Size",
"Unite",
"Cost1",
"Page1",
"Cost2",
"Page2",
"Cost3",
"Page3",
"Cost4",
"Page4",
"Cost5",
"Page5",
"Cost6",
"Page6",
"TC",
"Bigo",
],
],
[
"EstiMateDat",
"견적단가",
["Code", "Name", "Size", "GN", "Unite", "TC", "NC", "GC", "JC", "Bigo"],
],
[
"SilJukDat",
"표준시장단가",
["Code", "Name", "Size", "Unite", "TC", "NC", "GC", "JC", "NCRatio", "EXPLEAN", "Bigo"],
],
["CostMainDat", "단가산출", ["Code", "LineData", "TC", "NC", "GC", "JC"]],
[
"HeadMainDat",
"일위대가",
[
"Code",
"Name",
"Size",
"GN",
"Unite",
"TU",
"TC",
"NU",
"NC",
"GU",
"GC",
"JU",
"JC",
"Bigo",
],
],
[
"EstMainDat",
"내역서",
[
"Code",
"Pr",
"Name",
"Size",
"GN",
"Unite",
"TU",
"TC",
"NU",
"NC",
"GU",
"GC",
"JU",
"JC",
"Bigo",
],
],
];
const TEXT = new Set([
"Code",
"Pr",
"Name",
"Size",
"Unite",
"Bigo",
"LineData",
"EXPLEAN",
"Page1",
"Page2",
"Page3",
"Page4",
"Page5",
"Page6",
]);
const num = (v) => Number(v) || 0;
const letter = (n) => {
let s = "";
for (n += 1; n > 0; n = Math.floor((n - 1) / 26))
s = String.fromCharCode(65 + ((n - 1) % 26)) + s;
return s;
};
const col = (s) =>
[...String(s).toUpperCase()].reduce((n, c) => n * 26 + c.charCodeAt(0) - 64, 0) - 1;
const unit = (s) =>
String(s)
.replace(/^[Mm]2$/, "㎡")
.replace(/^[Mm]3$/, "㎥");
function cell(table, r, spec) {
const [a, b] = String(spec).toUpperCase().split(":");
if (!b) return table[r]?.[col(a)];
let out = "";
for (let c = col(a); c <= col(b); c++) {
const v = table[r]?.[c];
if (v != null && String(v) !== "") out += String(v);
else if (out.length <= 1) out += " ";
}
return out;
}
function value(field, raw) {
if (TEXT.has(field)) {
let t = String(raw ?? "").replace(/\r?\n/g, "");
t = field === "LineData" ? t.replaceAll("*", "×") : t.trim();
if (field === "Unite") t = unit(t);
if (field === "Name" || field === "Size") t = t.replace(/^\.+/, "");
return t;
}
let v = typeof raw === "number" ? raw : Number(String(raw ?? "").replaceAll(",", "")) || 0;
if (field === "NCRatio" && v <= 1) v *= 100;
return v;
}
function readRows(table, tab, fields, columns) {
const rows = [];
for (let r = tab.start - 1; r <= tab.end - 1; r++) {
const row = { Index: 0 };
fields.forEach((f, i) => {
if (columns[i]) row[f] = value(f, cell(table, r, columns[i]));
});
rows.push(row);
}
return rows;
}
// 머리 줄 꼴 → { Name, Size, Unite, skipNext }
function costHead(text, next) {
const t = String(text ?? "").trim(),
split = (s) => {
const parts = s.trim().split(/\s+/);
return {
Name: parts[0] || "",
Size: parts.slice(1, -1).join(" "),
Unite: parts.length > 1 ? parts.at(-1) : "",
};
};
if (/^#\./.test(t) && t.includes("산근")) {
if (t.includes("|")) {
const [, n = "", s = "", u = ""] = t.split("|");
return { Name: n.trim(), Size: s.trim(), Unite: u.trim() };
}
return split(t.replace(/^.*?산근\s*\d*/, "").replace(/\//g, " "));
}
if (/^#/.test(t) && t.includes("|")) {
const [, n = "", s = "", u = ""] = t.split("|");
return { Name: n.trim(), Size: s.trim(), Unite: u.trim() };
}
if (/산근.*호표/.test(t) && t.includes("당")) {
const body = t.split(/[::]/).slice(1).join(":"),
u = /\(\s*[\d.]*\s*([^()]*?)\s*당\s*\)/.exec(body)?.[1] || "",
rest = body
.replace(/\([^()]*당\s*\)/, "")
.trim()
.split(/\s+/);
return { Name: rest[0] || "", Size: rest.slice(1).join(" "), Unite: u };
}
if (/제\s*\d+\s*호표/.test(t) && next != null)
return { ...split(String(next)), skipNext: true };
if (t.replace(/\s/g, "").includes("(산근")) return split(t.replace(/\(산근[^)]*\)/, ""));
return null;
}
function headHead(name) {
const t = String(name ?? "").trim();
if (/No/.test(t) && t.includes("호표"))
return { Name: t.split("호표").slice(1).join("호표").trim(), Size: "", Unite: "" };
if (/^No\.\s*[1-9]/.test(t))
return { Name: t.replace(/^No\.\s*\d+\s*/, ""), Size: "", Unite: "" };
if (/\(호표/.test(t)) {
const parts = t
.replace(/\(호표[^)]*\)/, "")
.trim()
.split(/\s+/);
return { Name: parts[0] || "", Size: parts.slice(1).join(" "), Unite: "" };
}
return null;
}
function groups(rows, titles, kind, headOf) {
let pk = 0;
const out = [];
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (kind === 103 && String(row.Name ?? "").replace(/\s/g, "") === "[합계]") continue;
const h = headOf(row, rows[i + 1]);
if (!h) {
out.push(row);
continue;
}
const index = titles.length + 1,
code = `N${String(++pk).padStart(10, "0")}`;
titles.push({
Index: index,
Code: code,
Name: h.Name,
Size: h.Size,
Unite: unit(h.Unite),
TC: num(row.TC),
NC: num(row.NC),
GC: num(row.GC),
JC: num(row.JC),
});
Object.assign(row, { Code: code, TitleCode: code, Index: index });
if (kind === 104)
row.LineData = `\x0e${h.Name}\x0e${h.Size ? `\x13${h.Size}\x13` : ""}${h.Unite ? `\x15${unit(h.Unite)}\x15` : ""}`;
out.push(row);
if (h.skipNext) i++;
}
return out;
}
function convert(sheets, tabs, template) {
const project = EstNative.empty();
project.name = template.name;
project.options = { ...project.options, AutoCalc: false, SourceRefresh: false };
project.tables.Wonga = structuredClone(template.wonga || []);
const failed = [];
for (const [table, label, fields] of TABS) {
const tab = tabs[table];
if (!tab?.sheet || !(tab.start > 0) || !(tab.end > 0)) continue;
const data = sheets[tab.sheet];
if (!data) {
failed.push(`${tab.sheet || label} 시트가 없습니다.`);
continue;
}
let rows = readRows(data, tab, fields, tab.columns);
if (table === "LaborcDat") rows.forEach((r) => (r.TC = r.Cost1));
if (table === "SilJukDat" || table === "EstiMateDat")
rows.forEach((r) =>
Object.assign(r, {
OTC: r.TC,
ONC: r.NC,
OGC: r.GC,
OJC: r.JC,
...(table === "SilJukDat" && { Cost1: r.TC }),
}),
);
if (table === "CostMainDat") {
project.tables.CostTitleDat = [];
rows = groups(rows, project.tables.CostTitleDat, 104, (r, n) =>
costHead(r.LineData, n?.LineData),
);
} else if (table === "HeadMainDat") {
project.tables.HeadTitleDat = [];
rows = groups(rows, project.tables.HeadTitleDat, 103, (r) => headHead(r.Name));
}
project.tables[table] = rows;
}
while (project.tables.EstMainDat.length < 36) project.tables.EstMainDat.push({ Index: 0 });
let pk = 0;
for (const t of Object.keys(project.tables))
for (const r of project.tables[t])
if (r && typeof r === "object" && r.PkID == null) r.PkID = ++pk;
for (const t of ["LaborcDat", "StuffcDat", "SilJukDat", "EstiMateDat"])
for (const r of project.tables[t] || [])
if (!String(r.Code ?? "").trim()) r.Code = `N${String(r.PkID).padStart(10, "0")}`;
return { project, failed };
}
// 화면
function form(state) {
const x = (state.e2e ||= {
tab: "LaborcDat",
tabs: Object.fromEntries(
TABS.map(([t, label, fields]) => [
t,
{ sheet: label, start: 0, end: 0, columns: fields.map((_, i) => letter(i)) },
]),
),
});
const { esc } = EstApp,
[, , fields] = TABS.find(([t]) => t === x.tab),
tab = x.tabs[x.tab];
EstApp.showDialog(
"엑셀데이타를 EST로 변환",
`<div class="business-tools">${TABS.map(([t, l]) => `<button data-action="e2e-tab" data-tab="${t}"${t === x.tab ? ' style="font-weight:bold"' : ""}>${l}</button>`).join("")}</div><p><button data-action="e2e-open">엑셀파일열기</button> ${esc(x.file || "")} ${x.sheets ? `(시트 ${Object.keys(x.sheets).length}개)` : ""}</p><div class="form-fields"><label>시트명<input name="e2e-sheet" value="${esc(tab.sheet)}"></label><label>시작행<input name="e2e-start" type="number" value="${tab.start}"></label><label>끝행<input name="e2e-end" type="number" value="${tab.end}"></label></div><p>해당 칼럼명칭 입력(엑셀 열 글자, 비우면 건너뜀, 「B:D」 는 이어 붙임)</p><div class="form-fields">${fields.map((f, i) => `<label>${f}<input name="e2e-col" data-i="${i}" value="${esc(tab.columns[i] || "")}" size="4"></label>`).join("")}</div><button data-action="e2e-run"${x.sheets ? "" : " disabled"}>변 환</button>`,
);
}
function read(state) {
const x = state.e2e,
tab = x.tabs[x.tab],
q = (n) => document.querySelector(`#dialog-content [name="${n}"]`);
if (!q("e2e-sheet")) return;
tab.sheet = q("e2e-sheet").value.trim();
tab.start = Number(q("e2e-start").value) || 0;
tab.end = Number(q("e2e-end").value) || 0;
document
.querySelectorAll('#dialog-content [name="e2e-col"]')
.forEach((n) => (tab.columns[Number(n.dataset.i)] = n.value.trim().toUpperCase()));
}
async function run(name, state, node) {
if (name === "excel-to-est") return form(state);
read(state);
const x = state.e2e;
if (name === "e2e-tab") x.tab = node.dataset.tab;
else if (name === "e2e-open") {
const file = await new Promise((resolve) => {
const picker = document.createElement("input");
picker.type = "file";
picker.accept = ".xlsx,.xls";
picker.addEventListener("change", () => resolve(picker.files[0]));
picker.click();
});
if (!file) return;
const body = await file.arrayBuffer();
x.sheets = /\.xls$/i.test(file.name)
? EstJdlCompare.xlsTables(await EstApp.api("/api/read-xls", { method: "POST", body }))
: EstExcelMap.sheetTables(await EstApp.api("/api/import-xlsx", { method: "POST", body }));
x.file = file.name;
} else if (name === "e2e-run") {
const tabs = Object.values(x.tabs);
if (
tabs.every((t) => !t.start) ||
tabs.every((t) => !t.end) ||
tabs.some((t) => t.end < t.start)
)
throw Error("변환할 엑셀자료의 범위설정이 잘못되었습니다.");
if (!confirm("현재 작업을 엑셀 자료로 만든 새 공사로 바꾸시겠습니까?")) return;
const out = convert(x.sheets, x.tabs, {
name: x.file.replace(/\.xlsx?$/i, ""),
wonga: (state.project.tables.Wonga || []).map(({ Cost, ...rest }) => rest),
});
if (
out.failed.length &&
!confirm(`${out.failed.join("\n")}\n변환을 못한 시트가 있습니다.\n\n종료하시겠습니까?`)
)
return;
EstApp.byId("dialog").close();
EstApp.setProject(out.project);
state.notice =
"엑셀 역변환 작업이 완료되었습니다. 금액은 엑셀 값 그대로이며 자동계산은 꺼 두었습니다.";
EstApp.render();
return;
}
form(state);
}
const names = new Set(["excel-to-est", "e2e-tab", "e2e-open", "e2e-run"]);
function handle(name, state, node) {
if (!names.has(name)) return false;
run(name, state, node).catch((error) => {
state.notice = error.message;
EstApp.render();
});
return true;
}
return { convert, costHead, headHead, handle };
})();