Files
M-Project/frontend/sw.js
Hyungi Ahn 58156da987 🐛 Fix: Project.name → project_name 속성명 수정 및 보고서 시스템 안정화
- backend/routers/reports.py: project.name을 project.project_name으로 수정 (3곳)
- 일일보고서 엑셀 내보내기 오류 해결
- 배포 가이드 업데이트 (DEPLOYMENT_GUIDE_20251028.md)
- 프로젝트 속성명 불일치로 인한 500 에러 해결

Fixes: 'Project' object has no attribute 'name' 오류
2025-10-28 16:36:56 +09:00

336 lines
8.9 KiB
JavaScript

/**
* 서비스 워커 - 페이지 및 리소스 캐싱
* M-Project 작업보고서 시스템
*/
const CACHE_NAME = 'mproject-v1.0.1';
const STATIC_CACHE = 'mproject-static-v1.0.1';
const DYNAMIC_CACHE = 'mproject-dynamic-v1.0.1';
// 캐시할 정적 리소스
const STATIC_ASSETS = [
'/',
'/index.html',
'/issue-view.html',
'/daily-work.html',
'/project-management.html',
'/admin.html',
'/static/js/api.js',
'/static/js/core/permissions.js',
'/static/js/components/common-header.js',
'/static/js/core/page-manager.js',
'/static/js/core/page-preloader.js',
'/static/js/date-utils.js',
'/static/js/image-utils.js',
'https://cdn.tailwindcss.com',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'
];
// 캐시 전략별 URL 패턴
const CACHE_STRATEGIES = {
// 네트워크 우선 (API 호출)
networkFirst: [
/\/api\//,
/\/auth\//
],
// 캐시 우선 (정적 리소스)
cacheFirst: [
/\.css$/,
/\.js$/,
/\.png$/,
/\.jpg$/,
/\.jpeg$/,
/\.gif$/,
/\.svg$/,
/\.woff$/,
/\.woff2$/,
/cdn\.tailwindcss\.com/,
/cdnjs\.cloudflare\.com/
],
// 스테일 허용 (HTML 페이지)
staleWhileRevalidate: [
/\.html$/,
/\/$/
]
};
/**
* 서비스 워커 설치
*/
self.addEventListener('install', (event) => {
console.log('🔧 서비스 워커 설치 중...');
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('📦 정적 리소스 캐싱 중...');
return cache.addAll(STATIC_ASSETS);
})
.then(() => {
console.log('✅ 서비스 워커 설치 완료');
return self.skipWaiting();
})
.catch((error) => {
console.error('❌ 서비스 워커 설치 실패:', error);
})
);
});
/**
* 서비스 워커 활성화
*/
self.addEventListener('activate', (event) => {
console.log('🚀 서비스 워커 활성화 중...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 이전 버전 캐시 삭제
if (cacheName !== STATIC_CACHE &&
cacheName !== DYNAMIC_CACHE &&
cacheName !== CACHE_NAME) {
console.log('🗑️ 이전 캐시 삭제:', cacheName);
return caches.delete(cacheName);
}
})
);
})
.then(() => {
console.log('✅ 서비스 워커 활성화 완료');
return self.clients.claim();
})
);
});
/**
* 네트워크 요청 가로채기
*/
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// CORS 요청이나 외부 도메인은 기본 처리
if (url.origin !== location.origin && !isCDNResource(url)) {
return;
}
// 캐시 전략 결정
const strategy = getCacheStrategy(request.url);
event.respondWith(
handleRequest(request, strategy)
);
});
/**
* 요청 처리 (캐시 전략별)
*/
async function handleRequest(request, strategy) {
try {
switch (strategy) {
case 'networkFirst':
return await networkFirst(request);
case 'cacheFirst':
return await cacheFirst(request);
case 'staleWhileRevalidate':
return await staleWhileRevalidate(request);
default:
return await fetch(request);
}
} catch (error) {
console.error('요청 처리 실패:', request.url, error);
return await handleOffline(request);
}
}
/**
* 네트워크 우선 전략
*/
async function networkFirst(request) {
try {
const networkResponse = await fetch(request);
// 성공적인 응답만 캐시
if (networkResponse.ok) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 네트워크 실패 시 캐시에서 반환
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
throw error;
}
}
/**
* 캐시 우선 전략
*/
async function cacheFirst(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 캐시에 없으면 네트워크에서 가져와서 캐시
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, networkResponse.clone());
}
return networkResponse;
}
/**
* 스테일 허용 전략
*/
async function staleWhileRevalidate(request) {
const cachedResponse = await caches.match(request);
// 백그라운드에서 업데이트
const networkResponsePromise = fetch(request)
.then((networkResponse) => {
if (networkResponse.ok) {
const cache = caches.open(DYNAMIC_CACHE);
cache.then(c => c.put(request, networkResponse.clone()));
}
return networkResponse;
})
.catch(() => null);
// 캐시된 응답이 있으면 즉시 반환, 없으면 네트워크 대기
return cachedResponse || await networkResponsePromise;
}
/**
* 캐시 전략 결정
*/
function getCacheStrategy(url) {
for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
if (patterns.some(pattern => pattern.test(url))) {
return strategy;
}
}
return 'networkFirst'; // 기본값
}
/**
* CDN 리소스 확인
*/
function isCDNResource(url) {
const cdnDomains = [
'cdn.tailwindcss.com',
'cdnjs.cloudflare.com',
'fonts.googleapis.com',
'fonts.gstatic.com'
];
return cdnDomains.some(domain => url.hostname.includes(domain));
}
/**
* 오프라인 처리
*/
async function handleOffline(request) {
// HTML 요청에 대한 오프라인 페이지
if (request.destination === 'document') {
const offlinePage = await caches.match('/index.html');
if (offlinePage) {
return offlinePage;
}
}
// 이미지 요청에 대한 기본 이미지
if (request.destination === 'image') {
return new Response(
'<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><rect width="200" height="200" fill="#f3f4f6"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#9ca3af">오프라인</text></svg>',
{ headers: { 'Content-Type': 'image/svg+xml' } }
);
}
// 기본 오프라인 응답
return new Response('오프라인 상태입니다.', {
status: 503,
statusText: 'Service Unavailable',
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
/**
* 메시지 처리 (캐시 관리)
*/
self.addEventListener('message', (event) => {
const { type, payload } = event.data;
switch (type) {
case 'CLEAR_CACHE':
clearAllCaches().then(() => {
event.ports[0].postMessage({ success: true });
});
break;
case 'CACHE_PAGE':
cachePage(payload.url).then(() => {
event.ports[0].postMessage({ success: true });
});
break;
case 'GET_CACHE_STATUS':
getCacheStatus().then((status) => {
event.ports[0].postMessage({ status });
});
break;
}
});
/**
* 모든 캐시 정리
*/
async function clearAllCaches() {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
console.log('🗑️ 모든 캐시 정리 완료');
}
/**
* 특정 페이지 캐시
*/
async function cachePage(url) {
try {
const cache = await caches.open(DYNAMIC_CACHE);
await cache.add(url);
console.log('📦 페이지 캐시 완료:', url);
} catch (error) {
console.error('페이지 캐시 실패:', url, error);
}
}
/**
* 캐시 상태 조회
*/
async function getCacheStatus() {
const cacheNames = await caches.keys();
const status = {};
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
status[cacheName] = keys.length;
}
return status;
}