📍 Menu Structure Change: - 현황판을 목록관리 서브메뉴에서 제거 - 목록관리 옆에 독립적인 메인 메뉴로 배치 - 보고서 앞에 위치하도록 순서 조정 🎯 New Menu Layout: 작업보고서 | 일일 공수 | 부적합 등록 | 부적합 조회 | 목록관리 | 현황판 | 보고서 | 프로젝트 관리 | 사용자 관리 🎨 Menu Properties: - 아이콘: fas fa-chart-line - 색상: 보라색 (text-purple-600) - 배경: bg-purple-50 hover:bg-purple-100 - URL: /issues-dashboard.html Expected Result: ✅ 현황판이 목록관리 옆에 독립 메뉴로 표시 ✅ 서브메뉴 없는 직접 접근 가능한 메뉴 ✅ 목록관리와 보고서 사이에 위치
544 lines
21 KiB
JavaScript
544 lines
21 KiB
JavaScript
/**
|
|
* 공통 헤더 컴포넌트
|
|
* 권한 기반으로 메뉴를 동적으로 생성하고 부드러운 페이지 전환을 제공
|
|
*/
|
|
|
|
class CommonHeader {
|
|
constructor() {
|
|
this.currentUser = null;
|
|
this.currentPage = '';
|
|
this.menuItems = this.initMenuItems();
|
|
}
|
|
|
|
/**
|
|
* 메뉴 아이템 정의
|
|
*/
|
|
initMenuItems() {
|
|
return [
|
|
{
|
|
id: 'daily_work',
|
|
title: '일일 공수',
|
|
icon: 'fas fa-calendar-check',
|
|
url: '/daily-work.html',
|
|
pageName: 'daily_work',
|
|
color: 'text-blue-600',
|
|
bgColor: 'bg-blue-50 hover:bg-blue-100'
|
|
},
|
|
{
|
|
id: 'issues_create',
|
|
title: '부적합 등록',
|
|
icon: 'fas fa-plus-circle',
|
|
url: '/index.html',
|
|
pageName: 'issues_create',
|
|
color: 'text-green-600',
|
|
bgColor: 'bg-green-50 hover:bg-green-100'
|
|
},
|
|
{
|
|
id: 'issues_view',
|
|
title: '부적합 조회',
|
|
icon: 'fas fa-search',
|
|
url: '/issue-view.html',
|
|
pageName: 'issues_view',
|
|
color: 'text-purple-600',
|
|
bgColor: 'bg-purple-50 hover:bg-purple-100'
|
|
},
|
|
{
|
|
id: 'issues_manage',
|
|
title: '목록 관리',
|
|
icon: 'fas fa-tasks',
|
|
url: '/index.html#list',
|
|
pageName: 'issues_manage',
|
|
color: 'text-orange-600',
|
|
bgColor: 'bg-orange-50 hover:bg-orange-100',
|
|
subMenus: [
|
|
{
|
|
id: 'issues_inbox',
|
|
title: '수신함',
|
|
icon: 'fas fa-inbox',
|
|
url: '/issues-inbox.html',
|
|
pageName: 'issues_inbox',
|
|
color: 'text-blue-600'
|
|
},
|
|
{
|
|
id: 'issues_management',
|
|
title: '관리함',
|
|
icon: 'fas fa-cog',
|
|
url: '/issues-management.html',
|
|
pageName: 'issues_management',
|
|
color: 'text-green-600'
|
|
},
|
|
{
|
|
id: 'issues_archive',
|
|
title: '폐기함',
|
|
icon: 'fas fa-archive',
|
|
url: '/issues-archive.html',
|
|
pageName: 'issues_archive',
|
|
color: 'text-gray-600'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'issues_dashboard',
|
|
title: '현황판',
|
|
icon: 'fas fa-chart-line',
|
|
url: '/issues-dashboard.html',
|
|
pageName: 'issues_dashboard',
|
|
color: 'text-purple-600',
|
|
bgColor: 'bg-purple-50 hover:bg-purple-100'
|
|
},
|
|
{
|
|
id: 'reports',
|
|
title: '보고서',
|
|
icon: 'fas fa-chart-bar',
|
|
url: '/reports.html',
|
|
pageName: 'reports',
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-50 hover:bg-red-100'
|
|
},
|
|
{
|
|
id: 'projects_manage',
|
|
title: '프로젝트 관리',
|
|
icon: 'fas fa-folder-open',
|
|
url: '/project-management.html',
|
|
pageName: 'projects_manage',
|
|
color: 'text-indigo-600',
|
|
bgColor: 'bg-indigo-50 hover:bg-indigo-100'
|
|
},
|
|
{
|
|
id: 'users_manage',
|
|
title: '사용자 관리',
|
|
icon: 'fas fa-users-cog',
|
|
url: '/admin.html',
|
|
pageName: 'users_manage',
|
|
color: 'text-gray-600',
|
|
bgColor: 'bg-gray-50 hover:bg-gray-100'
|
|
}
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 헤더 초기화
|
|
* @param {Object} user - 현재 사용자 정보
|
|
* @param {string} currentPage - 현재 페이지 ID
|
|
*/
|
|
async init(user, currentPage = '') {
|
|
this.currentUser = user;
|
|
this.currentPage = currentPage;
|
|
|
|
// 권한 시스템이 로드될 때까지 대기
|
|
await this.waitForPermissionSystem();
|
|
|
|
this.render();
|
|
this.bindEvents();
|
|
|
|
// 키보드 단축키 초기화
|
|
this.initializeKeyboardShortcuts();
|
|
|
|
// 페이지 프리로더 초기화
|
|
this.initializePreloader();
|
|
}
|
|
|
|
/**
|
|
* 권한 시스템 로드 대기
|
|
*/
|
|
async waitForPermissionSystem() {
|
|
let attempts = 0;
|
|
const maxAttempts = 50; // 5초 대기
|
|
|
|
while (!window.pagePermissionManager && attempts < maxAttempts) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
attempts++;
|
|
}
|
|
|
|
if (window.pagePermissionManager && this.currentUser) {
|
|
window.pagePermissionManager.setUser(this.currentUser);
|
|
// 권한 로드 대기
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 헤더 렌더링
|
|
*/
|
|
render() {
|
|
const headerHTML = this.generateHeaderHTML();
|
|
|
|
// 기존 헤더가 있으면 교체, 없으면 body 상단에 추가
|
|
let headerContainer = document.getElementById('common-header');
|
|
if (headerContainer) {
|
|
headerContainer.innerHTML = headerHTML;
|
|
} else {
|
|
headerContainer = document.createElement('div');
|
|
headerContainer.id = 'common-header';
|
|
headerContainer.innerHTML = headerHTML;
|
|
document.body.insertBefore(headerContainer, document.body.firstChild);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 페이지 업데이트
|
|
* @param {string} pageName - 새로운 페이지 이름
|
|
*/
|
|
updateCurrentPage(pageName) {
|
|
this.currentPage = pageName;
|
|
this.render();
|
|
}
|
|
|
|
/**
|
|
* 헤더 HTML 생성
|
|
*/
|
|
generateHeaderHTML() {
|
|
const accessibleMenus = this.getAccessibleMenus();
|
|
const userDisplayName = this.currentUser?.full_name || this.currentUser?.username || '사용자';
|
|
const userRole = this.getUserRoleDisplay();
|
|
|
|
return `
|
|
<header class="bg-white shadow-sm border-b sticky top-0 z-50">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-16">
|
|
<!-- 로고 및 제목 -->
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0 flex items-center">
|
|
<i class="fas fa-clipboard-check text-2xl text-blue-600 mr-3"></i>
|
|
<h1 class="text-xl font-bold text-gray-900">작업보고서</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 네비게이션 메뉴 -->
|
|
<nav class="hidden md:flex space-x-2">
|
|
${accessibleMenus.map(menu => this.generateMenuItemHTML(menu)).join('')}
|
|
</nav>
|
|
|
|
<!-- 사용자 정보 및 메뉴 -->
|
|
<div class="flex items-center space-x-4">
|
|
<!-- 사용자 정보 -->
|
|
<div class="flex items-center space-x-3">
|
|
<div class="text-right">
|
|
<div class="text-sm font-medium text-gray-900">${userDisplayName}</div>
|
|
<div class="text-xs text-gray-500">${userRole}</div>
|
|
</div>
|
|
<div class="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
|
|
<span class="text-white text-sm font-semibold">
|
|
${userDisplayName.charAt(0).toUpperCase()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 드롭다운 메뉴 -->
|
|
<div class="relative">
|
|
<button id="user-menu-button" class="p-2 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-md">
|
|
<i class="fas fa-chevron-down"></i>
|
|
</button>
|
|
<div id="user-menu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5">
|
|
<a href="#" onclick="CommonHeader.showPasswordModal()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-key mr-2"></i>비밀번호 변경
|
|
</a>
|
|
<a href="#" onclick="CommonHeader.logout()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
|
<i class="fas fa-sign-out-alt mr-2"></i>로그아웃
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모바일 메뉴 버튼 -->
|
|
<button id="mobile-menu-button" class="md:hidden p-2 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-md">
|
|
<i class="fas fa-bars"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 모바일 메뉴 -->
|
|
<div id="mobile-menu" class="md:hidden hidden border-t border-gray-200 py-3">
|
|
<div class="space-y-1">
|
|
${accessibleMenus.map(menu => this.generateMobileMenuItemHTML(menu)).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 접근 가능한 메뉴 필터링
|
|
*/
|
|
getAccessibleMenus() {
|
|
return this.menuItems.filter(menu => {
|
|
// admin은 모든 메뉴 접근 가능
|
|
if (this.currentUser?.role === 'admin') {
|
|
// 하위 메뉴가 있는 경우 하위 메뉴도 필터링
|
|
if (menu.subMenus) {
|
|
menu.accessibleSubMenus = menu.subMenus;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// 권한 시스템이 로드되지 않았으면 기본 메뉴만
|
|
if (!window.canAccessPage) {
|
|
return ['issues_create', 'issues_view'].includes(menu.id);
|
|
}
|
|
|
|
// 메인 메뉴 권한 체크
|
|
const hasMainAccess = window.canAccessPage(menu.pageName);
|
|
|
|
// 하위 메뉴가 있는 경우 접근 가능한 하위 메뉴 필터링
|
|
if (menu.subMenus) {
|
|
menu.accessibleSubMenus = menu.subMenus.filter(subMenu =>
|
|
window.canAccessPage(subMenu.pageName)
|
|
);
|
|
|
|
// 메인 메뉴 접근 권한이 없어도 하위 메뉴 중 하나라도 접근 가능하면 표시
|
|
return hasMainAccess || menu.accessibleSubMenus.length > 0;
|
|
}
|
|
|
|
return hasMainAccess;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 데스크톱 메뉴 아이템 HTML 생성
|
|
*/
|
|
generateMenuItemHTML(menu) {
|
|
const isActive = this.currentPage === menu.id;
|
|
const activeClass = isActive ? 'bg-blue-100 text-blue-700' : `${menu.bgColor} ${menu.color}`;
|
|
|
|
// 하위 메뉴가 있는 경우 드롭다운 메뉴 생성
|
|
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
|
|
return `
|
|
<div class="relative group">
|
|
<button class="nav-item flex items-center px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${activeClass}"
|
|
data-page="${menu.id}">
|
|
<i class="${menu.icon} mr-2"></i>
|
|
${menu.title}
|
|
<i class="fas fa-chevron-down ml-1 text-xs"></i>
|
|
</button>
|
|
|
|
<!-- 드롭다운 메뉴 -->
|
|
<div class="absolute left-0 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-200 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
|
|
<div class="py-1">
|
|
${menu.accessibleSubMenus.map(subMenu => `
|
|
<a href="${subMenu.url}"
|
|
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 ${this.currentPage === subMenu.id ? 'bg-blue-50 text-blue-700' : ''}"
|
|
data-page="${subMenu.id}"
|
|
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
|
|
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
|
|
${subMenu.title}
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 일반 메뉴 아이템
|
|
return `
|
|
<a href="${menu.url}"
|
|
class="nav-item flex items-center px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${activeClass}"
|
|
data-page="${menu.id}"
|
|
onclick="CommonHeader.navigateToPage(event, '${menu.url}', '${menu.id}')">
|
|
<i class="${menu.icon} mr-2"></i>
|
|
${menu.title}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 모바일 메뉴 아이템 HTML 생성
|
|
*/
|
|
generateMobileMenuItemHTML(menu) {
|
|
const isActive = this.currentPage === menu.id;
|
|
const activeClass = isActive ? 'bg-blue-50 text-blue-700 border-blue-500' : 'text-gray-700 hover:bg-gray-50';
|
|
|
|
// 하위 메뉴가 있는 경우
|
|
if (menu.accessibleSubMenus && menu.accessibleSubMenus.length > 0) {
|
|
return `
|
|
<div class="mobile-submenu-container">
|
|
<button class="w-full flex items-center justify-between px-3 py-2 rounded-md text-base font-medium border-l-4 border-transparent ${activeClass}"
|
|
onclick="this.nextElementSibling.classList.toggle('hidden')"
|
|
data-page="${menu.id}">
|
|
<div class="flex items-center">
|
|
<i class="${menu.icon} mr-3"></i>
|
|
${menu.title}
|
|
</div>
|
|
<i class="fas fa-chevron-down text-xs"></i>
|
|
</button>
|
|
|
|
<!-- 하위 메뉴 -->
|
|
<div class="hidden ml-6 mt-1 space-y-1">
|
|
${menu.accessibleSubMenus.map(subMenu => `
|
|
<a href="${subMenu.url}"
|
|
class="flex items-center px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:bg-gray-100 ${this.currentPage === subMenu.id ? 'bg-blue-50 text-blue-700' : ''}"
|
|
data-page="${subMenu.id}"
|
|
onclick="CommonHeader.navigateToPage(event, '${subMenu.url}', '${subMenu.id}')">
|
|
<i class="${subMenu.icon} mr-3 ${subMenu.color}"></i>
|
|
${subMenu.title}
|
|
</a>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 일반 메뉴 아이템
|
|
return `
|
|
<a href="${menu.url}"
|
|
class="nav-item block px-3 py-2 rounded-md text-base font-medium border-l-4 border-transparent ${activeClass}"
|
|
data-page="${menu.id}"
|
|
onclick="CommonHeader.navigateToPage(event, '${menu.url}', '${menu.id}')">
|
|
<i class="${menu.icon} mr-3"></i>
|
|
${menu.title}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 사용자 역할 표시명 가져오기
|
|
*/
|
|
getUserRoleDisplay() {
|
|
const roleNames = {
|
|
'admin': '관리자',
|
|
'user': '사용자'
|
|
};
|
|
return roleNames[this.currentUser?.role] || '사용자';
|
|
}
|
|
|
|
/**
|
|
* 이벤트 바인딩
|
|
*/
|
|
bindEvents() {
|
|
// 사용자 메뉴 토글
|
|
const userMenuButton = document.getElementById('user-menu-button');
|
|
const userMenu = document.getElementById('user-menu');
|
|
|
|
if (userMenuButton && userMenu) {
|
|
userMenuButton.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
userMenu.classList.toggle('hidden');
|
|
});
|
|
|
|
// 외부 클릭 시 메뉴 닫기
|
|
document.addEventListener('click', () => {
|
|
userMenu.classList.add('hidden');
|
|
});
|
|
}
|
|
|
|
// 모바일 메뉴 토글
|
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
|
const mobileMenu = document.getElementById('mobile-menu');
|
|
|
|
if (mobileMenuButton && mobileMenu) {
|
|
mobileMenuButton.addEventListener('click', () => {
|
|
mobileMenu.classList.toggle('hidden');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 페이지 네비게이션 (부드러운 전환)
|
|
*/
|
|
static navigateToPage(event, url, pageId) {
|
|
event.preventDefault();
|
|
|
|
// 현재 페이지와 같으면 무시
|
|
if (window.commonHeader?.currentPage === pageId) {
|
|
return;
|
|
}
|
|
|
|
// 로딩 표시
|
|
CommonHeader.showPageTransition();
|
|
|
|
// 페이지 이동
|
|
setTimeout(() => {
|
|
window.location.href = url;
|
|
}, 150); // 부드러운 전환을 위한 딜레이
|
|
}
|
|
|
|
/**
|
|
* 페이지 전환 로딩 표시
|
|
*/
|
|
static showPageTransition() {
|
|
// 기존 로딩이 있으면 제거
|
|
const existingLoader = document.getElementById('page-transition-loader');
|
|
if (existingLoader) {
|
|
existingLoader.remove();
|
|
}
|
|
|
|
const loader = document.createElement('div');
|
|
loader.id = 'page-transition-loader';
|
|
loader.className = 'fixed inset-0 bg-white bg-opacity-75 flex items-center justify-center z-50';
|
|
loader.innerHTML = `
|
|
<div class="text-center">
|
|
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
<p class="mt-2 text-sm text-gray-600">페이지를 로드하는 중...</p>
|
|
</div>
|
|
`;
|
|
|
|
document.body.appendChild(loader);
|
|
}
|
|
|
|
/**
|
|
* 비밀번호 변경 모달 표시
|
|
*/
|
|
static showPasswordModal() {
|
|
// 비밀번호 변경 모달 구현 (기존 코드 재사용)
|
|
alert('비밀번호 변경 기능은 관리자 페이지에서 이용해주세요.');
|
|
}
|
|
|
|
/**
|
|
* 로그아웃
|
|
*/
|
|
static logout() {
|
|
if (confirm('로그아웃 하시겠습니까?')) {
|
|
localStorage.removeItem('access_token');
|
|
localStorage.removeItem('currentUser');
|
|
window.location.href = '/index.html';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 페이지 업데이트
|
|
*/
|
|
updateCurrentPage(pageId) {
|
|
this.currentPage = pageId;
|
|
|
|
// 활성 메뉴 업데이트
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
const itemPageId = item.getAttribute('data-page');
|
|
if (itemPageId === pageId) {
|
|
item.classList.add('bg-blue-100', 'text-blue-700');
|
|
item.classList.remove('bg-blue-50', 'hover:bg-blue-100');
|
|
} else {
|
|
item.classList.remove('bg-blue-100', 'text-blue-700');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 키보드 단축키 초기화
|
|
*/
|
|
initializeKeyboardShortcuts() {
|
|
if (window.keyboardShortcuts) {
|
|
window.keyboardShortcuts.setUser(this.currentUser);
|
|
console.log('⌨️ 키보드 단축키 사용자 설정 완료');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 페이지 프리로더 초기화
|
|
*/
|
|
initializePreloader() {
|
|
if (window.pagePreloader) {
|
|
// 사용자 설정 후 프리로더 초기화
|
|
setTimeout(() => {
|
|
window.pagePreloader.init();
|
|
console.log('🚀 페이지 프리로더 초기화 완료');
|
|
}, 1000); // 권한 시스템 로드 후 실행
|
|
}
|
|
}
|
|
}
|
|
|
|
// 전역 인스턴스
|
|
window.commonHeader = new CommonHeader();
|
|
|
|
// 전역 함수로 노출
|
|
window.CommonHeader = CommonHeader;
|