Files
M-Project/frontend/static/js/core/page-manager.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

369 lines
11 KiB
JavaScript

/**
* 페이지 관리자
* 모듈화된 페이지들의 생명주기를 관리하고 부드러운 전환을 제공
*/
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;