diff --git a/resources/tester/test_est_attachments.py b/resources/tester/test_est_attachments.py new file mode 100644 index 000000000..4e4f2d50b --- /dev/null +++ b/resources/tester/test_est_attachments.py @@ -0,0 +1,34 @@ +from test_est_rea_parity import run +import subprocess +from pathlib import Path + + +def test_attachment_validation_keeps_valid_data_and_rejects_malformed_content(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.attachments=[{id:'one',name:'test.bin',type:'application/octet-stream',size:4,data:'AAECAw=='}]; + const valid=EstModel.validate(p).attachments[0];let rejected=0; + for(const item of [{...valid,data:'AAECAw=A'},{...valid,size:5},{...valid,id:''},{...valid,name:''}]) { + const q=EstNative.empty();q.attachments=[item];try{EstModel.validate(q)}catch{rejected++} + } + const dup=EstNative.empty();dup.attachments=[valid,valid];try{EstModel.validate(dup)}catch{rejected++} + const empty=EstNative.empty();empty.attachments=[{id:'zero',name:'empty',type:'',size:0,data:''}]; + return {valid,rejected,empty:EstModel.validate(empty).attachments[0].size}; + })()""", + ) + assert result["valid"]["data"] == "AAECAw==" + assert result["rejected"] == 5 + assert result["empty"] == 0 + + +def test_estx_export_rejects_web_attachments_instead_of_dropping_them(): + script = """try{require('./tools/est-test/export-estx.cjs').exportEstx({originalArchive:'AA==',attachments:[{}]});process.exit(1)}catch(e){process.stdout.write(e.message)}""" + result = subprocess.run( + ["node", "-e", script], + cwd=Path(__file__).resolve().parents[2], + capture_output=True, + check=True, + timeout=10, + ) + assert "JSON" in result.stdout.decode("utf-8") diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 8fe4b1262..21cc30870 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -1,4 +1,7 @@ ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] 로컬 첨부파일 추가·내려받기·삭제·JSON 보관 구현 → 파일 바이트·저장·취소 시험. + - 자체 검증: 입력 검증·ESTX 누락 차단 시험 2개 통과, Edge에서 파일 추가·JSON 저장·재열기·바이트 내려받기·삭제 취소 확인. 업무 회귀 시험 82개 통과(openpyxl 경고 7건). + - [x] 선택 행 자릿수·사사오입·절사·올림식 일괄 설정 → 선택 범위·재계산·취소 시험. - 자체 검증: 업무·수량 시험 21개 통과, Edge에서 두 행 절사 2자리 설정·다른 행 유지·재계산·실행 취소 확인. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 010b8421f..11ac6f6a0 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -16,6 +16,8 @@ 저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. JSON은 모든 웹 업무와 원본 압축 파일·바이너리를 보관하는 전체 저장 형식입니다. ESTX 수정 저장은 아래 지원 조건을 따릅니다. +작업모드의 `첨부파일`에서 파일을 추가·내려받기·삭제할 수 있습니다. 파일은 작업 JSON에 포함되며 이 앱의 저장 요청은 전체 256MB 이하입니다. 첨부파일이 있는 작업은 JSON으로 저장해야 하며, 첨부파일이 빠진 ESTX가 만들어지지 않도록 ESTX 저장을 차단합니다. + ## 추가 입출력·문서 기능 (2026-11-06) - **Excel `.xlsx`**: CAD를 제외한 전체 표의 스칼라 필드, 직접 작성 수식표·예정공정표·계약심사 입력·연차 수식·미확정 기성과 수량산출서의 문서 정보·산출 행·항목을 내보내고 다시 읽어 해당 자료를 교체합니다. 수량산출서는 `QDocuments`, `QLines`, `QItems`, 미확정 기성은 `ProgressDrafts`, `ProgressInputs` 시트를 함께 가져옵니다. 확정 기성의 계약 자료와 누계는 Excel 가져오기 시 유지합니다. 수식표·수량산출서·공정표의 내역 행 연결도 보존합니다. 기존 PkID의 바이너리는 보존하며 코드 변경·중복과 미계산 수식/오류 셀을 거부합니다. 산출근거의 내부 표지는 Excel이 열 수 있는 형식으로 바꿔 기록하고 다시 읽을 때 복원합니다. 출처 단가 수정은 연결 내역에 전파합니다. 파일에 바이너리·원본 글자 서식·확정 기성 자료·당초 사본은 넣지 않으므로 전체 백업은 JSON을 사용합니다. diff --git a/tools/est-test/export-estx.cjs b/tools/est-test/export-estx.cjs index 4b994e233..26f49c2dc 100644 --- a/tools/est-test/export-estx.cjs +++ b/tools/est-test/export-estx.cjs @@ -135,6 +135,8 @@ function updateBlob(file, start, id, bytes) { function exportEstx(project) { if (!project.originalArchive) throw Error("원본 ESTX를 가져온 작업에서만 ESTX 수정 저장이 가능합니다."); + if (project.attachments?.length) + throw Error("웹 첨부파일은 ESTX에 저장할 수 없습니다. JSON 저장을 사용하세요."); const original = Buffer.from(project.originalArchive, "base64"), base = importEstx(original), files = unzip(original); diff --git a/tools/est-test/index.html b/tools/est-test/index.html index 47c9f6c03..9d1231275 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -20,6 +20,7 @@ + @@ -118,6 +119,7 @@ > + diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index 483f16711..c75da4923 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -485,6 +485,7 @@ const EstActions = (() => { EstExchange.install(); EstBusinessActions.install(); EstIssues.install(); + EstAttachments.install(); document.addEventListener("click", async (event) => { try { const marker = event.target.closest("[data-marker]"); diff --git a/tools/est-test/native-app.js b/tools/est-test/native-app.js index 23cfd2541..098bf79ad 100644 --- a/tools/est-test/native-app.js +++ b/tools/est-test/native-app.js @@ -35,6 +35,7 @@ const EstApp = (() => { resources: "동원 집계", schedule: "예정공정표", issues: "자료 오류 검색", + attachments: "첨부파일", drawing: "도면·문서", }; const esc = (value) => diff --git a/tools/est-test/native-attachments.js b/tools/est-test/native-attachments.js new file mode 100644 index 000000000..fce488c7c --- /dev/null +++ b/tools/est-test/native-attachments.js @@ -0,0 +1,61 @@ +const EstAttachments = (() => { + function render() { + const { state, esc, number, byId } = EstApp; + const rows = state.project.attachments || []; + byId("work-content").innerHTML = + `
첨부파일은 이 작업의 JSON 저장에 포함됩니다.
| 파일명 | 크기 (바이트) | 작업 |
|---|---|---|
| ${esc(row.name)} | ${number(row.size)} |