diff --git a/resources/tester/test_est_standalone.py b/resources/tester/test_est_standalone.py index 562d2b76b..08de7551a 100644 --- a/resources/tester/test_est_standalone.py +++ b/resources/tester/test_est_standalone.py @@ -6,6 +6,28 @@ ROOT = Path(__file__).resolve().parents[2] CALC = ROOT / "tools" / "est-test" / "calc.js" +def test_work_ids_do_not_require_secure_context_random_uuid(): + script = """const fs = require('fs'), vm = require('vm'); +const webcrypto = require('crypto').webcrypto; +const context = {crypto: {getRandomValues: a => webcrypto.getRandomValues(a)}, + document: {addEventListener() {}}}; +vm.runInNewContext(fs.readFileSync(process.argv[1], 'utf8') + + ';this.ids = [EstApp.uid(), EstApp.uid()]', context); +console.log(JSON.stringify(context.ids));""" + ids = json.loads( + subprocess.check_output( + ["node", "-e", script, str(ROOT / "tools" / "est-test" / "native-app.js")], + text=True, + encoding="utf-8", + timeout=10, + ) + ) + import uuid + + assert ids[0] != ids[1] + assert all(str(uuid.UUID(value, version=4)) == value for value in ids) + + def calculate(data): script = 'const vm=require("vm"), fs=require("fs"); const ctx={}; vm.runInNewContext(fs.readFileSync(process.argv[1],"utf8")+";this.calc=EstCalc",ctx); console.log(JSON.stringify(ctx.calc.calculate(JSON.parse(process.argv[2]))));' return json.loads( diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index c64ab7442..301aead26 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -1,4 +1,8 @@ ## Codex · EST 독립 테스트 앱 (2026-11-04) +- [x] Tailscale HTTP 접속의 새 항목·첨부·공정 생성 오류 수정 → 실제 원격 주소에서 생성과 저장 확인. + - 자체 검증: Tailscale HTTP Edge의 비보안 컨텍스트에서 UUID·공정·첨부 생성·JSON 저장 확인. 관련 시험 31개 통과. +- [x] EST 백업 EBKX 파일 열기 → 실무 백업 읽기·JSON 저장·재열기 확인. + - 자체 검증: 거창 실무 EBKX의 내역 106행을 Edge에서 열고 JSON 저장·재열기; 독립 서버 시험 9개 통과. 원본 EST에서의 백업 호환성은 미확인. - [x] Tailscale 주소에서 독립 테스트 서버 접속 → 접속과 계산 요청 확인. - 자체 검증: Tailscale IP에서 Edge 화면과 계산 POST 200, 다른 출처 403 확인. 다른 PC에서의 접속은 미확인. - [x] 표준시장단가 설계변경 내역·증감 화면·출력 구현 → 신규·삭제·연결 변경 시험. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index e39165a51..7c41ec5ae 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -2,6 +2,8 @@ `start.cmd`를 실행한 뒤 **http://127.0.0.1:5175** 로 접속합니다. Node.js가 필요하며 별도 패키지 설치는 없습니다. Aislo 서버·로그인·DB와 연결하지 않습니다. +파일 열기에서 ESTX 작업 파일과 EBKX 백업 파일을 읽을 수 있습니다. EBKX를 연 작업도 JSON으로 보관하며 원본 파일을 덮어쓰지 않습니다. + 내역·일위대가·수식 단가산출·기계경비·기초 단가·원가계산서를 원본 테이블 구조로 편집합니다. 수량 연결, 설계변경 당초 보존, 낙찰률, 기성 회차와 사정 누계, 품목조정, 연차 배분, 소요 자원 전개, 예정공정표 화면을 제공합니다. 실행 취소·다시 실행, 검색, 행 편집, JSON 저장·열기, CSV·Excel·조달청 XML·JDL 기본 표 입출력과 브라우저 인쇄를 사용할 수 있습니다. 작업모드의 `자료 오류 검색`은 코드 중복, 연결 자료 누락, 연결된 품명·규격 차이와 계산 오류를 표시합니다. 항목의 `행 열기`로 해당 자료 위치로 이동할 수 있습니다. 품명·규격 차이는 사용자가 확인할 항목으로 표시합니다. diff --git a/tools/est-test/index.html b/tools/est-test/index.html index c06b6cfef..5500d22bb 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -179,7 +179,7 @@ - +
diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index 8e7d501b5..c76d9184d 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -3,7 +3,7 @@ const EstActions = (() => { clipboard = [], clipboardTable = "", restoring = false; - const uid = () => globalThis.crypto.randomUUID(); + const uid = () => EstApp.uid(); function form(title, fields, callback) { const { esc, showDialog } = EstApp; showDialog( diff --git a/tools/est-test/native-app.js b/tools/est-test/native-app.js index 118ed07e0..dd37b5584 100644 --- a/tools/est-test/native-app.js +++ b/tools/est-test/native-app.js @@ -47,6 +47,13 @@ const EstApp = (() => { ); const number = (value) => new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 4 }).format(Number(value) || 0); + function uid() { + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 15) | 64; + bytes[8] = (bytes[8] & 63) | 128; + const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; + } const byId = (id) => document.getElementById(id); async function api(url, options = {}) { const response = await fetch(url, options), @@ -252,9 +259,9 @@ const EstApp = (() => { ) return; let project; - if (/\.estx$/i.test(file.name)) + if (/\.(estx|ebkx)$/i.test(file.name)) project = await api( - `/api/import?name=${encodeURIComponent(file.name.replace(/\.estx$/i, ""))}`, + `/api/import?name=${encodeURIComponent(file.name.replace(/\.(estx|ebkx)$/i, ""))}`, { method: "POST", body: await file.arrayBuffer() }, ); else project = JSON.parse(await file.text()); @@ -287,6 +294,7 @@ const EstApp = (() => { titles, esc, number, + uid, byId, api, cache, diff --git a/tools/est-test/native-attachments.js b/tools/est-test/native-attachments.js index e28b393d7..7f90aadc8 100644 --- a/tools/est-test/native-attachments.js +++ b/tools/est-test/native-attachments.js @@ -11,7 +11,7 @@ const EstAttachments = (() => { reader.onerror = () => reject(Error(`파일 읽기 실패: ${file.name}`)); reader.onload = () => resolve({ - id: crypto.randomUUID(), + id: EstApp.uid(), name: file.name, type: file.type || "", size: file.size, diff --git a/tools/est-test/native-schedule.js b/tools/est-test/native-schedule.js index aff4a7731..80d75ea7c 100644 --- a/tools/est-test/native-schedule.js +++ b/tools/est-test/native-schedule.js @@ -151,7 +151,7 @@ const EstSchedule = (() => { ) { try { if (node.dataset.action === "schedule-import") { - const added = fromEstimate(p, () => crypto.randomUUID()); + const added = fromEstimate(p, EstApp.uid); EstApp.change(() => (p.schedule ||= []).push(...added), false); } else { const rows = @@ -166,8 +166,8 @@ const EstSchedule = (() => { } if (node.dataset.action === "schedule-add") { try { - const row = fromEstimate(p, () => crypto.randomUUID())[0] || { - id: crypto.randomUUID(), + const row = fromEstimate(p, EstApp.uid)[0] || { + id: EstApp.uid(), Code: "", Name: "새 공정", start: "",