Files
sst_site/.history/public/sw_20251021172445.js
2025-10-22 21:22:44 +09:00

407 lines
12 KiB
JavaScript

// Service Worker for SmartSolTech PWA
const CACHE_NAME = 'smartsoltech-v1.0.1';
const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.1';
const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.1';
// Files to cache immediately
const STATIC_FILES = [
'/',
'/css/main.css',
'/css/fixes.css',
'/css/dark-theme.css',
'/js/main.js',
'/images/logo.png',
'/images/icon-192x192.png',
'/images/icon-144x144.png',
'/manifest.json',
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css',
'https://unpkg.com/aos@2.3.1/dist/aos.css',
'https://unpkg.com/aos@2.3.1/dist/aos.js'
];
// Routes to cache dynamically
const DYNAMIC_ROUTES = [
'/about',
'/services',
'/portfolio',
'/calculator',
'/contact'
];
// API endpoints to cache
const API_CACHE_PATTERNS = [
/^\/api\/portfolio/,
/^\/api\/services/,
/^\/api\/calculator\/services/
];
// Install event - cache static files
self.addEventListener('install', event => {
console.log('Service Worker: Installing...');
event.waitUntil(
caches.open(STATIC_CACHE_NAME)
.then(cache => {
console.log('Service Worker: Caching static files');
return cache.addAll(STATIC_FILES);
})
.then(() => {
console.log('Service Worker: Static files cached');
return self.skipWaiting();
})
.catch(error => {
console.error('Service Worker: Error caching static files', error);
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', event => {
console.log('Service Worker: Activating...');
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== STATIC_CACHE_NAME &&
cacheName !== DYNAMIC_CACHE_NAME) {
console.log('Service Worker: Deleting old cache', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('Service Worker: Activated');
return self.clients.claim();
})
);
});
// Fetch event - serve cached files or fetch from network
self.addEventListener('fetch', event => {
const request = event.request;
const url = new URL(request.url);
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// Skip Chrome extension requests
if (url.protocol === 'chrome-extension:') {
return;
}
// Handle different types of requests
if (isStaticFile(request.url)) {
event.respondWith(cacheFirst(request));
} else if (isAPIRequest(request.url)) {
event.respondWith(networkFirst(request));
} else if (isDynamicRoute(request.url)) {
event.respondWith(staleWhileRevalidate(request));
} else {
event.respondWith(networkFirst(request));
}
});
// Cache strategies
async function cacheFirst(request) {
try {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
const networkResponse = await fetch(request);
const cache = await caches.open(STATIC_CACHE_NAME);
cache.put(request, networkResponse.clone());
return networkResponse;
} catch (error) {
console.error('Cache first strategy failed:', error);
return new Response('Offline', { status: 503 });
}
}
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('Network first: Falling back to cache for', request.url);
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for navigation requests
if (request.mode === 'navigate') {
return caches.match('/offline.html') || new Response('Offline', {
status: 503,
headers: { 'Content-Type': 'text/html' }
});
}
return new Response('Network Error', { status: 503 });
}
}
async function staleWhileRevalidate(request) {
try {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
const cachedResponse = await cache.match(request);
const fetchPromise = fetch(request).then(networkResponse => {
if (networkResponse && networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
}).catch(error => {
console.log('staleWhileRevalidate fetch failed:', error);
return null;
});
return cachedResponse || fetchPromise || new Response('Not available', { status: 503 });
} catch (error) {
console.error('staleWhileRevalidate error:', error);
return new Response('Service unavailable', { status: 503 });
}
}
// Helper functions
function isStaticFile(url) {
return url.includes('/css/') ||
url.includes('/js/') ||
url.includes('/images/') ||
url.includes('/fonts/') ||
url.includes('googleapis.com') ||
url.includes('cdnjs.cloudflare.com');
}
function isAPIRequest(url) {
return url.includes('/api/') ||
API_CACHE_PATTERNS.some(pattern => pattern.test(url));
}
function isDynamicRoute(url) {
const pathname = new URL(url).pathname;
return DYNAMIC_ROUTES.includes(pathname) ||
pathname.startsWith('/portfolio/') ||
pathname.startsWith('/services/');
}
// Background sync for form submissions
self.addEventListener('sync', event => {
console.log('Service Worker: Background sync triggered', event.tag);
if (event.tag === 'contact-form-sync') {
event.waitUntil(syncContactForms());
}
});
async function syncContactForms() {
try {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
const requests = await cache.keys();
const contactRequests = requests.filter(request =>
request.url.includes('/api/contact/submit')
);
for (const request of contactRequests) {
try {
await fetch(request);
await cache.delete(request);
console.log('Contact form synced successfully');
} catch (error) {
console.error('Failed to sync contact form:', error);
}
}
} catch (error) {
console.error('Background sync failed:', error);
}
}
// Push notification handling
self.addEventListener('push', event => {
console.log('Service Worker: Push received', event);
let data = {};
if (event.data) {
data = event.data.json();
}
const title = data.title || 'SmartSolTech';
const options = {
body: data.body || 'You have a new notification',
icon: '/images/icon-192x192.png',
badge: '/images/icon-72x72.png',
tag: data.tag || 'default',
data: data.url || '/',
actions: [
{
action: 'open',
title: '열기',
icon: '/images/icon-open.png'
},
{
action: 'close',
title: '닫기',
icon: '/images/icon-close.png'
}
],
requireInteraction: data.requireInteraction || false,
silent: data.silent || false,
vibrate: data.vibrate || [200, 100, 200]
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Notification click handling
self.addEventListener('notificationclick', event => {
console.log('Service Worker: Notification clicked', event);
event.notification.close();
if (event.action === 'close') {
return;
}
const url = event.notification.data || '/';
event.waitUntil(
clients.matchAll({ type: 'window' }).then(clientList => {
// Check if window is already open
for (const client of clientList) {
if (client.url === url && 'focus' in client) {
return client.focus();
}
}
// Open new window
if (clients.openWindow) {
return clients.openWindow(url);
}
})
);
});
// Handle messages from main thread
self.addEventListener('message', event => {
console.log('Service Worker: Message received', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CACHE_URLS') {
cacheUrls(event.data.urls);
}
});
async function cacheUrls(urls) {
try {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
await cache.addAll(urls);
console.log('URLs cached successfully:', urls);
} catch (error) {
console.error('Failed to cache URLs:', error);
}
}
// Periodic background sync (if supported)
self.addEventListener('periodicsync', event => {
console.log('Service Worker: Periodic sync triggered', event.tag);
if (event.tag === 'content-sync') {
event.waitUntil(syncContent());
}
});
async function syncContent() {
try {
// Fetch fresh portfolio and services data
const portfolioResponse = await fetch('/api/portfolio?featured=true');
const servicesResponse = await fetch('/api/services?featured=true');
if (portfolioResponse.ok && servicesResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE_NAME);
cache.put('/api/portfolio?featured=true', portfolioResponse.clone());
cache.put('/api/services?featured=true', servicesResponse.clone());
console.log('Content synced successfully');
}
} catch (error) {
console.error('Content sync failed:', error);
}
}
// Cache management utilities
async function cleanupCaches() {
const cacheNames = await caches.keys();
const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
return Promise.all(
cacheNames.map(cacheName => {
if (!currentCaches.includes(cacheName)) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}
// Limit cache size
async function limitCacheSize(cacheName, maxItems) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxItems) {
const keysToDelete = keys.slice(0, keys.length - maxItems);
return Promise.all(keysToDelete.map(key => cache.delete(key)));
}
}
// Performance monitoring
self.addEventListener('fetch', event => {
if (event.request.url.includes('/api/')) {
const start = performance.now();
event.respondWith(
fetch(event.request).then(response => {
const duration = performance.now() - start;
// Log slow API requests
if (duration > 2000) {
console.warn('Slow API request:', event.request.url, duration + 'ms');
}
return response;
})
);
}
});
// Error tracking
self.addEventListener('error', event => {
console.error('Service Worker error:', event.error);
// Could send to analytics service
});
self.addEventListener('unhandledrejection', event => {
console.error('Service Worker unhandled rejection:', event.reason);
// Could send to analytics service
});