const CACHE_NAME = "habit-tracker-v5"; const APP_SHELL = [ "/static/css/style.css", "/static/js/app.js", "/static/js/push-register.js", "/static/js/habit-reorder.js", "/static/js/journal-category-reorder.js", "/static/js/vendor/htmx.min.js", "/static/js/vendor/alpine.min.js", "/static/js/vendor/sortable.min.js", "/static/icons/icon-192.png", "/static/icons/icon-512.png", "/static/icons/icon-apple-180.png", "/static/manifest.json", "/static/offline.html", ]; self.addEventListener("install", (event) => { event.waitUntil( caches .open(CACHE_NAME) .then((cache) => cache.addAll(APP_SHELL)) .then(() => self.skipWaiting()) ); }); self.addEventListener("activate", (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key)))) .then(() => self.clients.claim()) ); }); self.addEventListener("fetch", (event) => { const { request } = event; if (request.method !== "GET") return; const url = new URL(request.url); if (url.pathname.startsWith("/api/")) return; // 페이지 탐색(/today, /habits, /history 등)은 습관 추가·수정 직후 리다이렉트되는 화면이라 // 캐시된 옛 내용이 먼저 보이면 "저장한 게 사라졌다"처럼 보인다. 네트워크를 우선 시도하고 // 오프라인일 때만 캐시로 폴백한다. if (request.mode === "navigate") { event.respondWith( fetch(request) .then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); } return response; }) .catch(() => caches.match(request).then((cached) => cached || caches.match("/static/offline.html"))) ); return; } // 정적 자산은 캐시 우선 응답 후 백그라운드로 갱신(stale-while-revalidate) — 자주 안 바뀌므로 속도 우선. event.respondWith( caches.match(request).then((cached) => { const network = fetch(request) .then((response) => { if (response.ok) { const clone = response.clone(); caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); } return response; }) .catch(() => cached); return cached || network; }) ); }); self.addEventListener("push", (event) => { if (!event.data) return; const data = event.data.json(); event.waitUntil( self.registration.showNotification(data.title || "해빗랩", { body: data.body || "", icon: "/static/icons/icon-192.png", badge: "/static/icons/icon-192.png", data: { url: data.url || "/today" }, }) ); }); self.addEventListener("notificationclick", (event) => { event.notification.close(); const targetUrl = (event.notification.data && event.notification.data.url) || "/today"; event.waitUntil( self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientsList) => { for (const client of clientsList) { if (client.url.includes(targetUrl) && "focus" in client) { return client.focus(); } } if (self.clients.openWindow) { return self.clients.openWindow(targetUrl); } }) ); });