/** * 페이지 프리로더 * 사용자가 방문할 가능성이 높은 페이지들을 미리 로드하여 성능 향상 */ class PagePreloader { constructor() { this.preloadedPages = new Set(); this.preloadQueue = []; this.isPreloading = false; this.preloadCache = new Map(); this.resourceCache = new Map(); } /** * 프리로더 초기화 */ init() { // 유휴 시간에 프리로딩 시작 this.schedulePreloading(); // 링크 호버 시 프리로딩 this.setupHoverPreloading(); // 서비스 워커 등록 (캐싱용) this.registerServiceWorker(); } /** * 우선순위 기반 프리로딩 스케줄링 */ schedulePreloading() { // 현재 사용자 권한에 따른 접근 가능한 페이지들 const accessiblePages = this.getAccessiblePages(); // 우선순위 설정 const priorityPages = this.getPriorityPages(accessiblePages); // 유휴 시간에 프리로딩 시작 if ('requestIdleCallback' in window) { requestIdleCallback(() => { this.startPreloading(priorityPages); }, { timeout: 2000 }); } else { // requestIdleCallback 미지원 브라우저 setTimeout(() => { this.startPreloading(priorityPages); }, 1000); } } /** * 접근 가능한 페이지 목록 가져오기 */ getAccessiblePages() { const allPages = [ { id: 'issues_create', url: '/index.html', priority: 1 }, { id: 'issues_view', url: '/issue-view.html', priority: 1 }, { id: 'issues_manage', url: '/index.html#list', priority: 2 }, { id: 'projects_manage', url: '/project-management.html', priority: 3 }, { id: 'daily_work', url: '/daily-work.html', priority: 2 }, { id: 'reports', url: '/reports.html', priority: 3 }, { id: 'users_manage', url: '/admin.html', priority: 4 } ]; // 권한 체크 return allPages.filter(page => { if (!window.canAccessPage) return false; return window.canAccessPage(page.id); }); } /** * 우선순위 기반 페이지 정렬 */ getPriorityPages(pages) { return pages .sort((a, b) => a.priority - b.priority) .slice(0, 3); // 최대 3개 페이지만 프리로드 } /** * 프리로딩 시작 */ async startPreloading(pages) { if (this.isPreloading) return; this.isPreloading = true; console.log('🚀 페이지 프리로딩 시작:', pages.map(p => p.id)); for (const page of pages) { if (this.preloadedPages.has(page.url)) continue; try { await this.preloadPage(page); // 네트워크 상태 확인 (느린 연결에서는 중단) if (this.isSlowConnection()) { console.log('⚠️ 느린 연결 감지, 프리로딩 중단'); break; } // CPU 부하 방지를 위한 딜레이 await this.delay(500); } catch (error) { console.warn('프리로딩 실패:', page.id, error); } } this.isPreloading = false; console.log('✅ 페이지 프리로딩 완료'); } /** * 개별 페이지 프리로드 */ async preloadPage(page) { try { // HTML 프리로드 const htmlResponse = await fetch(page.url, { method: 'GET', headers: { 'Accept': 'text/html' } }); if (htmlResponse.ok) { const html = await htmlResponse.text(); this.preloadCache.set(page.url, html); // 페이지 내 리소스 추출 및 프리로드 await this.preloadPageResources(html, page.url); this.preloadedPages.add(page.url); console.log(`📄 프리로드 완료: ${page.id}`); } } catch (error) { console.warn(`프리로드 실패: ${page.id}`, error); } } /** * 페이지 리소스 프리로드 (CSS, JS) */ async preloadPageResources(html, baseUrl) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // CSS 파일 프리로드 const cssLinks = doc.querySelectorAll('link[rel="stylesheet"]'); for (const link of cssLinks) { const href = this.resolveUrl(link.href, baseUrl); if (!this.resourceCache.has(href)) { this.preloadResource(href, 'style'); } } // JS 파일 프리로드 (중요한 것만) const scriptTags = doc.querySelectorAll('script[src]'); for (const script of scriptTags) { const src = this.resolveUrl(script.src, baseUrl); if (this.isImportantScript(src) && !this.resourceCache.has(src)) { this.preloadResource(src, 'script'); } } } /** * 리소스 프리로드 */ preloadResource(url, type) { const link = document.createElement('link'); link.rel = 'preload'; link.href = url; link.as = type; link.onload = () => { this.resourceCache.set(url, true); }; link.onerror = () => { console.warn('리소스 프리로드 실패:', url); }; document.head.appendChild(link); } /** * 중요한 스크립트 판별 */ isImportantScript(src) { const importantScripts = [ 'api.js', 'permissions.js', 'common-header.js', 'page-manager.js' ]; return importantScripts.some(script => src.includes(script)); } /** * URL 해결 */ resolveUrl(url, baseUrl) { if (url.startsWith('http') || url.startsWith('//')) { return url; } const base = new URL(baseUrl, window.location.origin); return new URL(url, base).href; } /** * 호버 시 프리로딩 설정 */ setupHoverPreloading() { let hoverTimeout; document.addEventListener('mouseover', (e) => { const link = e.target.closest('a[href]'); if (!link) return; const href = link.getAttribute('href'); if (!href || href.startsWith('#') || href.startsWith('javascript:')) return; // 300ms 후 프리로드 (실제 클릭 의도 확인) hoverTimeout = setTimeout(() => { this.preloadOnHover(href); }, 300); }); document.addEventListener('mouseout', (e) => { if (hoverTimeout) { clearTimeout(hoverTimeout); hoverTimeout = null; } }); } /** * 호버 시 프리로드 */ async preloadOnHover(url) { if (this.preloadedPages.has(url)) return; try { const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'text/html' } }); if (response.ok) { const html = await response.text(); this.preloadCache.set(url, html); this.preloadedPages.add(url); console.log('🖱️ 호버 프리로드 완료:', url); } } catch (error) { console.warn('호버 프리로드 실패:', url, error); } } /** * 느린 연결 감지 */ isSlowConnection() { if ('connection' in navigator) { const connection = navigator.connection; return connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g' || connection.saveData === true; } return false; } /** * 딜레이 유틸리티 */ delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 서비스 워커 등록 */ async registerServiceWorker() { if ('serviceWorker' in navigator) { try { const registration = await navigator.serviceWorker.register('/sw.js'); console.log('🔧 서비스 워커 등록 완료:', registration); } catch (error) { console.log('서비스 워커 등록 실패:', error); } } } /** * 프리로드된 페이지 가져오기 */ getPreloadedPage(url) { return this.preloadCache.get(url); } /** * 캐시 정리 */ clearCache() { this.preloadCache.clear(); this.resourceCache.clear(); this.preloadedPages.clear(); console.log('🗑️ 프리로드 캐시 정리 완료'); } } // 전역 인스턴스 window.pagePreloader = new PagePreloader();