feat(est): open backups and support remote creation
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,6 +2,8 @@
|
||||
|
||||
`start.cmd`를 실행한 뒤 **http://127.0.0.1:5175** 로 접속합니다. Node.js가 필요하며 별도 패키지 설치는 없습니다. Aislo 서버·로그인·DB와 연결하지 않습니다.
|
||||
|
||||
파일 열기에서 ESTX 작업 파일과 EBKX 백업 파일을 읽을 수 있습니다. EBKX를 연 작업도 JSON으로 보관하며 원본 파일을 덮어쓰지 않습니다.
|
||||
|
||||
내역·일위대가·수식 단가산출·기계경비·기초 단가·원가계산서를 원본 테이블 구조로 편집합니다. 수량 연결, 설계변경 당초 보존, 낙찰률, 기성 회차와 사정 누계, 품목조정, 연차 배분, 소요 자원 전개, 예정공정표 화면을 제공합니다. 실행 취소·다시 실행, 검색, 행 편집, JSON 저장·열기, CSV·Excel·조달청 XML·JDL 기본 표 입출력과 브라우저 인쇄를 사용할 수 있습니다.
|
||||
|
||||
작업모드의 `자료 오류 검색`은 코드 중복, 연결 자료 누락, 연결된 품명·규격 차이와 계산 오류를 표시합니다. 항목의 `행 열기`로 해당 자료 위치로 이동할 수 있습니다. 품명·규격 차이는 사용자가 확인할 항목으로 표시합니다.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
Reference in New Issue
Block a user