// 원본 「엑셀데이타를 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로 변환", `
${esc(x.file || "")} ${x.sheets ? `(시트 ${Object.keys(x.sheets).length}개)` : ""}
해당 칼럼명칭 입력(엑셀 열 글자, 비우면 건너뜀, 「B:D」 는 이어 붙임)