diff --git a/web-ui/index.html b/web-ui/index.html index 406948d..e87b534 100644 --- a/web-ui/index.html +++ b/web-ui/index.html @@ -22,8 +22,8 @@ - - - + + + \ No newline at end of file diff --git a/web-ui/js/api-config.js b/web-ui/js/api-config.js index fd73cbd..96730fc 100644 --- a/web-ui/js/api-config.js +++ b/web-ui/js/api-config.js @@ -1,4 +1,6 @@ // api-config.js - nginx 프록시 대응 API 설정 +import { config } from './config.js'; +import { redirectToLogin } from './navigation.js'; function getApiBaseUrl() { const hostname = window.location.hostname; @@ -13,8 +15,8 @@ function getApiBaseUrl() { hostname === 'localhost' || hostname === '127.0.0.1' || hostname.includes('.local') || hostname.includes('hyungi')) { - // 현재 웹서버의 도메인/IP를 그대로 사용하되 API 포트(20005)로 직접 연결 - const baseUrl = `${protocol}//${hostname}:20005/api`; + // 현재 웹서버의 도메인/IP를 그대로 사용하되 API 포트(config.api.port)로 직접 연결 + const baseUrl = `${protocol}//${hostname}:${config.api.port}${config.api.path}`; console.log('✅ nginx 프록시 사용:', baseUrl); return baseUrl; @@ -22,7 +24,7 @@ function getApiBaseUrl() { // 🚨 백업: 직접 접근 (nginx 프록시 실패시에만) console.warn('⚠️ 직접 API 접근 (백업 모드)'); - return `${protocol}//${hostname}:20005/api`; + return `${protocol}//${hostname}:${config.api.port}${config.api.path}`; } // API 설정 @@ -37,7 +39,7 @@ function ensureAuthenticated() { if (!token || token === 'undefined' || token === 'null') { console.log('🚨 인증되지 않은 사용자. 로그인 페이지로 이동합니다.'); clearAuthData(); // 만약을 위해 한번 더 정리 - window.location.href = '/index.html'; + redirectToLogin(); return false; // 이후 코드 실행 방지 } @@ -46,7 +48,7 @@ function ensureAuthenticated() { console.log('🚨 토큰이 만료되었습니다. 로그인 페이지로 이동합니다.'); clearAuthData(); alert('세션이 만료되었습니다. 다시 로그인해주세요.'); - window.location.href = '/index.html'; + redirectToLogin(); return false; } @@ -108,7 +110,7 @@ async function apiCall(url, method = 'GET', data = null) { console.error('🚨 인증 실패: 토큰이 만료되었거나 유효하지 않습니다.'); clearAuthData(); alert('세션이 만료되었습니다. 다시 로그인해주세요.'); - window.location.href = '/index.html'; + redirectToLogin(); throw new Error('인증에 실패했습니다.'); } @@ -193,6 +195,6 @@ setInterval(() => { console.log('🚨 주기적 확인: 토큰이 만료되었습니다.'); clearAuthData(); alert('세션이 만료되었습니다. 다시 로그인해주세요.'); - window.location.href = '/index.html'; + redirectToLogin(); } -}, 5 * 60 * 1000); // 5분마다 확인 \ No newline at end of file +}, config.app.tokenRefreshInterval); // 5분마다 확인 \ No newline at end of file diff --git a/web-ui/js/component-loader.js b/web-ui/js/component-loader.js new file mode 100644 index 0000000..7edc0ac --- /dev/null +++ b/web-ui/js/component-loader.js @@ -0,0 +1,53 @@ +// /js/component-loader.js +import { config } from './config.js'; + +/** + * 공용 HTML 컴포넌트를 페이지의 특정 위치에 동적으로 로드합니다. + * @param {string} componentName - 로드할 컴포넌트의 이름 (e.g., 'sidebar', 'navbar'). config.js의 components 객체에 정의된 키와 일치해야 합니다. + * @param {string} containerSelector - 컴포넌트가 삽입될 DOM 요소의 CSS 선택자 (e.g., '#sidebar-container'). + * @param {function(Document): void} [domProcessor=null] - DOM에 삽입하기 전에 로드된 HTML(Document)을 조작하는 선택적 함수. + * (e.g., 역할 기반 메뉴 필터링) + */ +export async function loadComponent(componentName, containerSelector, domProcessor = null) { + const container = document.querySelector(containerSelector); + if (!container) { + console.error(`🔴 컴포넌트를 삽입할 컨테이너를 찾을 수 없습니다: ${containerSelector}`); + return; + } + + const componentPath = config.components[componentName]; + if (!componentPath) { + console.error(`🔴 설정 파일(config.js)에서 '${componentName}' 컴포넌트의 경로를 찾을 수 없습니다.`); + container.innerHTML = `

${componentName} 로딩 실패

`; + return; + } + + try { + const response = await fetch(componentPath); + if (!response.ok) { + throw new Error(`컴포넌트 파일을 불러올 수 없습니다: ${response.statusText}`); + } + const htmlText = await response.text(); + + if (domProcessor) { + // 1. 텍스트를 가상 DOM으로 파싱 + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlText, 'text/html'); + + // 2. DOM 프로세서(콜백)를 실행하여 DOM 조작 + await domProcessor(doc); + + // 3. 조작된 HTML을 실제 DOM에 삽입 + container.innerHTML = doc.body.innerHTML; + } else { + // DOM 조작이 필요 없는 경우, 바로 삽입 + container.innerHTML = htmlText; + } + + console.log(`✅ '${componentName}' 컴포넌트 로딩 완료: ${containerSelector}`); + + } catch (error) { + console.error(`🔴 '${componentName}' 컴포넌트 로딩 실패:`, error); + container.innerHTML = `

${componentName} 로딩에 실패했습니다. 관리자에게 문의하세요.

`; + } +} diff --git a/web-ui/js/config.js b/web-ui/js/config.js new file mode 100644 index 0000000..5d02dfc --- /dev/null +++ b/web-ui/js/config.js @@ -0,0 +1,40 @@ +// /js/config.js + +// ES6 모듈을 사용하여 설정을 내보냅니다. +// 이 파일을 통해 프로젝트의 모든 하드코딩된 값을 관리합니다. + +export const config = { + // API 관련 설정 + api: { + // 로컬 개발 및 Docker 환경에서 사용하는 API 서버 포트 + port: 20005, + // API의 기본 경로 + path: '/api', + }, + + // 페이지 경로 설정 + paths: { + // 로그인 페이지 경로 + loginPage: '/index.html', + // 로그인 후 기본적으로 이동할 대시보드 경로 + defaultDashboard: '/pages/dashboard/user.html', + // 시스템 대시보드 경로 + systemDashboard: '/pages/dashboard/system.html', + // 그룹 리더 대시보드 경로 + groupLeaderDashboard: '/pages/dashboard/group-leader.html', + }, + + // 공용 컴포넌트 경로 설정 + components: { + // 사이드바 HTML 파일 경로 + sidebar: '/components/sidebar.html', + // 네비게이션 바 HTML 파일 경로 (예상) + navbar: '/components/navbar.html', + }, + + // 애플리케이션 관련 기타 설정 + app: { + // 토큰 만료 확인 주기 (밀리초 단위, 예: 5분) + tokenRefreshInterval: 5 * 60 * 1000, + } +}; diff --git a/web-ui/js/load-navbar.js b/web-ui/js/load-navbar.js index 6a23cec..51b0d4f 100644 --- a/web-ui/js/load-navbar.js +++ b/web-ui/js/load-navbar.js @@ -1,5 +1,7 @@ -// js/load-navbar.js -// 브라우저 호환 버전 - ES6 모듈 제거 +// /js/load-navbar.js +import { getUser, clearAuthData } from './auth.js'; +import { loadComponent } from './component-loader.js'; +import { config } from './config.js'; // 역할 이름을 한글로 변환하는 맵 const ROLE_NAMES = { @@ -11,6 +13,21 @@ const ROLE_NAMES = { default: '사용자', }; +/** + * 네비게이션 바 DOM을 사용자 정보와 역할에 맞게 수정하는 프로세서입니다. + * @param {Document} doc - 파싱된 HTML 문서 객체 + */ +function processNavbarDom(doc) { + const currentUser = getUser(); + if (!currentUser) return; + + // 1. 역할 기반 메뉴 필터링 + filterMenuByRole(doc, currentUser.role); + + // 2. 사용자 정보 채우기 + populateUserInfo(doc, currentUser); +} + /** * 사용자 역할에 따라 메뉴 항목을 필터링합니다. * @param {Document} doc - 파싱된 HTML 문서 객체 @@ -24,8 +41,7 @@ function filterMenuByRole(doc, userRole) { ]; selectors.forEach(({ role, selector }) => { - // 사용자가 해당 역할을 가지고 있지 않으면 메뉴 항목을 제거 - if (userRole !== role && userRole !== 'system') { // system 권한도 admin 메뉴 접근 가능 + if (userRole !== role && userRole !== 'system') { doc.querySelectorAll(selector).forEach(el => el.remove()); } }); @@ -40,25 +56,18 @@ 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 elements = { + 'user-name': displayName, + 'user-role': roleName, + 'dropdown-user-fullname': displayName, + 'dropdown-user-id': `@${user.username}`, + }; - // 상단 바 사용자 역할 - const userRoleEl = doc.getElementById('user-role'); - if (userRoleEl) userRoleEl.textContent = roleName; + for (const id in elements) { + const el = doc.getElementById(id); + if (el) el.textContent = elements[id]; + } - // 드롭다운 메뉴 사용자 이름 - 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}`; - - // Admin 버튼 제거됨 - - // System 버튼 표시 여부 결정 (system 권한만) const systemBtn = doc.getElementById('systemBtn'); if (systemBtn && user.role === 'system') { systemBtn.style.display = 'flex'; @@ -72,7 +81,6 @@ 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(); @@ -81,36 +89,30 @@ function setupNavbarEvents() { }); } - // 로그아웃 버튼 const logoutButton = document.getElementById('dropdown-logout'); if (logoutButton) { logoutButton.addEventListener('click', () => { if (confirm('로그아웃 하시겠습니까?')) { clearAuthData(); - window.location.href = '/index.html'; + window.location.href = config.paths.loginPage; } }); } - // Admin 버튼 제거됨 - - // System 버튼 클릭 이벤트 const systemButton = document.getElementById('systemBtn'); if (systemButton) { systemButton.addEventListener('click', () => { - window.location.href = '/pages/dashboard/system.html'; + window.location.href = config.paths.systemDashboard; }); } - // Dashboard 버튼 클릭 이벤트 const dashboardButton = document.querySelector('.dashboard-btn'); if (dashboardButton) { dashboardButton.addEventListener('click', () => { - window.location.href = '/pages/dashboard/group-leader.html'; + window.location.href = config.paths.groupLeaderDashboard; }); } - // 외부 클릭 시 드롭다운 닫기 document.addEventListener('click', (e) => { if (profileDropdownMenu && !userInfoDropdown.contains(e.target) && !profileDropdownMenu.contains(e.target)) { profileDropdownMenu.classList.remove('show'); @@ -130,41 +132,17 @@ function updateTime() { } } - // 메인 로직: DOMContentLoaded 시 실행 document.addEventListener('DOMContentLoaded', async () => { - const navbarContainer = document.getElementById('navbar-container'); - if (!navbarContainer) return; - - const currentUser = getUser(); - if (!currentUser) return; // 사용자가 없으면 아무 작업도 하지 않음 - - try { - const response = await fetch('/components/navbar.html'); - const htmlText = await response.text(); - - // 1. 텍스트를 가상 DOM으로 파싱 - const parser = new DOMParser(); - const doc = parser.parseFromString(htmlText, 'text/html'); - - // 2. DOM에 삽입하기 *전*에 내용 수정 - filterMenuByRole(doc, currentUser.role); - populateUserInfo(doc, currentUser); - - // 3. 수정 완료된 HTML을 실제 DOM에 삽입 (깜빡임 방지) - navbarContainer.innerHTML = doc.body.innerHTML; - - // 4. DOM에 삽입된 후에 이벤트 리스너 설정 + if (getUser()) { + // 1. 컴포넌트 로드 및 DOM 수정 + await loadComponent('navbar', '#navbar-container', processNavbarDom); + + // 2. DOM에 삽입된 후에 이벤트 리스너 설정 setupNavbarEvents(); - // 5. 실시간 시간 업데이트 시작 + // 3. 실시간 시간 업데이트 시작 updateTime(); setInterval(updateTime, 1000); - - console.log('✅ 네비게이션 바 로딩 완료'); - - } catch (error) { - console.error('🔴 네비게이션 바 로딩 중 오류 발생:', error); - navbarContainer.innerHTML = '

네비게이션 바를 불러오는 데 실패했습니다.

'; } }); \ No newline at end of file diff --git a/web-ui/js/load-sidebar.js b/web-ui/js/load-sidebar.js index 5f912a2..1ca087f 100644 --- a/web-ui/js/load-sidebar.js +++ b/web-ui/js/load-sidebar.js @@ -1,12 +1,17 @@ // /js/load-sidebar.js import { getUser } from './auth.js'; +import { loadComponent } from './component-loader.js'; /** - * 사용자 역할에 따라 사이드바 메뉴 항목을 필터링합니다. + * 사용자 역할에 따라 사이드바 메뉴 항목을 필터링하는 DOM 프로세서입니다. * @param {Document} doc - 파싱된 HTML 문서 객체 - * @param {string} userRole - 현재 사용자의 역할 */ -function filterSidebarByRole(doc, userRole) { +function filterSidebarByRole(doc) { + const currentUser = getUser(); + if (!currentUser) return; // 비로그인 상태면 필터링하지 않음 + + const userRole = currentUser.role; + // 'system' 역할은 모든 메뉴를 볼 수 있으므로 필터링하지 않음 if (userRole === 'system') { return; @@ -16,7 +21,7 @@ function filterSidebarByRole(doc, userRole) { const roleClassMap = { admin: '.admin-only', leader: '.leader-only', - user: '.user-only', // 또는 'worker-only' 등, sidebar.html에 정의된 클래스에 맞춰야 함 + user: '.user-only', support: '.support-only' }; @@ -33,35 +38,10 @@ function filterSidebarByRole(doc, userRole) { }); } - -document.addEventListener('DOMContentLoaded', async () => { - const sidebarContainer = document.getElementById('sidebar-container'); - if (!sidebarContainer) return; - - const currentUser = getUser(); - if (!currentUser) return; // 비로그인 상태면 사이드바를 로드하지 않음 - - 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 = '

메뉴 로딩 실패

'; +// 페이지 로드 시 사이드바를 로드하고 역할에 따라 필터링합니다. +document.addEventListener('DOMContentLoaded', () => { + // 'getUser'를 통해 로그인 상태 확인. 비로그인 시 아무 작업도 하지 않음. + if (getUser()) { + loadComponent('sidebar', '#sidebar-container', filterSidebarByRole); } }); \ No newline at end of file diff --git a/web-ui/js/login.js b/web-ui/js/login.js index 1cecd98..8dedb51 100644 --- a/web-ui/js/login.js +++ b/web-ui/js/login.js @@ -1,16 +1,8 @@ // /js/login.js -// ES6 모듈 의존성 제거 - 브라우저 호환성 개선 - -// 인증 데이터 저장 함수 (직접 구현) -function saveAuthData(token, user) { - localStorage.setItem('token', token); - localStorage.setItem('user', JSON.stringify(user)); -} - -function clearAuthData() { - localStorage.removeItem('token'); - localStorage.removeItem('user'); -} +import { saveAuthData, clearAuthData } from './auth.js'; +import { redirectToDefaultDashboard } from './navigation.js'; +// api-helper.js가 ES6 모듈로 변환되면 import를 사용해야 합니다. +// import { login } from './api-helper.js'; document.getElementById('loginForm').addEventListener('submit', async function (e) { e.preventDefault(); @@ -28,27 +20,18 @@ document.getElementById('loginForm').addEventListener('submit', async function ( errorDiv.style.display = 'none'; try { - // API 헬퍼를 통해 로그인 요청 (window 객체에서 가져오기) + // 현재는 window 객체를 통해 호출하지만, 향후 모듈화 필요 const result = await window.login(username, password); if (result.success && result.data && result.data.token) { - // 인증 정보 저장 + // auth.js에서 가져온 함수로 인증 정보 저장 saveAuthData(result.data.token, result.data.user); - // 백엔드가 지정한 URL로 리디렉션 - const redirectUrl = result.data.redirectUrl || '/pages/dashboard/user.html'; // 혹시 모를 예외처리 - - // 부드러운 화면 전환 효과 - document.body.style.transition = 'opacity 0.3s ease-out'; - document.body.style.opacity = '0'; - - setTimeout(() => { - window.location.href = redirectUrl; - }, 300); + // navigation.js를 통해 리디렉션 + redirectToDefaultDashboard(result.data.redirectUrl); } else { - // 이 케이스는 api-helper에서 throw new Error()로 처리되어 catch 블록으로 바로 이동합니다. - // 하지만, 만약의 경우를 대비해 방어 코드를 남겨둡니다. + // api-helper가 에러를 throw하므로 이 블록은 실행될 가능성이 낮음 clearAuthData(); errorDiv.textContent = result.error || '로그인에 실패했습니다.'; errorDiv.style.display = 'block'; @@ -56,7 +39,6 @@ document.getElementById('loginForm').addEventListener('submit', async function ( } catch (err) { console.error('로그인 오류:', err); clearAuthData(); - // api-helper에서 보낸 에러 메시지를 표시 errorDiv.textContent = err.message || '서버 연결에 실패했습니다.'; errorDiv.style.display = 'block'; } finally { diff --git a/web-ui/js/navigation.js b/web-ui/js/navigation.js new file mode 100644 index 0000000..32ce921 --- /dev/null +++ b/web-ui/js/navigation.js @@ -0,0 +1,55 @@ +// /js/navigation.js +import { config } from './config.js'; + +/** + * 지정된 URL로 페이지를 리디렉션합니다. + * @param {string} url - 이동할 URL + */ +function redirect(url) { + window.location.href = url; +} + +/** + * 로그인 페이지로 리디렉션합니다. + */ +export function redirectToLogin() { + console.log(`🔄 로그인 페이지로 이동합니다: ${config.paths.loginPage}`); + redirect(config.paths.loginPage); +} + +/** + * 사용자의 기본 대시보드 페이지로 리디렉션합니다. + * 백엔드가 지정한 URL이 있으면 그곳으로, 없으면 기본 URL로 이동합니다. + * @param {string} [backendRedirectUrl=null] - 백엔드에서 전달받은 리디렉션 URL + */ +export function redirectToDefaultDashboard(backendRedirectUrl = null) { + const destination = backendRedirectUrl || config.paths.defaultDashboard; + console.log(`🔄 대시보드로 이동합니다: ${destination}`); + + // 부드러운 화면 전환 효과 + document.body.style.transition = 'opacity 0.3s ease-out'; + document.body.style.opacity = '0'; + + setTimeout(() => { + redirect(destination); + }, 300); +} + +/** + * 시스템 대시보드 페이지로 리디렉션합니다. + */ +export function redirectToSystemDashboard() { + console.log(`🔄 시스템 대시보드로 이동합니다: ${config.paths.systemDashboard}`); + redirect(config.paths.systemDashboard); +} + +/** + * 그룹 리더 대시보드 페이지로 리디렉션합니다. + */ +export function redirectToGroupLeaderDashboard() { + console.log(`🔄 그룹 리더 대시보드로 이동합니다: ${config.paths.groupLeaderDashboard}`); + redirect(config.paths.groupLeaderDashboard); +} + +// 필요에 따라 더 많은 리디렉션 함수를 추가할 수 있습니다. +// export function redirectToUserProfile() { ... }