- 권한 기반 공통 헤더 컴포넌트 구현 - 모바일 친화적 캘린더 날짜 필터 추가 - 페이지 프리로더 및 키보드 단축키 시스템 구현 - 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 (구버전 파일들)
318 lines
9.2 KiB
JavaScript
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();
|