diff --git a/resources/tester/test_est_tidy.py b/resources/tester/test_est_tidy.py new file mode 100644 index 000000000..38cdd15ca --- /dev/null +++ b/resources/tester/test_est_tidy.py @@ -0,0 +1,90 @@ +"""EST 테스트 앱 정렬·데이타 위치정리·호표 정리(원본 BarSort·TFRMTABLESORT·popTitleSort).""" + +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-tidy.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_sort_base_groups_by_gangub_and_drops_blank(): + result = run_js( + """(() => { + const p = { tables: { StuffcDat: [ + { Index: 1, Code: 'M3', Name: '철근', Gangub: 0 }, { Index: 2, Code: 'M1', Name: '레미콘', Gangub: 1 }, + { Index: 3, Code: 'M2', Name: '거푸집', Gangub: 0 }, { Index: 0, Code: 'M9', Name: '번호없음' }, { Index: 4 } ] } }; + const dropped = EstTidy.sortBase(p, 'StuffcDat'); + const s = { tables: { SilJukDat: [{ Index: 1, Code: 'S2', Name: '가' }, { Index: 2, Code: 'S1', Name: '나' }] } }; + EstTidy.sortBase(s, 'SilJukDat', true); + let bad = ''; try { EstTidy.sortBase(p, 'HeadMainDat'); } catch (e) { bad = e.message.slice(0, 4); } + return { order: p.tables.StuffcDat.map((r) => [r.Code, r.Index]), dropped, code: s.tables.SilJukDat.map((r) => r.Code), bad }; + })()""" + ) + assert result == { + "order": [["M2", 1], ["M3", 2], ["M1", 3]], + "dropped": 2, + "code": ["S1", "S2"], + "bad": "자재·노", + } + + +def test_arrange_groups_and_number_heads_keep_totals(): + result = run_js( + """(() => { + const calc = (x) => EstNative.calculate(EstModel.validate(x)); + const p = calc(structuredClone(source)); + const total = p.calculation.total; + const n = EstTidy.arrange(p, 'HeadMainDat', false, 10); + const heads = [...EstNative.groups(p.tables.HeadMainDat).values()].map((g) => g.rows[0]); + const sorted = heads.every((h, i) => i === 0 || String(heads[i - 1].Name) <= String(h.Name)); + const numbers = heads.map((h) => h.Index); + const titles = p.tables.HeadTitleDat.filter((t) => heads.some((h) => h.Code === t.Code)).every((t) => t.Index === heads.find((h) => h.Code === t.Code).Index); + EstTidy.numberHeads(p, 'HeadMainDat', 1); + const after = calc(p); + return { n: n === heads.length, sorted, first: numbers[0], step: numbers[1] - numbers[0], titles, + renum: [...EstNative.groups(p.tables.HeadMainDat).values()].map((g) => g.rows[0].Index).join() === heads.map((_, i) => i + 1).join(), + total: after.calculation.total === total, errors: after.calculation.errors.length }; + })()""" + ) + assert result == { + "n": True, + "sorted": True, + "first": 10, + "step": 1, + "titles": True, + "renum": True, + "total": True, + "errors": 0, + } diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 89091a08d..42c7e961c 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -464,6 +464,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 검증완료: `test_est_cleanup.py` 3개(안 쓰는 자재·일위 삭제·합계 불변·오류 0, 내역서순 순서·코드 유지, 빈 줄·수량산출서) 통과. 설치 main·sample·산림토목 ESTX에서 정리 전후 도급액·오류 수 같음. ORCA 브라우저에서 내역서순 정리 → 자재 30 → 17·도급액 79,603,000 유지·되살리기 복원. - [x] 품명정리·자재/견적단가 등록(원본 ActionNameDeivde 0xf5a2b4·TfrmNameDevide 0xf595c0/0xf59640, popMainStuffcReg/EstimateReg → c_f00264/c_f00670 — 디스어셈블 근거) — 커서 자리 나누기·삭제할 칸·빈 단위만 채움·호표 목록 동기화, 등록 시 같은 품명·규격 거부·호표 거부·M/W+PkID 10자리 코드·자재 Cost1=TC=재료단가·견적 NC/GC/JC·원래 행 연결. 원본은 품명정리를 2개 이상 선택 때만 돌리나 웹은 1개도 허용. - 검증완료: `test_est_name_reg.py` 2개(나누기·삭제할 칸·제목 동기화·호표 아님 거부, 자재/견적 등록 코드·가격·연결·금액 불변·중복/호표 거부·계산 오류 0) 통과. ORCA 브라우저에서 내역 행 자재 등록 → M0000000038·연결 5, 일위대가 「레미콘타설(…)」 3자리 나누기 → 품명 「레미콘」·규격 「타설(인력운반타설)철근구조물」, 샘플 복구. + - [x] 정렬·데이타 위치정리·호표 정리·근거(메모)(원본 BarSort 0x110ADEC·TfrmSiljukSort, mnuTableSort 0x10CCAEC·TFRMTABLESORT, popTitleSort 0x10D88EC, mnuGunguEdit/Del·DifineMemo — 디스어셈블 근거) — 관급구분별 가나다·Index 재번호·빈 행 제외(30줄 채우기는 안 함), 품명 오름/내림·시작번호·묶음 통째 이동, 번호만 재매김·제목표 동기화, 메모 WPTools 글자 읽기 전용 보기·삭제. 원본 위치정리 콤보 글자 뒤바뀜은 웹에서 실제 순서대로 표기. + - 검증완료: `test_est_tidy.py` 2개(관급구분·빈 행·실적 코드 정렬·화면 거부, 위치정리·번호정렬·제목 동기화·도급액 불변·오류 0) 통과. ORCA 브라우저에서 설치 main.estx 메모를 넣어 보기 → 「2015년 건설표준품셈 … 잔디블록」 글자 표시, 삭제 → null, 자재 정렬 → 빈 행 12개 제외, 샘플 복구. + - 남음: 메모 편집(원본 서식 저장), 위치정리 드래그 이동. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 52d612101..d6283418c 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -24,6 +24,8 @@ `편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다. +`편집 → 정렬(가나다)`은 기초단가 표를 관급구분별로 묶어 품명·규격 순(실적은 코드 순 선택)으로 정렬하고 번호를 다시 매기며 빈 행은 뺍니다. `데이타 위치정리`는 기초단가 또는 일위대가·단가산출·기계경비 호표를 품명 오름/내림으로 다시 놓고 시작번호부터 번호를 매깁니다. `호표 정리`는 차례는 그대로 두고 호표 번호만 시작번호부터 다시 매깁니다. `근거(메모) 보기`는 원본 서식 메모의 글자를 읽기 전용으로 보이고, `근거(메모) 삭제`는 고른 행의 메모를 지웁니다. + `편집 → 무관자료 삭제`는 원본처럼 내역서가 쓰는 자재·노임·실적·견적과 일위대가·단가산출·기계경비 호표를 하위까지 따라가 표시하고 나머지를 지웁니다. 내역서·원가계산서·기계경비 적용기준은 지우지 않고, 묶음 머리 앞의 빈 줄을 정리합니다. `(내역서순)`은 표를 내역서 첫 등장 순서로 다시 늘어놓으며 코드는 바꾸지 않습니다. `무관 수량산출서 삭제`는 내역에 연결되지 않은 수량산출서를 지웁니다. 모두 되살리기로 되돌릴 수 있습니다. `원가계산서 → 정산원가계산서 작성/해제`는 원본처럼 원가 표에 당초금액 열을 금액 앞에 보이고 금액 열을 정산금액으로 바꿉니다. 처음 켤 때(또는 `당초금액 초기화`) 원가 행마다 현재 금액을 당초금액으로 옮기고 도급액 고정 연결(55)을 풀며, 맨 아래 금액 행의 차이를 `(감)`·`(증)` 금액 행으로 적습니다. 켜져 있는 동안 재계산마다 차액 행을 다시 씁니다. 끄면 차액 행만 비우고 당초금액은 남깁니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 8282dd3c4..d95acf973 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -48,6 +48,7 @@ + @@ -122,6 +123,11 @@
${EstApp.esc(body)}${typeof row.DifineMemo === "string" ? "" : "원본 서식 메모라 웹에서는 읽기만 합니다.
"}` + : "이 행에는 근거(메모)가 없습니다.
", + ); + } else if (name === "memo-delete") { + const { index } = memoTarget(state); + EstApp.change((project) => (project.tables[state.view][index].DifineMemo = null), false); + state.notice = "근거(메모)를 지웠습니다."; + } + } + const names = new Set(["tidy-sort", "tidy-arrange", "tidy-number", "memo-view", "memo-delete"]); + function handle(name, state) { + if (!names.has(name)) return false; + run(name, state).catch((error) => { + state.notice = error.message; + EstApp.render(); + }); + return true; + } + + return { sortBase, arrange, numberHeads, memoText, handle }; +})(); diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 2d301643a..fe208131c 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -69,6 +69,7 @@ const allowed = new Set([ "native-forms-library.js", "native-cleanup.js", "native-name-reg.js", + "native-tidy.js", "native-issues.js", "native-attachments.js", "native-standard.js",