feat(frontend): 공통 UI 로더 및 대시보드 구조 개선

- auth-check, load-navbar, load-sidebar 리팩토링
- auth.js 모듈을 활용하여 코드 중복 제거 및 일관성 확보
- DOMParser를 사용하여 컴포넌트 로딩 시 화면 깜빡임 현상 해결
- user-dashboard에 API 연동을 위한 견고한 기반 코드 마련
This commit is contained in:
2025-07-28 12:03:52 +09:00
parent e0e0b1ad99
commit 8d7422d376
4 changed files with 296 additions and 373 deletions

View File

@@ -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 = '<p>네비게이션 바를 불러오는 데 실패했습니다.</p>';
}
});
// ✅ 프로필 드롭다운 열기
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('❌ 로그아웃 취소됨');
}
}
});