// WPTools bookmarks retain the original quantity sheet's result identities. const EstQuantity = (() => { function entities(text) { return String(text) .replace(/&#(x[0-9a-f]+|\d+);/gi, (_, n) => { const code = n[0].toLowerCase() === "x" ? parseInt(n.slice(1), 16) : Number(n); return code <= 0x10ffff ? String.fromCodePoint(code) : ""; }) .replace(/"/g, '"') .replace(/'|'/g, "'") .replace(/</g, "<") .replace(/>/g, ">") .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 = []; 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)}
${output.items.map((item, index) => ``).join("")}
산출코드품명규격원본 저장 수량재계산 수량연결 내역코드
${esc(item.Code)}${esc(item.Name)}${esc(item.Size)}${number(sheet.items[index].GN)}${number(item.GN)}

${esc(output.errors.join(" / "))} 원본 결과 표지의 이름을 유지해 수량을 연결합니다. 원본 ESTX의 편집기·도면 바이너리는 별도로 보존합니다.

${sheet.rows.map((row, index) => ``).join("")}
줄산출근거 편집
${index + 1}
`; } 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;