SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,69 @@
|
||||
// 자동 백업: versioned INP (+ asset attributes) copies of the model kept in a
|
||||
// separate IndexedDB, so a bad edit hours ago can still be undone after the
|
||||
// autosave (which keeps only the latest state) has overwritten it.
|
||||
import type { AssetStore } from './assets';
|
||||
|
||||
const DB_NAME = 'spipe-backups';
|
||||
const STORE = 'backups';
|
||||
export const MAX_BACKUPS = 30;
|
||||
|
||||
export interface BackupRecord {
|
||||
id: string;
|
||||
savedAt: string;
|
||||
fileName: string;
|
||||
reason: 'auto' | 'manual';
|
||||
inp: string;
|
||||
assets?: AssetStore;
|
||||
nodes: number;
|
||||
links: number;
|
||||
}
|
||||
|
||||
function open(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, 1);
|
||||
req.onupgradeneeded = () => {
|
||||
if (!req.result.objectStoreNames.contains(STORE)) req.result.createObjectStore(STORE, { keyPath: 'id' });
|
||||
};
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error ?? new Error('백업 저장소를 열 수 없습니다.'));
|
||||
});
|
||||
}
|
||||
|
||||
async function withStore<T>(mode: IDBTransactionMode, fn: (store: IDBObjectStore) => IDBRequest<T> | void): Promise<T | undefined> {
|
||||
const db = await open();
|
||||
try {
|
||||
return await new Promise<T | undefined>((resolve, reject) => {
|
||||
const tx = db.transaction(STORE, mode);
|
||||
const req = fn(tx.objectStore(STORE));
|
||||
tx.oncomplete = () => resolve(req ? req.result : undefined);
|
||||
tx.onerror = () => reject(tx.error ?? new Error('백업 저장소 오류'));
|
||||
tx.onabort = () => reject(tx.error ?? new Error('백업 저장 중단'));
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function listBackups(): Promise<BackupRecord[]> {
|
||||
const all = ((await withStore<BackupRecord[]>('readonly', (s) => s.getAll())) ?? []) as BackupRecord[];
|
||||
return all.sort((a, b) => (a.savedAt < b.savedAt ? 1 : -1));
|
||||
}
|
||||
|
||||
/** Save a backup unless it is identical to the newest one; trims to MAX_BACKUPS. */
|
||||
export async function saveBackup(rec: Omit<BackupRecord, 'id' | 'savedAt'>): Promise<boolean> {
|
||||
const existing = await listBackups();
|
||||
const last = existing[0];
|
||||
if (last && last.inp === rec.inp && JSON.stringify(last.assets ?? null) === JSON.stringify(rec.assets ?? null)) return false;
|
||||
const savedAt = new Date().toISOString();
|
||||
await withStore('readwrite', (s) => {
|
||||
s.put({ ...rec, id: `${savedAt}_${Math.random().toString(36).slice(2, 7)}`, savedAt });
|
||||
for (const old of existing.slice(MAX_BACKUPS - 1)) s.delete(old.id);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteBackup(id: string): Promise<void> {
|
||||
await withStore('readwrite', (s) => {
|
||||
s.delete(id);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user