feat(est-test): 단가산출 전역변수·새내역서 파일로저장·산출서 삽입과 선택 삭제

- 전역변수:  표지 대입 변수를 호표 넘어 유지, 지정·해제, 목록·이동
- 새내역서 파일로저장: 당초값·신규 표시·당초 사본 지운 새 작업 따로 저장
- 수량산출서 삽입(공종명칭·규격)·선택공종 삭제

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 13:43:43 +09:00
co-authored by Claude Opus 5.5
parent 0f353f8606
commit b97ecf47de
9 changed files with 261 additions and 9 deletions
+25 -6
View File
@@ -1,4 +1,6 @@
// REA f2a1fc/f254c8: LineData 표지와 일반·성분·마지막 합계의 정밀도.
// 전역변수(spec_cost_tools §2 · 0x93c22c): \x05이름\x05 표지로 대입한 변수는 호표가 바뀌어도 남아(전역표) 다른 호표에서 표지 없이 써도 찾는다.
// 찾는 차례 = 그 호표 변수 → 전역표. 표지는 계산 동안 떼고 계산 뒤 같은 낱말에 다시 붙인다.
const EstText = (() => {
const { num, components, floating, sum } = EstMath;
const marker = /\x12([-\d.,]+)\x12/;
@@ -6,7 +8,7 @@ const EstText = (() => {
const clean = (s) =>
String(s || "")
.replace(/[\x06\x07][^\x06\x07]*[\x06\x07]/g, "")
.replace(/[\x03\x0c\x0e\x11\x12\x13\x15\x1a]/g, "");
.replace(/[\x03\x05\x0c\x0e\x11\x12\x13\x15\x1a]/g, "");
const numeric = (s) => Number(String(s).replaceAll(",", ""));
const sumCurrency = (values) =>
values.reduce((n, value) => n + EstMath.even(num(value) * 10000), 0) / 10000;
@@ -56,8 +58,10 @@ const EstText = (() => {
titles = [],
errors = [],
checks = [];
const globals = Object.create(null),
globalList = [];
let head = null,
variables = Object.create(null),
variables = Object.create(globals),
changed = new Map(),
currentSource = null,
finalOverride = null,
@@ -94,7 +98,7 @@ const EstText = (() => {
if (num(row.Index) > 0) {
finish();
head = row;
variables = Object.create(null);
variables = Object.create(globals);
changed = new Map();
currentSource = null;
finalOverride = null;
@@ -170,7 +174,10 @@ const EstText = (() => {
errors.push(`${head.Code}:${index + 1} ${error.message}`);
}
}
let text = original.replace(/\{([+-])\}/g, "$1");
const globalNames = [...original.matchAll(/\x05([A-Za-z][A-Za-z0-9_]*)\x05/g)].map(
(m) => m[1],
);
let text = original.replace(/\x05/g, "").replace(/\{([+-])\}/g, "$1");
let selectionError = "";
if (num(row.LinkIndex) === 62) {
text = text.replace(/(\x07([A-Z])\x07\s*)\x1a[^\x1a]*\x1a/g, (whole, reference, group) => {
@@ -599,16 +606,28 @@ const EstText = (() => {
errors.push(`${head.Code}:${index + 1} ${error.message}`);
row.Error = error.message;
}
for (const name of globalNames) {
if (Object.hasOwn(variables, name)) {
globals[name] = variables[name];
globalList.push({ name, value: variables[name], index });
}
if (!String(row.LineData).includes(`\x05${name}\x05`))
row.LineData = String(row.LineData).replace(
new RegExp(`(?<![A-Za-z0-9_])${name}(?![A-Za-z0-9_])`),
`\x05${name}\x05`,
);
}
if (!hasAutoTotals && component[sc])
plainTotals[component[sc]] = num(plainTotals[component[sc]]) + num(row.TC);
}
finish();
return { rows, titles, errors, checks };
return { rows, titles, errors, checks, globals: globalList };
}
const names = {
"\x06": "구간지정",
"\x07": "구간합계",
"\x03": "단가",
"\x05": "전역",
"\x0c": "다른줄",
"\x0e": "품명",
"\x11": "변수",
@@ -620,7 +639,7 @@ const EstText = (() => {
};
const display = (text) =>
String(text || "").replace(
/[\x03\x06\x07\x0c\x0e\x11\x12\x13\x15\x16\x1a]/g,
/[\x03\x05\x06\x07\x0c\x0e\x11\x12\x13\x15\x16\x1a]/g,
(ch) => `⟦${names[ch]}⟧`,
);
const encode = (text) =>