diff --git a/frontend/index.html b/frontend/index.html index 01a3dcc..03480f4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -478,6 +478,7 @@ + @@ -544,66 +545,59 @@ } } - // API 로드 후 앱 초기화 + // API 로드 후 앱 초기화 (AuthManager 사용) async function initializeApp() { - console.log('🚀 앱 초기화 시작'); + console.log('🚀 앱 초기화 시작 (AuthManager 사용)'); - // 토큰이 있으면 사용자 정보 가져오기 - const token = localStorage.getItem('access_token'); - if (token) { - try { - // 토큰으로 사용자 정보 가져오기 (API 호출) - const user = await AuthAPI.getCurrentUser(); + try { + // AuthManager를 통한 인증 체크 (캐시 우선, 필요시에만 API 호출) + const user = await window.authManager.checkAuth(); + + if (user) { currentUser = user; - // localStorage에도 백업 저장 - localStorage.setItem('currentUser', JSON.stringify(user)); + // 공통 헤더 초기화 + console.log('🔧 공통 헤더 초기화 시작:', user.username); - // 공통 헤더 초기화 - console.log('🔧 공통 헤더 초기화 시작:', user); - console.log('window.commonHeader 존재:', !!window.commonHeader); - - if (window.commonHeader && typeof window.commonHeader.init === 'function') { - await window.commonHeader.init(user, 'issues_create'); - console.log('✅ 공통 헤더 초기화 완료'); - } else { - console.error('❌ 공통 헤더 모듈이 로드되지 않음'); - // 대안: 기본 사용자 정보 표시 - setTimeout(() => { - if (window.commonHeader && typeof window.commonHeader.init === 'function') { - console.log('🔄 지연된 공통 헤더 초기화'); - window.commonHeader.init(user, 'issues_create'); - } - }, 200); - } + if (window.commonHeader && typeof window.commonHeader.init === 'function') { + await window.commonHeader.init(user, 'issues_create'); + console.log('✅ 공통 헤더 초기화 완료'); + } else { + console.error('❌ 공통 헤더 모듈이 로드되지 않음'); + setTimeout(() => { + if (window.commonHeader && typeof window.commonHeader.init === 'function') { + console.log('🔄 지연된 공통 헤더 초기화'); + window.commonHeader.init(user, 'issues_create'); + } + }, 200); + } - // 페이지 접근 권한 체크 (부적합 등록 페이지) + // 페이지 접근 권한 체크 setTimeout(() => { - if (!canAccessPage('issues_create')) { + if (typeof canAccessPage === 'function' && !canAccessPage('issues_create')) { alert('부적합 등록 페이지에 접근할 권한이 없습니다.'); window.location.href = '/issue-view.html'; return; } }, 500); - // 사용자 정보는 공통 헤더에서 표시됨 + // 메인 화면 표시 document.getElementById('loginScreen').classList.add('hidden'); document.getElementById('mainScreen').classList.remove('hidden'); - // 프로젝트 로드 + // 데이터 로드 await loadProjects(); - loadIssues(); - - // URL 해시 처리 handleUrlHash(); - } catch (error) { - console.error('토큰 검증 실패:', error); - // 토큰이 유효하지 않으면 로그아웃 - localStorage.removeItem('access_token'); - localStorage.removeItem('currentUser'); + } else { + console.log('❌ 인증되지 않은 사용자 - 로그인 화면 표시'); + // 로그인 화면은 이미 기본으로 표시됨 } + + } catch (error) { + console.error('❌ 앱 초기화 실패:', error); + // 로그인 화면 표시 (기본 상태) } } @@ -612,41 +606,43 @@ console.log('📄 DOM 로드 완료 - API 스크립트 로딩 대기 중...'); }); - // 로그인 + // 로그인 (AuthManager 사용) document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const userId = document.getElementById('userId').value; const password = document.getElementById('password').value; try { - const data = await AuthAPI.login(userId, password); + console.log('🔑 AuthManager를 통한 로그인 시도'); + const data = await window.authManager.login(userId, password); currentUser = data.user; - // 토큰과 사용자 정보 저장 - localStorage.setItem('access_token', data.access_token); - localStorage.setItem('currentUser', JSON.stringify(currentUser)); + console.log('✅ 로그인 성공 - 메인 화면 초기화'); - // 사용자 정보는 공통 헤더에서 표시됨 + // 공통 헤더 초기화 + if (window.commonHeader && typeof window.commonHeader.init === 'function') { + await window.commonHeader.init(currentUser, 'issues_create'); + } + + // 메인 화면 표시 document.getElementById('loginScreen').classList.add('hidden'); document.getElementById('mainScreen').classList.remove('hidden'); - // 공통 헤더에서 권한 기반 메뉴 처리됨 - - // 프로젝트 로드 + // 데이터 로드 await loadProjects(); - loadIssues(); - - // URL 해시 처리 handleUrlHash(); + } catch (error) { + console.error('❌ 로그인 실패:', error); alert(error.message || '로그인에 실패했습니다.'); } }); - // 로그아웃 + // 로그아웃 (AuthManager 사용) function logout() { - AuthAPI.logout(); + console.log('🚪 AuthManager를 통한 로그아웃'); + window.authManager.logout(); } // 네비게이션은 공통 헤더에서 처리됨 diff --git a/frontend/static/js/core/auth-manager.js b/frontend/static/js/core/auth-manager.js new file mode 100644 index 0000000..2c3c079 --- /dev/null +++ b/frontend/static/js/core/auth-manager.js @@ -0,0 +1,263 @@ +/** + * 중앙화된 인증 관리자 + * 페이지 간 이동 시 불필요한 API 호출을 방지하고 인증 상태를 효율적으로 관리 + */ +class AuthManager { + constructor() { + this.currentUser = null; + this.isAuthenticated = false; + this.lastAuthCheck = null; + this.authCheckInterval = 5 * 60 * 1000; // 5분마다 토큰 유효성 체크 + this.listeners = new Set(); + + // 초기화 + this.init(); + } + + /** + * 초기화 + */ + init() { + console.log('🔐 AuthManager 초기화'); + + // localStorage에서 사용자 정보 복원 + this.restoreUserFromStorage(); + + // 토큰 만료 체크 타이머 설정 + this.setupTokenExpiryCheck(); + + // 페이지 가시성 변경 시 토큰 체크 + document.addEventListener('visibilitychange', () => { + if (!document.hidden && this.shouldCheckAuth()) { + this.refreshAuth(); + } + }); + } + + /** + * localStorage에서 사용자 정보 복원 + */ + restoreUserFromStorage() { + const token = localStorage.getItem('access_token'); + const userStr = localStorage.getItem('currentUser'); + + if (token && userStr) { + try { + this.currentUser = JSON.parse(userStr); + this.isAuthenticated = true; + this.lastAuthCheck = Date.now(); + console.log('✅ 저장된 사용자 정보 복원:', this.currentUser.username); + } catch (error) { + console.error('❌ 사용자 정보 복원 실패:', error); + this.clearAuth(); + } + } + } + + /** + * 인증이 필요한지 확인 + */ + shouldCheckAuth() { + if (!this.isAuthenticated) return true; + if (!this.lastAuthCheck) return true; + + const timeSinceLastCheck = Date.now() - this.lastAuthCheck; + return timeSinceLastCheck > this.authCheckInterval; + } + + /** + * 인증 상태 확인 (필요시에만 API 호출) + */ + async checkAuth() { + console.log('🔍 인증 상태 확인 시작'); + + const token = localStorage.getItem('access_token'); + if (!token) { + console.log('❌ 토큰 없음'); + this.clearAuth(); + return null; + } + + // 최근에 체크했으면 캐시된 정보 사용 + if (this.isAuthenticated && !this.shouldCheckAuth()) { + console.log('✅ 캐시된 인증 정보 사용:', this.currentUser.username); + return this.currentUser; + } + + // API 호출이 필요한 경우 + return await this.refreshAuth(); + } + + /** + * 강제로 인증 정보 새로고침 (API 호출) + */ + async refreshAuth() { + console.log('🔄 인증 정보 새로고침 (API 호출)'); + + try { + // API가 로드될 때까지 대기 + await this.waitForAPI(); + + const user = await AuthAPI.getCurrentUser(); + + this.currentUser = user; + this.isAuthenticated = true; + this.lastAuthCheck = Date.now(); + + // localStorage 업데이트 + localStorage.setItem('currentUser', JSON.stringify(user)); + + console.log('✅ 인증 정보 새로고침 완료:', user.username); + + // 리스너들에게 알림 + this.notifyListeners('auth-success', user); + + return user; + + } catch (error) { + console.error('❌ 인증 실패:', error); + this.clearAuth(); + this.notifyListeners('auth-failed', error); + throw error; + } + } + + /** + * API 로드 대기 + */ + async waitForAPI() { + let attempts = 0; + const maxAttempts = 50; + + while (typeof AuthAPI === 'undefined' && attempts < maxAttempts) { + await new Promise(resolve => setTimeout(resolve, 100)); + attempts++; + } + + if (typeof AuthAPI === 'undefined') { + throw new Error('AuthAPI를 로드할 수 없습니다'); + } + } + + /** + * 인증 정보 클리어 + */ + clearAuth() { + console.log('🧹 인증 정보 클리어'); + + this.currentUser = null; + this.isAuthenticated = false; + this.lastAuthCheck = null; + + localStorage.removeItem('access_token'); + localStorage.removeItem('currentUser'); + + this.notifyListeners('auth-cleared'); + } + + /** + * 로그인 처리 + */ + async login(username, password) { + console.log('🔑 로그인 시도:', username); + + try { + await this.waitForAPI(); + const data = await AuthAPI.login(username, password); + + this.currentUser = data.user; + this.isAuthenticated = true; + this.lastAuthCheck = Date.now(); + + // localStorage 저장 + localStorage.setItem('access_token', data.access_token); + localStorage.setItem('currentUser', JSON.stringify(data.user)); + + console.log('✅ 로그인 성공:', data.user.username); + + this.notifyListeners('login-success', data.user); + + return data; + + } catch (error) { + console.error('❌ 로그인 실패:', error); + this.clearAuth(); + throw error; + } + } + + /** + * 로그아웃 처리 + */ + logout() { + console.log('🚪 로그아웃'); + + this.clearAuth(); + this.notifyListeners('logout'); + + // 로그인 페이지로 이동 + window.location.href = '/index.html'; + } + + /** + * 토큰 만료 체크 타이머 설정 + */ + setupTokenExpiryCheck() { + // 30분마다 토큰 유효성 체크 + setInterval(() => { + if (this.isAuthenticated) { + console.log('⏰ 정기 토큰 유효성 체크'); + this.refreshAuth().catch(() => { + console.log('🔄 토큰 만료 - 로그아웃 처리'); + this.logout(); + }); + } + }, 30 * 60 * 1000); + } + + /** + * 이벤트 리스너 등록 + */ + addEventListener(callback) { + this.listeners.add(callback); + } + + /** + * 이벤트 리스너 제거 + */ + removeEventListener(callback) { + this.listeners.delete(callback); + } + + /** + * 리스너들에게 알림 + */ + notifyListeners(event, data = null) { + this.listeners.forEach(callback => { + try { + callback(event, data); + } catch (error) { + console.error('리스너 콜백 오류:', error); + } + }); + } + + /** + * 현재 사용자 정보 반환 + */ + getCurrentUser() { + return this.currentUser; + } + + /** + * 인증 상태 반환 + */ + isLoggedIn() { + return this.isAuthenticated && !!this.currentUser; + } +} + +// 전역 인스턴스 생성 +window.authManager = new AuthManager(); + +console.log('🎯 AuthManager 로드 완료');