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