Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
|
||||
@@ -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,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 수정 저장은 미구현입니다. 원본 대체품의 완료 판정으로 보지 않습니다.
|
||||
|
||||
검사:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
})();
|
||||
|
||||
@@ -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 };
|
||||
})();
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user