feat(est): connect original quantity sheets
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// 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(/<cs\b[^>]*\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(/<begin\b[^>]*\bTag=(\d+)[^>]*>/gi)) {
|
||||
const name = entities(match[0]).match(/Name:["']([^"']+)["']/)?.[1];
|
||||
if (name) tags.set(match[1], name);
|
||||
}
|
||||
for (const match of text.matchAll(/<div\b[^>]*>([\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(/<c\s+nr=(\d+)\s*\/>/gi, (_, id) => {
|
||||
const next = superscripts.has(id),
|
||||
value = next && !raised ? "^(" : !next && raised ? ")" : "";
|
||||
raised = next;
|
||||
return value;
|
||||
});
|
||||
if (raised) body += ")";
|
||||
body = body
|
||||
.replace(/<begin\b[^>]*\bTag=(\d+)[^>]*>/gi, (_, id) => {
|
||||
const name = tags.get(id) || "";
|
||||
if (tag(name) === "\x12") markers.push(name);
|
||||
return tag(name);
|
||||
})
|
||||
.replace(/<end\b[^>]*\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 `<div class="business-tools"><label>수량산출서 <select id="quantity-sheet-choice"><option value="manual">수식표 직접 작성</option>${state.project.quantitySheets.map((sheet) => `<option value="${esc(sheet.Code)}" ${state.quantitySheet === sheet.Code ? "selected" : ""}>${esc(sheet.Name || sheet.Code)}</option>`).join("")}</select></label></div>`;
|
||||
}
|
||||
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) +
|
||||
`<div class="business-tools"><b>${esc(sheet.Name)}</b><button data-quantity-apply>내역 수량 반영</button></div><table class="sheet"><thead><tr><th>산출코드</th><th>품명</th><th>규격</th><th>원본 저장 수량</th><th>재계산 수량</th><th>연결 내역코드</th></tr></thead><tbody>${output.items.map((item, index) => `<tr><td>${esc(item.Code)}</td><td>${esc(item.Name)}</td><td>${esc(item.Size)}</td><td>${number(sheet.items[index].GN)}</td><td>${number(item.GN)}</td><td><input data-quantity-target="${index}" value="${esc(item.TargetCode || item.LinkCode || "")}" aria-label="연결 내역코드"></td></tr>`).join("")}</tbody></table><p class="business-note">${esc(output.errors.join(" / "))} 원본 결과 표지의 이름을 유지해 수량을 연결합니다. 원본 ESTX의 편집기·도면 바이너리는 별도로 보존합니다.</p><table class="sheet"><thead><tr><th>줄</th><th>산출근거 편집</th></tr></thead><tbody>${sheet.rows.map((row, index) => `<tr><td>${index + 1}</td><td><textarea data-quantity-line="${index}" rows="1" style="min-width:750px" aria-label="산출근거 ${index + 1}">${esc(EstText.display(output.rows[index].LineData))}</textarea></td></tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
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;
|
||||
Reference in New Issue
Block a user