// 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 manual(items) { const values = Object.create(null), codes = new Set(), errors = []; const rows = items.map((row) => { try { if (!row.Code || codes.has(row.Code)) throw Error("산출코드가 비어 있거나 중복됩니다."); codes.add(row.Code); const GN = EstMath.floating( EstExpression.evaluate(row.formula || "0", (key) => values[key]), row.digits ?? 2, 1, ); values[row.Code] = GN; return { ...row, GN }; } catch (error) { errors.push(`${row.Code || "수량"}: ${error.message}`); return { ...row, GN: null, Error: error.message }; } }); return { rows, errors }; } function totals(project, selected) { let items; if (selected && selected !== "manual") { const sheet = project.quantitySheets?.find((s) => s.Code === selected); if (!sheet) throw Error("선택한 수량산출서가 없습니다."); const output = calculate(sheet); if (output.errors.length) throw Error(output.errors.join(" / ")); items = output.items; } else { const output = manual(project.quantities || []); if (output.errors.length) throw Error(output.errors.join(" / ")); items = output.rows; } const quantities = new Map(); for (const item of items) { const code = item.TargetCode ?? item.LinkCode; if (!code && !item.TargetKey) continue; const targets = project.tables.EstMainDat.map((row, index) => ({ row, index })).filter( ({ row }) => !Number(row.Level) && (item.TargetKey ? row.PkID != null && `id:${row.PkID}` === item.TargetKey : row.Code === code), ); if (!targets.length) throw Error(`연결 내역 없음: ${item.TargetKey || code}`); if (targets.length > 1) throw Error(`연결 내역 중복: ${item.TargetKey || code}`); const index = targets[0].index; quantities.set(index, (quantities.get(index) || 0) + Number(item.GN || 0)); } if (!quantities.size) throw Error("연결할 내역코드를 먼저 입력하세요."); return quantities; } function applyTotals(project, quantities) { for (const [index, value] of quantities) project.tables.EstMainDat[index].GN = value; } function targetSelect(project, item, section, index) { const { esc } = EstApp; const rows = project.tables.EstMainDat.filter((row) => row.PkID != null && !Number(row.Level)); return ``; } function newCode(items) { const codes = new Set(items.map((row) => row.Code)); for (const row of items) for (const match of String(row.formula || "").matchAll(/\[([^\]]+)\]/g)) codes.add(match[1]); let index = 1; while (codes.has(`Q${index}`)) index++; return `Q${index}`; } function selector(state) { 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) + `
${output.items.map((item, index) => ``).join("")}
산출코드품명규격저장 수량재계산 수량연결 내역코드
${esc(item.Code)}${number(sheet.items[index].GN)}${number(item.GN)}${targetSelect(state.project, item, "sheet", index)}

${esc(output.errors.join(" / "))} 산출 행의 결과 표지로 수량 항목을 연결합니다. 신규 산출서와 행 변경은 JSON으로 저장합니다.

${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; } if (node.dataset.quantitySelect !== undefined) { const item = node.dataset.quantitySelect === "manual" ? state.project.quantities[Number(node.dataset.index)] : state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet)?.items[ Number(node.dataset.index) ]; if (!item) return; const row = state.project.tables.EstMainDat.find( (r) => r.PkID != null && `id:${r.PkID}` === node.value && !Number(r.Level), ); EstApp.change(() => { item.TargetKey = row ? node.value : ""; item.TargetCode = row?.Code || ""; }, false); return; } const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet); if (!sheet) return; if (node.dataset.quantityName !== undefined) EstApp.change(() => (sheet.Name = node.value), false); if (["Name", "Size"].includes(node.dataset.quantityItem)) EstApp.change( () => (sheet.items[Number(node.dataset.index)][node.dataset.quantityItem] = node.value), false, ); 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(() => { const item = sheet.items[Number(node.dataset.quantityTarget)]; item.TargetCode = node.value; item.TargetKey = ""; }, false); }); document.addEventListener("click", (event) => { const { state } = EstApp; if (event.target.closest("[data-quantity-new]")) { EstApp.change(() => { const code = `Q${crypto.randomUUID()}`; state.project.quantitySheets ||= []; state.project.quantitySheets.push({ Code: code, Name: "새 수량산출서", rows: [], items: [], options: { digits: 2, roundMode: 2 }, }); state.quantitySheet = code; }, false); return; } const sheet = state.project.quantitySheets?.find((s) => s.Code === state.quantitySheet); const addRow = event.target.closest("[data-quantity-add]"); if (addRow && sheet) { EstApp.change(() => { const code = `r${crypto.randomUUID()}`; const at = addRow.dataset.index === undefined ? sheet.rows.length : Number(addRow.dataset.index) + 1; sheet.rows.splice(at, 0, { LineData: "1 = \x120\x12", markers: [code] }); sheet.items.push({ Code: code, Name: "새 항목", Size: "", GN: 0, TargetCode: "" }); }, false); return; } const moveRow = event.target.closest("[data-quantity-move]"); if (moveRow && sheet) { const index = Number(moveRow.dataset.index), next = index + Number(moveRow.dataset.quantityMove); if (index >= 0 && index < sheet.rows.length && next >= 0 && next < sheet.rows.length) EstApp.change(() => { [sheet.rows[index], sheet.rows[next]] = [sheet.rows[next], sheet.rows[index]]; }, false); return; } const deleteRow = event.target.closest("[data-quantity-delete-row]"); if (deleteRow && sheet) { if (confirm("이 산출 행과 연결된 수량 항목을 삭제하시겠습니까?")) EstApp.change(() => { const [removed] = sheet.rows.splice(Number(deleteRow.dataset.quantityDeleteRow), 1); const remaining = new Set(sheet.rows.flatMap((row) => row.markers || [])); sheet.items = sheet.items.filter( (item) => !removed.markers.includes(item.Code) || remaining.has(item.Code), ); }, false); return; } if (event.target.closest("[data-quantity-delete-sheet]") && sheet) { if (confirm("이 수량산출서를 삭제하시겠습니까?")) EstApp.change(() => { state.project.quantitySheets = state.project.quantitySheets.filter( (item) => item !== sheet, ); state.quantitySheet = state.project.quantitySheets[0]?.Code || "manual"; }, false); return; } if (!event.target.closest("[data-quantity-apply]")) return; try { const quantities = totals(state.project, state.quantitySheet); EstApp.change(() => applyTotals(state.project, quantities)); } catch (error) { EstApp.state.notice = error.message; EstApp.render(); } }); } return { entities, fromWpt, calculate, manual, totals, applyTotals, targetSelect, newCode, selector, render, install, }; })(); if (typeof module !== "undefined" && module.exports) module.exports = EstQuantity;