]*>([\s\S]*?)<\/div>/gi)) {
+ const markers = [];
+ let raised = false;
+ const tag = (name) =>
+ /^r\d+$/.test(name) || /^L\d+$/.test(name)
+ ? "\x12"
+ : /^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);
+ 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 });
+ }
+ return { Code: code, Name: name, rows, items, options };
+ }
+ function calculate(sheet) {
+ const input = [
+ { Index: 1, Code: sheet.Code, LineData: "" },
+ ...sheet.rows.map((row) => {
+ const ResultFormats = (row.markers || []).map((name) => {
+ const override = name.match(/^[LrG]\d{6}([123])(\d)$/);
+ return {
+ digits: override ? Number(override[2]) : (sheet.options?.digits ?? 2),
+ method: override ? Number(override[1]) : (sheet.options?.roundMode ?? 2),
+ };
+ });
+ const format = ResultFormats[0] || {
+ digits: sheet.options?.digits ?? 2,
+ method: sheet.options?.roundMode ?? 2,
+ };
+ return {
+ ...row,
+ LineData: row.LineData.replace(/\*개소당|\bPIPE\b|소\s*계|∴/g, ""),
+ Index: 0,
+ SCheck: 0,
+ LinkIndex: 0,
+ Precision: format.digits,
+ RoundMode: format.method,
+ DigetIndex: format.method,
+ Diget: format.digits + 1,
+ ResultFormats,
+ };
+ }),
+ ];
+ const result = EstText.calculate(input),
+ values = new Map(),
+ checks = [];
+ result.rows.slice(1).forEach((row, index) => {
+ const actual = [...String(row.LineData || "").matchAll(/\x12([-\d.,]+)\x12/g)];
+ const stored = [...String(sheet.rows[index].LineData || "").matchAll(/\x12([-\d.,]+)\x12/g)];
+ (sheet.rows[index].markers || []).forEach((name, part) => {
+ if (!actual[part]) return;
+ const value = Number(actual[part][1].replaceAll(",", ""));
+ values.set(name, value);
+ checks.push({
+ name,
+ stored: Number(stored[part]?.[1].replaceAll(",", "")),
+ calculated: value,
+ });
+ });
+ let part = 0;
+ row.LineData = sheet.rows[index].LineData.replace(
+ /\x12([-\d.,]+)\x12/g,
+ (whole) => actual[part++]?.[0] || whole,
+ );
+ });
+ return {
+ rows: result.rows.slice(1),
+ items: sheet.items.map((item) => ({ ...item, GN: values.get(item.Code) ?? item.GN })),
+ checks,
+ errors: result.errors,
+ };
+ }
+ function selector(state) {
+ if (!state.project.quantitySheets?.length) return "";
+ const { esc } = EstApp;
+ return ``;
+ }
+ function render(state) {
+ state.quantitySheet ||= state.project.quantitySheets[0]?.Code;
+ const sheet =
+ state.project.quantitySheets.find((s) => s.Code === state.quantitySheet) ||
+ state.project.quantitySheets[0];
+ state.quantitySheet = sheet.Code;
+ const output = calculate(sheet),
+ { esc, number, byId } = EstApp;
+ byId("work-content").innerHTML =
+ selector(state) +
+ `${esc(sheet.Name)}
${esc(output.errors.join(" / "))} 원본 결과 표지의 이름을 유지해 수량을 연결합니다. 원본 ESTX의 편집기·도면 바이너리는 별도로 보존합니다.
| 줄 | 산출근거 편집 |
${sheet.rows.map((row, index) => `| ${index + 1} | |
`).join("")}
`;
+ }
+ function install() {
+ document.addEventListener("change", (event) => {
+ const node = event.target,
+ { state } = EstApp;
+ if (node.id === "quantity-sheet-choice") {
+ state.quantitySheet = node.value;
+ EstApp.render();
+ return;
+ }
+ const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet);
+ if (!sheet) return;
+ if (node.dataset.quantityLine !== undefined)
+ EstApp.change(
+ () =>
+ (sheet.rows[Number(node.dataset.quantityLine)].LineData = EstText.encode(node.value)),
+ false,
+ );
+ if (node.dataset.quantityTarget !== undefined)
+ EstApp.change(
+ () => (sheet.items[Number(node.dataset.quantityTarget)].TargetCode = node.value),
+ false,
+ );
+ });
+ document.addEventListener("click", (event) => {
+ if (!event.target.closest("[data-quantity-apply]")) return;
+ try {
+ const { state } = EstApp,
+ sheet = state.project.quantitySheets.find((s) => s.Code === state.quantitySheet),
+ output = calculate(sheet);
+ if (output.errors.length) throw Error(output.errors.join(" / "));
+ const quantities = new Map();
+ for (const item of output.items)
+ if (item.TargetCode || item.LinkCode) {
+ const code = item.TargetCode || item.LinkCode;
+ if (!state.project.tables.EstMainDat.some((r) => r.Code === code))
+ throw Error(`연결 내역코드 없음: ${code}`);
+ quantities.set(code, (quantities.get(code) || 0) + Number(item.GN || 0));
+ }
+ if (!quantities.size) throw Error("연결할 내역코드를 먼저 입력하세요.");
+ EstApp.change(() => {
+ for (const row of state.project.tables.EstMainDat)
+ if (quantities.has(row.Code) && !Number(row.Level)) row.GN = quantities.get(row.Code);
+ });
+ } catch (error) {
+ EstApp.state.notice = error.message;
+ EstApp.render();
+ }
+ });
+ }
+ return { fromWpt, calculate, selector, render, install };
+})();
+if (typeof module !== "undefined" && module.exports) module.exports = EstQuantity;
diff --git a/tools/est-test/native-text.js b/tools/est-test/native-text.js
index 2a14e11e4..165303115 100644
--- a/tools/est-test/native-text.js
+++ b/tools/est-test/native-text.js
@@ -15,15 +15,16 @@ const EstText = (() => {
function expression(text) {
return clean(
String(text).replace(
- /\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|\([^()]*[가-힣][^()]*\)/g,
+ /\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|\([가-힣][^()]*\)/g,
"",
),
)
.replace(
- /(\d)\s*(?:공\/?㎥|공|면|경간|개월|월|인|㎥|㎢|㎡|hr|sec|min|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|μ|EA|mm|cm|m|g|L)(?:\/(?:개소|개|대|본|인|분|포|\$|hr|일|㎥|㎡|m|a))?/gi,
+ /(\d)\s*(?:공\/?㎥|공|단|면|경간|개월|월|인|㎥|㎢|㎡|hr|sec|min|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|μ|EA|mm|cm|m|g|L)(?:\/(?:개소|개|대|본|인|분|포|\$|hr|일|㎥|㎡|m|a))?/gi,
"$1",
)
.replace(/(?<=\d)\s*\/(?:일|㎥|㎡)(?=\s|$)/g, "")
+ .replace(/(?<=\))\s*(?:개|인|본|면|단)(?:\/(?:㎡|㎥|hr))?/g, "")
.replace(/[①-⑳]/g, "")
.replace(/=\s*$/, "");
}
@@ -159,15 +160,18 @@ const EstText = (() => {
body = body.split("=")[0];
calculated = EstExpression.evaluate(expression(body), (key) => variables[key]);
}
- const assignmentText = text.replace(/\{[^}]*\}/g, (value) => " ".repeat(value.length));
+ const assignmentText = text.replace(
+ /\{[^}]*\}|\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15/g,
+ (value) => " ".repeat(value.length),
+ );
const assignments =
sc < 0
? []
: [
...assignmentText.matchAll(
- /(?:^| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
+ /(?:^\s*| {2,}|[,,]\s*|(?<=[가-힣])\s+|[::]\s*)([A-Za-z][A-Za-z0-9]*)\s*=/g,
),
- ].filter((a) => !text.includes("\x0e") || a.index < text.indexOf("\x0e"));
+ ];
for (let j = 0; j < assignments.length; j++) {
const assignment = assignments[j],
end = assignments[j + 1]?.index ?? text.length;
@@ -241,6 +245,7 @@ const EstText = (() => {
for (let part = 1; part < results.length; part++) {
const previous = results[part - 1],
next = results[part];
+ const format = row.ResultFormats?.[part];
const operation = expression(
text.slice(previous.index + previous[0].length, next.index),
).trim();
@@ -251,8 +256,8 @@ const EstText = (() => {
expression(`${calculated}${operation}`),
(key) => variables[key],
),
- digits,
- method,
+ format?.digits ?? digits,
+ format?.method ?? method,
);
assignmentName = assignedNames.get(next.index) || assignmentName;
if (assignmentName) variables[assignmentName] = calculated;
diff --git a/tools/est-test/native.css b/tools/est-test/native.css
index 1995ab29e..178c5dfa4 100644
--- a/tools/est-test/native.css
+++ b/tools/est-test/native.css
@@ -1,6 +1,9 @@
* {
box-sizing: border-box;
}
+[hidden] {
+ display: none !important;
+}
html,
body {
height: 100%;
diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs
index 9c9310eed..acbc28081 100644
--- a/tools/est-test/server.cjs
+++ b/tools/est-test/server.cjs
@@ -14,6 +14,7 @@ const scripts = [
"native-machine.js",
"native-rows.js",
"native-text.js",
+ "native-quantity.cjs",
"native-wonga.js",
"native-engine.js",
"native-business.js",