1/** 2 * ë¡ëëª ë¹ Service Worker 3 * ======================== 4 * PWA ì¤íë¼ì¸ ì§ì ë° ìºì± 5 */ 6 7const CACHE_NAME = 'lotto-meongdang-v4-20251220'; 8const OFFLINE_URL = '/offline.html'; 9 10// ìºìí ì ì íì¼ (HTML íì´ì§ë ì ì¸) 11const STATIC_ASSETS = [ 12 '/app.css', 13 '/manifest.json', 14 '/images/icon-192x192.png', 15 '/images/icon-512x512.png', 16]; 17 18// ìºì ì°ì (ì ì íì¼) 19const CACHE_FIRST = [ 20 /\.(?:css|js|woff2?|ttf|eot)$/, 21 /\/images\//, 22 /\/fonts\//, 23]; 24 25// ë¤í¸ìí¬ ì°ì (ëì ì½í ì¸ ë° SEO íì´ì§) 26const NETWORK_FIRST = [ 27 /\/api\//, 28 /\/ë¹ì²¨ë´ì\//, 29 /\/ì측\//, 30 /\/ë구\//, 31 /\/íµê³\//, 32 /\/ê°ì´ë\//, 33 /\/ëí¹\//, 34 /\/ë¡ëëª ë¹\//, 35]; 36 37/** 38 * ì¤ì¹ ì´ë²¤í¸ 39 */ 40self.addEventListener('install', (event) => { 41 console.log('[SW] ì¤ì¹ ì¤...'); 42 43 event.waitUntil( 44 caches.open(CACHE_NAME) 45 .then((cache) => { 46 console.log('[SW] ì ì íì¼ ìºì±'); 47 return cache.addAll(STATIC_ASSETS); 48 }) 49 .then(() => self.skipWaiting()) 50 ); 51}); 52 53/** 54 * íì±í ì´ë²¤í¸ 55 */ 56self.addEventListener('activate', (event) => { 57 console.log('[SW] íì±í'); 58 59 event.waitUntil( 60 caches.keys() 61 .then((cacheNames) => { 62 return Promise.all( 63 cacheNames 64 .filter((name) => name !== CACHE_NAME) 65 .map((name) => { 66 console.log('[SW] ì¤ëë ìºì ìì :', name); 67 return caches.delete(name); 68 }) 69 ); 70 }) 71 .then(() => self.clients.claim()) 72 ); 73}); 74 75/** 76 * Fetch ì´ë²¤í¸ 77 */ 78self.addEventListener('fetch', (event) => { 79 const { request } = event; 80 const url = new URL(request.url); 81 82 // ê°ì originë§ ì²ë¦¬ 83 if (url.origin !== location.origin) { 84 return; 85 } 86 87 // POST ìì²ì ìºìíì§ ìì 88 if (request.method !== 'GET') { 89 return; 90 } 91 92 // ìºì ì°ì ì ëµ (ì ì íì¼) 93 if (CACHE_FIRST.some((pattern) => pattern.test(url.pathname))) { 94 event.respondWith(cacheFirst(request)); 95 return; 96 } 97 98 // ë¤í¸ìí¬ ì°ì ì ëµ (API, ëì ì½í ì¸ ) 99 if (NETWORK_FIRST.some((pattern) => pattern.test(url.pathname))) { 100 event.respondWith(networkFirst(request)); 101 return; 102 } 103 104 // HTML íì´ì§ ìì² (ë¤ë¹ê²ì´ì )ì íì ë¤í¸ìí¬ ì°ì 105 if (request.mode === 'navigate' || request.destination === 'document' || url.pathname.endsWith('/')) { 106 event.respondWith(networkFirst(request)); 107 return; 108 } 109 110 // 기본: Stale While Revalidate (ì ì íì¼) 111 event.respondWith(staleWhileRevalidate(request)); 112}); 113 114/** 115 * ìºì ì°ì ì ëµ 116 */ 117async function cacheFirst(request) { 118 const cached = await caches.match(request); 119 if (cached) { 120 return cached; 121 } 122 123 try { 124 const response = await fetch(request); 125 if (response.ok) { 126 const cache = await caches.open(CACHE_NAME); 127 cache.put(request, response.clone()); 128 } 129 return response; 130 } catch (error) { 131 return new Response('ì¤íë¼ì¸', { status: 503 }); 132 } 133} 134 135/** 136 * ë¤í¸ìí¬ ì°ì ì ëµ 137 */ 138async function networkFirst(request) { 139 try { 140 const response = await fetch(request); 141 if (response.ok) { 142 const cache = await caches.open(CACHE_NAME); 143 cache.put(request, response.clone()); 144 } 145 return response; 146 } catch (error) { 147 const cached = await caches.match(request); 148 if (cached) { 149 return cached; 150 } 151 return new Response('ì¤íë¼ì¸ - ìºìë ë°ì´í° ìì', { status: 503 }); 152 } 153} 154 155/** 156 * Stale While Revalidate ì ëµ 157 */ 158async function staleWhileRevalidate(request) { 159 const cache = await caches.open(CACHE_NAME); 160 const cached = await cache.match(request); 161 162 const fetchPromise = fetch(request).then((response) => { 163 if (response.ok) { 164 cache.put(request, response.clone()); 165 } 166 return response; 167 }).catch(() => cached); 168 169 return cached || fetchPromise; 170} 171 172/** 173 * í¸ì ì림 (ìµì ) 174 */ 175self.addEventListener('push', (event) => { 176 if (!event.data) return; 177 178 const data = event.data.json(); 179 180 const options = { 181 body: data.body || 'ë¡ëëª ë¹ìì ìë¦¼ì´ ììµëë¤.', 182 icon: '/images/icon-192x192.png', 183 badge: '/images/badge-72x72.png', 184 vibrate: [100, 50, 100], 185 data: { 186 url: data.url || '/', 187 }, 188 actions: [ 189 { action: 'open', title: 'ì´ê¸°' }, 190 { action: 'close', title: 'ë«ê¸°' }, 191 ], 192 }; 193 194 event.waitUntil( 195 self.registration.showNotification(data.title || 'ë¡ëëª ë¹', options) 196 ); 197}); 198 199/** 200 * ì림 í´ë¦ 201 */ 202self.addEventListener('notificationclick', (event) => { 203 event.notification.close(); 204 205 if (event.action === 'close') return; 206 207 const url = event.notification.data?.url || '/'; 208 209 event.waitUntil( 210 clients.matchAll({ type: 'window' }) 211 .then((clientList) => { 212 // ì´ë¯¸ ì´ë¦° ì°½ì´ ìì¼ë©´ í¬ì»¤ì¤ 213 for (const client of clientList) {
214 if (client.url === url && 'focus' in client) { 215 return client.focus(); 216 } 217 } 218 // ìì¼ë©´ ì ì°½ ì´ê¸° 219 if (clients.openWindow) { 220 return clients.openWindow(url); 221 } 222 }) 223 ); 224}); 225 226console.log('[SW] ë¡ëëª ë¹ Service Worker ë¡ëë¨'); 227
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.