diff --git a/resources/tester/test_est_exchange.py b/resources/tester/test_est_exchange.py index d2df60622..14b349718 100644 --- a/resources/tester/test_est_exchange.py +++ b/resources/tester/test_est_exchange.py @@ -236,6 +236,23 @@ def test_excel_quantity_links_export_and_import_reject_invalid_targets(): assert result == {"links": [link], "rejected": 3, "original": 1} +def test_excel_import_does_not_reuse_deleted_excel_link_target(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A',GN:1}]; + const link={target:2,file:'source.xlsx',sheet:'Data',cell:'B2'};p.excelQuantityLinks=[link]; + const rows=[{PkID:1,Code:'A',GN:2},{PkID:2,Code:'B',GN:5}];let rejected=0; + for(const data of [{EstMainDat:rows},{EstMainDat:rows,ExcelQuantityLinks:[link]}]){ + try{EstExchange.apply(p,data)}catch{rejected++} + } + const cleared=EstExchange.apply(p,{EstMainDat:rows,ExcelQuantityLinks:[]}); + return {rejected,original:p.tables.EstMainDat[0].GN,cleared:cleared.excelQuantityLinks.length}; + })()""", + ) + assert result == {"rejected": 2, "original": 1, "cleared": 0} + + def test_excel_imports_manual_quantities_and_schedule_and_rejects_invalid(): result = run( "math", diff --git a/resources/tester/test_est_quantity_workflow.py b/resources/tester/test_est_quantity_workflow.py index 78cb2c685..3ababc872 100644 --- a/resources/tester/test_est_quantity_workflow.py +++ b/resources/tester/test_est_quantity_workflow.py @@ -43,6 +43,20 @@ def test_excel_quantity_links_validate_and_survive_project_validation(): assert result["rejected"] == 5 +def test_deleted_excel_link_target_id_is_reserved_for_new_estimate_rows(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.tables.EstMainDat=[{PkID:1,Code:'A'}]; + p.excelQuantityLinks=[{target:2,file:'a.xlsx',sheet:'Data',cell:'B2'}]; + const row=EstModel.newRow('EstMainDat',p.tables.EstMainDat,p); + const other=EstModel.newRow('StuffcDat',[],p); + return {next:row.PkID,other:other.PkID}; + })()""", + ) + assert result == {"next": 3, "other": 1} + + def test_apply_all_quantities_sums_sheets_and_rejects_errors_atomically(): result = run( "math", diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index bb27d1045..3ea3ffc55 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -1,4 +1,6 @@ ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] 삭제된 Excel 연결 대상의 행 식별자 재사용 방지 → 새 행·붙여넣기·Excel 가져오기 오연결 차단. + - 자체 검증: 삭제된 2번을 예약해 Edge 추가·붙여넣기가 3·4번을 사용. Excel 가져오기 오연결 차단·연결 제거 후 적용 확인. 관련 시험 35개 통과, openpyxl 경고 8건. - [x] Excel 수량 연결을 전체 자료 Excel에 포함 → 재가져오기와 잘못된 내역 연결의 전체 적용 차단 확인. - 자체 검증: Tailscale HTTP Edge에서 실제 Excel 내보내기·재가져오기와 잘못된 내역 연결 전체 차단 확인. 수량·Excel 시험 25개 통과, openpyxl 경고 8건. - [x] Excel 셀과 내역 행 연결을 JSON에 보관하고 다시 선택한 파일로 일괄 갱신 → 중복·누락·오류 시 전체 변경 차단. diff --git a/tools/est-test/native-exchange.js b/tools/est-test/native-exchange.js index 49f21ab38..5a63e5078 100644 --- a/tools/est-test/native-exchange.js +++ b/tools/est-test/native-exchange.js @@ -466,6 +466,14 @@ const EstExchange = (() => { throw Error(`Excel 연결 내역이 없거나 중복되었거나 상세 행이 아닙니다: ${link.target}`); } } + if (Object.hasOwn(tables, "EstMainDat")) + for (const link of project.excelQuantityLinks || []) + if ( + (next.excelQuantityLinks || []).some((item) => item.target === link.target) && + !project.tables.EstMainDat.some((row) => row.PkID === link.target) && + next.tables.EstMainDat.some((row) => row.PkID === link.target) + ) + throw Error(`삭제된 Excel 연결 내역의 식별자를 다시 사용할 수 없습니다: ${link.target}`); if (Object.hasOwn(tables, "EscalationSettings")) { const rows = tables.EscalationSettings; if ( diff --git a/tools/est-test/native-model.js b/tools/est-test/native-model.js index 5d7423456..f2831b502 100644 --- a/tools/est-test/native-model.js +++ b/tools/est-test/native-model.js @@ -626,11 +626,13 @@ const EstModel = (() => { if (table === "EstMainDat") for (const round of project?.progress || []) historical.push(round.contract?.rows || [], round.result?.rows || []); - const max = historical.reduce( + let max = historical.reduce( (maximum, list) => list.reduce((value, row) => Math.max(value, Number(row.PkID) || 0), maximum), 0, ); + if (table === "EstMainDat") + for (const link of project?.excelQuantityLinks || []) max = Math.max(max, link.target); const row = { PkID: max + 1, Index: 0,