feat: Stage 2 TypeScript 타입 안전성 개선 - any 타입 83개 → 62개 대폭 감소
✨ 주요 개선사항: - 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>
This commit is contained in:
393
public/sw.js
Normal file
393
public/sw.js
Normal file
@@ -0,0 +1,393 @@
|
||||
// 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)));
|
||||
}
|
||||
Reference in New Issue
Block a user