diff --git a/resources/tester/helper_est_native.cjs b/resources/tester/helper_est_native.cjs index e471b9f79..5165e54c7 100644 --- a/resources/tester/helper_est_native.cjs +++ b/resources/tester/helper_est_native.cjs @@ -2,7 +2,7 @@ const fs = require("node:fs"); const path = require("node:path"); const vm = require("node:vm"); const base = path.resolve(__dirname, "../../tools/est-test"); -const context = vm.createContext({ structuredClone, console }); +const context = vm.createContext({ structuredClone, console, Blob }); for (const file of [ "native-math.js", "native-formula.js", @@ -19,6 +19,7 @@ for (const file of [ "native-resources.js", "native-schedule.js", "native-issues.js", + "native-attachments.js", "native-model.js", "native-c3r.js", "native-exchange.js", diff --git a/resources/tester/test_est_attachments.py b/resources/tester/test_est_attachments.py index 4e4f2d50b..90ee60318 100644 --- a/resources/tester/test_est_attachments.py +++ b/resources/tester/test_est_attachments.py @@ -3,6 +3,21 @@ import subprocess from pathlib import Path +def test_attachment_size_prediction_includes_utf8_and_base64_overhead(): + result = run( + "math", + """(()=>{ + const p=EstNative.empty();p.name='한글';const file={name:'파일.bin',type:'',size:4}; + const expected={...p,id:'0'.repeat(24),attachments:[{id:'0'.repeat(36),...file,data:'AAECAw=='}]}; + return {predicted:EstAttachments.requiredBytes(p,[file]),actual:new Blob([JSON.stringify(expected)]).size, + large:EstAttachments.requiredBytes(p,[{name:'large',type:'',size:210000000}]),count:p.attachments.length}; + })()""", + ) + assert result["predicted"] == result["actual"] + assert result["large"] > 256 * 1024 * 1024 + assert result["count"] == 0 + + def test_attachment_validation_keeps_valid_data_and_rejects_malformed_content(): result = run( "math", diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 955e2547a..0fb27f7ae 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -1,4 +1,7 @@ ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] 첨부파일 추가 전 256MB 저장 한도 확인 → 대형 파일 차단·기존 작업 보존 시험. + - 자체 검증: UTF-8·base64 실제 저장 크기 일치·한도 계산 포함 4개 통과. Edge에서 초과 파일 읽기 전 차단·안내 확인. + - [x] 전체 수량 반영 시 중복·예약 산출서 코드 차단 → 중복 합산 방지 시험. - 자체 검증: 중복·빈·예약 코드 차단과 기존 수량 보존 포함 시험 19개 통과. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 11bdd59e5..9a0cd43e3 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -18,7 +18,7 @@ 저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. JSON은 모든 웹 업무와 원본 압축 파일·바이너리를 보관하는 전체 저장 형식입니다. ESTX 수정 저장은 아래 지원 조건을 따릅니다. -작업모드의 `첨부파일`에서 파일을 추가·내려받기·삭제할 수 있습니다. 파일은 작업 JSON에 포함되며 이 앱의 저장 요청은 전체 256MB 이하입니다. 첨부파일이 있는 작업은 JSON으로 저장해야 하며, 첨부파일이 빠진 ESTX가 만들어지지 않도록 ESTX 저장을 차단합니다. +작업모드의 `첨부파일`에서 파일을 추가·내려받기·삭제할 수 있습니다. 파일은 작업 JSON에 포함되며 이 앱의 저장 요청은 전체 256MB 이하입니다. 제한을 초과하는 파일은 읽기 전에 추가를 차단합니다. 첨부파일이 있는 작업은 JSON으로 저장해야 하며, 첨부파일이 빠진 ESTX가 만들어지지 않도록 ESTX 저장을 차단합니다. ## 추가 입출력·문서 기능 (2026-11-06) diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index c75da4923..acc3359b9 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -485,7 +485,6 @@ const EstActions = (() => { EstExchange.install(); EstBusinessActions.install(); EstIssues.install(); - EstAttachments.install(); document.addEventListener("click", async (event) => { try { const marker = event.target.closest("[data-marker]"); @@ -677,6 +676,7 @@ const EstActions = (() => { } } }); + EstAttachments.install(); } return { install, form }; })(); diff --git a/tools/est-test/native-attachments.js b/tools/est-test/native-attachments.js index fce488c7c..e28b393d7 100644 --- a/tools/est-test/native-attachments.js +++ b/tools/est-test/native-attachments.js @@ -20,6 +20,24 @@ const EstAttachments = (() => { reader.readAsDataURL(file); }); } + function requiredBytes(project, files) { + const metadata = files.map((file) => ({ + id: "0".repeat(36), + name: file.name, + type: file.type || "", + size: file.size, + data: "", + })); + const candidate = { + ...project, + id: project.id || "0".repeat(24), + attachments: [...(project.attachments || []), ...metadata], + }; + return ( + new Blob([JSON.stringify(candidate)]).size + + files.reduce((sum, file) => sum + 4 * Math.ceil(file.size / 3), 0) + ); + } function install() { document.addEventListener("change", async (event) => { const input = event.target; @@ -28,7 +46,10 @@ const EstAttachments = (() => { key = state.documentKey, revision = state.revision; try { - const files = await Promise.all([...input.files].map(read)); + const selected = [...input.files]; + if (requiredBytes(state.project, selected) > 256 * 1024 * 1024) + throw Error("첨부파일을 포함한 작업이 저장 한도 256MB를 초과합니다."); + const files = await Promise.all(selected.map(read)); if (key !== state.documentKey || revision !== state.revision) throw Error("작업이 변경되었습니다. 첨부파일을 다시 추가하세요."); EstApp.change(() => (state.project.attachments ||= []).push(...files), false); @@ -57,5 +78,5 @@ const EstAttachments = (() => { } }); } - return { render, install }; + return { render, install, requiredBytes }; })();