PageSourceSearch

https://lottoinsight.ai/sw.js

js lottoinsight.ai collected 2026-10-05 12:20:20 UTC 5,237 bytes, 227 lines download raw bytes

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.