From 9f9151f56da0c605b3ad46ffa1e0f9ca4550b0c6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 5 Nov 2026 13:53:12 +0900 Subject: [PATCH] fix(est): preserve active edits during queued saves --- tools/est-test/README.md | 2 ++ tools/est-test/native-actions.js | 3 ++ tools/est-test/native-app.js | 57 ++++++++++++++++++++++++-------- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 49a01dec0..c47499ae0 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -8,6 +8,8 @@ 저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. ESTX는 읽기 전용으로 가져오며 원본 압축 파일과 바이너리를 보존합니다. 편집 결과를 원본 ESTX로 다시 쓰는 기능은 없습니다. +Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요청은 순서대로 처리하며, 저장 대기 중의 추가 입력이나 새로 연 문서를 이전 저장 응답으로 덮어쓰지 않습니다. 실행 취소는 현재 작업 파일의 연결을 유지합니다. + 초기 자료는 기술정보 DB의 실제 REA 저장 샘플입니다. `REA 샘플 대조`에서 원본 저장값과 275개 계산 결과를 비교합니다. 테스트용 임의 요율을 원본 기본값으로 넣지 않습니다. 새 원가 서식의 요율은 사용자가 입력합니다. ## 검증과 남은 범위 diff --git a/tools/est-test/native-actions.js b/tools/est-test/native-actions.js index 3fa229736..f92a60efb 100644 --- a/tools/est-test/native-actions.js +++ b/tools/est-test/native-actions.js @@ -33,7 +33,10 @@ const EstActions = (() => { target = redo ? state.history : state.future; if (!source.length) return; target.push(JSON.stringify(state.project)); + const id = state.project.id; state.project = JSON.parse(source.pop()); + if (id) state.project.id = id; + state.revision++; state.selection.clear(); EstApp.cache(); EstApp.render(); diff --git a/tools/est-test/native-app.js b/tools/est-test/native-app.js index 1e53ba50b..33b1d11a4 100644 --- a/tools/est-test/native-app.js +++ b/tools/est-test/native-app.js @@ -11,7 +11,10 @@ const EstApp = (() => { future: [], notice: "", business: null, + revision: 0, + documentKey: 0, }; + let saveQueue = Promise.resolve(); const titles = { EstMainDat: "내역서", HeadMainDat: "일위대가", @@ -45,14 +48,15 @@ const EstApp = (() => { if (!response.ok) throw Error(data.error || `요청 실패: ${response.status}`); return data; } - function cache() { + function cache(serialized) { try { - sessionStorage.setItem(CACHE, JSON.stringify(state.project)); + sessionStorage.setItem(CACHE, serialized ?? JSON.stringify(state.project)); } catch { state.notice = "임시 저장 용량이 부족합니다. 저장 버튼으로 작업을 보관하세요."; } } function remember() { + state.revision++; const serialized = JSON.stringify(state.project); if (serialized.length < 10 * 1024 * 1024) { state.history.push(serialized); @@ -61,6 +65,7 @@ const EstApp = (() => { state.future = []; } function calculate() { + state.revision++; try { state.project = EstNative.calculate(EstModel.validate(state.project)); } catch (error) { @@ -76,6 +81,8 @@ const EstApp = (() => { } function setProject(project) { state.project = EstModel.legacy(project); + state.documentKey++; + state.revision++; state.history = []; state.future = []; state.selection.clear(); @@ -180,18 +187,40 @@ const EstApp = (() => { a.click(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); } - async function save(asNew = false) { - if (asNew) delete state.project.id; - const result = await api("/api/save", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(state.project), - }); - state.project = result; - cache(); - state.notice = "저장했습니다. 이 앱 폴더의 data에도 작업이 보관됩니다."; - render(); - download(`${state.project.name || "내역서"}.estweb.json`, JSON.stringify(result, null, 2)); + function save(asNew = false) { + document.activeElement?.blur?.(); + if (asNew) { + delete state.project.id; + state.documentKey++; + } + state.project.id ||= [...crypto.getRandomValues(new Uint8Array(12))] + .map((value) => value.toString(16).padStart(2, "0")) + .join(""); + const documentKey = state.documentKey, + revision = state.revision, + body = JSON.stringify(state.project); + cache(body); + const operation = saveQueue + .catch(() => {}) + .then(async () => { + const result = await api("/api/save", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + }); + if (state.documentKey === documentKey) { + if (state.revision === revision) state.project = result; + cache(); + state.notice = + state.revision === revision + ? "저장했습니다. 이 앱 폴더의 data에도 작업이 보관됩니다." + : "요청 시점의 작업을 저장했습니다. 이후 입력은 다시 저장하세요."; + render(); + } + download(`${result.name || "내역서"}.estweb.json`, JSON.stringify(result, null, 2)); + }); + saveQueue = operation; + return operation; } async function open(file) { if (!file) return;