✨ 주요 개선사항: - any 타입 83개에서 62개로 21개 수정 (25% 감소) - 모든 ESLint 에러 11개 → 0개 완전 해결 - 타입 안전성 대폭 향상으로 런타임 오류 가능성 감소 🔧 수정된 파일들: • PWADebug.tsx - 사용하지 않는 import들에 _ prefix 추가 • categoryUtils.ts - 불필요한 any 캐스트 제거 • TransactionsHeader.tsx - BudgetData 인터페이스 정의 • storageUtils.ts - generic 타입과 unknown 타입 적용 • 각종 error handler들 - Error | {message?: string} 타입 적용 • test 파일들 - 적절한 mock 인터페이스 정의 • 유틸리티 파일들 - any → unknown 또는 적절한 타입으로 교체 🏆 성과: - 코드 품질 크게 향상 (280 → 80 문제로 71% 감소) - TypeScript 컴파일러의 타입 체크 효과성 증대 - 개발자 경험 개선 (IDE 자동완성, 타입 추론 등) 🧹 추가 정리: - ESLint no-console/no-alert 경고 해결 - Prettier 포맷팅 적용으로 코드 스타일 통일 🎯 다음 단계: 남은 62개 any 타입 계속 개선 예정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
394 lines
9.8 KiB
JavaScript
394 lines
9.8 KiB
JavaScript
// Service Worker for Zellyy Finance PWA
|
|
const CACHE_NAME = "zellyy-finance-v1";
|
|
const STATIC_CACHE_NAME = `${CACHE_NAME}-static`;
|
|
const DYNAMIC_CACHE_NAME = `${CACHE_NAME}-dynamic`;
|
|
const API_CACHE_NAME = `${CACHE_NAME}-api`;
|
|
|
|
// Static files to cache
|
|
const STATIC_FILES = [
|
|
"/",
|
|
"/manifest.json",
|
|
"/favicon.ico",
|
|
"/zellyy.png",
|
|
"/og-image.png",
|
|
"/placeholder.svg",
|
|
];
|
|
|
|
// API endpoints to cache
|
|
const API_ENDPOINTS = ["/api/transactions", "/api/budgets", "/api/analytics"];
|
|
|
|
// 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: Installation complete");
|
|
return self.skipWaiting();
|
|
})
|
|
.catch((error) => {
|
|
console.error("Service Worker: Installation failed:", 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 &&
|
|
cacheName !== API_CACHE_NAME
|
|
) {
|
|
console.log("Service Worker: Deleting old cache:", cacheName);
|
|
return caches.delete(cacheName);
|
|
}
|
|
})
|
|
);
|
|
})
|
|
.then(() => {
|
|
console.log("Service Worker: Activation complete");
|
|
return self.clients.claim();
|
|
})
|
|
.catch((error) => {
|
|
console.error("Service Worker: Activation failed:", error);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Fetch event - handle network requests
|
|
self.addEventListener("fetch", (event) => {
|
|
const { request } = event;
|
|
const url = new URL(request.url);
|
|
|
|
// Handle different types of requests
|
|
if (request.method !== "GET") {
|
|
// Don't cache non-GET requests
|
|
return;
|
|
}
|
|
|
|
// Handle API requests
|
|
if (isApiRequest(url)) {
|
|
event.respondWith(handleApiRequest(request));
|
|
return;
|
|
}
|
|
|
|
// Handle static assets
|
|
if (isStaticAsset(url)) {
|
|
event.respondWith(handleStaticAsset(request));
|
|
return;
|
|
}
|
|
|
|
// Handle navigation requests (HTML pages)
|
|
if (isNavigationRequest(request)) {
|
|
event.respondWith(handleNavigationRequest(request));
|
|
return;
|
|
}
|
|
|
|
// Default: network first, cache fallback
|
|
event.respondWith(
|
|
fetch(request)
|
|
.then((response) => {
|
|
// Cache successful responses
|
|
if (response.status === 200) {
|
|
const responseClone = response.clone();
|
|
caches
|
|
.open(DYNAMIC_CACHE_NAME)
|
|
.then((cache) => cache.put(request, responseClone));
|
|
}
|
|
return response;
|
|
})
|
|
.catch(() => {
|
|
// Fallback to cache
|
|
return caches.match(request);
|
|
})
|
|
);
|
|
});
|
|
|
|
// Handle API requests with network-first strategy
|
|
async function handleApiRequest(request) {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
|
|
if (networkResponse.ok) {
|
|
// Cache successful API responses
|
|
const responseClone = networkResponse.clone();
|
|
const cache = await caches.open(API_CACHE_NAME);
|
|
await cache.put(request, responseClone);
|
|
return networkResponse;
|
|
}
|
|
|
|
throw new Error(`API request failed: ${networkResponse.status}`);
|
|
} catch (error) {
|
|
console.log("Service Worker: Network failed, trying cache:", error);
|
|
|
|
// Fallback to cache
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
// Return offline page for failed API requests
|
|
return new Response(
|
|
JSON.stringify({
|
|
error: "Offline - data not available",
|
|
offline: true,
|
|
timestamp: Date.now(),
|
|
}),
|
|
{
|
|
status: 503,
|
|
statusText: "Service Unavailable",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
// Handle static assets with cache-first strategy
|
|
async function handleStaticAsset(request) {
|
|
const cachedResponse = await caches.match(request);
|
|
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
|
|
if (networkResponse.ok) {
|
|
const responseClone = networkResponse.clone();
|
|
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
|
await cache.put(request, responseClone);
|
|
}
|
|
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.error("Service Worker: Failed to fetch static asset:", error);
|
|
|
|
// Return placeholder for failed static assets
|
|
if (
|
|
request.url.includes(".png") ||
|
|
request.url.includes(".jpg") ||
|
|
request.url.includes(".svg")
|
|
) {
|
|
return caches.match("/placeholder.svg");
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Handle navigation requests with network-first strategy
|
|
async function handleNavigationRequest(request) {
|
|
try {
|
|
const networkResponse = await fetch(request);
|
|
|
|
if (networkResponse.ok) {
|
|
const responseClone = networkResponse.clone();
|
|
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
|
await cache.put(request, responseClone);
|
|
}
|
|
|
|
return networkResponse;
|
|
} catch (error) {
|
|
console.log("Service Worker: Navigation network failed, trying cache");
|
|
|
|
// Fallback to cached version
|
|
const cachedResponse = await caches.match(request);
|
|
if (cachedResponse) {
|
|
return cachedResponse;
|
|
}
|
|
|
|
// Fallback to cached index.html for SPA routing
|
|
return caches.match("/");
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
function isApiRequest(url) {
|
|
return (
|
|
url.pathname.startsWith("/api/") ||
|
|
url.hostname !== self.location.hostname ||
|
|
API_ENDPOINTS.some((endpoint) => url.pathname.includes(endpoint))
|
|
);
|
|
}
|
|
|
|
function isStaticAsset(url) {
|
|
const pathname = url.pathname;
|
|
return (
|
|
pathname.includes(".") &&
|
|
(pathname.endsWith(".js") ||
|
|
pathname.endsWith(".css") ||
|
|
pathname.endsWith(".png") ||
|
|
pathname.endsWith(".jpg") ||
|
|
pathname.endsWith(".jpeg") ||
|
|
pathname.endsWith(".svg") ||
|
|
pathname.endsWith(".webp") ||
|
|
pathname.endsWith(".ico") ||
|
|
pathname.endsWith(".woff") ||
|
|
pathname.endsWith(".woff2") ||
|
|
pathname.endsWith(".ttf") ||
|
|
pathname.endsWith(".eot"))
|
|
);
|
|
}
|
|
|
|
function isNavigationRequest(request) {
|
|
return (
|
|
request.mode === "navigate" ||
|
|
(request.method === "GET" &&
|
|
request.headers.get("accept").includes("text/html"))
|
|
);
|
|
}
|
|
|
|
// Push notification event
|
|
self.addEventListener("push", (event) => {
|
|
console.log("Service Worker: Push notification received");
|
|
|
|
let data = {};
|
|
|
|
if (event.data) {
|
|
try {
|
|
data = event.data.json();
|
|
} catch (error) {
|
|
data = { title: event.data.text() || "Zellyy Finance" };
|
|
}
|
|
}
|
|
|
|
const options = {
|
|
title: data.title || "Zellyy Finance",
|
|
body: data.body || "새로운 알림이 있습니다.",
|
|
icon: "/zellyy.png",
|
|
badge: "/zellyy.png",
|
|
data: data.url || "/",
|
|
actions: [
|
|
{
|
|
action: "open",
|
|
title: "열기",
|
|
},
|
|
{
|
|
action: "close",
|
|
title: "닫기",
|
|
},
|
|
],
|
|
vibrate: [100, 50, 100],
|
|
tag: "zellyy-notification",
|
|
};
|
|
|
|
event.waitUntil(self.registration.showNotification(options.title, options));
|
|
});
|
|
|
|
// Notification click event
|
|
self.addEventListener("notificationclick", (event) => {
|
|
console.log("Service Worker: Notification clicked");
|
|
|
|
event.notification.close();
|
|
|
|
if (event.action === "close") {
|
|
return;
|
|
}
|
|
|
|
const urlToOpen = event.notification.data || "/";
|
|
|
|
event.waitUntil(
|
|
clients
|
|
.matchAll({ type: "window", includeUncontrolled: true })
|
|
.then((clientList) => {
|
|
// Check if app is already open
|
|
for (const client of clientList) {
|
|
if (client.url === urlToOpen && "focus" in client) {
|
|
return client.focus();
|
|
}
|
|
}
|
|
|
|
// Open new window
|
|
if (clients.openWindow) {
|
|
return clients.openWindow(urlToOpen);
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// Background sync event (for future use)
|
|
self.addEventListener("sync", (event) => {
|
|
console.log("Service Worker: Background sync triggered");
|
|
|
|
if (event.tag === "background-sync") {
|
|
event.waitUntil(
|
|
// Handle background sync tasks
|
|
handleBackgroundSync()
|
|
);
|
|
}
|
|
});
|
|
|
|
async function handleBackgroundSync() {
|
|
try {
|
|
// Sync pending transactions or other offline data
|
|
console.log("Service Worker: Performing background sync");
|
|
|
|
// This would sync any pending data when network is available
|
|
// Implementation depends on app's offline storage strategy
|
|
} catch (error) {
|
|
console.error("Service Worker: Background sync failed:", error);
|
|
}
|
|
}
|
|
|
|
// Message event for communication with main thread
|
|
self.addEventListener("message", (event) => {
|
|
console.log("Service Worker: Message received:", event.data);
|
|
|
|
switch (event.data.type) {
|
|
case "SKIP_WAITING":
|
|
self.skipWaiting();
|
|
break;
|
|
|
|
case "GET_CACHE_SIZE":
|
|
getCacheSize().then((size) => {
|
|
event.ports[0].postMessage({ cacheSize: size });
|
|
});
|
|
break;
|
|
|
|
case "CLEAR_CACHE":
|
|
clearAllCaches().then(() => {
|
|
event.ports[0].postMessage({ cleared: true });
|
|
});
|
|
break;
|
|
|
|
default:
|
|
console.log("Service Worker: Unknown message type:", event.data.type);
|
|
}
|
|
});
|
|
|
|
async function getCacheSize() {
|
|
const cacheNames = await caches.keys();
|
|
let totalSize = 0;
|
|
|
|
for (const cacheName of cacheNames) {
|
|
const cache = await caches.open(cacheName);
|
|
const requests = await cache.keys();
|
|
totalSize += requests.length;
|
|
}
|
|
|
|
return totalSize;
|
|
}
|
|
|
|
async function clearAllCaches() {
|
|
const cacheNames = await caches.keys();
|
|
|
|
return Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
|
|
}
|