Files
M-Project/frontend/static/js/core/page-preloader.js
Hyungi Ahn 25123be806 feat: 프론트엔드 모듈화 및 공통 헤더 시스템 구현
- 권한 기반 공통 헤더 컴포넌트 구현
- 모바일 친화적 캘린더 날짜 필터 추가
- 페이지 프리로더 및 키보드 단축키 시스템 구현
- Service Worker 기반 캐싱 시스템 추가

Frontend Changes:
- components/common-header.js: 권한 기반 동적 메뉴 생성
- components/mobile-calendar.js: 터치/스와이프 지원 캘린더
- core/permissions.js: 페이지 접근 권한 관리
- core/page-manager.js: 페이지 라이프사이클 관리
- core/page-preloader.js: 페이지 프리로딩 최적화
- core/keyboard-shortcuts.js: 키보드 네비게이션
- css/mobile-calendar.css: 모바일 최적화 캘린더 스타일
- sw.js: 3단계 캐싱 전략 서비스 워커

Removed:
- auth-common.js, common-header.js (구버전 파일들)
2025-10-25 09:00:30 +09:00

318 lines
9.2 KiB
JavaScript

/**
* 페이지 프리로더
* 사용자가 방문할 가능성이 높은 페이지들을 미리 로드하여 성능 향상
*/
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: '/issue-view.html#manage', 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();