diff --git a/resources/tester/test_est_exchange.py b/resources/tester/test_est_exchange.py index fcdd1158a..d2df60622 100644 --- a/resources/tester/test_est_exchange.py +++ b/resources/tester/test_est_exchange.py @@ -213,6 +213,29 @@ def test_excel_exports_manual_quantities_and_schedule(): ) +def test_excel_quantity_links_export_and_import_reject_invalid_targets(): + import openpyxl + + link = {"target": 5, "file": "source.xlsx", "sheet": "Data", "cell": "B2"} + data = export_workbook({"name": "project", "tables": {}, "excelQuantityLinks": [link]}) + workbook = openpyxl.load_workbook(io.BytesIO(data), data_only=False) + assert list(workbook["ExcelQuantityLinks"].values)[1] == (5, "source.xlsx", "Data", "B2") + workbook.close() + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.tables.EstMainDat=[{PkID:5,Code:'X',GN:1}]; + const link={target:5,file:'source.xlsx',sheet:'Data',cell:'B2'}; + const q=EstExchange.apply(p,{ExcelQuantityLinks:[link]});let rejected=0; + for(const rows of [[{...link,target:9}],[link,link],[{...link,cell:'XFE1'}]]){ + try{EstExchange.apply(p,{ExcelQuantityLinks:rows,EstMainDat:[{PkID:5,Code:'X',GN:7}]})}catch{rejected++} + } + return {links:q.excelQuantityLinks,rejected,original:p.tables.EstMainDat[0].GN}; + })()""", + ) + assert result == {"links": [link], "rejected": 3, "original": 1} + + def test_excel_imports_manual_quantities_and_schedule_and_rejects_invalid(): result = run( "math", diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 739296c6f..bb27d1045 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에 포함 → 재가져오기와 잘못된 내역 연결의 전체 적용 차단 확인. + - 자체 검증: Tailscale HTTP Edge에서 실제 Excel 내보내기·재가져오기와 잘못된 내역 연결 전체 차단 확인. 수량·Excel 시험 25개 통과, openpyxl 경고 8건. - [x] Excel 셀과 내역 행 연결을 JSON에 보관하고 다시 선택한 파일로 일괄 갱신 → 중복·누락·오류 시 전체 변경 차단. - 자체 검증: Tailscale HTTP Edge에서 2행 연결·JSON 재열기·일괄 갱신·실행 취소·연결 해제 확인. 한 셀 오류 시 두 행 모두 유지, ESTX 저장에서 연결 누락 차단. 수량·첨부·독립 서버 시험 25개 통과. - [x] Excel 숫자 셀을 선택한 내역 행의 수량으로 반영 → 파일·시트·셀 오류 차단, 계산·저장·취소 확인. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 2252c4717..e51ab2988 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -55,6 +55,7 @@ Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요 수량산출서와 수식표에서 내역 행을 직접 선택해 연결할 수 있습니다. 내역 행 선택을 사용하면 코드가 없거나 중복되어도 해당 행에만 수량을 반영합니다. 코드를 직접 입력할 때는 해당 상세 내역이 정확히 하나여야 하며, 대상이 없거나 중복되면 다른 항목을 포함해 반영을 중단하고 오류를 표시합니다. 수량 화면의 `Excel 셀 수량 연결`은 `.xlsx` 파일과 시트·셀·내역 행을 연결하고 저장된 숫자 값을 반영합니다. 연결 정보는 작업 JSON에 저장되며, `Excel 연결 관리`에서 같은 이름의 파일을 다시 선택해 해당 파일의 연결을 일괄 갱신하거나 연결을 해제할 수 있습니다. 어느 연결에 오류가 있어도 갱신 전체를 중단합니다. 수식 셀은 파일에 저장된 계산값을 사용하며, 저장된 값이 없거나 숫자가 아니면 반영하지 않습니다. Excel 파일은 자동 감시하지 않습니다. 웹 Excel 연결은 원본 ESTX에 저장할 수 없어 JSON으로 보관합니다. +전체 자료 Excel의 `ExcelQuantityLinks` 시트에도 연결 정보가 포함됩니다. 연결된 원본 Excel 파일은 별도로 보관해야 합니다. 수량 화면에서 새 산출서를 만들고 이름과 산출 항목의 품명·규격을 편집할 수 있습니다. `산출 행 추가`로 결과 표지가 연결된 행과 항목을 함께 만든 뒤 수식을 입력하고, 연결 내역코드를 지정해 계산 수량을 내역에 반영합니다. 행 아래 삽입, 위·아래 이동과 산출 행·산출서 삭제도 가능하며, 이 구조 변경은 JSON 저장과 다시 열기를 지원합니다. 기존 연결은 내역코드를 비워 해제할 수 있습니다. diff --git a/tools/est-test/exchange.cjs b/tools/est-test/exchange.cjs index 4f4b2e7f1..8969f62d7 100644 --- a/tools/est-test/exchange.cjs +++ b/tools/est-test/exchange.cjs @@ -95,6 +95,7 @@ function workbook(project) { project.schedule, ["id", "SourceKey", "Code", "Name", "start", "end", "weight", "progress"], ], + ["ExcelQuantityLinks", project.excelQuantityLinks, ["target", "file", "sheet", "cell"]], ]) if (Array.isArray(rows) && rows.length) sheets.push({ diff --git a/tools/est-test/native-exchange.js b/tools/est-test/native-exchange.js index 8cdd610de..49f21ab38 100644 --- a/tools/est-test/native-exchange.js +++ b/tools/est-test/native-exchange.js @@ -38,6 +38,7 @@ const EstExchange = (() => { ![ "Quantities", "Schedule", + "ExcelQuantityLinks", "QDocuments", "QLines", "QItems", @@ -289,6 +290,7 @@ const EstExchange = (() => { "ProgressInputs", "Escalation", "EscalationSettings", + "ExcelQuantityLinks", ].includes(table) ) continue; @@ -450,6 +452,20 @@ const EstExchange = (() => { } } } + if (Object.hasOwn(tables, "ExcelQuantityLinks")) { + next.excelQuantityLinks = tables.ExcelQuantityLinks.map((row) => ({ + target: row.target, + file: row.file, + sheet: row.sheet, + cell: row.cell, + })); + EstModel.validate(next); + for (const link of next.excelQuantityLinks) { + const rows = next.tables.EstMainDat.filter((row) => row.PkID === link.target); + if (rows.length !== 1 || !EstQuantity.excelTarget(rows[0])) + throw Error(`Excel 연결 내역이 없거나 중복되었거나 상세 행이 아닙니다: ${link.target}`); + } + } if (Object.hasOwn(tables, "EscalationSettings")) { const rows = tables.EscalationSettings; if ( diff --git a/tools/est-test/native-quantity.cjs b/tools/est-test/native-quantity.cjs index e4e2f65d3..74c80bbe5 100644 --- a/tools/est-test/native-quantity.cjs +++ b/tools/est-test/native-quantity.cjs @@ -535,6 +535,7 @@ const EstQuantity = (() => { allTotals, applyTotals, excelCell, + excelTarget, validExcelAddress, excelUpdates, targetSelect,