zioinfo-mail/itsm/static/sw.js
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +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);
})
);
});