zioinfo-mail/workspace/guardia-itsm/static/sw.js
DESKTOP-TKLFCPR\ython cfe2901a55 refactor(structure): consolidate all projects under workspace/
- itsm/    -> workspace/guardia-itsm/
- manager/ -> workspace/guardia-manager/
- app/     -> workspace/guardia-messenger/
- manual/  -> workspace/guardia-docs/

workspace/zioinfo-web/ unchanged.
git mv preserves full commit history.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 23:50:56 +09:00

213 lines
6.7 KiB
JavaScript

/**
* GUARDiA ITSM — Service Worker (F-4 Mobile PWA)
*
* 전략:
* - 정적 자산 : Cache-First (빠른 로딩)
* - API 요청 : Network-First (최신 데이터 우선, 오프라인 시 캐시)
* - 오프라인 : /static/offline.html 폴백
*
* 캐시 버전이 바뀌면 이전 캐시를 자동 정리합니다.
*/
const CACHE_VERSION = 'guardia-v1';
const STATIC_CACHE = `${CACHE_VERSION}-static`;
const API_CACHE = `${CACHE_VERSION}-api`;
const OFFLINE_URL = '/static/offline.html';
// 설치 시 사전 캐싱할 정적 자산
const PRECACHE_URLS = [
'/',
'/static/offline.html',
'/static/manifest.json',
'/static/style.css',
'/static/app.js',
'/static/login.html',
'/static/login.css',
'/static/login.js',
'/static/index.html',
'/static/customer.html',
];
// API 캐시 대상 접두사
const API_CACHE_PREFIXES = [
'/api/dashboard',
'/api/sr',
'/api/incidents',
'/api/metrics/summary',
'/api/metrics/health',
];
// 캐시하지 않을 API 경로 (실시간·민감 데이터)
const NO_CACHE_PATTERNS = [
'/api/auth',
'/api/audit',
'/api/pam',
'/api/otp',
'/ws',
];
// ── 설치 (Install) ─────────────────────────────────────────────────────────
self.addEventListener('install', event => {
event.waitUntil(
caches.open(STATIC_CACHE)
.then(cache => cache.addAll(PRECACHE_URLS).catch(() => {
// 일부 파일이 없어도 SW 설치 계속 진행
return Promise.resolve();
}))
.then(() => self.skipWaiting())
);
});
// ── 활성화 (Activate) — 이전 캐시 정리 ─────────────────────────────────────
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(k => k.startsWith('guardia-') && k !== STATIC_CACHE && k !== API_CACHE)
.map(k => caches.delete(k))
)
).then(() => self.clients.claim())
);
});
// ── Fetch 인터셉트 ─────────────────────────────────────────────────────────
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// 다른 오리진 요청은 처리하지 않음 (Ollama 등 내부 서비스 포함)
if (url.origin !== self.location.origin) return;
// WebSocket / POST/PUT/DELETE → 캐시 안 함
if (request.method !== 'GET') return;
// 보안 민감 경로 → 캐시 안 함
if (NO_CACHE_PATTERNS.some(p => url.pathname.startsWith(p))) return;
// API 요청 → Network-First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request, API_CACHE));
return;
}
// 정적 자산 → Cache-First
event.respondWith(cacheFirst(request, STATIC_CACHE));
});
// ── 전략 함수 ──────────────────────────────────────────────────────────────
/**
* Cache-First: 캐시 → 네트워크 → 오프라인 폴백
*/
async function cacheFirst(request, cacheName) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
cache.put(request, response.clone());
}
return response;
} catch {
const offline = await caches.match(OFFLINE_URL);
return offline || new Response('오프라인 상태입니다.', {
status: 503,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
}
/**
* Network-First: 네트워크 → 캐시 → 오프라인 폴백
* API 응답은 5분간 캐시 유지
*/
async function networkFirst(request, cacheName) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(cacheName);
// API 응답에 캐시 타임스탬프 추가
const cloned = response.clone();
cache.put(request, cloned);
}
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
// HTML 요청 시 오프라인 페이지
if (request.headers.get('Accept')?.includes('text/html')) {
const offline = await caches.match(OFFLINE_URL);
if (offline) return offline;
}
return new Response(
JSON.stringify({ error: '오프라인 상태입니다.', offline: true }),
{
status: 503,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
}
);
}
}
// ── 백그라운드 동기화 ──────────────────────────────────────────────────────
self.addEventListener('sync', event => {
if (event.tag === 'guardia-sync-sr') {
event.waitUntil(syncPendingSR());
}
});
async function syncPendingSR() {
// IndexedDB에서 오프라인 중 생성된 SR 대기열 처리 (향후 구현)
// 현재는 로그만 기록
console.log('[GUARDiA SW] 오프라인 SR 동기화 시작');
}
// ── 푸시 알림 ──────────────────────────────────────────────────────────────
self.addEventListener('push', event => {
if (!event.data) return;
let data;
try {
data = event.data.json();
} catch {
data = { title: 'GUARDiA 알림', body: event.data.text() };
}
const options = {
body: data.body || '새 알림이 있습니다.',
icon: '/static/icons/icon-192.png',
badge: '/static/icons/icon-72.png',
tag: data.tag || 'guardia-notification',
data: data.data || {},
actions: data.actions || [
{ action: 'view', title: '확인' },
{ action: 'dismiss', title: '닫기' },
],
requireInteraction: data.severity === 'CRITICAL',
};
event.waitUntil(
self.registration.showNotification(data.title || 'GUARDiA ITSM', options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'dismiss') return;
const url = event.notification.data?.url || '/';
event.waitUntil(
clients.matchAll({ type: 'window', includeUncontrolled: true }).then(clientList => {
for (const client of clientList) {
if (client.url === url && 'focus' in client) return client.focus();
}
return clients.openWindow(url);
})
);
});