diff --git a/web-ui/js/auth-check.js b/web-ui/js/auth-check.js index 9a4ea1d..d43e51a 100644 --- a/web-ui/js/auth-check.js +++ b/web-ui/js/auth-check.js @@ -1,79 +1,27 @@ -// ✅ /js/auth-check.js -// 토큰 검증과 권한 체크 +// /js/auth-check.js +import { isLoggedIn, getUser, clearAuthData } from './auth.js'; -const token = localStorage.getItem('token'); - -function isValidJWT(token) { - return typeof token === 'string' && token.split('.').length === 3; -} - -function getPayload(token) { - try { - return JSON.parse(atob(token.split('.')[1])); - } catch { - return null; - } -} - -if (!token || !isValidJWT(token)) { - console.log('🚨 토큰이 없거나 유효하지 않음'); - localStorage.removeItem('token'); - localStorage.removeItem('user'); - window.location.href = '/index.html'; -} else { - const user = getPayload(token); - const storedUser = JSON.parse(localStorage.getItem('user') || '{}'); - - console.log('🔐 JWT 사용자 정보:', user); - console.log('💾 저장된 사용자 정보:', storedUser); - - // 사용자 정보 우선순위: localStorage > JWT payload - const currentUser = storedUser.access_level ? storedUser : user; - - if (!currentUser || !currentUser.username || !currentUser.access_level) { - console.log('🚨 사용자 정보가 유효하지 않음'); - localStorage.removeItem('token'); - localStorage.removeItem('user'); +// 즉시 실행 함수로 스코프를 보호하고 로직을 실행 +(function() { + if (!isLoggedIn()) { + console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.'); + clearAuthData(); // 만약을 위해 한번 더 정리 window.location.href = '/index.html'; - } else { - console.log('✅ 인증 성공:', currentUser.username, currentUser.access_level); - - // 사용자 이름 표시 - const userNameElements = document.querySelectorAll('#user-name, .user-name'); - userNameElements.forEach(el => { - if (el) el.textContent = currentUser.name || currentUser.username; - }); - - // 🎯 역할별 메뉴 표시/숨김 처리 - const accessLevel = currentUser.access_level; - - // 관리자 전용 메뉴 - if (accessLevel !== 'admin' && accessLevel !== 'system') { - const adminOnly = document.querySelectorAll('.admin-only, .system-only'); - adminOnly.forEach(el => el.remove()); - } - - // 그룹장 전용 메뉴 - if (accessLevel !== 'group_leader') { - const groupLeaderOnly = document.querySelectorAll('.group-leader-only'); - groupLeaderOnly.forEach(el => el.remove()); - } - - // 지원팀 전용 메뉴 - if (accessLevel !== 'support') { - const supportOnly = document.querySelectorAll('.support-only'); - supportOnly.forEach(el => el.remove()); - } - - // 일반 작업자 전용 메뉴 - if (accessLevel !== 'worker' && accessLevel !== 'user') { - const workerOnly = document.querySelectorAll('.worker-only'); - workerOnly.forEach(el => el.remove()); - } - - // 전역 사용자 정보 저장 - window.currentUser = currentUser; - - console.log('🎭 역할별 메뉴 필터링 완료:', accessLevel); + return; // 이후 코드 실행 방지 } -} \ No newline at end of file + + const currentUser = getUser(); + + // 사용자 정보가 유효한지 확인 (토큰은 있지만 유저 정보가 깨졌을 경우) + if (!currentUser || !currentUser.username || !currentUser.role) { + console.error('🚨 사용자 정보가 유효하지 않습니다. 강제 로그아웃 처리합니다.'); + clearAuthData(); + window.location.href = '/index.html'; + return; + } + + console.log(`✅ ${currentUser.username}(${currentUser.role})님 인증 성공.`); + + // 역할 기반 메뉴 제어 로직은 각 컴포넌트 로더(load-navbar.js 등)로 이전함. + // 전역 변수 할당(window.currentUser) 제거. +})(); \ No newline at end of file diff --git a/web-ui/js/load-navbar.js b/web-ui/js/load-navbar.js index 7051c98..79e000f 100644 --- a/web-ui/js/load-navbar.js +++ b/web-ui/js/load-navbar.js @@ -1,252 +1,144 @@ // js/load-navbar.js -// 네비게이션바 로드 및 프로필 드롭다운 기능 구현 +import { getUser, clearAuthData } from './auth.js'; +// 역할 이름을 한글로 변환하는 맵 +const ROLE_NAMES = { + admin: '관리자', + system: '시스템 관리자', + leader: '그룹장', + user: '작업자', + support: '지원팀', + default: '사용자', +}; + +/** + * 사용자 역할에 따라 메뉴 항목을 필터링합니다. + * @param {Document} doc - 파싱된 HTML 문서 객체 + * @param {string} userRole - 현재 사용자의 역할 + */ +function filterMenuByRole(doc, userRole) { + const selectors = [ + { role: 'admin', selector: '.admin-only' }, + { role: 'system', selector: '.system-only' }, + { role: 'leader', selector: '.leader-only' }, + ]; + + selectors.forEach(({ role, selector }) => { + // 사용자가 해당 역할을 가지고 있지 않으면 메뉴 항목을 제거 + if (userRole !== role) { + doc.querySelectorAll(selector).forEach(el => el.remove()); + } + }); +} + +/** + * 네비게이션 바에 사용자 정보를 채웁니다. + * @param {Document} doc - 파싱된 HTML 문서 객체 + * @param {object} user - 현재 사용자 객체 + */ +function populateUserInfo(doc, user) { + const displayName = user.name || user.username; + const roleName = ROLE_NAMES[user.role] || ROLE_NAMES.default; + + // 상단 바 사용자 이름 + const userNameEl = doc.getElementById('user-name'); + if (userNameEl) userNameEl.textContent = displayName; + + // 상단 바 사용자 역할 + const userRoleEl = doc.getElementById('user-role'); + if (userRoleEl) userRoleEl.textContent = roleName; + + // 드롭다운 메뉴 사용자 이름 + const dropdownNameEl = doc.getElementById('dropdown-user-fullname'); + if (dropdownNameEl) dropdownNameEl.textContent = displayName; + + // 드롭다운 메뉴 사용자 아이디 + const dropdownIdEl = doc.getElementById('dropdown-user-id'); + if (dropdownIdEl) dropdownIdEl.textContent = `@${user.username}`; +} + +/** + * 네비게이션 바와 관련된 모든 이벤트를 설정합니다. + */ +function setupNavbarEvents() { + const userInfoDropdown = document.getElementById('user-info-dropdown'); + const profileDropdownMenu = document.getElementById('profile-dropdown-menu'); + + // 드롭다운 토글 + if (userInfoDropdown && profileDropdownMenu) { + userInfoDropdown.addEventListener('click', (e) => { + e.stopPropagation(); + profileDropdownMenu.classList.toggle('show'); + userInfoDropdown.classList.toggle('active'); + }); + } + + // 로그아웃 버튼 + const logoutButton = document.getElementById('dropdown-logout'); + if (logoutButton) { + logoutButton.addEventListener('click', () => { + if (confirm('로그아웃 하시겠습니까?')) { + clearAuthData(); + window.location.href = '/index.html'; + } + }); + } + + // 외부 클릭 시 드롭다운 닫기 + document.addEventListener('click', (e) => { + if (profileDropdownMenu && !userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) { + profileDropdownMenu.classList.remove('show'); + userInfoDropdown.classList.remove('active'); + } + }); +} + +/** + * 현재 시간을 업데이트하는 함수 + */ +function updateTime() { + const timeElement = document.getElementById('current-time'); + if (timeElement) { + const now = new Date(); + timeElement.textContent = now.toLocaleTimeString('ko-KR', { hour12: false }); + } +} + + +// 메인 로직: DOMContentLoaded 시 실행 document.addEventListener('DOMContentLoaded', async () => { + const navbarContainer = document.getElementById('navbar-container'); + if (!navbarContainer) return; + + const currentUser = getUser(); + if (!currentUser) return; // 사용자가 없으면 아무 작업도 하지 않음 + try { - // navbar.html 파일 로드 - const res = await fetch('/components/navbar.html'); - const html = await res.text(); - - // navbar 컨테이너 찾기 - const container = document.getElementById('navbar-container') || document.getElementById('navbar-placeholder'); - if (!container) { - console.error('네비게이션 컨테이너를 찾을 수 없습니다'); - return; - } - - // HTML 삽입 - container.innerHTML = html; + const response = await fetch('/components/navbar.html'); + const htmlText = await response.text(); - // 토큰 확인 - const token = localStorage.getItem('token'); - if (!token) return; + // 1. 텍스트를 가상 DOM으로 파싱 + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlText, 'text/html'); - // 역할 매핑 테이블 - const roleMap = { - worker: '작업자', - group_leader: '그룹장', - groupleader: '그룹장', - leader: '리더', - supervisor: '감독자', - team_leader: '팀장', - support_team: '지원팀', - support: '지원팀', - admin_ceo: '업무관리자', - admin_plant: '시스템관리자', - admin: '관리자', - administrator: '관리자', - system: '시스템관리자' - }; + // 2. DOM에 삽입하기 *전*에 내용 수정 + filterMenuByRole(doc, currentUser.role); + populateUserInfo(doc, currentUser); - // JWT 토큰 파싱 - let payload; - try { - payload = JSON.parse(atob(token.split('.')[1])); - } catch (err) { - console.warn('JWT 파싱 실패:', err); - return; - } + // 3. 수정 완료된 HTML을 실제 DOM에 삽입 (깜빡임 방지) + navbarContainer.innerHTML = doc.body.innerHTML; - // 저장된 사용자 정보 확인 - const storedUser = JSON.parse(localStorage.getItem('user') || '{}'); - const currentUser = storedUser.access_level ? storedUser : payload; + // 4. DOM에 삽입된 후에 이벤트 리스너 설정 + setupNavbarEvents(); - // ✅ 사용자 정보 표시 - const nameEl = document.getElementById('user-name'); - if (nameEl) { - nameEl.textContent = currentUser.name || currentUser.username || '사용자'; - } - - const roleEl = document.getElementById('user-role'); - if (roleEl) { - const accessLevel = (currentUser.access_level || '').toLowerCase(); - const roleName = roleMap[accessLevel] || '사용자'; - roleEl.textContent = roleName; - } - - // ✅ 드롭다운 헤더 사용자 정보 - const dropdownFullname = document.getElementById('dropdown-user-fullname'); - if (dropdownFullname) { - dropdownFullname.textContent = currentUser.name || currentUser.username || '사용자'; - } - - const dropdownUserId = document.getElementById('dropdown-user-id'); - if (dropdownUserId) { - dropdownUserId.textContent = `@${currentUser.username || 'user'}`; - } - - // ✅ 현재 시간 업데이트 시작 + // 5. 실시간 시간 업데이트 시작 updateTime(); setInterval(updateTime, 1000); - // ✅ 프로필 드롭다운 이벤트 설정 - const userInfoDropdown = document.getElementById('user-info-dropdown'); - const profileDropdownMenu = document.getElementById('profile-dropdown-menu'); - - if (userInfoDropdown && profileDropdownMenu) { - // 드롭다운 토글 - userInfoDropdown.addEventListener('click', function(e) { - e.stopPropagation(); - const isOpen = profileDropdownMenu.classList.contains('show'); - - if (isOpen) { - closeProfileDropdown(); - } else { - openProfileDropdown(); - } - }); + console.log('✅ 네비게이션 바 로딩 완료'); - // 드롭다운 외부 클릭 시 닫기 - document.addEventListener('click', function(e) { - if (!userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) { - closeProfileDropdown(); - } - }); - - // ESC 키로 닫기 - document.addEventListener('keydown', function(e) { - if (e.key === 'Escape') { - closeProfileDropdown(); - } - }); - } - - // ✅ 대시보드 버튼 이벤트 - const dashboardBtn = document.querySelector('.dashboard-btn'); - if (dashboardBtn) { - dashboardBtn.addEventListener('click', function() { - navigateToDashboard(); - }); - } - - // ✅ 드롭다운 로그아웃 버튼 - const dropdownLogout = document.getElementById('dropdown-logout'); - if (dropdownLogout) { - dropdownLogout.addEventListener('click', function() { - logout(); - }); - } - - console.log('✅ 네비게이션 바 로딩 및 이벤트 설정 완료:', { - name: currentUser.name || currentUser.username, - role: currentUser.access_level, - dashboardBtn: !!dashboardBtn, - profileDropdown: !!userInfoDropdown - }); - - } catch (err) { - console.error('🔴 네비게이션 바 로딩 실패:', err); + } catch (error) { + console.error('🔴 네비게이션 바 로딩 중 오류 발생:', error); + navbarContainer.innerHTML = '
네비게이션 바를 불러오는 데 실패했습니다.
'; } -}); - -// ✅ 프로필 드롭다운 열기 -function openProfileDropdown() { - const userInfo = document.getElementById('user-info-dropdown'); - const dropdown = document.getElementById('profile-dropdown-menu'); - - if (userInfo && dropdown) { - userInfo.classList.add('active'); - dropdown.classList.add('show'); - console.log('📂 프로필 드롭다운 열림'); - } -} - -// ✅ 프로필 드롭다운 닫기 -function closeProfileDropdown() { - const userInfo = document.getElementById('user-info-dropdown'); - const dropdown = document.getElementById('profile-dropdown-menu'); - - if (userInfo && dropdown) { - userInfo.classList.remove('active'); - dropdown.classList.remove('show'); - console.log('📁 프로필 드롭다운 닫힘'); - } -} - -// ✅ 시간 업데이트 함수 -function updateTime() { - const now = new Date(); - const timeString = now.toLocaleTimeString('ko-KR', { - hour12: false, - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); - const timeElement = document.getElementById('current-time'); - if (timeElement) { - timeElement.textContent = timeString; - } -} - -// ✅ 역할별 대시보드 네비게이션 -function navigateToDashboard() { - console.log('🏠 대시보드 버튼 클릭됨'); - - const user = JSON.parse(localStorage.getItem('user') || '{}'); - const accessLevel = (user.access_level || '').toLowerCase().trim(); - - console.log('👤 현재 사용자:', user); - console.log('🔑 access_level:', accessLevel); - - // 그룹장/리더 관련 키워드들 - const leaderKeywords = [ - 'group_leader', 'groupleader', 'group-leader', - 'leader', 'supervisor', 'team_leader', 'teamleader', - '그룹장', '팀장', '현장책임자' - ]; - - // 관리자 관련 키워드들 - const adminKeywords = [ - 'admin', 'administrator', 'system', - '관리자', '시스템관리자' - ]; - - // 지원팀 관련 키워드들 - const supportKeywords = [ - 'support', 'support_team', 'supportteam', - '지원팀', '지원' - ]; - - let targetUrl = '/pages/dashboard/user.html'; - - // 키워드 매칭 - if (leaderKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) { - targetUrl = '/pages/dashboard/group-leader.html'; - console.log('✅ 그룹장 페이지로 이동'); - } else if (adminKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) { - targetUrl = '/pages/dashboard/admin.html'; - console.log('✅ 관리자 페이지로 이동'); - } else if (supportKeywords.some(keyword => accessLevel.includes(keyword.toLowerCase()))) { - targetUrl = '/pages/dashboard/support.html'; - console.log('✅ 지원팀 페이지로 이동'); - } else { - console.log('✅ 일반 사용자 페이지로 이동'); - } - - console.log('🎯 이동할 URL:', targetUrl); - window.location.href = targetUrl; -} - -// ✅ 로그아웃 함수 -function logout() { - console.log('🚪 로그아웃 버튼 클릭됨'); - - if (confirm('로그아웃 하시겠습니까?')) { - console.log('✅ 로그아웃 확인됨'); - - // 로컬 스토리지 정리 - localStorage.removeItem('token'); - localStorage.removeItem('user'); - - console.log('🗑️ 로컬 스토리지 정리 완료'); - - // 부드러운 전환 효과 - document.body.style.opacity = '0'; - setTimeout(() => { - console.log('🏠 로그인 페이지로 이동'); - window.location.href = '/index.html'; - }, 300); - } else { - console.log('❌ 로그아웃 취소됨'); - } -} \ No newline at end of file +}); \ No newline at end of file diff --git a/web-ui/js/load-sidebar.js b/web-ui/js/load-sidebar.js index d82306f..5f912a2 100644 --- a/web-ui/js/load-sidebar.js +++ b/web-ui/js/load-sidebar.js @@ -1,46 +1,67 @@ -// ✅ /js/load-sidebar.js (access_level 기반 메뉴 필터링) -document.addEventListener('DOMContentLoaded', async () => { - try { - // 1) 사이드바 HTML 로딩 - const res = await fetch('/components/sidebar.html'); - const html = await res.text(); - document.getElementById('sidebar-container').innerHTML = html; +// /js/load-sidebar.js +import { getUser } from './auth.js'; - // 2) 토큰 존재 확인 - const token = localStorage.getItem('token'); - if (!token) return; +/** + * 사용자 역할에 따라 사이드바 메뉴 항목을 필터링합니다. + * @param {Document} doc - 파싱된 HTML 문서 객체 + * @param {string} userRole - 현재 사용자의 역할 + */ +function filterSidebarByRole(doc, userRole) { + // 'system' 역할은 모든 메뉴를 볼 수 있으므로 필터링하지 않음 + if (userRole === 'system') { + return; + } + + // 역할과 그에 해당하는 클래스 선택자 매핑 + const roleClassMap = { + admin: '.admin-only', + leader: '.leader-only', + user: '.user-only', // 또는 'worker-only' 등, sidebar.html에 정의된 클래스에 맞춰야 함 + support: '.support-only' + }; - // 3) JWT 파싱해서 access_level 추출 - let access; - try { - const payload = JSON.parse(atob(token.split('.')[1])); - access = payload.access_level; - } catch (err) { - console.warn('JWT 파싱 실패:', err); - return; + // 모든 역할 기반 선택자를 가져옴 + const allRoleSelectors = Object.values(roleClassMap).join(', '); + const allRoleElements = doc.querySelectorAll(allRoleSelectors); + + allRoleElements.forEach(el => { + // 요소가 현재 사용자 역할에 해당하는 클래스를 가지고 있는지 확인 + const userRoleSelector = roleClassMap[userRole]; + if (!userRoleSelector || !el.matches(userRoleSelector)) { + el.remove(); } + }); +} - // 4) 시스템 계정은 전부 유지 - if (access === 'system') return; - // 5) 클래스 이름 목록 - const classMap = [ - 'worker-only', - 'group-leader-only', - 'support-only', - 'admin-only', - 'system-only' - ]; +document.addEventListener('DOMContentLoaded', async () => { + const sidebarContainer = document.getElementById('sidebar-container'); + if (!sidebarContainer) return; - // 6) 본인 권한에 해당하지 않는 요소 제거 - classMap.forEach(cls => { - const required = cls.replace('-only', '').replace('-', '_'); // 'group-leader-only' → 'group_leader' - if (access !== required) { - document.querySelectorAll(`.${cls}`).forEach(el => el.remove()); - } - }); + const currentUser = getUser(); + if (!currentUser) return; // 비로그인 상태면 사이드바를 로드하지 않음 - } catch (err) { - console.error('🔴 사이드바 로딩 실패:', err); + try { + const response = await fetch('/components/sidebar.html'); + if (!response.ok) { + throw new Error(`사이드바 파일을 불러올 수 없습니다: ${response.statusText}`); + } + const htmlText = await response.text(); + + // 1. 텍스트를 가상 DOM으로 파싱 + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlText, 'text/html'); + + // 2. DOM에 삽입하기 *전*에 역할에 따라 메뉴 필터링 + filterSidebarByRole(doc, currentUser.role); + + // 3. 수정 완료된 HTML을 실제 DOM에 삽입 + sidebarContainer.innerHTML = doc.body.innerHTML; + + console.log('✅ 사이드바 로딩 및 필터링 완료'); + + } catch (error) { + console.error('🔴 사이드바 로딩 실패:', error); + sidebarContainer.innerHTML = '메뉴 로딩 실패
'; } }); \ No newline at end of file diff --git a/web-ui/js/user-dashboard.js b/web-ui/js/user-dashboard.js index 026adc9..fee8d7a 100644 --- a/web-ui/js/user-dashboard.js +++ b/web-ui/js/user-dashboard.js @@ -1,31 +1,93 @@ -import { API, getAuthHeaders } from '/js/api-config.js'; +// /js/user-dashboard.js +import { getUser } from './auth.js'; +import { apiGet } from './api-helper.js'; // 개선된 api-helper를 사용합니다. -// 오늘 일정 로드 +/** + * API를 호출하여 오늘의 작업 일정을 불러와 화면에 표시합니다. + */ async function loadTodaySchedule() { - // 구현 필요 - document.getElementById('today-schedule').innerHTML = - '오늘의 작업 일정이 여기에 표시됩니다.
'; -} + const scheduleContainer = document.getElementById('today-schedule'); + scheduleContainer.innerHTML = '📅 오늘의 작업 일정을 불러오는 중...
'; -// 작업 통계 로드 -async function loadWorkStats() { - // 구현 필요 - document.getElementById('work-stats').innerHTML = - '이번 달 작업 시간: 160시간
'; -} - -// 환영 메시지 개인화 -function personalizeWelcome() { - const user = window.currentUser; - if (user) { - document.getElementById('welcome-message').textContent = - `${user.name}님, 환영합니다!`; + try { + // 예시: /api/dashboard/today-schedule 엔드포인트에서 데이터를 가져옵니다. + // 실제 엔드포인트는 백엔드 구현에 따라 달라질 수 있습니다. + const scheduleData = await apiGet('/dashboard/today-schedule'); + + if (scheduleData && scheduleData.length > 0) { + const scheduleHtml = scheduleData.map(item => ` +오늘 예정된 작업이 없습니다.
'; + } + } catch (error) { + console.error('오늘의 작업 일정 로드 실패:', error); + scheduleContainer.innerHTML = '일정 정보를 불러오는 데 실패했습니다.
'; } } -// 초기화 -document.addEventListener('DOMContentLoaded', () => { +/** + * API를 호출하여 현재 사용자의 작업 통계를 불러와 화면에 표시합니다. + */ +async function loadWorkStats() { + const statsContainer = document.getElementById('work-stats'); + statsContainer.innerHTML = '📈 내 작업 현황을 불러오는 중...
'; + + try { + // 예시: /api/dashboard/my-stats 엔드포인트에서 데이터를 가져옵니다. + const statsData = await apiGet('/dashboard/my-stats'); + + if (statsData) { + const statsHtml = ` +표시할 통계 정보가 없습니다.
'; + } + } catch (error) { + console.error('작업 통계 로드 실패:', error); + statsContainer.innerHTML = '통계 정보를 불러오는 데 실패했습니다.
'; + } +} + +/** + * 환영 메시지를 사용자 이름으로 개인화합니다. + */ +function personalizeWelcome() { + // 전역 변수 대신 auth.js 모듈을 통해 사용자 정보를 가져옵니다. + const user = getUser(); + if (user) { + const welcomeEl = document.getElementById('welcome-message'); + if (welcomeEl) { + welcomeEl.textContent = `${user.name || user.username}님, 환영합니다! 오늘 하루도 안전하게 작업하세요.`; + } + } +} + +// 페이지 초기화 함수 +function initializeDashboard() { personalizeWelcome(); loadTodaySchedule(); loadWorkStats(); -}); \ No newline at end of file +} + +// DOM이 로드되면 대시보드 초기화를 시작합니다. +document.addEventListener('DOMContentLoaded', initializeDashboard); \ No newline at end of file