diff --git a/resources/tester/test_est_name_reg.py b/resources/tester/test_est_name_reg.py new file mode 100644 index 000000000..ba807b62b --- /dev/null +++ b/resources/tester/test_est_name_reg.py @@ -0,0 +1,102 @@ +"""EST 테스트 앱 품명정리와 자재·견적단가 등록(원본 TfrmNameDevide·popMainStuffcReg·popMainEstimateReg).""" + +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-name-reg.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_divide_splits_at_position_and_syncs_title(): + result = run_js( + """(() => { + const p = { tables: { + HeadMainDat: [{ Index: 1, Code: 'U1', Name: '거푸집 설치 합판', Size: '3회', Unite: '' }, { Code: 'L1', LinkIndex: 6 }, + { Index: 2, Code: 'U2', Name: '거푸집 해체 강재', Size: '', Unite: '㎡' }], + HeadTitleDat: [{ Code: 'U1', Name: '거푸집 설치 합판' }, { Code: 'U2', Name: '거푸집 해체 강재' }] } }; + const n = EstNameReg.divide(p, [0, 1, 2], 4, 0, '㎡'); + const c = { tables: { HeadMainDat: [{ Index: 1, Code: 'X', Name: 'AB-CD', Size: '' }] } }; + EstNameReg.divide(c, [0], 2, 1, ''); + let bad = ''; try { EstNameReg.divide(p, [1], 4, 0, ''); } catch (e) { bad = e.message.slice(0, 6); } + const r = p.tables.HeadMainDat; + return { n, a: [r[0].Name, r[0].Size, r[0].Unite], b: [r[2].Name, r[2].Size, r[2].Unite], + title: p.tables.HeadTitleDat[0].Size, bad, cut: c.tables.HeadMainDat[0].Size }; + })()""" + ) + assert result["n"] == 2 + assert result["a"] == ["거푸집 ", "설치 합판3회", "㎡"] + assert result["b"] == ["거푸집 ", "해체 강재", "㎡"] + assert result["title"] == "설치 합판3회" + assert result["bad"] == "품명을 정리" + assert result["cut"] == "CD" + + +def test_register_stuff_and_estimate_link_row_and_keep_price(): + result = run_js( + r"""(() => { + const calc = (x) => EstNative.calculate(EstModel.validate(x)); + const p = calc(structuredClone(source)); + const i = p.tables.EstMainDat.findIndex((r) => Number(r.GU) > 0 && !Number(r.Level) && Number(r.LinkIndex) === 5); + p.tables.EstMainDat[i].Name = '시험 자재 등록품'; + const before = p.tables.EstMainDat[i].TC; + const code = EstNameReg.register(p, 'EstMainDat', i, 'stuff'); + const rec = p.tables.StuffcDat.find((r) => r.Code === code); + const j = p.tables.EstMainDat.findIndex((r, k) => k !== i && Number(r.JU) + Number(r.NU) > 0 && !Number(r.Level) && r.Name); + p.tables.EstMainDat[j].Name = '시험 견적 등록품'; + const w = EstNameReg.register(p, 'EstMainDat', j, 'estimate'); + const est = p.tables.EstiMateDat.find((r) => r.Code === w); + const after = calc(p); + const errors = []; + for (const fn of [() => EstNameReg.register(p, 'EstMainDat', i, 'stuff'), + () => EstNameReg.register(p, 'HeadMainDat', p.tables.HeadMainDat.findIndex((r) => Number(r.Index) > 0), 'stuff')]) + try { fn(); } catch (e) { errors.push(e.message.slice(0, 6)); } + return { code: /^M\d{10}$/.test(code), price: rec.Cost1 === Number(p.tables.EstMainDat[i].GU) && rec.TC === rec.Cost1, + link: [p.tables.EstMainDat[i].LinkIndex, p.tables.EstMainDat[i].Code === code], + same: after.tables.EstMainDat[i].TC === before, w: /^W\d{10}$/.test(w), + est: est.TC === est.NC + est.GC + est.JC && est.ONC === est.NC, estLink: p.tables.EstMainDat[j].LinkIndex, + errors, calcErrors: after.calculation.errors.length }; + })()""" + ) + assert result == { + "code": True, + "price": True, + "link": [5, True], + "same": True, + "w": True, + "est": True, + "estLink": 101, + "errors": ["같은 명칭의", "호표는 자재"], + "calcErrors": 0, + } diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 5f59b317d..89091a08d 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -462,6 +462,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 당초분·정산분 색상 옵션. - [x] 무관자료 삭제(원본 mnuNotLinkDelete·DelSort → f039f0, MulDelete → 1116db0 — 디스어셈블 근거) — 내역서에서 LinkIndex 5·6·21·101·103·104·105·Code(설계변경 Code_ 포함)로 하위까지 따라가 쓰는 자료만 남김, 묶음 머리 앞 빈 줄 정리, 내역서순 재배열(코드 유지), 내역 미연결 수량산출서 삭제. 원본의 한 번 훑기 누락(기계경비 안에서만 쓰인 일위대가 하위)은 웹에서 끝까지 따라가 막음. LinkIndex 211–218 코드 비우기는 뜻 미확인으로 하지 않음. - 검증완료: `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자리 나누기 → 품명 「레미콘」·규격 「타설(인력운반타설)철근구조물」, 샘플 복구. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 7e7eb7693..52d612101 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -22,6 +22,8 @@ 내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다. +`편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다. + `편집 → 무관자료 삭제`는 원본처럼 내역서가 쓰는 자재·노임·실적·견적과 일위대가·단가산출·기계경비 호표를 하위까지 따라가 표시하고 나머지를 지웁니다. 내역서·원가계산서·기계경비 적용기준은 지우지 않고, 묶음 머리 앞의 빈 줄을 정리합니다. `(내역서순)`은 표를 내역서 첫 등장 순서로 다시 늘어놓으며 코드는 바꾸지 않습니다. `무관 수량산출서 삭제`는 내역에 연결되지 않은 수량산출서를 지웁니다. 모두 되살리기로 되돌릴 수 있습니다. `원가계산서 → 정산원가계산서 작성/해제`는 원본처럼 원가 표에 당초금액 열을 금액 앞에 보이고 금액 열을 정산금액으로 바꿉니다. 처음 켤 때(또는 `당초금액 초기화`) 원가 행마다 현재 금액을 당초금액으로 옮기고 도급액 고정 연결(55)을 풀며, 맨 아래 금액 행의 차이를 `(감)`·`(증)` 금액 행으로 적습니다. 켜져 있는 동안 재계산마다 차액 행을 다시 씁니다. 끄면 차액 행만 비우고 당초금액은 남깁니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 378f79f3a..8282dd3c4 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -47,6 +47,7 @@ + @@ -103,6 +104,9 @@ >
기존명칭에서 품명과 규격을 나눌 자리에 커서를 두고 「규격으로 지정」을 누르세요. 고른 ${picked.length}개 호표에 같은 자리로 적용합니다.
`, + ); + state.nameDivide = { picked, position: null }; + } + function field(name) { + return document.querySelector(`#dialog-content [name="${name}"]`); + } + + function handle(name, state) { + if (name === "name-divide") divideDialog(state); + else if (name === "name-divide-set") { + const all = field("nd-all"), + position = all.selectionStart ?? 0; + state.nameDivide.position = position; + field("nd-name").value = all.value.slice(0, position); + field("nd-size").value = all.value.slice(position); + } else if (name === "name-divide-apply") { + const info = state.nameDivide; + if (info?.position == null) throw Error("먼저 「규격으로 지정」으로 나눌 자리를 정하세요."); + const cut = Number(field("nd-cut").value), + unit = field("nd-unit").value.trim(); + EstApp.byId("dialog").close(); + let count; + EstApp.change((project) => (count = divide(project, info.picked, info.position, cut, unit))); + state.notice = `${count}개 호표의 품명을 정리했습니다.`; + } else if (name === "register-stuff" || name === "register-estimate") { + const which = name === "register-stuff" ? "stuff" : "estimate", + index = [...state.selection][0], + row = state.project.tables[state.view]?.[index]; + if (!["EstMainDat", "HeadMainDat"].includes(state.view) || state.selection.size !== 1 || !row) + throw Error("내역서·일위대가에서 등록할 행 하나를 고르세요."); + const label = targets[which].label; + register(structuredClone(state.project), state.view, index, which); + if (!confirm(`"${row.Name ?? ""}" ${row.Size ?? ""} 를 ${label}에 등록하시겠습니까?`)) + return true; + let code; + EstApp.change((project) => (code = register(project, state.view, index, which))); + state.notice = `${label}에 ${code}로 등록하고 이 행을 연결했습니다.`; + } else return false; + return true; + } + + return { divide, register, handle }; +})(); diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index b0cd4d7c9..2d301643a 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -68,6 +68,7 @@ const allowed = new Set([ "native-bid.js", "native-forms-library.js", "native-cleanup.js", + "native-name-reg.js", "native-issues.js", "native-attachments.js", "native-standard.js",