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:
368
frontend/static/js/core/page-manager.js
Normal file
368
frontend/static/js/core/page-manager.js
Normal file
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* 페이지 관리자
|
||||
* 모듈화된 페이지들의 생명주기를 관리하고 부드러운 전환을 제공
|
||||
*/
|
||||
|
||||
class PageManager {
|
||||
constructor() {
|
||||
this.currentPage = null;
|
||||
this.loadedModules = new Map();
|
||||
this.pageHistory = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 초기화
|
||||
* @param {string} pageId - 페이지 식별자
|
||||
* @param {Object} options - 초기화 옵션
|
||||
*/
|
||||
async initializePage(pageId, options = {}) {
|
||||
try {
|
||||
// 로딩 표시
|
||||
this.showPageLoader();
|
||||
|
||||
// 사용자 인증 확인
|
||||
const user = await this.checkAuthentication();
|
||||
if (!user) return;
|
||||
|
||||
// 공통 헤더 초기화
|
||||
await this.initializeCommonHeader(user, pageId);
|
||||
|
||||
// 페이지별 권한 체크
|
||||
if (!this.checkPagePermission(pageId, user)) {
|
||||
this.redirectToAccessiblePage();
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지 모듈 로드 및 초기화
|
||||
await this.loadPageModule(pageId, options);
|
||||
|
||||
// 페이지 히스토리 업데이트
|
||||
this.updatePageHistory(pageId);
|
||||
|
||||
// 로딩 숨기기
|
||||
this.hidePageLoader();
|
||||
|
||||
} catch (error) {
|
||||
console.error('페이지 초기화 실패:', error);
|
||||
this.showErrorPage(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 인증 확인
|
||||
*/
|
||||
async checkAuthentication() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
window.location.href = '/index.html';
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// API가 로드될 때까지 대기
|
||||
await this.waitForAPI();
|
||||
|
||||
const user = await AuthAPI.getCurrentUser();
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error('인증 실패:', error);
|
||||
localStorage.removeItem('access_token');
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.href = '/index.html';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API 로드 대기
|
||||
*/
|
||||
async waitForAPI() {
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
while (!window.AuthAPI && attempts < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!window.AuthAPI) {
|
||||
throw new Error('API를 로드할 수 없습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 공통 헤더 초기화
|
||||
*/
|
||||
async initializeCommonHeader(user, pageId) {
|
||||
// 권한 시스템 초기화
|
||||
if (window.pagePermissionManager) {
|
||||
window.pagePermissionManager.setUser(user);
|
||||
}
|
||||
|
||||
// 공통 헤더 초기화
|
||||
if (window.commonHeader) {
|
||||
await window.commonHeader.init(user, pageId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 권한 체크
|
||||
*/
|
||||
checkPagePermission(pageId, user) {
|
||||
// admin은 모든 페이지 접근 가능
|
||||
if (user.role === 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 권한 시스템이 로드되지 않았으면 기본 페이지만 허용
|
||||
if (!window.canAccessPage) {
|
||||
return ['issues_create', 'issues_view'].includes(pageId);
|
||||
}
|
||||
|
||||
return window.canAccessPage(pageId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 가능한 페이지로 리다이렉트
|
||||
*/
|
||||
redirectToAccessiblePage() {
|
||||
alert('이 페이지에 접근할 권한이 없습니다.');
|
||||
|
||||
// 기본적으로 접근 가능한 페이지로 이동
|
||||
if (window.canAccessPage && window.canAccessPage('issues_view')) {
|
||||
window.location.href = '/issue-view.html';
|
||||
} else {
|
||||
window.location.href = '/index.html';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 모듈 로드
|
||||
*/
|
||||
async loadPageModule(pageId, options) {
|
||||
// 이미 로드된 모듈이 있으면 재사용
|
||||
if (this.loadedModules.has(pageId)) {
|
||||
const module = this.loadedModules.get(pageId);
|
||||
if (module.reinitialize) {
|
||||
await module.reinitialize(options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 페이지별 모듈 로드
|
||||
const module = await this.createPageModule(pageId, options);
|
||||
if (module) {
|
||||
this.loadedModules.set(pageId, module);
|
||||
this.currentPage = pageId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 모듈 생성
|
||||
*/
|
||||
async createPageModule(pageId, options) {
|
||||
switch (pageId) {
|
||||
case 'issues_create':
|
||||
return new IssuesCreateModule(options);
|
||||
case 'issues_view':
|
||||
return new IssuesViewModule(options);
|
||||
case 'issues_manage':
|
||||
return new IssuesManageModule(options);
|
||||
case 'projects_manage':
|
||||
return new ProjectsManageModule(options);
|
||||
case 'daily_work':
|
||||
return new DailyWorkModule(options);
|
||||
case 'reports':
|
||||
return new ReportsModule(options);
|
||||
case 'users_manage':
|
||||
return new UsersManageModule(options);
|
||||
default:
|
||||
console.warn(`알 수 없는 페이지 ID: ${pageId}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 히스토리 업데이트
|
||||
*/
|
||||
updatePageHistory(pageId) {
|
||||
this.pageHistory.push({
|
||||
pageId,
|
||||
timestamp: new Date(),
|
||||
url: window.location.href
|
||||
});
|
||||
|
||||
// 히스토리 크기 제한 (최대 10개)
|
||||
if (this.pageHistory.length > 10) {
|
||||
this.pageHistory.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 로더 표시
|
||||
*/
|
||||
showPageLoader() {
|
||||
const existingLoader = document.getElementById('page-loader');
|
||||
if (existingLoader) return;
|
||||
|
||||
const loader = document.createElement('div');
|
||||
loader.id = 'page-loader';
|
||||
loader.className = 'fixed inset-0 bg-white bg-opacity-90 flex items-center justify-center z-50';
|
||||
loader.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mb-4"></div>
|
||||
<p class="text-lg font-medium text-gray-700">페이지를 로드하는 중...</p>
|
||||
<p class="text-sm text-gray-500 mt-1">잠시만 기다려주세요</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(loader);
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 로더 숨기기
|
||||
*/
|
||||
hidePageLoader() {
|
||||
const loader = document.getElementById('page-loader');
|
||||
if (loader) {
|
||||
loader.remove();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 페이지 표시
|
||||
*/
|
||||
showErrorPage(error) {
|
||||
this.hidePageLoader();
|
||||
|
||||
const errorContainer = document.createElement('div');
|
||||
errorContainer.className = 'fixed inset-0 bg-gray-50 flex items-center justify-center z-50';
|
||||
errorContainer.innerHTML = `
|
||||
<div class="text-center max-w-md mx-auto p-8">
|
||||
<div class="mb-6">
|
||||
<i class="fas fa-exclamation-triangle text-6xl text-red-500"></i>
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-4">페이지 로드 실패</h2>
|
||||
<p class="text-gray-600 mb-6">${error.message || '알 수 없는 오류가 발생했습니다.'}</p>
|
||||
<div class="space-x-4">
|
||||
<button onclick="window.location.reload()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
다시 시도
|
||||
</button>
|
||||
<button onclick="window.location.href='/index.html'"
|
||||
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
|
||||
홈으로
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(errorContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 페이지 정리
|
||||
*/
|
||||
cleanup() {
|
||||
if (this.currentPage && this.loadedModules.has(this.currentPage)) {
|
||||
const module = this.loadedModules.get(this.currentPage);
|
||||
if (module.cleanup) {
|
||||
module.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 페이지 모듈 클래스
|
||||
* 모든 페이지 모듈이 상속받아야 하는 기본 클래스
|
||||
*/
|
||||
class BasePageModule {
|
||||
constructor(options = {}) {
|
||||
this.options = options;
|
||||
this.initialized = false;
|
||||
this.eventListeners = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 초기화 (하위 클래스에서 구현)
|
||||
*/
|
||||
async initialize() {
|
||||
throw new Error('initialize 메서드를 구현해야 합니다.');
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 재초기화
|
||||
*/
|
||||
async reinitialize(options = {}) {
|
||||
this.cleanup();
|
||||
this.options = { ...this.options, ...options };
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트 리스너 등록 (자동 정리를 위해)
|
||||
*/
|
||||
addEventListener(element, event, handler) {
|
||||
element.addEventListener(event, handler);
|
||||
this.eventListeners.push({ element, event, handler });
|
||||
}
|
||||
|
||||
/**
|
||||
* 모듈 정리
|
||||
*/
|
||||
cleanup() {
|
||||
// 등록된 이벤트 리스너 제거
|
||||
this.eventListeners.forEach(({ element, event, handler }) => {
|
||||
element.removeEventListener(event, handler);
|
||||
});
|
||||
this.eventListeners = [];
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로딩 표시
|
||||
*/
|
||||
showLoading(container, message = '로딩 중...') {
|
||||
if (typeof container === 'string') {
|
||||
container = document.getElementById(container);
|
||||
}
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-center">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mb-3"></div>
|
||||
<p class="text-gray-600">${message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 표시
|
||||
*/
|
||||
showError(container, message = '오류가 발생했습니다.') {
|
||||
if (typeof container === 'string') {
|
||||
container = document.getElementById(container);
|
||||
}
|
||||
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="flex items-center justify-center py-12">
|
||||
<div class="text-center">
|
||||
<i class="fas fa-exclamation-triangle text-4xl text-red-500 mb-3"></i>
|
||||
<p class="text-gray-600">${message}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 인스턴스
|
||||
window.pageManager = new PageManager();
|
||||
window.BasePageModule = BasePageModule;
|
||||
Reference in New Issue
Block a user