feat(view): project save/open, Ctrl+S, save status, IndexedDB auto-backup (plan B2–B5)

- 저장 (Ctrl+S) downloads <교량명>_교각설계.pzr.json; 프로젝트 열기 reads it
  back and regenerates every pier without the source workbook.
- Save status in the header: 저장 전 / 변경 있음 · 미저장 / 저장됨. The
  snapshot excludes the history so a save is not counted as a change.
- Auto-backup to IndexedDB 0.8 s after changes while unsaved; on start a banner
  offers recovery or discard. Storage failures are reported, not fatal.
- beforeunload warns while there are unsaved changes.
- B1 (default-vs-changed tracking) is not added yet: inputs are not editable
  until the 2단계 input cards exist.
- Not verified in a browser (no debuggable Chrome in this session).

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-22 09:44:49 +09:00
1 parent 5ce6d0e2f1
commit f5303e4c2b
3 files changed
+205

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
/**
* Browser auto-backup in IndexedDB (spec §8, plan B4). Only the latest project text is kept.
* Every call can fail (private window, blocked storage); callers catch and show the failure.
*/
const DB_NAME = 'pierzainer';
const STORE = 'backup';
const KEY = 'current';
export interface Backup {
text: string;
at: string;
}
function openDb(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
function run<T>(mode: IDBTransactionMode, action: (store: IDBObjectStore) => IDBRequest<T>): Promise<T> {
return openDb().then(
(db) =>
new Promise<T>((resolve, reject) => {
const tx = db.transaction(STORE, mode);
const req = action(tx.objectStore(STORE));
tx.oncomplete = () => {
db.close();
resolve(req.result);
};
tx.onerror = () => {
db.close();
reject(tx.error);
};
}),
);
}
export function writeBackup(text: string): Promise<unknown> {
const value: Backup = { text, at: new Date().toISOString() };
return run('readwrite', (store) => store.put(value, KEY));
}
export async function readBackup(): Promise<Backup | null> {
const value = await run<Backup | undefined>('readonly', (store) => store.get(KEY));
return value ?? null;
}
export function clearBackup(): Promise<unknown> {
return run('readwrite', (store) => store.delete(KEY));
}