/** * offlineQueue.ts — WS-2 Field Expansion * 오프라인 중 제출 요청을 IndexedDB에 큐잉하고, * 온라인 복귀 시 Background Sync(또는 폴링) 로 재전송. */ export interface QueueItem { id?: number; url: string; method: string; headers: Record; body: string; enqueuedAt: number; /** 원본 미션 ID — UI 표시용 */ missionId: string | null; /** 사용자 ID */ userId: string; } interface SyncRegistration extends ServiceWorkerRegistration { sync: { register: (tag: string) => Promise; }; } // ── DB 초기화 ───────────────────────────────────────────────────────────────── const DB_NAME = 'carboany-queue'; const DB_VERSION = 1; const STORE = 'queue'; let _db: IDBDatabase | null = null; function openDb(): Promise { if (_db) return Promise.resolve(_db); return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (e) => { const db = (e.target as IDBOpenDBRequest).result; if (!db.objectStoreNames.contains(STORE)) { db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true }); } }; req.onsuccess = (e) => { _db = (e.target as IDBOpenDBRequest).result; resolve(_db!); }; req.onerror = () => reject(req.error); }); } // ── Enqueue ─────────────────────────────────────────────────────────────────── export async function enqueueSubmission(item: Omit): Promise { const db = await openDb(); const entry: Omit = { ...item, enqueuedAt: Date.now() }; await new Promise((res, rej) => { const tx = db.transaction(STORE, 'readwrite'); const req = tx.objectStore(STORE).add(entry); req.onsuccess = () => res(); req.onerror = () => rej(req.error); }); // Background Sync 등록 (지원하는 브라우저) if ('serviceWorker' in navigator && 'SyncManager' in window) { const reg = await navigator.serviceWorker.ready; await (reg as SyncRegistration).sync.register('carboany-offline-queue').catch(() => {}); } } // ── Flush (온라인 복귀 시 호출) ─────────────────────────────────────────────── export async function flushQueue( onSuccess?: (item: QueueItem) => void, onError?: (item: QueueItem, err: unknown) => void ): Promise { const db = await openDb(); const items = await getAllItems(db); if (items.length === 0) return; for (const item of items) { try { const res = await fetch(item.url, { method: item.method, headers: item.headers, body: item.body, }); if (res.ok) { await deleteItem(db, item.id!); onSuccess?.(item); } else { onError?.(item, new Error(`HTTP ${res.status}`)); } } catch (err) { onError?.(item, err); } } } // ── 큐 크기 조회 ────────────────────────────────────────────────────────────── export async function getQueueSize(): Promise { const db = await openDb(); return new Promise((res, rej) => { const tx = db.transaction(STORE, 'readonly'); const req = tx.objectStore(STORE).count(); req.onsuccess = () => res(req.result); req.onerror = () => rej(req.error); }); } // ── 내부 유틸 ───────────────────────────────────────────────────────────────── function getAllItems(db: IDBDatabase): Promise { return new Promise((res, rej) => { const tx = db.transaction(STORE, 'readonly'); const req = tx.objectStore(STORE).getAll(); req.onsuccess = () => res(req.result as QueueItem[]); req.onerror = () => rej(req.error); }); } function deleteItem(db: IDBDatabase, id: number): Promise { return new Promise((res, rej) => { const tx = db.transaction(STORE, 'readwrite'); const req = tx.objectStore(STORE).delete(id); req.onsuccess = () => res(); req.onerror = () => rej(req.error); }); }