diff --git a/resources/tester/test_est_transfer.py b/resources/tester/test_est_transfer.py new file mode 100644 index 000000000..1d5fc9f7d --- /dev/null +++ b/resources/tester/test_est_transfer.py @@ -0,0 +1,102 @@ +"""EST 테스트 앱 연결 자료와 함께 복사·붙여넣기(원본 TFRMOTHERLOAD).""" + +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-transfer.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))); + """ + out = subprocess.check_output(["node", "-e", runner], cwd=ROOT) + return json.loads(out.decode("utf-8")) + + +PICK = """ + const calc = (p) => EstNative.calculate(EstModel.validate(p)); + const src = calc(source); + const linked = src.tables.EstMainDat.filter((r) => r.Code && !Number(r.Level)); + const picked = [5, 103, 104].flatMap((k) => linked.filter((r) => Number(r.LinkIndex) === k).slice(0, 2)); + const deps = EstTransfer.collect(src, picked); +""" + + +def test_copy_into_empty_project_keeps_prices_and_links(): + result = run_js( + "(() => {" + + PICK + + """ + const target = EstNative.empty(); + target.options = structuredClone(src.options); + const merged = EstTransfer.merge(target, structuredClone(deps)); + EstTransfer.paste(target, picked, 0); + const done = calc(target); + const pasted = done.tables.EstMainDat.slice(0, picked.length); + return { + kinds: [...new Set(picked.map((r) => Number(r.LinkIndex)))].sort((a, b) => a - b), + groups: Object.keys(deps.groups).sort(), + nested: (deps.flat.LaborcDat || []).length > 0, + missing: done.calculation.errors.filter((e) => e.includes('연결 자료 없음') || e.includes('하위 자료 없음')), + same: pasted.every((r, i) => Math.abs(Number(r.TC) - Number(picked[i].TC)) < 1), + added: merged.added.length > 0, kept: merged.kept.length, criteria: merged.criteria, + unique: new Set(done.tables.HeadMainDat.map((r) => r.PkID)).size === done.tables.HeadMainDat.length, + }; + })()""" + ) + assert result["kinds"] == [5, 103, 104] + assert result["groups"] == ["CostMainDat", "HeadMainDat", "MachMainDat"] + assert result["nested"] is True + assert result["missing"] == [] + assert result["same"] is True + assert result["added"] is True + assert result["kept"] == 0 + assert result["criteria"] is True + assert result["unique"] is True + + +def test_same_code_in_target_is_kept_not_overwritten(): + result = run_js( + "(() => {" + + PICK + + """ + const target = structuredClone(source); + const labor = target.tables.LaborcDat[0]; + labor.Cost1 = Number(labor.Cost1) * 2; labor.TC = Number(labor.TC) * 2; + const before = target.tables.HeadMainDat.length; + target.tables.MachGijunDat[0].TC = 12345; + const merged = EstTransfer.merge(target, structuredClone(deps)); + return { added: merged.added.length, kept: merged.kept.length > 0, + criteria: merged.criteria === false && target.tables.MachGijunDat[0].TC === 12345, + heads: target.tables.HeadMainDat.length === before, + labor: target.tables.LaborcDat[0].TC === labor.TC }; + })()""" + ) + assert result == {"added": 0, "kept": True, "criteria": True, "heads": True, "labor": True} diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index b2c65f256..0a284af62 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -444,11 +444,14 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - [x] Excel 내역 열 맞춰 가져오기(원본 TFRMEXCELTOEST 내역서 탭 — tvViewEst 열 Code·Pr·Name·Size·GN·Unite·TU·TC·NU·NC·GU·GC·JU·JC·Bigo) — 시트·시작행·끝행·항목별 열 문자, 미리보기, 순공사비 위 추가·바꾸기. 단위·수량 없는 줄 = 공종 제목(Level 1), 금액은 다시 계산, 수식 셀은 저장된 계산값. 합계단가만 있는 줄은 비목 임의 배분 없이 거부. - 검증완료: `test_est_excel_map.py` 3개(열 문자·쉼표 숫자·빈 줄·잘못된 입력, 제목·상세 구분·PkID 중복 없음·순공사비 위 배치·금액 계산, 합계단가만 거부·바꾸기 모드), 관련 9개 통과. ORCA 브라우저에서 openpyxl로 만든 수식 포함 xlsx를 서버로 읽어 시트 2개 표시·미리보기, 5행(제목 2) 가져오기 → 순공사비 45,293,066 → 61,798,566 확인 후 샘플 복구. - 남음: 일위대가·단가산출·표준·견적·자재·노임 탭, 원본 「셀부호 밀기·당기기」. + - [x] 연결 자료와 함께 복사·붙여넣기(원본 TFRMOTHERLOAD) — 내역 행의 자재·노임·실적·견적 행과 일위대가·단가산출·기계경비 호표를 하위까지 모아 다른 공사에 붙여넣기. 같은 코드는 붙여넣을 공사 자료 유지(덮어쓰지 않음), 기계경비 적용기준은 붙여넣을 공사에 없을 때만 옮김. + - 검증완료: `test_est_transfer.py` 2개(빈 공사로 옮겨 연결 오류 0·금액 동일·하위 기계경비 호표 포함·PkID 중복 없음, 같은 코드·적용기준 덮어쓰지 않음), 관련 시험 통과. ORCA 브라우저에서 샘플 내역 3행 복사 → 새 내역 붙여넣기 → 연결 자료 12건 추가·금액 1,285,046·417,024·680,414 원본과 동일·계산 오류 0, 샘플 복구. + - 남음: 프로젝트 추가(다른 파일의 내역 전체 합치기), 두 공사 나란히 보기. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기. 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. - 5. 다른 공사 불러오기 시 딸린 일위·단가산출 함께 복사, 프로젝트 추가(합치기). + 5. 프로젝트 추가(다른 파일의 내역 합치기). 6. 낙찰률 전체·범위·공종별 적용. 7. 무관자료 삭제, 데이타 정리(정렬·번호), 품명정리. 8. 원가계산서 — 정산원가계산서, 기타경비·요율 대화, 서식 저장·불러오기, 기성 원가 편집. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 03cc8e034..67e697f16 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -22,6 +22,8 @@ 내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다. +`편집 → 내역 행 연결 자료와 함께 복사`는 고른 내역 행이 쓰는 자재·노임·실적·견적 자료와 일위대가·단가산출·기계경비 호표를 하위까지 모아 복사합니다. 다른 공사를 연 뒤 `연결 자료와 함께 붙여넣기`를 누르면 없는 자료만 추가하고, 같은 코드가 이미 있으면 그 공사의 자료를 그대로 씁니다. 붙여넣을 공사에 기계경비 적용기준이 비어 있을 때만 적용기준도 함께 옮깁니다. + `파일 → Excel 내역 열 맞춰 가져오기`는 원본 엑셀→EST 변환처럼 다른 프로그램이나 발주처의 Excel 내역을 읽습니다. 시트·시작행·끝행과 번호·품명·규격·단위·수량·단가 등 항목별 Excel 열 문자를 적고 미리보기로 확인한 뒤, 순공사비 줄 위에 추가하거나 그 위 행을 바꿉니다. 단위와 수량이 없는 줄은 공종 제목이 되고, 금액은 단가×수량으로 다시 계산합니다. 수식 셀은 Excel이 저장해 둔 값을 씁니다. 합계단가만 있고 노무·재료·경비로 나눈 단가가 없으면 비목을 정할 수 없어 가져오지 않습니다. `파일 → 공내역 파일로 저장`은 원본처럼 내역·일위대가의 금액을 비운 사본을 내려받습니다. 남길 값은 수량만(단가 비움)·단가만(수량 비움)·수량·단가(금액만 비움) 중에서 고르고, 원본 ESTX를 가져온 작업은 ESTX로도 받을 수 있습니다. 사본은 자동계산을 끈 상태로 저장해 열어도 빈칸이 유지되며, 원가계산서 금액도 비웁니다. 현재 작업은 바뀌지 않습니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index f0a555e59..d00944f8e 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -42,6 +42,7 @@ + @@ -87,6 +88,8 @@ > +