Merge remote-tracking branch 'origin/dev' into main_laptop_1

This commit is contained in:
2026-11-05 15:22:08 +09:00
13 changed files with 321 additions and 20 deletions
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -40,7 +40,15 @@ else if (command === "text")
else if (command === "wonga")
result = vm.runInContext("EstWonga.calculate(project.tables.Wonga, project.options)", context);
else if (command === "all") result = vm.runInContext("EstNative.calculate(project)", context);
else if (command === "manual") {
else if (command === "text-layout") {
context.cases = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures/est_text_layout_cases.json"), "utf8"),
);
result = vm.runInContext(
"cases.map(item=>({code:item.code,...EstText.calculate(item.rows)}))",
context,
);
} else if (command === "manual") {
project.options.AutoCalc = false;
project.tables.EstMainDat[1].GN += 100;
result = require(path.join(base, "server.cjs")).calculate(project);
+108
View File
@@ -281,6 +281,8 @@ def test_estx_import_reads_original_tables_options_and_templates(tmp_path):
assert len(result["wongaTemplates"]) == 4
assert result["options"]["TotalFix"] is True
assert result["options"]["GCToWonga"] is False
assert result["options"]["CostValueTrunc"] is False
assert result["options"]["MidCostDiget"] == 2
assert result["originalArchive"]
@@ -428,3 +430,109 @@ def test_text_material_only_subtotal_does_not_reuse_prior_labor():
assert result["errors"] == []
assert result["rows"][3]["TC"] == 120
assert result["rows"][6]["TC"] == 30
def test_original_sea_forest_and_temporary_works_text_layouts():
results = run("text-layout")
assert len(results) == 3
checked = 0
for result in results:
assert result["errors"] == [], result["code"]
for check in result["checks"]:
assert abs(check["stored"] - check["calculated"]) < 1e-6, (result["code"], check)
checked += 1
assert checked > 100
def test_text_preserves_braced_signs_and_comments_inside_parentheses():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'T'},
{LineData:' hm={-}6.76m {?} H={-}7.5m {계획수심}'},
{LineData:' d = hm-H = \x120.74\x12'},
{LineData:' L = 4인÷(4.8개{?}×8hr) = \x120.1\x12'},
{LineData:' q = 1 × 2조/회 = \x122\x12 t=15분 〔접이안시간〕'},
{LineData:' 전력비 = 0.74hr × 52.1 = \x1238.5\x12 원/본',SCheck:-3}
])""",
)
assert result["errors"] == []
assert result["rows"][2]["TC"] == 0.74
assert result["rows"][3]["TC"] == 0.1
assert result["rows"][4]["TC"] == 2
assert result["rows"][5]["TC"] == 38.5
def test_source_code_edit_updates_links_and_preserves_original_contract():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.StuffcDat=[{PkID:1,Code:'M',Name:'material',ValueIndex:4,InputTC:100},{PkID:2,Code:'OTHER',ValueIndex:4,InputTC:10}];
p.tables.HeadMainDat=[{PkID:1,Index:1,Code:'H',Name:'head'},{PkID:2,Index:0,Code:'M',TitleCode:'H',LinkIndex:5,GN:2}];
p.tables.HeadTitleDat=[{Code:'H',Name:'head'}];
p.tables.EstMainDat=[{PkID:1,Code:'H',LinkIndex:103,GN:10},{LinkIndex:54}];
let q=EstBusiness.baseline(EstNative.calculate(p));
const before=JSON.stringify(q.baseline);
EstModel.editCode(q,'StuffcDat',0,'M2');
EstModel.editCode(q,'HeadMainDat',0,'H2');
q.tables.StuffcDat[0].InputTC=120;
q=EstNative.calculate(q);
const original=EstBusiness.original(q,2);
let duplicate=false;try{EstModel.editCode(q,'StuffcDat',0,'OTHER')}catch{duplicate=true;}
return {q,original,preserved:before===JSON.stringify(q.baseline),duplicate};
})()""",
)
project = result["q"]
assert project["calculation"]["errors"] == []
assert project["calculation"]["direct"]["TC"] == 2400
assert project["tables"]["HeadMainDat"][1]["Code"] == "M2"
assert project["tables"]["HeadMainDat"][1]["TitleCode"] == "H2"
assert project["tables"]["EstMainDat"][0]["Code"] == "H2"
assert project["tables"]["HeadTitleDat"][0]["Code"] == "H2"
assert result["original"]["tables"]["EstMainDat"][-1]["TC"] == 2400
assert result["preserved"] and result["duplicate"]
def test_text_fraction_columns_preserve_numbered_variables_and_unmarked_result():
result = run(
"math",
r"""EstText.calculate([
{Index:1,Code:'T'},
{LineData:' L11=0.35 L21=0.35 V51=15 V52=15 N=2'},
{LineData:' L11 L21'},
{LineData:' t23=(──+──)×60 = \x122.8\x12'},
{LineData:' V51 V52'},
{LineData:' t23 + t23'},
{LineData:' Tc = ────────────'},
{LineData:' 1 · N'},
{LineData:' Q=Tc×2 = \x125.6\x12'},
{LineData:' to1 = 0.6 min/m2 to2 = 3 min/m2'},
{LineData:' T=2×30×3.5×to2/60=\x1210.5\x12'}
])""",
)
assert result["errors"] == []
assert result["rows"][3]["TC"] == 2.8
assert result["rows"][8]["TC"] == 5.6
assert result["rows"][10]["TC"] == 10.5
def test_original_cost_truncation_and_intermediate_precision_options():
result = run(
"math",
r"""(()=>{
const rows=[{Index:1,Code:'T'},
{LineData:' 노무비: \x03100.19\x03 = \x12100.1\x12',SCheck:-1}];
const intermediate=[{Index:1,Code:'I'},
{LineData:' 노무비: \x0359020\x03 / 81.8 = \x12721.51\x12 ×0.5=\x12360.7\x12',SCheck:-1}];
return {normal:EstText.calculate(rows),whole:EstText.calculate(rows,{CostValueTrunc:true}),
mid1:EstText.calculate(intermediate,{MidCostDiget:1}),mid2:EstText.calculate(intermediate,{MidCostDiget:2}),
override:EstText.calculate([rows[0],{...rows[1],Precision:2}],{CostValueTrunc:true})};
})()""",
)
assert all(item["errors"] == [] for item in result.values())
assert result["normal"]["rows"][1]["TC"] == 100.1
assert result["whole"]["rows"][1]["TC"] == 100
assert result["override"]["rows"][1]["TC"] == 100.19
assert result["mid1"]["checks"][-1]["calculated"] == 721.5
assert result["mid2"]["checks"][-1]["calculated"] == 721.51
+4
View File
@@ -4,6 +4,8 @@
내역·일위대가·수식 단가산출·기계경비·기초 단가·원가계산서를 원본 테이블 구조로 편집합니다. 수량 연결, 설계변경 당초 보존, 낙찰률, 기성 회차와 사정 누계, 품목조정, 연차 배분, 소요 자원 전개 화면을 제공합니다. 실행 취소·다시 실행, 검색, 행 편집, JSON 저장·열기, CSV와 브라우저 인쇄를 사용할 수 있습니다.
인쇄 미리보기는 화면의 100행 페이지 제한 없이 검색 결과/현재 호표의 전체 행을 표시합니다. 출력 범위에서 현재 자료의 모든 호표·행도 선택할 수 있습니다. 브라우저 인쇄 창에서 종이 출력이나 PDF 저장을 선택합니다. 원본 인쇄 양식과 같은 레이아웃이라는 의미는 아닙니다.
저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. ESTX는 읽기 전용으로 가져오며 원본 압축 파일과 바이너리를 보존합니다. 편집 결과를 원본 ESTX로 다시 쓰는 기능은 없습니다.
초기 자료는 기술정보 DB의 실제 REA 저장 샘플입니다. `REA 샘플 대조`에서 원본 저장값과 275개 계산 결과를 비교합니다. 테스트용 임의 요율을 원본 기본값으로 넣지 않습니다. 새 원가 서식의 요율은 사용자가 입력합니다.
@@ -18,6 +20,8 @@
실무 산불 내역 3건은 DBISAM의 재사용 BLOB 할당 공간을 논리 길이까지만 읽도록 수정한 뒤 저장 금액 8,312개 모두 일치했습니다. 다만 세 파일에 공통으로 닫히지 않은 괄호가 있는 원본 수식 한 건이 남아 해석 오류로 표시됩니다. 저장 금액 일치와 수식 오류 0을 구분합니다. 직접재료비 구획·관급 조달수수료·산업안전보건관리비 제외 옵션, 낙찰 단가를 유지한 동원 집계도 시험합니다. 자동계산을 끄면 열기·편집·저장 시 기존 금액을 보존합니다.
계산 환경에서 원본 `CostValueTrunc`(성분 금액 원 미만 절사)와 `MidCostDiget`(중간금액 소수자리)을 제어합니다. 원천·호표 코드를 바꾸면 작업본 연결도 함께 바뀌며, 당초 계약 자료는 보존됩니다. 해양·산림·가시설의 분수 배치와 구획 소계 원본 발췌를 회귀 시험에 포함합니다. 전체 해양 자료는 133,710개 저장값 중 133,635개 일치, 수식 해석 오류 99건이 남아 있습니다. 저장 결과 대조만으로 원본 실행 동등성을 판정하지 않습니다.
**전체 원본 동등성은 아직 확보하지 못했습니다.** 기성·물가변동·연차 등은 분석 분기와 합성 시나리오를 시험했으며 원본 업무 샘플 대조는 없습니다. 원본 화면 실행 비교는 못했습니다. 원본 페이지·도면 배치, CAD/VEC0650 편집, 원본 인쇄 서식, Excel/XML/JDL 변환, ESTX 수정 저장은 미구현입니다. 원본 대체품의 완료 판정으로 보지 않습니다.
검사:
+2
View File
@@ -330,6 +330,8 @@ function importEstx(buffer, name = "가져온 내역서") {
VerFormat: props.TWongaOption?.VerFormat ?? true,
TotalFix: props.TWongaOption?.TotalFix ?? tables.Wonga.some((r) => r.LinkIndex === 55),
AutoCalc: props.TEstOption?.CalcAuto ?? true,
CostValueTrunc: props.TCostOption?.CostValueTrunc ?? false,
MidCostDiget: props.TCostOption?.MidCostDiget ?? 2,
HeadDigits: props.THeadOption?.SumValueDiget || 0,
TotalFit: props.THeadOption?.CalcTrunc || false,
DollarCostCut: props.TMachOption?.DollarCostCut || false,
+1
View File
@@ -19,6 +19,7 @@
<script defer src="native-views.js"></script>
<script defer src="native-extra.js"></script>
<script defer src="native-settings.js"></script>
<script defer src="native-report.js"></script>
<script defer src="native-actions.js"></script>
<script defer src="native-app.js"></script>
</head>
+8 -2
View File
@@ -162,6 +162,7 @@ const EstActions = (() => {
copies.forEach((r) => {
r.PkID = ++next;
r.NewItem = true;
delete r.PreviousCodes;
if (["StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat"].includes(state.view)) {
const base = r.Code || "U";
let suffix = next;
@@ -192,7 +193,8 @@ const EstActions = (() => {
if (!selected().length) throw Error("레벨을 바꿀 행을 선택하세요.");
const level = Number(node.dataset.level);
EstApp.change(() => selected().forEach((i) => (activeRows()[i].Level = level)));
} else if (name === "print") window.print();
} else if (name === "print") EstReport.open();
else if (name === "print-document") EstReport.print();
else if (name === "csv") {
const rows = activeRows(),
keys = EstModel.columns(state.view);
@@ -528,7 +530,11 @@ const EstActions = (() => {
if (key === "Precision" && (value < 0 || value > 6 || !Number.isInteger(value)))
throw Error("소수 자릿수는 0~6 정수입니다.");
EstApp.remember();
row[key] = value;
if (key === "Code") {
const previous = row.Code;
EstModel.editCode(project, node.dataset.table, Number(node.dataset.row), value);
if (state.group === previous && Number(row.Index) > 0) state.group = row.Code;
} else row[key] = value;
if (["StuffcDat", "LaborcDat", "EstiMateDat", "SilJukDat"].includes(node.dataset.table)) {
if (key === "TC") {
row.InputTC = value;
+3
View File
@@ -137,6 +137,9 @@ const EstNative = (() => {
if (!names.has(key)) names.set(key, []);
names.get(key).push(row);
}
for (const row of tables[table] || [])
for (const previous of row.PreviousCodes || [])
if (!codes.has(previous)) codes.set(previous, row);
sourceIndexes.set(num(kind), { codes, names });
}
function find(row, kind) {
+43 -1
View File
@@ -374,6 +374,48 @@ const EstModel = (() => {
});
return validate(project);
}
function editCode(project, table, index, value) {
const rows = project.tables[table],
row = rows[index];
const grouped = ["HeadMainDat", "CostMainDat", "MachMainDat"].includes(table);
const kind = Number(
Object.entries(EstNative.sourceTables).find(([, name]) => name === table)?.[0] ||
{ HeadMainDat: 103, CostMainDat: 104, MachMainDat: 105 }[table],
);
if (!kind || (grouped && !(Number(row.Index) > 0))) {
row.Code = value;
return;
}
const code = String(value || "").trim(),
old = row.Code;
if (
!code ||
rows.some(
(other, at) =>
at !== index &&
(!grouped || Number(other.Index) > 0) &&
(other.Code === code || other.PreviousCodes?.includes(code)),
)
)
throw Error("비어 있거나 중복된 원천 코드입니다.");
if (old === code) return;
row.Code = code;
row.PreviousCodes = [...new Set([...(row.PreviousCodes || []), old].filter(Boolean))];
for (const list of Object.values(project.tables))
for (const linked of list)
if (old && Number(linked.LinkIndex) === kind && linked.Code === old) linked.Code = code;
if (grouped) {
const titles = project.tables[EstNative.sourceTables[kind]] || [];
const title = titles.find((item) => item.Code === old);
if (title) Object.assign(title, { Code: code, PreviousCodes: [...row.PreviousCodes] });
for (
let at = index;
at < rows.length && (at === index || !(Number(rows[at].Index) > 0));
at++
)
if (rows[at].TitleCode === old) rows[at].TitleCode = code;
}
}
function newRow(table, rows) {
const max = Math.max(0, ...rows.map((r) => Number(r.PkID) || 0));
const row = {
@@ -397,5 +439,5 @@ const EstModel = (() => {
if (table === "CostMainDat") Object.assign(row, { LineData: "Q = 1 + 1 = \x122\x12" });
return row;
}
return { labels, numeric, columns, validate, legacy, newRow };
return { labels, numeric, columns, validate, legacy, newRow, editCode };
})();
+100
View File
@@ -0,0 +1,100 @@
const EstReport = (() => {
function preview(all = false) {
const { state, esc } = EstApp;
const number = (value) =>
new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 12 }).format(Number(value) || 0);
const root = document.createElement("div");
if (state.project.tables[state.view]) {
const rows = all
? state.project.tables[state.view].map((row, index) => ({ row, index }))
: EstApp.visibleRows();
const common = ["Code", "Name", "Size", "Unite"];
const columns = {
EstMainDat: [...common, "GN", "TU", "TC", "NC", "GC", "JC", "SC"],
HeadMainDat: [...common, "GN", "NU", "GU", "JU", "SU", "TU", "NC", "GC", "JC", "SC", "TC"],
CostMainDat: ["LineData"],
MachMainDat: [...common, "GN", "TU", "NC", "GC", "JC", "TC"],
Wonga: ["Gubun", "Name", "Youl", "VSusic", "Cost"],
};
root.innerHTML = EstViews.grid(
state.view,
rows,
columns[state.view] ||
EstModel.columns(state.view).filter(
(key) =>
![
"ValueIndex",
"SelectField",
"Ratio",
"NachalRatio",
"Diget",
"DigetIndex",
].includes(key),
),
);
} else root.innerHTML = EstApp.byId("work-content").innerHTML;
root
.querySelectorAll("button,.row-number,.business-tools,select#quantity-sheet")
.forEach((node) => node.remove());
root.querySelectorAll("input,textarea,select").forEach((node) => {
const value =
node.tagName === "SELECT"
? node.selectedOptions[0]?.textContent || ""
: node.type === "number" && node.value !== ""
? number(node.value)
: EstText.clean(EstText.encode(node.value));
if (node.tagName === "TEXTAREA") node.parentElement.classList.add("col-LineData");
if (node.type === "number") node.parentElement.classList.add("numeric");
node.replaceWith(document.createTextNode(value));
});
root
.querySelectorAll("[contenteditable]")
.forEach((node) => node.removeAttribute("contenteditable"));
const title = EstApp.titles[state.view] || state.view;
const detail =
state.view === "quantity"
? state.project.quantitySheets?.find((sheet) => sheet.Code === state.quantitySheet)?.Name
: state.view === "progress"
? state.project.progress.find((round) => round.id === state.roundId)?.name
: state.group
? EstNative.groups(state.project.tables[state.view] || []).get(state.group)?.rows[0]
?.Name
: "";
const totals = state.project.calculation;
const errors = totals?.errors || [];
const source = `<!doctype html><html lang="ko"><head><meta charset="UTF-8"><title>${esc(state.project.name)} · ${esc(title)}</title>
<style>@page{size:A3 landscape;margin:12mm}*{box-sizing:border-box}body{font:9pt "Malgun Gothic",sans-serif;color:#111;margin:0}
h1{font-size:17pt;margin:0 0 4mm}h2{font-size:12pt;margin:0 0 3mm}.summary{margin:2mm 0 4mm}
table{width:100%;border-collapse:collapse;font-size:8pt}thead{display:table-header-group}tr{break-inside:avoid}
th,td{border:1px solid #777;padding:1.5mm;vertical-align:top;overflow-wrap:anywhere}th{background:#eee}
.heading,.level{font-weight:bold;background:#f4f4f4}.selected{background:transparent}
.numeric{text-align:right;font-variant-numeric:tabular-nums}
.col-LineData{font-family:"굴림체",Consolas,monospace;white-space:pre-wrap;min-width:130mm}
.error-row{outline:1px solid #a00}.errors{color:#900;white-space:pre-wrap}.muted{color:#555}
</style></head><body><h1>${esc(state.project.name)}</h1><h2>${esc(title)}</h2>
${detail ? `<h2>${esc(detail)}</h2>` : ""}${totals ? `<div class="summary">순공사비 ${number(totals.direct?.TC)}원 · 도급액 ${number(totals.total)}원</div>` : ""}
${root.innerHTML}${errors.length ? `<p class="errors">계산 오류 ${errors.length}건: ${esc(errors.join(" / "))}</p>` : ""}</body></html>`;
EstApp.byId("print-frame").srcdoc = source;
}
function open() {
const table = EstApp.state.project.tables[EstApp.state.view];
EstApp.showDialog(
"인쇄 미리보기",
`<div class="business-tools">
${table ? '<label>출력 범위 <select id="print-scope"><option value="current">현재 호표·검색 결과 전체 행</option><option value="all">현재 자료의 모든 호표·행</option></select></label>' : ""}
<button data-action="print-document">인쇄 / PDF 저장</button></div>
<iframe id="print-frame" title="인쇄할 문서" style="width:100%;height:65vh;border:1px solid #aaa"></iframe>`,
);
EstApp.byId("print-scope")?.addEventListener("change", (event) =>
preview(event.target.value === "all"),
);
preview();
}
function print() {
const frame = EstApp.byId("print-frame");
if (!frame?.contentDocument?.body) throw Error("인쇄 미리보기가 준비되지 않았습니다.");
frame.contentWindow.focus();
frame.contentWindow.print();
}
return { open, preview, print };
})();
+22 -1
View File
@@ -16,6 +16,21 @@ const EstSettings = (() => {
type: "number",
value: project.options.EstDigits || 0,
},
{
name: "costTrunc",
value: project.options.CostValueTrunc ? "1" : "0",
label: "단가산출 성분 금액 절사",
choices: [
["0", "소수 1자리"],
["1", "원 미만"],
],
},
{
name: "midCost",
label: "단가산출 중간금액 소수자리",
type: "number",
value: project.options.MidCostDiget ?? 2,
},
{
name: "total",
value: project.options.TotalFix ? "1" : "0",
@@ -72,12 +87,18 @@ const EstSettings = (() => {
},
],
(values) => {
if (![values.head, values.estimate].every((n) => Number.isInteger(n) && n >= 0 && n <= 6))
if (
![values.head, values.estimate, values.midCost].every(
(n) => Number.isInteger(n) && n >= 0 && n <= 6,
)
)
throw Error("소수 자릿수는 0~6 정수입니다.");
EstApp.change(() =>
Object.assign(project.options, {
HeadDigits: values.head,
EstDigits: values.estimate,
CostValueTrunc: values.costTrunc === "1",
MidCostDiget: values.midCost,
TotalFix: values.total === "1",
TotalFit: values.fit === "1",
DollarCostCut: values.dollar === "1",
+19 -15
View File
@@ -17,15 +17,16 @@ const EstText = (() => {
function expression(text) {
return clean(
String(text).replace(
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
/\x0e[^\x0e]*\x0e|\x13[^\x13]*\x13|\x15[^\x15]*\x15|\{[^}]*\}|〔[^〕]*〕|[[^]]*]|\([가-힣][^()]*\)|\(\s*\d+(?:\.\d+)?\s*[˚°]\s*\)|<[^>]*>/g,
"",
),
)
.replace(
/(\d)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎡|hr|sec|min|kwh|kw|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|μ|₩|EA|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|개|대|본|인|분|포|매|주|톤|\$|hr|일|㎥|㎡|m|a|EA))?/gi,
/(?<![A-Za-z0-9_가-힣.])(\d[\d,.]*|\.\d+)\s*(?:공\/?㎥|공|단|면|겹|경간|개월|월|년|인|㎥|㎢|㎠|㎡|hr|sec|min|kwh|kw|km|ton|kg|㎏|㎞|ℓ|회|개소|개|대|분|초|본|장|매|병|조|식|일|시간|원|포|동|주|톤|μ|₩|EA|mm|cm|m3|m2|m|g|L|a)(?:\/(?:개소|경간|개|대|본|인|분|포|매|주|톤|공|회|조|span|ton|\$|hr|일|㎥|㎡|m3|m2|m|a|EA))?/gi,
"$1",
)
.replace(/(?<=\d)\s*\/(?:일|㎥|㎡)(?=\s|$)/g, "")
.replace(/(?<=\d)\s*\/\s*(?:일|㎥|㎡|hr)(?=\s|$|[×*+)])/gi, "")
.replace(/[】]〕]\s*$/, "")
.replace(/(?<=\))\s*(?:개|인|본|면|단|회)(?:\/(?:㎡|㎥|hr))?/g, "")
.replace(/^\s*[-*•]?\s*[가-힣][가-힣\s]*\s+(?=[\d.(])/, "")
.replace(/[①-⑳]/g, "")
@@ -99,7 +100,7 @@ const EstText = (() => {
errors.push(`${head.Code}:${index + 1} ${error.message}`);
}
}
let text = original;
let text = original.replace(/\{([+-])\}/g, "$1");
if (currentSource && sc < 0 && component[sc])
text = text.replace(
/\x03([-\d.,]+)\x03/g,
@@ -134,14 +135,14 @@ const EstText = (() => {
const assignedNames = new Map();
let componentDeclaration;
try {
if (matched && /─/.test(text)) {
if ((matched || /^\s*[A-Za-z][A-Za-z0-9]*\s*=/.test(text)) && /─/.test(text)) {
const above = clean(rows[index - 1]?.LineData),
below = clean(rows[index + 1]?.LineData);
const columns = (value) =>
value
.replace(/\s*([×÷+*/−-])\s*/g, "$1")
.replace(/\s*([×÷·+*/−-])\s*/g, "$1")
.trim()
.split(/\s{2,}/);
.split(/\s+/);
const tops = columns(above),
bottoms = columns(below);
let part = 0;
@@ -187,7 +188,7 @@ const EstText = (() => {
let body = stripLabels(text.slice(0, text.indexOf("\x12"))).replace(/=\s*$/, "");
if (body.includes(":")) body = body.slice(body.indexOf(":") + 1);
const parts = body.split("=");
if (parts.length > 1 && /^[A-Za-z][A-Za-z0-9]*$/.test(parts[0].trim())) {
if (parts.length > 1 && /^[A-Za-z가-힣][A-Za-z0-9가-힣]*$/.test(parts[0].trim())) {
componentDeclaration = parts[0].trim();
body = parts[1];
} else body = parts[0];
@@ -210,7 +211,9 @@ const EstText = (() => {
end = assignments[j + 1]?.index ?? text.length;
let body = text.slice(assignment.index + assignment[0].length, end);
if (!matched && /^\s*\*.*\*\s*$/.test(text)) body = body.replace(/\s+\*\s*$/, "");
body = body.split(/\{(?!%)|\s+[::]\s*/)[0].replace(/[},,]\s*$/, "");
body = body.replace(/\{[^}]*\}/g, "");
if (!matched) body = body.split("{")[0];
body = body.split(/\s+[::]\s*/)[0].replace(/[,,]\s*$/, "");
if (body.includes("─") && calculated !== null) {
variables[assignment[1]] = floating(calculated, 2, 1);
continue;
@@ -265,7 +268,7 @@ const EstText = (() => {
.pop();
if (body.includes(":")) body = body.slice(body.indexOf(":") + 1);
body = expression(body)
.replace(/\s*([×÷+*/−-])\s*/g, "$1")
.replace(/\s*([×÷·+*/−-])\s*/g, "$1")
.trim()
.split(/\s{2,}/)
.pop();
@@ -278,7 +281,8 @@ const EstText = (() => {
explicit?.digits ??
(sc < 0 || num(row.LinkIndex) !== 0
? (num(row.Level) === finalLevel && text.includes("\x1a")) ||
index === singleComponent
index === singleComponent ||
options.CostValueTrunc
? 0
: 1
: Math.max(2, matched[1].split(".")[1]?.length || 0));
@@ -289,8 +293,8 @@ const EstText = (() => {
results.length > 1 && sc < 0 && !explicit && row.Precision === undefined;
calculated = floating(
calculated,
intermediate ? Math.max(2, matched[1].split(".")[1]?.length || 0) : digits,
intermediate ? 1 : method,
intermediate ? (options.MidCostDiget ?? 2) : digits,
intermediate ? 2 : method,
);
const firstValue = calculated;
let rendered = text.replace(marker, `\x12${calculated}\x12`);
@@ -311,9 +315,9 @@ const EstText = (() => {
),
format?.digits ??
(intermediate && part < results.length - 1
? Math.max(2, next[1].split(".")[1]?.length || 0)
? (options.MidCostDiget ?? 2)
: digits),
format?.method ?? (intermediate && part < results.length - 1 ? 1 : method),
format?.method ?? (intermediate && part < results.length - 1 ? 2 : method),
);
assignmentName = assignedNames.get(next.index) || assignmentName;
if (assignmentName) variables[assignmentName] = calculated;
+1
View File
@@ -47,6 +47,7 @@ const allowed = new Set([
"native-actions.js",
"native-extra.js",
"native-settings.js",
"native-report.js",
...scripts,
]);
function reply(res, status, data, type = "application/json; charset=utf-8") {