feat(est): open backups and support remote creation

This commit is contained in:
2026-11-07 13:16:11 +09:00
parent 2bcb8bf9b9
commit 07a292a009
8 changed files with 44 additions and 8 deletions
+22
View File
@@ -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(
+4
View File
@@ -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] 표준시장단가 설계변경 내역·증감 화면·출력 구현 → 신규·삭제·연결 변경 시험.
+2
View File
@@ -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 기본 표 입출력과 브라우저 인쇄를 사용할 수 있습니다.
작업모드의 `자료 오류 검색`은 코드 중복, 연결 자료 누락, 연결된 품명·규격 차이와 계산 오류를 표시합니다. 항목의 `행 열기`로 해당 자료 위치로 이동할 수 있습니다. 품명·규격 차이는 사용자가 확인할 항목으로 표시합니다.
+1 -1
View File
@@ -179,7 +179,7 @@
<footer>
<span id="status">시작 중…</span><span>독립 저장 · ESTX 원본은 변경하지 않음</span>
</footer>
<input id="open-file" type="file" accept=".json,.estx" hidden />
<input id="open-file" type="file" accept=".json,.estx,.ebkx" hidden />
<dialog id="dialog">
<div class="dialog-head">
<b id="dialog-title"></b><button data-action="close-dialog" aria-label="닫기">×</button>
+1 -1
View File
@@ -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(
+10 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+3 -3
View File
@@ -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: "",