]*>([\s\S]*?)<\/tr>/gi)].map((tr) =>
+ [...tr[1].matchAll(/]*>([\s\S]*?)<\/td>/gi)].map((td) => {
+ const c = convert(td[1], true),
+ mark = c.names.find((n) => tag(n) === "\x12");
+ return {
+ text: c.body.replace(/[\x0e\x12\x13\x16]/g, "").trim(),
+ ...(mark && { mark }),
+ ...(c.names.some((n) => /^N\d+$/.test(n)) && { name: true }),
+ ...(c.names.some((n) => /^S\d+$/.test(n)) && { size: true }),
+ };
+ }),
+ );
+ if (table.length) rows.push({ LineData: "", markers: [], table });
+ continue;
+ }
+ const c = convert(match[1]);
+ if (c.body.trim())
+ rows.push({
+ LineData: c.body,
+ markers: c.markers,
+ ...(c.totals.length && { totals: c.totals }),
+ });
+ }
+ return { Code: code, Name: name, rows, items, options };
+ }
+
+ // ── 표 계산 ──
+ const L = (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 numeric = (t) => /^[+-]?[\d,]*\.?\d+$/.test(String(t).trim());
+ const value = (t) => (numeric(t) ? Number(String(t).replaceAll(",", "")) : 0);
+ const show = (v) => v.toLocaleString("en-US", { maximumFractionDigits: 9 });
+ function recalc(table) {
+ const errors = [];
+ table.forEach((row) =>
+ row.forEach((cell) => {
+ if (!cell.formula) return;
+ try {
+ const X =
+ typeof EstExcelSheet !== "undefined"
+ ? EstExcelSheet
+ : require("./native-excel-sheet.cjs");
+ const v = X.evaluate(cell.formula, (r, c) => value(table[r]?.[c]?.text ?? ""));
+ cell.text = show(typeof v === "number" ? v : Number(v) || 0);
+ } catch (error) {
+ errors.push(error.message);
+ }
+ }),
+ );
+ return errors;
+ }
+ // 항목 수량: 수량 표지 칸의 수
+ function values(sheet) {
+ const out = new Map();
+ for (const row of sheet.rows || []) {
+ if (!row.table) continue;
+ recalc(row.table);
+ for (const r of row.table)
+ for (const c of r) if (c.mark && !c.vmerge) out.set(c.mark, value(c.text));
+ }
+ return out;
+ }
+ function sumFormula(table, r, c, dir) {
+ const [dr, dc] = { right: [0, -1], left: [0, 1], up: [1, 0], down: [-1, 0] }[dir] || [];
+ if (dr == null) throw Error("방향을 고르세요.");
+ // 방향 = 결과 칸이 있는 쪽(원본 Ctrl+← 는 오른쪽 칸들을 더함)
+ let rr = r + dr,
+ cc = c + dc,
+ last = null;
+ const first = [rr, cc];
+ while (table[rr]?.[cc]) {
+ const cell = table[rr][cc];
+ if (cell.name || cell.size) break;
+ if (!cell.text.trim() || numeric(cell.text)) last = [rr, cc];
+ rr += dr;
+ cc += dc;
+ }
+ if (!table[first[0]]?.[first[1]]) throw Error("더할 셀이 없습니다.");
+ if (!last) last = dir === "right" ? [r, 0] : first;
+ const a = [Math.min(first[0], last[0]), Math.min(first[1], last[1])],
+ b = [Math.max(first[0], last[0]), Math.max(first[1], last[1])];
+ return `SUM(${L(a[1])}${a[0] + 1}:${L(b[1])}${b[0] + 1})`;
+ }
+ function vmerge(table, r1, r2, c) {
+ if (r2 <= r1) throw Error("세로로 여러 칸을 고르세요.");
+ const top = table[r1][c];
+ for (let r = r1 + 1; r <= r2; r++) {
+ const cell = table[r][c];
+ if (cell.text.trim()) top.text = `${top.text} ${cell.text}`.trim();
+ cell.text = "";
+ cell.vmerge = true;
+ }
+ top.vtop = r2 - r1 + 1;
+ }
+ function vsplit(table, r, c) {
+ const top = table[r][c];
+ for (let i = r + 1; i < r + (top.vtop || 1); i++) if (table[i]?.[c]) delete table[i][c].vmerge;
+ delete top.vtop;
+ }
+ function create(rows, cols) {
+ if (!(rows >= 1 && rows <= 100 && cols >= 1 && cols <= 30))
+ throw Error("줄 1~100, 칸 1~30 입니다.");
+ return Array.from({ length: rows }, () => Array.from({ length: cols }, () => ({ text: "" })));
+ }
+
+ return { entities, fromWpt, recalc, values, sumFormula, vmerge, vsplit, create, letters: L };
+})();
+if (typeof module !== "undefined" && module.exports) module.exports = EstQuantityWpt;
diff --git a/tools/est-test/native-quantity.cjs b/tools/est-test/native-quantity.cjs
index f0ffd7ccf..16bb77624 100644
--- a/tools/est-test/native-quantity.cjs
+++ b/tools/est-test/native-quantity.cjs
@@ -13,52 +13,10 @@ const EstQuantity = (() => {
.replace(/ /g, " ")
.replace(/&/g, "&");
}
- function fromWpt(code, name, text, items, options) {
- const tags = new Map(),
- rows = [];
- const superscripts = new Set(
- [...text.matchAll(/]*\bnr=(\d+)[^>]*>/gi)]
- .filter((m) => (Number(m[0].match(/CharStyleON:(\d+)/)?.[1] || 0) & 16) !== 0)
- .map((m) => m[1]),
- );
- for (const match of text.matchAll(/]*\bTag=(\d+)[^>]*>/gi)) {
- const name = entities(match[0]).match(/Name:["']([^"']+)["']/)?.[1];
- if (name) tags.set(match[1], name);
- }
- for (const match of text.matchAll(/]*>([\s\S]*?)<\/div>/gi)) {
- const markers = [],
- totals = [];
- let raised = false;
- const tag = (name) =>
- /^r\d+$/.test(name) || /^L\d+$/.test(name)
- ? "\x12"
- : /^[Fa]\d+$/.test(name)
- ? "\x16"
- : /^N\d+$/.test(name)
- ? "\x0e"
- : /^S\d+$/.test(name)
- ? "\x13"
- : "";
- let body = match[1].replace(/ /gi, (_, id) => {
- const next = superscripts.has(id),
- value = next && !raised ? "^(" : !next && raised ? ")" : "";
- raised = next;
- return value;
- });
- if (raised) body += ")";
- body = body
- .replace(/ ]*\bTag=(\d+)[^>]*>/gi, (_, id) => {
- const name = tags.get(id) || "";
- if (tag(name) === "\x12") markers.push(name);
- if (tag(name) === "\x16") totals.push(name);
- return tag(name);
- })
- .replace(/]*\bTag=(\d+)[^>]*>/gi, (_, id) => tag(tags.get(id) || ""));
- body = entities(body.replace(/<[^>]*>/g, "").replace(/\r?\n/g, ""));
- if (body.trim()) rows.push({ LineData: body, markers, ...(totals.length && { totals }) });
- }
- return { Code: code, Name: name, rows, items, options };
- }
+ // 워드형 문서 읽기·표는 native-quantity-wpt.cjs
+ const wpt = () =>
+ typeof EstQuantityWpt !== "undefined" ? EstQuantityWpt : require("./native-quantity-wpt.cjs");
+ const fromWpt = (...args) => wpt().fromWpt(...args);
function calculate(sheet) {
const markerErrors = sheet.rows.flatMap((row, index) => {
const count = [...String(row.LineData || "").matchAll(/\x12([-\d.,]+)\x12/g)].length;
@@ -82,7 +40,10 @@ const EstQuantity = (() => {
};
return {
...row,
- LineData: row.LineData.replace(/\*개소당|\bPIPE\b|소\s*계|∴|\x16[^\x16]*\x16/g, ""),
+ LineData:
+ row.table || row.image
+ ? ""
+ : row.LineData.replace(/\*개소당|\bPIPE\b|소\s*계|∴|\x16[^\x16]*\x16/g, ""),
Index: 0,
SCheck: 0,
LinkIndex: 0,
@@ -118,13 +79,18 @@ const EstQuantity = (() => {
});
if (typeof EstQuantityTotal !== "undefined")
EstQuantityTotal.apply(sheet, result.rows.slice(1));
+ const tableValues = sheet.rows.some((row) => row.table) ? wpt().values(sheet) : null;
const cellValues =
sheet.excel && typeof EstExcelSheet !== "undefined" ? EstExcelSheet.itemValues(sheet) : null;
return {
rows: result.rows.slice(1),
items: sheet.items.map((item) => ({
...item,
- GN: values.get(item.Code) ?? cellValues?.get(item.Code) ?? item.GN,
+ GN:
+ values.get(item.Code) ??
+ tableValues?.get(item.Code) ??
+ cellValues?.get(item.Code) ??
+ item.GN,
})),
checks,
errors: [...result.errors, ...markerErrors],
@@ -325,7 +291,7 @@ const EstQuantity = (() => {
{ esc, number, byId } = EstApp;
byId("work-content").innerHTML =
selector(state) +
- `${typeof EstQuantitySheets !== "undefined" ? EstQuantitySheets.excelView(sheet) : ""}${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다. | 줄 | 산출근거 편집 | 작업 | ${sheet.rows.map((row, index) => `| ${index + 1} | | | `).join("")} `;
+ `${typeof EstQuantitySheets !== "undefined" ? EstQuantitySheets.excelView(sheet) : ""}${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 지원되지 않는 원본 구조 변경은 JSON으로 저장합니다. | 줄 | 산출근거 편집 | 작업 | ${sheet.rows.map((row, index) => `| ${index + 1} | ${(sheet.rows[index].table || sheet.rows[index].image) && typeof EstQuantityTable !== "undefined" ? EstQuantityTable.html(sheet, index, state) : ``} | | `).join("")} `;
}
// 원본 수량목록 자리수 메뉴(mnuAuto·mnuTrunc·mnuUp·mnuRound): 결과 코드 뒤 방식(1 반올림·2 절사·3 올림)+자리수.
function resultFormat(code) {
diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs
index 248e3fb1a..e5be16fe4 100644
--- a/tools/est-test/server.cjs
+++ b/tools/est-test/server.cjs
@@ -28,6 +28,7 @@ const scripts = [
"native-text.js",
"native-excel-sheet.cjs",
"native-quantity-total.js",
+ "native-quantity-wpt.cjs",
"native-quantity.cjs",
"native-wonga.js",
"native-jungsang.js",
@@ -94,6 +95,9 @@ const allowed = new Set([
"native-new-estimate.js",
"native-book.js",
"native-data-update.js",
+ "native-quantity-table.js",
+ "native-quantity-list.js",
+ "native-actions-more.js",
"native-wonga-tools.js",
"native-estimate-print.js",
"native-xml-check.js",
@@ -271,9 +275,17 @@ const server = http.createServer(async (req, res) => {
mainData = {
name: "Main",
tables: Object.fromEntries(keys.map((k) => [k, project.tables[k] || []])),
+ quantitySheets: project.quantitySheets || [],
};
}
- reply(res, 200, mainData);
+ // 수량 불러오기(TFRMMULLANG btnMulLoad)는 수량산출서만
+ reply(
+ res,
+ 200,
+ url.searchParams.get("part") === "quantity"
+ ? { quantitySheets: mainData.quantitySheets }
+ : { name: mainData.name, tables: mainData.tables },
+ );
return;
}
if (req.method === "GET" && url.pathname.startsWith("/api/book")) {
|