diff --git a/resources/tester/test_est_est_tools.py b/resources/tester/test_est_est_tools.py new file mode 100644 index 000000000..b4b68eac9 --- /dev/null +++ b/resources/tester/test_est_est_tools.py @@ -0,0 +1,98 @@ +"""EST 테스트 앱 하단으로 집계·공종 다듬기·단가산출 들여쓰기·레벨 확인(원본 f01a90·0x113eb44·0xf0770c·0xd10840).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +FILES = [ + "native-math.js", + "native-formula.js", + "native-machine.js", + "native-rows.js", + "native-text.js", + "native-quantity.cjs", + "native-wonga.js", + "native-engine.js", + "native-row-match.js", + "native-escalation.js", + "native-progress.js", + "native-business.js", + "native-review.js", + "native-resources.js", + "native-schedule.js", + "native-model.js", + "native-verify.js", + "native-est-tools.js", +] + + +def run_js(code: str): + runner = f""" + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({{ structuredClone, console }}); + for (const f of {json.dumps(FILES)}) + vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }}); + ctx.source = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8')); + process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx))); + """ + return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8")) + + +def test_down_sum_inserts_nested_subtotals_and_engine_fills_them(): + result = run_js( + """(() => { + const calc = (p) => EstNative.calculate(EstModel.validate(p)); + const p = calc(structuredClone(source)); + const total = p.calculation.total, direct = p.calculation.direct.TC; + const added = EstEstTools.downSum(p); + const again = EstEstTools.downSum(p); + const done = calc(p); + const rows = done.tables.EstMainDat; + const subs = rows.map((r, i) => [r, i]).filter(([r]) => Number(r.LinkIndex) === 53); + const first = subs[0][1]; + let head = first - 1; while (head >= 0 && !(Number(rows[head].Level) > 0)) head--; + const expected = rows.slice(head + 1, first).filter((r) => !Number(r.Level)).reduce((n, r) => n + Number(r.TC || 0), 0); + const removed = EstEstTools.upSum(done); + return { added: added > 0, again, firstOk: Math.abs(Number(rows[first].TC) - expected) < 1 && expected > 0, + name: rows[first].Name, total: done.calculation.total === total, direct: done.calculation.direct.TC === direct, + removed: removed === added, errors: done.calculation.errors.length }; + })()""" + ) + assert result == { + "added": True, + "again": 0, + "firstOk": True, + "name": "소 계", + "total": True, + "direct": True, + "removed": True, + "errors": 0, + } + + +def test_left_space_indent_and_level_tree(): + result = run_js( + """(() => { + const p = { options: {}, tables: { EstMainDat: [ + { Level: 3, Name: 'A' }, { Level: 2, Name: 'B' }, { Level: 0, Name: 'b1' }, { Level: 2, Name: 'C' }, + { Level: 1, Name: 'D' }, { Level: 0, Name: 'd1' }, { Level: 3, Name: 'E' }, { LinkIndex: 54, Level: 0 } ], + CostMainDat: [ + { Index: 1, Code: 'D1', Name: '터파기', Size: '토사' }, + { LineData: ' 가 = \x123\x12' }, + { LineData: ' 1', Level: 0 }, { LineData: ' ───── = \x120.5\x12' }, { LineData: ' 2' }, + { Level: 2, LineData: ' 큰 공종' }, + { SCheck: -1, LineData: '노무 \x1212,345\x12' } ] } }; + EstEstTools.leftSpace(p, 2); + EstEstTools.costIndent(p, 4); + const tree = EstEstTools.levelTree(p, 'D1'); + return { left: p.tables.EstMainDat.map((r) => r.LeftSpace), lines: p.tables.CostMainDat.map((r) => r.LineData), + tree: tree.title, items: tree.lines.map((l) => l.text.replace(/ {2,}/g, '_')) }; + })()""" + ) + assert result["left"] == [0, 2, 4, 2, 4, 6, 0, 0] + assert result["lines"][1] == " 가 = \x123\x12" + assert result["lines"][2:5] == [" 1", " ───── = \x120.5\x12", " 2"] + assert result["lines"][5] == " 큰 공종" + assert result["tree"] == "터파기 (토사)" + assert result["items"] == ["큰 공종_[2]", "노무비 : 12,345"] diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 297a57e60..c93279e95 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -495,6 +495,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - [x] 데이타 자동연결(원본 ActionAutoLink 0x10d54f4·TfrmDataAutoLink 0x10d3008·10d2a48·맞춤 10e0e58 — 디스어셈블 근거) — 9개 선택·Over Write, 정규화·맞춤 단계·노임 맞바꿈·기계 주연료·건너뛰기 규칙·쓰는 값 원본대로, 전체 순서 노임→자재→견적→표준→기계(내역·일위만)→단가산출→일위대가. - 검증완료: `test_est_autolink.py`(노임 품명만·규격 재시도 맞바꿈·단위 정규화·일위 비목 단가·SCheck 3·Over Write·이미 연결 건너뜀·전체 끊기) 통과. ORCA 브라우저에서 샘플 전체자동(Over Write) → 7행 연결·순공사비 45,293,066 그대로, 되돌리기 원상. - 남음: 전역변수(`` 표지 — 설치 자료 사용 0건, 단가산출 엔진 깊은 변경이 필요해 보류). + - [x] 하단/상단으로 집계·공종 다듬기·단가산출 들여쓰기·레벨 확인(원본 f01a90·f018d0·f01750, 0x113eb44, 0xf0770c, 0xd10840 — 디스어셈블 근거) — 「소 계」(53, Level −10) 넣기 규칙, 내역 엔진에 53 행 합(닫는 공종 안쪽부터, 상세 행) 추가, LeftSpace 계산·이름 칸 들여 보이기, 들여쓰기 n 칸·분수 세 줄 함께, 레벨 나무(60−Level 칸 [레벨]·비목 소계). 다시 집계 시 겹침 방지는 웹 보완. + - 검증완료: `test_est_est_tools.py` 2개(소계 넣기·재실행 0·첫 소계 = 공종 합·도급액/순공사비 불변·상단 집계로 원상, 들여쓰기·LeftSpace·레벨 나무) 통과, 대조·업무·설계변경·연차·ESTX 저장 시험 111개 통과(엔진 변경 회귀). ORCA 브라우저에서 하단 집계 11줄(토공 2,382,484 등 공종 합과 같음)·도급액 유지, 공종 다듬기 59칸 들여쓰기, 레벨 나무 표시, 샘플 복구. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index d85c28752..bdd5f4ef0 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -22,6 +22,8 @@ 내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다. +`편집 → 하단으로 집계`는 원본처럼 공종이 끝나는 자리(다음 공종 머리·순공사비 앞·마지막)에 「소 계」 줄을 넣고, 계산할 때 그 공종 상세 행의 합을 채웁니다. 겹쳐 넣지 않으며 `상단으로 집계`는 소계 줄을 지웁니다. `공종 다듬기`는 레벨마다 공종명을 들여 보입니다. `단가산출 들여쓰기`는 산출근거 줄 앞 빈칸을 같은 칸 수로 맞추고(분수 세 줄은 함께 옮김), `단가산출 레벨 확인`은 현재 호표의 레벨 줄과 비목 소계를 계층으로 보입니다. + `편집 → 데이타 자동연결`은 원본처럼 내역서·일위대가·기계경비 행을 현재 공사의 노임·자재·견적·표준·기계경비·단가산출·일위대가 표와 품명(·규격·단위)으로 맞춰 코드·단가·연결종류를 적습니다. 빈칸·|·괄호를 무시하고 단위의 「당」·[ ]를 지우며, 노임은 품명만 맞추고 못 찾으면 규격으로 찾아 품명·규격을 맞바꿉니다. `Over Write`를 끄면 이미 연결된 행은 건너뜁니다. `전체연결 끊기`는 원본처럼 모든 행의 연결종류를 0으로 합니다. `편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 86ecc3e89..5a86c3d06 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -57,6 +57,7 @@ + @@ -140,6 +141,11 @@
${esc(tree.title)}
${tree.lines.map((l) => esc(l.text)).join("\n") || "레벨 지정 줄이 없습니다."}`,
+ );
+ } else return false;
+ return true;
+ }
+
+ return { downSum, upSum, leftSpace, costIndent, levelTree, handle };
+})();
diff --git a/tools/est-test/native-rows.js b/tools/est-test/native-rows.js
index b5d28302f..7a4d9f387 100644
--- a/tools/est-test/native-rows.js
+++ b/tools/est-test/native-rows.js
@@ -163,6 +163,7 @@ const EstRows = (() => {
segment = [];
continue;
}
+ if (link === 53) continue;
if (link === 54) {
Object.assign(row, add(prior));
boundary = true;
@@ -192,6 +193,19 @@ const EstRows = (() => {
segment.push(row);
if (!boundary) prior.push(row);
}
+ // 원본 하단으로 집계(popMainDownSum 0xf01a90)의 「소 계」 행(53): 닫는 공종(안쪽부터)의 상세 행 합.
+ const open = [];
+ output.forEach((row, i) => {
+ const level = num(row.Level);
+ if (level > 0) {
+ while (open.length && open.at(-1).level <= level) open.pop();
+ open.push({ level, start: i });
+ } else if (num(row.LinkIndex) === 53) {
+ const group = open.pop();
+ const body = group ? output.slice(group.start + 1, i) : [];
+ Object.assign(row, add(body.filter((r) => !num(r.Level) && num(r.LinkIndex) !== 53)));
+ }
+ });
for (let i = output.length - 1; i >= 0; i--) {
const row = output[i],
level = num(row.Level);
diff --git a/tools/est-test/native-views.js b/tools/est-test/native-views.js
index 51de43517..41d3c55e0 100644
--- a/tools/est-test/native-views.js
+++ b/tools/est-test/native-views.js
@@ -76,7 +76,7 @@ const EstViews = (() => {
}
function grid(table, list, columns) {
const { esc, number, state } = EstApp;
- return `| 선택 | ${columns.map((key) => `${esc((table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || EstModel.labels[key] || key)} | `).join("")}
|---|---|
| ${columns.map((key) => ` | ${cell(table, index, key, row[key])} | `).join("")}
| 선택 | ${columns.map((key) => `${esc((table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || EstModel.labels[key] || key)} | `).join("")}
|---|---|
| ${columns.map((key) => ` | 0 ? ` style="padding-left:${Number(row.LeftSpace) * 6}px"` : ""}>${cell(table, index, key, row[key])} | `).join("")}