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 (구버전 파일들)
This commit is contained in:
317
frontend/static/js/core/page-preloader.js
Normal file
317
frontend/static/js/core/page-preloader.js
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 페이지 프리로더
|
||||
* 사용자가 방문할 가능성이 높은 페이지들을 미리 로드하여 성능 향상
|
||||
*/
|
||||
|
||||
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();
|
||||
Reference in New Issue
Block a user