fix(est): preserve active edits during queued saves

This commit is contained in:
2026-11-05 13:53:12 +09:00
parent 9a4070fae5
commit 9f9151f56d
3 changed files with 48 additions and 14 deletions
+2
View File
@@ -8,6 +8,8 @@
저장은 이 폴더의 `data/`에 작업을 보관하고 `.estweb.json` 파일도 내려받습니다. 브라우저 임시 저장은 같은 탭에서 복원하는 용도입니다. ESTX는 읽기 전용으로 가져오며 원본 압축 파일과 바이너리를 보존합니다. 편집 결과를 원본 ESTX로 다시 쓰는 기능은 없습니다.
Ctrl+S는 입력 중인 셀도 확정한 뒤 저장합니다. 연속 저장 요청은 순서대로 처리하며, 저장 대기 중의 추가 입력이나 새로 연 문서를 이전 저장 응답으로 덮어쓰지 않습니다. 실행 취소는 현재 작업 파일의 연결을 유지합니다.
초기 자료는 기술정보 DB의 실제 REA 저장 샘플입니다. `REA 샘플 대조`에서 원본 저장값과 275개 계산 결과를 비교합니다. 테스트용 임의 요율을 원본 기본값으로 넣지 않습니다. 새 원가 서식의 요율은 사용자가 입력합니다.
## 검증과 남은 범위
+3
View File
@@ -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();
+43 -14
View File
@@ -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;