// /js/load-navbar.js import { getUser, clearAuthData } from './auth.js'; import { loadComponent } from './component-loader.js'; import { config } from './config.js'; // 역할 이름을 한글로 변환하는 맵 const ROLE_NAMES = { 'system admin': '시스템 관리자', 'admin': '관리자', 'system': '시스템 관리자', 'leader': '그룹장', 'user': '작업자', 'support': '지원팀', 'default': '사용자', }; /** * 네비게이션 바 DOM을 사용자 정보와 역할에 맞게 수정하는 프로세서입니다. * @param {Document} doc - 파싱된 HTML 문서 객체 */ async function processNavbarDom(doc) { const currentUser = getUser(); if (!currentUser) return; // 1. 역할 및 페이지 권한 기반 메뉴 필터링 await filterMenuByPageAccess(doc, currentUser); // 2. 사용자 정보 채우기 populateUserInfo(doc, currentUser); } /** * 사용자의 페이지 접근 권한에 따라 메뉴 항목을 필터링합니다. * @param {Document} doc - 파싱된 HTML 문서 객체 * @param {object} currentUser - 현재 사용자 객체 */ async function filterMenuByPageAccess(doc, currentUser) { const userRole = (currentUser.role || '').toLowerCase(); // Admin은 모든 메뉴 표시 + .admin-only 요소 활성화 if (userRole === 'admin' || userRole === 'system admin') { doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible')); return; } try { // 사용자의 페이지 접근 권한 조회 const cached = localStorage.getItem('userPageAccess'); let accessiblePages = null; if (cached) { const cacheData = JSON.parse(cached); // 캐시가 5분 이내인 경우 사용 if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) { accessiblePages = cacheData.pages; } } // 캐시가 없으면 API 호출 if (!accessiblePages) { 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) { console.error('페이지 권한 조회 실패:', response.status); return; } const data = await response.json(); accessiblePages = data.data.pageAccess || []; // 캐시 저장 localStorage.setItem('userPageAccess', JSON.stringify({ pages: accessiblePages, timestamp: Date.now() })); } // 접근 가능한 페이지 키 목록 const accessiblePageKeys = accessiblePages .filter(p => p.can_access === 1) .map(p => p.page_key); // 메뉴 항목에 data-page-key 속성이 있으면 해당 권한 체크 const menuItems = doc.querySelectorAll('[data-page-key]'); menuItems.forEach(item => { const pageKey = item.getAttribute('data-page-key'); // 대시보드와 프로필 페이지는 모든 사용자 접근 가능 if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) { return; } // 권한이 없으면 메뉴 항목 제거 if (!accessiblePageKeys.includes(pageKey)) { item.remove(); } }); // Admin 전용 메뉴는 무조건 제거 doc.querySelectorAll('.admin-only').forEach(el => el.remove()); } catch (error) { console.error('메뉴 필터링 오류:', error); } } /** * 네비게이션 바에 사용자 정보를 채웁니다. * @param {Document} doc - 파싱된 HTML 문서 객체 * @param {object} user - 현재 사용자 객체 */ function populateUserInfo(doc, user) { const displayName = user.name || user.username; // 대소문자 구분 없이 처리 const roleLower = (user.role || '').toLowerCase(); const roleName = ROLE_NAMES[roleLower] || ROLE_NAMES.default; const elements = { 'userName': displayName, 'userRole': roleName, 'userInitial': displayName.charAt(0), }; for (const id in elements) { const el = doc.getElementById(id); if (el) el.textContent = elements[id]; } // 메인 대시보드 URL 설정 const dashboardBtn = doc.getElementById('dashboardBtn'); if (dashboardBtn) { dashboardBtn.href = '/pages/dashboard.html'; } } /** * 네비게이션 바와 관련된 모든 이벤트를 설정합니다. */ function setupNavbarEvents() { const logoutButton = document.getElementById('logoutBtn'); if (logoutButton) { logoutButton.addEventListener('click', () => { if (confirm('로그아웃 하시겠습니까?')) { clearAuthData(); window.location.href = config.paths.loginPage; } }); } } /** * 현재 날짜와 시간을 업데이트하는 함수 */ function updateDateTime() { const now = new Date(); // 시간 업데이트 (시 분 초 형식으로 고정) const timeElement = document.getElementById('timeValue'); if (timeElement) { const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); timeElement.textContent = `${hours}시 ${minutes}분 ${seconds}초`; } // 날짜 업데이트 const dateElement = document.getElementById('dateValue'); if (dateElement) { const days = ['일', '월', '화', '수', '목', '금', '토']; const month = now.getMonth() + 1; const date = now.getDate(); const day = days[now.getDay()]; dateElement.textContent = `${month}월 ${date}일 (${day})`; } } // 날씨 아이콘 매핑 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 response = await fetch(`${window.API_BASE_URL}/tbm/weather/current`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); if (!response.ok) { throw new Error('날씨 API 호출 실패'); } const result = await response.json(); if (result.success && result.data) { const { temperature, conditions, weatherData } = result.data; // 온도 표시 const tempElement = document.getElementById('weatherTemp'); if (tempElement && temperature !== null && temperature !== undefined) { tempElement.textContent = `${Math.round(temperature)}°C`; } // 날씨 아이콘 및 설명 const iconElement = document.getElementById('weatherIcon'); const descElement = document.getElementById('weatherDesc'); if (conditions && conditions.length > 0) { const primaryCondition = conditions[0]; if (iconElement) { iconElement.textContent = WEATHER_ICONS[primaryCondition] || '🌤️'; } if (descElement) { descElement.textContent = WEATHER_NAMES[primaryCondition] || '맑음'; } } else { if (iconElement) iconElement.textContent = '☀️'; if (descElement) descElement.textContent = '맑음'; } // 날씨 섹션 표시 const weatherSection = document.getElementById('weatherSection'); if (weatherSection) { weatherSection.style.opacity = '1'; } } } catch (error) { console.warn('날씨 정보 로드 실패:', error.message); // 실패해도 기본값 표시 const descElement = document.getElementById('weatherDesc'); if (descElement) { descElement.textContent = '날씨 정보 없음'; } } } // ========================================== // 알림 시스템 // ========================================== /** * 알림 관련 이벤트 설정 */ function setupNotificationEvents() { const notificationBtn = document.getElementById('notificationBtn'); const notificationDropdown = document.getElementById('notificationDropdown'); const notificationWrapper = document.getElementById('notificationWrapper'); if (notificationBtn) { notificationBtn.addEventListener('click', (e) => { e.stopPropagation(); notificationDropdown?.classList.toggle('show'); }); } // 외부 클릭시 드롭다운 닫기 document.addEventListener('click', (e) => { if (notificationWrapper && notificationDropdown && !notificationWrapper.contains(e.target)) { notificationDropdown.classList.remove('show'); } }); } /** * 알림 목록 로드 */ async function loadNotifications() { try { const token = localStorage.getItem('token'); if (!token) return; const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }); if (!response.ok) return; const result = await response.json(); if (result.success) { const notifications = result.data || []; updateNotificationBadge(notifications.length); renderNotificationList(notifications); } } catch (error) { console.warn('알림 로드 오류:', error.message); } } /** * 배지 업데이트 */ function updateNotificationBadge(count) { const badge = document.getElementById('notificationBadge'); const btn = document.getElementById('notificationBtn'); if (!badge) return; if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.style.display = 'flex'; btn?.classList.add('has-notifications'); } else { badge.style.display = 'none'; btn?.classList.remove('has-notifications'); } } /** * 알림 목록 렌더링 */ function renderNotificationList(notifications) { const list = document.getElementById('notificationList'); if (!list) return; if (notifications.length === 0) { list.innerHTML = '
새 알림이 없습니다.
'; return; } const NOTIF_ICONS = { repair: '🔧', safety: '⚠️', system: '📢', equipment: '🔩', maintenance: '🛠️' }; list.innerHTML = notifications.slice(0, 5).map(n => `
${NOTIF_ICONS[n.type] || '🔔'}
${escapeHtml(n.title)}
${escapeHtml(n.message || '')}
${formatTimeAgo(n.created_at)}
`).join(''); // 클릭 이벤트 추가 list.querySelectorAll('.notification-item').forEach(item => { item.addEventListener('click', () => { const linkUrl = item.dataset.url; // 수리 알림은 클릭해도 읽음 처리 안함 (수리 처리 페이지에서 확인 처리) window.location.href = linkUrl || '/pages/admin/notifications.html'; }); }); } /** * 시간 포맷팅 */ function formatTimeAgo(dateString) { const date = new Date(dateString); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return '방금 전'; if (diffMins < 60) return `${diffMins}분 전`; if (diffHours < 24) return `${diffHours}시간 전`; if (diffDays < 7) return `${diffDays}일 전`; return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' }); } /** * HTML 이스케이프 */ function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 메인 로직: DOMContentLoaded 시 실행 document.addEventListener('DOMContentLoaded', async () => { if (getUser()) { // 1. 컴포넌트 로드 및 DOM 수정 await loadComponent('navbar', '#navbar-container', processNavbarDom); // 2. DOM에 삽입된 후에 이벤트 리스너 설정 setupNavbarEvents(); // 3. 실시간 날짜/시간 업데이트 시작 updateDateTime(); setInterval(updateDateTime, 1000); // 4. 날씨 정보 로드 (10분마다 갱신) updateWeather(); setInterval(updateWeather, 10 * 60 * 1000); // 5. 알림 이벤트 설정 및 로드 (30초마다 갱신) setupNotificationEvents(); loadNotifications(); setInterval(loadNotifications, 30000); } });