PageSourceSearch

https://blackall.ai/sw.js

js blackall.ai collected 2026-10-02 01:11:26 UTC 3,552 bytes, 116 lines download raw bytes

1// Service Worker - PWA 오프라인 지원
2const CACHE_NAME = 'blackall-ai-v3';
3const urlsToCache = [
4  '/',
5  '/manifest.json',
6  '/192.png',
7  '/512.png',
8];
9
10// 설치 - 캐시 생성
11self.addEventListener('install', (event) => {
12  event.waitUntil(
13    caches.open(CACHE_NAME).then((cache) => {
14      console.log('✅ Cache opened');
15      return cache.addAll(urlsToCache);
16    })
17  );
18  self.skipWaiting();
19});
20
21// 웹 푸시 수신 - 실제 OS/브라우저 알림 표시 (push-send 엣지펑션이 발송)
22self.addEventListener('push', (event) => {
23  let data = {};
24  try {
25    data = event.data ? event.data.json() : {};
26  } catch (e) {
27    data = { title: 'BlackAll', body: event.data ? event.data.text() : '' };
28  }
29
30  const title = data.title || 'BlackAll';
31  const options = {
32    body: data.body || '',
33    icon: data.icon || '/192.png',
34    badge: '/192.png',
35    data: { url: data.url || '/' },
36  };
37
38  event.waitUntil(self.registration.showNotification(title, options));
39});
40
41// 알림 클릭 - 해당 URL 탭이 열려있으면 포커스, 없으면 새 탭
42self.addEventListener('notificationclick', (event) => {
43  event.notification.close();
44  const url = event.notification.data?.url || '/';
45
46  event.waitUntil(
47    self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clients) => {
48      for (const client of clients) {
49        if (client.url.includes(url) && 'focus' in client) return client.focus();
50      }
51      if (self.clients.openWindow) return self.clients.openWindow(url);
52    })
53  );
54});
55
56// 활성화 - 이전 캐시 삭제
57self.addEventListener('activate', (event) => {
58  event.waitUntil(
59    caches.keys().then((cacheNames) => {
60      return Promise.all(
61        cacheNames.map((cacheName) => {
62          if (cacheName !== CACHE_NAME) {
63            console.log('🗑️ Deleting old cache:', cacheName);
64            return caches.delete(cacheName);
65          }
66        })
67      );
68    })
69  );
70  self.clients.claim();
71});
72
73// Fetch - 네트워크 우선, 실패 시 캐시
74self.addEventListener('fetch', (event) => {
75  // POST 등 non-GET 요청은 캐시 불가 — 그대로 통과
76  if (event.request.method !== 'GET') {
77    return;
78  }
79
80  const url = event.request.url;
81
82  // http(s) 외 스킴은 캐시 불가 (chrome-extension://, data:, blob: 등)
83  if (!url.startsWith('http://') && !url.startsWith('https://')) {
84    return;
85  }
86
87  // anatomy-real.html(1.77MB 3D 뷰어)은 SW 캐시 제외 — 옛 버전이 굳어버리는 것 방지.
88  // 버전은 iframe ?v= 쿼리로 관리. 네트워크 직통.
89  if (url.includes('anatomy-real.html')) {
90    return;
91  }
92
93  // Supabase API 요청은 캐시하지 않음 (인증/세션 관련 stale 응답 방지)
94  if (url.includes('supabase.co')) {
95    return;
96  }
97
98  event.respondWith(
99    fetch(event.request)
100      .then((response) => {
101        // GET 200 응답만 캐시에 저장 (basic/cors 타입만 — opaque 응답 제외)
102        if (response.status === 200 && (response.type === 'basic' || response.type === 'cors')) {
103          const responseClone = response.clone();
104          caches.open(CACHE_NAME).then((cache) => {
105            cache.put(event.request, responseClone).catch(() => {});
106          });
107        }
108        return response;
109      })
110      .catch(async () => {
111        // 네트워크 실패 시 캐시에서 반환, 없으면 503
112        const cached = await caches.match(event.request);
113        return cached || new Response('Network unavailable', { status: 503, statusText: 'Service Unavailable' });
114      })
115  );
116});

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.