// /js/app-init.js // 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드 // 모든 페이지에서 이 하나의 스크립트만 로드하면 됨 (function() { 'use strict'; // ===== 캐시 설정 ===== const CACHE_DURATION = 10 * 60 * 1000; // 10분 const COMPONENT_CACHE_PREFIX = 'component_'; // ===== 인증 함수 ===== function isLoggedIn() { const token = localStorage.getItem('token'); return token && token !== 'undefined' && token !== 'null'; } function getUser() { const user = localStorage.getItem('user'); return user ? JSON.parse(user) : null; } function clearAuthData() { localStorage.removeItem('token'); localStorage.removeItem('user'); localStorage.removeItem('userPageAccess'); } // ===== 페이지 권한 캐시 ===== let pageAccessPromise = null; async function getPageAccess(currentUser) { if (!currentUser || !currentUser.user_id) return null; // 캐시 확인 const cached = localStorage.getItem('userPageAccess'); if (cached) { try { const cacheData = JSON.parse(cached); if (Date.now() - cacheData.timestamp < CACHE_DURATION) { return cacheData.pages; } } catch (e) { localStorage.removeItem('userPageAccess'); } } // 이미 로딩 중이면 기존 Promise 반환 if (pageAccessPromise) return pageAccessPromise; // 새로운 API 호출 pageAccessPromise = (async () => { try { const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); if (!response.ok) return null; const data = await response.json(); const pages = data.data.pageAccess || []; localStorage.setItem('userPageAccess', JSON.stringify({ pages: pages, timestamp: Date.now() })); return pages; } catch (error) { console.error('페이지 권한 조회 오류:', error); return null; } finally { pageAccessPromise = null; } })(); return pageAccessPromise; } async function getAccessiblePageKeys(currentUser) { const pages = await getPageAccess(currentUser); if (!pages) return []; return pages.filter(p => p.can_access === 1).map(p => p.page_key); } // ===== 현재 페이지 키 추출 ===== function getCurrentPageKey() { const path = window.location.pathname; if (!path.startsWith('/pages/')) return null; const pagePath = path.substring(7).replace('.html', ''); return pagePath.replace(/\//g, '.'); } // ===== 컴포넌트 로더 ===== async function loadComponent(name, selector, processor) { const container = document.querySelector(selector); if (!container) return; const paths = { 'navbar': '/components/navbar.html', 'sidebar-nav': '/components/sidebar-nav.html' }; const componentPath = paths[name]; if (!componentPath) return; try { const cacheKey = COMPONENT_CACHE_PREFIX + name; let html = sessionStorage.getItem(cacheKey); if (!html) { const response = await fetch(componentPath); if (!response.ok) throw new Error('컴포넌트 로드 실패'); html = await response.text(); try { sessionStorage.setItem(cacheKey, html); } catch (e) {} } if (processor) { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); await processor(doc); container.innerHTML = doc.body.innerHTML; } else { container.innerHTML = html; } } catch (error) { console.error(`컴포넌트 로드 오류 (${name}):`, error); } } // ===== 네비바 처리 ===== const ROLE_NAMES = { 'system admin': '시스템 관리자', 'admin': '관리자', 'leader': '그룹장', 'user': '작업자', 'support': '지원팀', 'default': '사용자' }; async function processNavbar(doc, currentUser, accessiblePageKeys) { const userRole = (currentUser.role || '').toLowerCase(); const isAdmin = userRole === 'admin' || userRole === 'system admin'; if (isAdmin) { doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible')); } else { doc.querySelectorAll('[data-page-key]').forEach(item => { const pageKey = item.getAttribute('data-page-key'); if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return; if (!accessiblePageKeys.includes(pageKey)) item.remove(); }); doc.querySelectorAll('.admin-only').forEach(el => el.remove()); } // 사용자 정보 표시 const displayName = currentUser.name || currentUser.username; const roleName = ROLE_NAMES[userRole] || ROLE_NAMES.default; const setElementText = (id, text) => { const el = doc.getElementById(id); if (el) el.textContent = text; }; setElementText('userName', displayName); setElementText('userRole', roleName); setElementText('userInitial', displayName.charAt(0)); } // ===== 사이드바 처리 ===== async function processSidebar(doc, currentUser, accessiblePageKeys) { const userRole = (currentUser.role || '').toLowerCase(); const accessLevel = (currentUser.access_level || '').toLowerCase(); // role 또는 access_level로 관리자 확인 const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' || accessLevel === 'admin' || accessLevel === 'system'; if (isAdmin) { doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible')); } else { doc.querySelectorAll('[data-page-key]').forEach(item => { const pageKey = item.getAttribute('data-page-key'); if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return; if (!accessiblePageKeys.includes(pageKey)) item.style.display = 'none'; }); doc.querySelectorAll('.nav-category.admin-only').forEach(el => el.remove()); } // 현재 페이지 하이라이트 const currentPath = window.location.pathname; doc.querySelectorAll('.nav-item').forEach(item => { const href = item.getAttribute('href'); if (href && currentPath.includes(href.replace(/^\//, ''))) { item.classList.add('active'); const category = item.closest('.nav-category'); if (category) category.classList.add('expanded'); } }); // 저장된 상태 복원 const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true'; const sidebar = doc.querySelector('.sidebar-nav'); if (isCollapsed && sidebar) { sidebar.classList.add('collapsed'); document.body.classList.add('sidebar-collapsed'); } const expandedCategories = JSON.parse(localStorage.getItem('sidebarExpanded') || '[]'); expandedCategories.forEach(category => { const el = doc.querySelector(`[data-category="${category}"]`); if (el) el.classList.add('expanded'); }); } // ===== 사이드바 이벤트 설정 ===== function setupSidebarEvents() { const sidebar = document.getElementById('sidebarNav'); const toggle = document.getElementById('sidebarToggle'); if (!sidebar || !toggle) return; toggle.addEventListener('click', () => { sidebar.classList.toggle('collapsed'); document.body.classList.toggle('sidebar-collapsed'); localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed')); }); sidebar.querySelectorAll('.nav-category-header').forEach(header => { header.addEventListener('click', () => { const category = header.closest('.nav-category'); category.classList.toggle('expanded'); const expanded = []; sidebar.querySelectorAll('.nav-category.expanded').forEach(cat => { const name = cat.getAttribute('data-category'); if (name) expanded.push(name); }); localStorage.setItem('sidebarExpanded', JSON.stringify(expanded)); }); }); } // ===== 네비바 이벤트 설정 ===== function setupNavbarEvents() { const logoutButton = document.getElementById('logoutBtn'); if (logoutButton) { logoutButton.addEventListener('click', () => { if (confirm('로그아웃 하시겠습니까?')) { clearAuthData(); window.location.href = '/index.html'; } }); } } // ===== 날짜/시간 업데이트 ===== function updateDateTime() { const now = new Date(); const timeEl = document.getElementById('timeValue'); if (timeEl) timeEl.textContent = now.toLocaleTimeString('ko-KR', { hour12: false }); const dateEl = document.getElementById('dateValue'); if (dateEl) { const days = ['일', '월', '화', '수', '목', '토']; dateEl.textContent = `${now.getMonth() + 1}월 ${now.getDate()}일 (${days[now.getDay()]})`; } } // ===== 날씨 업데이트 ===== const WEATHER_ICONS = { clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥', cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷', cloudy: '⛅', overcast: '☁️' }; const WEATHER_NAMES = { clear: '맑음', rain: '비', snow: '눈', heat: '폭염', cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지', cloudy: '구름많음', overcast: '흐림' }; async function updateWeather() { try { const token = localStorage.getItem('token'); if (!token) return; // 캐시 확인 const cached = sessionStorage.getItem('weatherCache'); let result; if (cached) { const cacheData = JSON.parse(cached); if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) { result = cacheData.data; } } if (!result) { const response = await fetch(`${window.API_BASE_URL}/tbm/weather/current`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) return; result = await response.json(); sessionStorage.setItem('weatherCache', JSON.stringify({ data: result, timestamp: Date.now() })); } if (result.success && result.data) { const { temperature, conditions } = result.data; const tempEl = document.getElementById('weatherTemp'); if (tempEl && temperature != null) tempEl.textContent = `${Math.round(temperature)}°C`; const iconEl = document.getElementById('weatherIcon'); const descEl = document.getElementById('weatherDesc'); if (conditions && conditions.length > 0) { const primary = conditions[0]; if (iconEl) iconEl.textContent = WEATHER_ICONS[primary] || '🌤️'; if (descEl) descEl.textContent = WEATHER_NAMES[primary] || '맑음'; } } } catch (error) { console.warn('날씨 정보 로드 실패'); } } // ===== 메인 초기화 ===== async function init() { console.log('🚀 app-init 시작'); // 1. 인증 확인 if (!isLoggedIn()) { clearAuthData(); window.location.href = '/index.html'; return; } const currentUser = getUser(); if (!currentUser || !currentUser.username) { clearAuthData(); window.location.href = '/index.html'; return; } console.log('✅ 인증 확인:', currentUser.username); const userRole = (currentUser.role || '').toLowerCase(); const accessLevel = (currentUser.access_level || '').toLowerCase(); // role 또는 access_level로 관리자 확인 const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' || accessLevel === 'admin' || accessLevel === 'system'; // 2. 페이지 접근 권한 체크 (Admin은 건너뛰기) let accessiblePageKeys = []; if (!isAdmin) { const pageKey = getCurrentPageKey(); if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) { accessiblePageKeys = await getAccessiblePageKeys(currentUser); if (!accessiblePageKeys.includes(pageKey)) { alert('이 페이지에 접근할 권한이 없습니다.'); window.location.href = '/pages/dashboard.html'; return; } } } // 3. 사이드바 컨테이너 생성 (없으면) let sidebarContainer = document.getElementById('sidebar-container'); if (!sidebarContainer) { sidebarContainer = document.createElement('div'); sidebarContainer.id = 'sidebar-container'; document.body.prepend(sidebarContainer); console.log('📦 사이드바 컨테이너 생성됨'); } // 4. 네비바와 사이드바 동시 로드 console.log('📥 컴포넌트 로딩 시작'); await Promise.all([ loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)), loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys)) ]); console.log('✅ 컴포넌트 로딩 완료'); // 5. 이벤트 설정 setupNavbarEvents(); setupSidebarEvents(); document.body.classList.add('has-sidebar'); // 6. 페이지 전환 로딩 인디케이터 설정 setupPageTransitionLoader(); // 7. 날짜/시간 (비동기) updateDateTime(); setInterval(updateDateTime, 1000); // 8. 날씨 (백그라운드) setTimeout(updateWeather, 100); console.log('✅ app-init 완료'); } // ===== 페이지 전환 로딩 인디케이터 ===== function setupPageTransitionLoader() { // 로딩 바 스타일 추가 const style = document.createElement('style'); style.textContent = ` #page-loader { position: fixed; top: 0; left: 0; width: 0; height: 3px; background: linear-gradient(90deg, #3b82f6, #60a5fa); z-index: 99999; transition: width 0.3s ease; box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); } #page-loader.loading { width: 70%; } #page-loader.done { width: 100%; opacity: 0; transition: width 0.2s ease, opacity 0.3s ease 0.2s; } body.page-loading { cursor: wait; } body.page-loading * { pointer-events: none; } `; document.head.appendChild(style); // 로딩 바 엘리먼트 생성 const loader = document.createElement('div'); loader.id = 'page-loader'; document.body.appendChild(loader); // 모든 내부 링크에 클릭 이벤트 추가 document.addEventListener('click', (e) => { const link = e.target.closest('a'); if (!link) return; const href = link.getAttribute('href'); if (!href) return; // 외부 링크, 해시 링크, javascript: 링크 제외 if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return; // 새 탭 링크 제외 if (link.target === '_blank') return; // 로딩 시작 loader.classList.remove('done'); loader.classList.add('loading'); document.body.classList.add('page-loading'); }); // 페이지 떠날 때 완료 표시 window.addEventListener('beforeunload', () => { const loader = document.getElementById('page-loader'); if (loader) { loader.classList.remove('loading'); loader.classList.add('done'); } }); } // DOMContentLoaded 시 실행 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // 전역 노출 (필요시) window.appInit = { getUser, clearAuthData, isLoggedIn }; })();