PageSourceSearch

https://hanttrail-8fwxhbgl.manus.space/sw.js

js hanttrail-8fwxhbgl.manus.space collected 2026-10-03 05:04:52 UTC 6,111 bytes, 156 lines download raw bytes

1/**
2 * HANT 트레일 앱 Service Worker v9
3 * - 해시된 정적 자원과 폰트만 제한적으로 캐싱
4 * - Google Maps 타일 캐싱 (오프라인 지도)
5 * - 오프라인 폴백
6 * - PWA 설치 지원
7 * - 다음 앱 시작 시 안전한 업데이트 적용
8 */
9
10// 배포 버전마다 앱 셸 캐시를 교체해 이전 HTML·JS 번들이 남지 않게 한다.
11const APP_CACHE = "hant-app-v9";
12const MAP_CACHE = "hant-map-tiles-v2";
13const FONT_CACHE = "hant-fonts-v1";
14const OFFLINE_URL = "/offline.html";
15
16// 공개 홈페이지에서만, 이전 페이지 앱 셸이 깨진 `/images/*`를 계속 참조하지 않도록
17// 새 워커를 즉시 활성화한다. Capacitor Android는 `https://localhost` 별도 origin이므로
18// 이 조건에 절대 포함되지 않으며 현재 지도·GPS 세션을 reload하지 않는다.
19const PUBLIC_WEB_HOSTS = new Set(["hantp.com", "www.hantp.com"]);
20
21
22// 설치 시 사전 캐싱할 앱 셸 자원
23const PRECACHE_ASSETS = [
24  "/",
25  "/offline.html",
26];
27
28// ── Install ──────────────────────────────────────────────────────────────────
29self.addEventListener("install", (event) => {
30  event.waitUntil(
31    caches.open(APP_CACHE).then((cache) => {
32      return cache.addAll(PRECACHE_ASSETS).catch(() => {
33        // 일부 자원 캐싱 실패 시 무시 (Vite 빌드 해시 때문에 정확한 경로 불명)
34      });
35    })
36  );
37  if (PUBLIC_WEB_HOSTS.has(self.location.hostname)) {
38    self.skipWaiting();
39  }
40  // 현재 Android 화면·GPS·관리 메뉴를 유지한 채, 모든 기존 클라이언트가 닫힌 다음 앱 시작에 활성화한다.
41  // 열린 앱을 강제로 reload하면 트래킹 화면이 홈으로 돌아가는 회귀가 생길 수 있다.
42});
43
44// ── Activate ─────────────────────────────────────────────────────────────────
45self.addEventListener("activate", (event) => {
46  const currentCaches = [APP_CACHE, MAP_CACHE, FONT_CACHE];
47  event.waitUntil(
48    caches.keys().then((keys) =>
49      Promise.all(
50        keys
51          .filter((key) => !currentCaches.includes(key))
52          .map((key) => caches.delete(key))
53      )
54    )
55  );
56  self.clients.claim();
57});
58
59// ── Fetch ─────────────────────────────────────────────────────────────────────
60self.addEventListener("fetch", (event) => {
61  const { request } = event;
62  const url = new URL(request.url);
63
64  // API 요청은 캐싱하지 않음
65  if (url.pathname.startsWith("/api/")) return;
66
67  // GET 요청만 처리
68  if (request.method !== "GET") return;
69
70  // Google Fonts 캐싱 (폰트는 장기 캐싱)
71  if (
72    url.hostname.includes("fonts.googleapis.com") ||
73    url.hostname.includes("fonts.gstatic.com")
74  ) {
75    event.respondWith(
76      caches.open(FONT_CACHE).then(async (cache) => {
77        const cached = await cache.match(request);
78        if (cached) return cached;
79        try {
80          const response = await fetch(request);
81          if (response.ok) cache.put(request, response.clone());
82          return response;
83        } catch {
84          return cached ?? new Response("Offline", { status: 503 });
85        }
86      })
87    );
88    return;
89  }
90
91  // Google Maps 타일 및 API 캐싱 (캐시 우선 - 오프라인 지도 지원)
92  if (
93    url.hostname.includes("maps.googleapis.com") ||
94    url.hostname.includes("maps.gstatic.com") ||
95    url.hostname.includes("khms0.googleapis.com") ||
96    url.hostname.includes("khms1.googleapis.com") ||
97    url.hostname.includes("mts0.google.com") ||
98    url.hostname.includes("mts1.google.com") ||
99    url.hostname.includes("mts2.google.com") ||
100    url.hostname.includes("mts3.google.com")
101  ) {
102    event.respondWith(
103      caches.open(MAP_CACHE).then(async (cache) => {
104        const cached = await cache.match(request);
105        if (cached) return cached;
106        try {
107          const response = await fetch(request);
108          if (response.ok && response.status === 200) {
109            // 타일 이미지만 캐싱 (JS/JSON 제외)
110            const contentType = response.headers.get("content-type") ?? "";
111            if (contentType.includes("image") || contentType.includes("octet-stream")) {
112              cache.put(request, response.clone());
113            }
114          }
115          return response;
116        } catch {
117          return cached ?? new Response("Offline", { status: 503 });
118        }
119      })
120    );
121    return;
122  }
123
124  // 기타 외부 도메인 - 캐싱 없이 통과
125  if (url.origin !== self.location.origin) return;
126
127  // HTML·API 외 내부 요청은 네트워크를 우선한다. 이전에는 모든 same-origin GET을
128  // 캐시해 페이지·번들·응답이 누적되고 이전 배포가 오래 남을 수 있었다.
129  const isHashedAsset = /^\/assets\/.+\.(js|css|woff2?|png|jpg|jpeg|webp|svg)$/i.test(url.pathname);
130  event.respondWith(
131    fetch(request)
132      .then((response) => {
133        if (response.ok && isHashedAsset) {
134          const clone = response.clone();
135          caches.open(APP_CACHE).then((cache) => cache.put(request, clone));
136        }
137        return response;
138      })
139      .catch(async () => {
140        const cached = await caches.match(request);
141        if (cached) return cached;
142        // HTML 요청이면 오프라인 페이지 반환
143        if (request.headers.get("accept")?.includes("text/html")) {
144          const offlinePage = await caches.match(OFFLINE_URL);
145          if (offlinePage) return offlinePage;
146          // 루트 캐시도 시도
147          const rootPage = await caches.match("/");
148          if (rootPage) return rootPage;
149        }
150        return new Response("오프라인 상태입니다", {
151          status: 503,
152          headers: { "Content-Type": "text/plain; charset=utf-8" },
153        });
154      })
155  );
156});

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.