diff --git a/resources/tester/test_est_native_export.py b/resources/tester/test_est_native_export.py index e7f985b49..e26cd6670 100644 --- a/resources/tester/test_est_native_export.py +++ b/resources/tester/test_est_native_export.py @@ -120,6 +120,31 @@ def test_estx_quantity_item_labels_round_trip_when_sample_exists(): } +def test_estx_quantity_description_preserves_mismatched_result_when_sample_exists(): + sample = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/main.estx") + if not sample.exists(): + pytest.skip("installed EST sample unavailable") + script = """const fs=require('fs'),m=require('./tools/est-test/import-estx.cjs'),e=require('./tools/est-test/export-estx.cjs'),w=require('./tools/est-test/export-quantity.cjs'); + const bytes=fs.readFileSync(process.argv[1]),p=m.importEstx(bytes),base=m.importEstx(bytes),sheet=p.quantitySheets.find(s=>s.Code==='N0000000083'); + sheet.rows[0].LineData=sheet.rows[0].LineData.replace('석축쌓기','석축공사'); + const q=m.importEstx(e.exportEstx(p)),saved=q.quantitySheets.find(s=>s.Code===sheet.Code); + sheet.rows[3].LineData=sheet.rows[3].LineData.replace('220','221');let rejected=false; + try{w.updateQuantities(p,base,e.cp949,e.memory)}catch{rejected=true} + sheet.rows[3].LineData=sheet.rows[3].LineData.replace('221','220'); + sheet.rows[5].LineData=sheet.rows[5].LineData.replace('286','287');let markerRejected=false; + try{w.updateQuantities(p,base,e.cp949,e.memory)}catch{markerRejected=true} + console.log(JSON.stringify({text:saved.rows[0].LineData,quantity:saved.items[4].GN, + preserved:JSON.stringify(saved.rows.slice(1))===JSON.stringify(base.quantitySheets.find(s=>s.Code===sheet.Code).rows.slice(1)),rejected,markerRejected}));""" + result = json.loads( + subprocess.check_output(["node", "-e", script, str(sample)], cwd=ROOT, timeout=45).decode( + "utf-8" + ) + ) + assert result["text"].startswith("석축공사") + assert result["quantity"] == 286 + assert result["preserved"] and result["rejected"] and result["markerRejected"] + + def test_quantity_document_edit_across_character_styles_preserves_tags(): script = r"""const q=require('./tools/est-test/native-quantity.cjs'),w=require('./tools/est-test/export-quantity.cjs'); const doc='
abcdefghi
'; diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 3ea3ffc55..4faf03265 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -1,4 +1,6 @@ ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] 계산 불일치 수량산출서의 계산값을 바꾸지 않는 문구 수정 저장 → 원본 수량·표지 보존과 수량 변경 차단 확인. + - 자체 검증: main.estx N0000000083 문구를 수정해 저장·재열기한 뒤 원래 수량 286과 다른 행 보존. 산식 계산값·결과 표지 변경 차단, 기존 산식 저장 시험 통과. - [x] 삭제된 Excel 연결 대상의 행 식별자 재사용 방지 → 새 행·붙여넣기·Excel 가져오기 오연결 차단. - 자체 검증: 삭제된 2번을 예약해 Edge 추가·붙여넣기가 3·4번을 사용. Excel 가져오기 오연결 차단·연결 제거 후 적용 확인. 관련 시험 35개 통과, openpyxl 경고 8건. - [x] Excel 수량 연결을 전체 자료 Excel에 포함 → 재가져오기와 잘못된 내역 연결의 전체 적용 차단 확인. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index e51ab2988..109728e24 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -61,6 +61,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요 원본 ESTX의 기존 수량산출 항목은 품명·규격을 수정해 ESTX에 저장할 수 있습니다. 연결 변경과 새 항목 추가·삭제는 계속 JSON으로 보관합니다. 품명·규격만 변경한 경우 기존 산식의 저장 수량과 재계산 수량이 다르더라도 산식과 저장 수량을 그대로 유지합니다. +산출서 문구를 수정해도 계산 결과와 결과 표지가 바뀌지 않으면 원래 저장 수량을 보존합니다. 원본 결과가 재계산값과 다른 문서에서 계산 결과를 바꾸는 편집은 JSON으로 보관해야 합니다. 재계산 메뉴의 수량 반영도 현재 선택한 산출서 또는 직접 작성 수식표를 사용합니다. 반영 전에 모든 연결 대상과 수식을 검사하며, 기존 원본 연결 데이터는 그대로 보관합니다. diff --git a/tools/est-test/export-quantity.cjs b/tools/est-test/export-quantity.cjs index c292e44d8..f5004612f 100644 --- a/tools/est-test/export-quantity.cjs +++ b/tools/est-test/export-quantity.cjs @@ -176,6 +176,31 @@ function updateQuantities(project, base, cp949, memory) { if (calculated.errors.length || baseline.errors.length) throw Error(`수량산출서 계산 오류: ${calculated.errors[0] || baseline.errors[0]}`); const markerCount = originals[i].rows.reduce((total, row) => total + row.markers.length, 0); + const results = (value) => ({ + checks: value.checks.map(({ name, calculated }) => ({ name, calculated })), + items: value.items.map(({ Code, GN }) => ({ Code, GN })), + }); + const resultMarkers = (sheet) => + sheet.rows.map((row) => [...row.LineData.matchAll(/\x12[^\x12]*\x12/g)].map((m) => m[0])); + if ( + baseline.checks.length === markerCount && + calculated.checks.length === markerCount && + baseline.checks.every((value) => Number.isFinite(value.calculated)) && + calculated.checks.every((value) => Number.isFinite(value.calculated)) && + same(resultMarkers(originals[i]), resultMarkers(sheets[i])) && + same(results(baseline), results(calculated)) + ) { + if (!same(row.MulList, oldRow.MulList)) throw Error("수량산출서 목록 데이터 충돌"); + if (!same(sheets[i].items, originals[i].items)) + row.MulList = { + binary: memory(Buffer.from(row.MulList.binary, "base64"), sheets[i].items).toString( + "base64", + ), + }; + const output = cp949(document); + row.Editor = { binary: (compressed ? zlib.deflateSync(output) : output).toString("base64") }; + continue; + } if ( baseline.checks.length !== markerCount || calculated.checks.length !== markerCount ||