🐛 해결된 문제: - auth-check.js의 ES6 import 문법 오류 - window.apiCall is not a function 오류 - 스크립트 로딩 순서로 인한 의존성 문제 🔧 수정 내용: 1. auth-check.js: - ES6 import 제거 → 함수 직접 구현 - isLoggedIn, getUser, clearAuthData 내장 - 모듈 의존성 완전 제거 2. 스크립트 로딩 순서 최적화: - api-config.js: defer 제거 (즉시 로드) - auth-check.js: defer 유지 - modern-dashboard.js: defer 유지 3. modern-dashboard.js: - API 함수 로드 대기 로직 추가 - 최대 5초 대기 후 오류 처리 - 안전한 초기화 보장 ✅ 개선 효과: - 모든 JavaScript 오류 해결 - 안정적인 스크립트 로딩 순서 - API 함수 의존성 문제 해결 - 대시보드 정상 초기화 보장 테스트: http://localhost:20000/pages/dashboard/group-leader.html
539 lines
17 KiB
JavaScript
539 lines
17 KiB
JavaScript
// ✅ modern-dashboard.js - 모던 대시보드 JavaScript
|
||
|
||
// API 설정 및 함수들은 api-config.js에서 로드됨
|
||
// window.API, window.apiCall, window.getAuthHeaders 사용
|
||
|
||
// 인증 관련 함수들
|
||
function getAuthData() {
|
||
const token = localStorage.getItem('token');
|
||
const user = localStorage.getItem('user');
|
||
return {
|
||
token,
|
||
user: user ? JSON.parse(user) : null
|
||
};
|
||
}
|
||
|
||
// 전역 변수
|
||
let currentUser = null;
|
||
let workersData = [];
|
||
let workData = [];
|
||
let selectedDate = new Date().toISOString().split('T')[0];
|
||
|
||
// DOM 요소
|
||
const elements = {
|
||
currentTime: document.getElementById('currentTime'),
|
||
timeValue: document.getElementById('timeValue'),
|
||
userName: document.getElementById('userName'),
|
||
userRole: document.getElementById('userRole'),
|
||
userInitial: document.getElementById('userInitial'),
|
||
selectedDate: document.getElementById('selectedDate'),
|
||
refreshBtn: document.getElementById('refreshBtn'),
|
||
logoutBtn: document.getElementById('logoutBtn'),
|
||
|
||
// 요약 카드
|
||
todayWorkers: document.getElementById('todayWorkers'),
|
||
totalHours: document.getElementById('totalHours'),
|
||
activeProjects: document.getElementById('activeProjects'),
|
||
errorCount: document.getElementById('errorCount'),
|
||
|
||
// 컨테이너
|
||
workStatusContainer: document.getElementById('workStatusContainer'),
|
||
workersContainer: document.getElementById('workersContainer'),
|
||
toastContainer: document.getElementById('toastContainer')
|
||
};
|
||
|
||
// ========== 초기화 ========== //
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
// API 함수가 로드될 때까지 기다림
|
||
let retryCount = 0;
|
||
const maxRetries = 50; // 5초 대기
|
||
|
||
while (!window.apiCall && retryCount < maxRetries) {
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
retryCount++;
|
||
}
|
||
|
||
if (!window.apiCall) {
|
||
console.error('❌ API 함수를 로드할 수 없습니다.');
|
||
showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await initializeDashboard();
|
||
} catch (error) {
|
||
console.error('대시보드 초기화 오류:', error);
|
||
showToast('대시보드를 불러오는 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
});
|
||
|
||
async function initializeDashboard() {
|
||
console.log('🚀 모던 대시보드 초기화 시작');
|
||
|
||
// 사용자 정보 설정
|
||
setupUserInfo();
|
||
|
||
// 시간 업데이트 시작
|
||
updateCurrentTime();
|
||
setInterval(updateCurrentTime, 1000);
|
||
|
||
// 날짜 설정
|
||
elements.selectedDate.value = selectedDate;
|
||
|
||
// 이벤트 리스너 설정
|
||
setupEventListeners();
|
||
|
||
// 데이터 로드
|
||
await loadDashboardData();
|
||
|
||
// 관리자 권한 확인
|
||
checkAdminAccess();
|
||
|
||
console.log('✅ 모던 대시보드 초기화 완료');
|
||
}
|
||
|
||
// ========== 사용자 정보 설정 ========== //
|
||
function setupUserInfo() {
|
||
const authData = getAuthData();
|
||
if (authData && authData.user) {
|
||
currentUser = authData.user;
|
||
|
||
// 사용자 이름 설정
|
||
elements.userName.textContent = currentUser.name || currentUser.username;
|
||
|
||
// 사용자 역할 설정
|
||
const roleMap = {
|
||
'admin': '관리자',
|
||
'system': '시스템 관리자',
|
||
'group_leader': '그룹장',
|
||
'leader': '그룹장',
|
||
'user': '작업자'
|
||
};
|
||
elements.userRole.textContent = roleMap[currentUser.role] || '작업자';
|
||
|
||
// 아바타 초기값 설정
|
||
const initial = (currentUser.name || currentUser.username).charAt(0);
|
||
elements.userInitial.textContent = initial;
|
||
|
||
console.log('👤 사용자 정보 설정 완료:', currentUser.name);
|
||
}
|
||
}
|
||
|
||
// ========== 시간 업데이트 ========== //
|
||
function updateCurrentTime() {
|
||
const now = new Date();
|
||
const timeString = now.toLocaleTimeString('ko-KR', {
|
||
hour12: false,
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
});
|
||
elements.timeValue.textContent = timeString;
|
||
}
|
||
|
||
// ========== 이벤트 리스너 ========== //
|
||
function setupEventListeners() {
|
||
// 날짜 변경
|
||
elements.selectedDate.addEventListener('change', (e) => {
|
||
selectedDate = e.target.value;
|
||
loadDashboardData();
|
||
});
|
||
|
||
// 새로고침 버튼
|
||
elements.refreshBtn.addEventListener('click', () => {
|
||
loadDashboardData();
|
||
showToast('데이터를 새로고침했습니다.', 'success');
|
||
});
|
||
|
||
// 로그아웃 버튼
|
||
elements.logoutBtn.addEventListener('click', () => {
|
||
if (confirm('로그아웃하시겠습니까?')) {
|
||
localStorage.clear();
|
||
window.location.href = '/index.html';
|
||
}
|
||
});
|
||
|
||
// 뷰 컨트롤 버튼들
|
||
const listViewBtn = document.getElementById('listViewBtn');
|
||
const cardViewBtn = document.getElementById('cardViewBtn');
|
||
|
||
if (listViewBtn) {
|
||
listViewBtn.addEventListener('click', () => {
|
||
displayWorkers(workersData, 'list');
|
||
updateViewButtons('list');
|
||
});
|
||
}
|
||
|
||
if (cardViewBtn) {
|
||
cardViewBtn.addEventListener('click', () => {
|
||
displayWorkers(workersData, 'card');
|
||
updateViewButtons('card');
|
||
});
|
||
}
|
||
}
|
||
|
||
// ========== 데이터 로드 ========== //
|
||
async function loadDashboardData() {
|
||
console.log('📊 대시보드 데이터 로딩 시작');
|
||
|
||
try {
|
||
// 로딩 상태 표시
|
||
showLoadingState();
|
||
|
||
// 병렬로 데이터 로드
|
||
const [workersResult, workResult] = await Promise.all([
|
||
loadWorkers(),
|
||
loadWorkData(selectedDate)
|
||
]);
|
||
|
||
// 요약 데이터 업데이트
|
||
updateSummaryCards();
|
||
|
||
// 작업 현황 표시
|
||
displayWorkStatus();
|
||
|
||
// 작업자 현황 표시
|
||
displayWorkers(workersData, 'card');
|
||
|
||
console.log('✅ 대시보드 데이터 로딩 완료');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 대시보드 데이터 로딩 오류:', error);
|
||
showErrorState();
|
||
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
|
||
}
|
||
}
|
||
|
||
async function loadWorkers() {
|
||
try {
|
||
console.log('👥 작업자 데이터 로딩...');
|
||
const response = await window.apiCall(`${window.API}/workers`);
|
||
workersData = Array.isArray(response) ? response : (response.data || []);
|
||
console.log(`✅ 작업자 ${workersData.length}명 로드 완료`);
|
||
return workersData;
|
||
} catch (error) {
|
||
console.error('작업자 데이터 로딩 오류:', error);
|
||
workersData = [];
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async function loadWorkData(date) {
|
||
try {
|
||
console.log(`📋 ${date} 작업 데이터 로딩...`);
|
||
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${date}&view_all=true`);
|
||
workData = Array.isArray(response) ? response : (response.data || []);
|
||
console.log(`✅ 작업 데이터 ${workData.length}건 로드 완료`);
|
||
return workData;
|
||
} catch (error) {
|
||
console.error('작업 데이터 로딩 오류:', error);
|
||
workData = [];
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// ========== 요약 카드 업데이트 ========== //
|
||
function updateSummaryCards() {
|
||
// 오늘 작업자 수
|
||
const todayWorkersCount = new Set(workData.map(w => w.worker_id)).size;
|
||
updateSummaryCard(elements.todayWorkers, todayWorkersCount, '명');
|
||
|
||
// 총 작업 시간
|
||
const totalHours = workData.reduce((sum, work) => sum + parseFloat(work.work_hours || 0), 0);
|
||
updateSummaryCard(elements.totalHours, totalHours.toFixed(1), '시간');
|
||
|
||
// 진행 중인 프로젝트
|
||
const activeProjectsCount = new Set(workData.map(w => w.project_id)).size;
|
||
updateSummaryCard(elements.activeProjects, activeProjectsCount, '개');
|
||
|
||
// 오류 발생 건수
|
||
const errorCount = workData.filter(w => w.work_status_id === 2).length;
|
||
updateSummaryCard(elements.errorCount, errorCount, '건');
|
||
}
|
||
|
||
function updateSummaryCard(element, value, unit) {
|
||
if (element) {
|
||
const numberElement = element.querySelector('.value-number');
|
||
const unitElement = element.querySelector('.value-unit');
|
||
|
||
if (numberElement) numberElement.textContent = value;
|
||
if (unitElement) unitElement.textContent = unit;
|
||
}
|
||
}
|
||
|
||
// ========== 작업 현황 표시 ========== //
|
||
function displayWorkStatus() {
|
||
if (!elements.workStatusContainer) return;
|
||
|
||
if (workData.length === 0) {
|
||
elements.workStatusContainer.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-icon">📭</div>
|
||
<h3>작업 데이터가 없습니다</h3>
|
||
<p>${selectedDate}에 등록된 작업이 없습니다.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 프로젝트별 작업 현황 그룹화
|
||
const projectGroups = groupWorkDataByProject();
|
||
|
||
elements.workStatusContainer.innerHTML = `
|
||
<div class="work-status-grid">
|
||
${Object.entries(projectGroups).map(([projectName, works]) => `
|
||
<div class="project-status-card">
|
||
<div class="project-header">
|
||
<h4 class="project-name">📁 ${projectName}</h4>
|
||
<span class="work-count badge badge-primary">${works.length}건</span>
|
||
</div>
|
||
<div class="project-stats">
|
||
<div class="stat-item">
|
||
<span class="stat-label">총 시간</span>
|
||
<span class="stat-value">${works.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0).toFixed(1)}h</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">작업자</span>
|
||
<span class="stat-value">${new Set(works.map(w => w.worker_id)).size}명</span>
|
||
</div>
|
||
<div class="stat-item">
|
||
<span class="stat-label">오류</span>
|
||
<span class="stat-value ${works.filter(w => w.work_status_id === 2).length > 0 ? 'error' : ''}">${works.filter(w => w.work_status_id === 2).length}건</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function groupWorkDataByProject() {
|
||
const groups = {};
|
||
workData.forEach(work => {
|
||
const projectName = work.project_name || '미지정 프로젝트';
|
||
if (!groups[projectName]) {
|
||
groups[projectName] = [];
|
||
}
|
||
groups[projectName].push(work);
|
||
});
|
||
return groups;
|
||
}
|
||
|
||
// ========== 작업자 현황 표시 ========== //
|
||
function displayWorkers(workers, viewType = 'card') {
|
||
if (!elements.workersContainer) return;
|
||
|
||
if (workers.length === 0) {
|
||
elements.workersContainer.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-icon">👥</div>
|
||
<h3>작업자 데이터가 없습니다</h3>
|
||
<p>등록된 작업자가 없습니다.</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
if (viewType === 'list') {
|
||
displayWorkersAsList(workers);
|
||
} else {
|
||
displayWorkersAsCards(workers);
|
||
}
|
||
}
|
||
|
||
function displayWorkersAsCards(workers) {
|
||
elements.workersContainer.innerHTML = `
|
||
<div class="workers-grid grid grid-cols-4">
|
||
${workers.map(worker => {
|
||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||
|
||
return `
|
||
<div class="worker-card card">
|
||
<div class="card-body">
|
||
<div class="worker-header">
|
||
<div class="worker-avatar">
|
||
<span>${worker.worker_name.charAt(0)}</span>
|
||
</div>
|
||
<div class="worker-info">
|
||
<h4 class="worker-name">${worker.worker_name}</h4>
|
||
<p class="worker-job">${worker.job_type || '작업자'}</p>
|
||
</div>
|
||
<div class="worker-status">
|
||
<span class="status-dot ${todayWork.length > 0 ? 'active' : 'inactive'}"></span>
|
||
</div>
|
||
</div>
|
||
<div class="worker-stats">
|
||
<div class="stat">
|
||
<span class="stat-label">오늘 작업</span>
|
||
<span class="stat-value">${todayWork.length}건</span>
|
||
</div>
|
||
<div class="stat">
|
||
<span class="stat-label">작업 시간</span>
|
||
<span class="stat-value">${totalHours.toFixed(1)}h</span>
|
||
</div>
|
||
${hasError ? `
|
||
<div class="stat error">
|
||
<span class="stat-label">오류</span>
|
||
<span class="stat-value">⚠️</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('')}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function displayWorkersAsList(workers) {
|
||
elements.workersContainer.innerHTML = `
|
||
<div class="workers-table">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>작업자</th>
|
||
<th>직종</th>
|
||
<th>오늘 작업</th>
|
||
<th>작업 시간</th>
|
||
<th>상태</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${workers.map(worker => {
|
||
const todayWork = workData.filter(w => w.worker_id === worker.worker_id);
|
||
const totalHours = todayWork.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0);
|
||
const hasError = todayWork.some(w => w.work_status_id === 2);
|
||
|
||
return `
|
||
<tr>
|
||
<td>
|
||
<div class="worker-cell">
|
||
<div class="worker-avatar small">
|
||
<span>${worker.worker_name.charAt(0)}</span>
|
||
</div>
|
||
<span class="worker-name">${worker.worker_name}</span>
|
||
</div>
|
||
</td>
|
||
<td>${worker.job_type || '작업자'}</td>
|
||
<td>${todayWork.length}건</td>
|
||
<td>${totalHours.toFixed(1)}시간</td>
|
||
<td>
|
||
<span class="badge ${todayWork.length > 0 ? 'badge-success' : 'badge-gray'}">
|
||
${todayWork.length > 0 ? '작업 중' : '대기'}
|
||
</span>
|
||
${hasError ? '<span class="badge badge-error">오류</span>' : ''}
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// ========== 뷰 버튼 업데이트 ========== //
|
||
function updateViewButtons(activeView) {
|
||
const listBtn = document.getElementById('listViewBtn');
|
||
const cardBtn = document.getElementById('cardViewBtn');
|
||
|
||
if (listBtn && cardBtn) {
|
||
listBtn.classList.toggle('btn-primary', activeView === 'list');
|
||
listBtn.classList.toggle('btn-secondary', activeView !== 'list');
|
||
|
||
cardBtn.classList.toggle('btn-primary', activeView === 'card');
|
||
cardBtn.classList.toggle('btn-secondary', activeView !== 'card');
|
||
}
|
||
}
|
||
|
||
// ========== 관리자 권한 확인 ========== //
|
||
function checkAdminAccess() {
|
||
const adminElements = document.querySelectorAll('.admin-only');
|
||
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.access_level);
|
||
|
||
adminElements.forEach(element => {
|
||
if (isAdmin) {
|
||
element.classList.add('visible');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ========== 상태 표시 ========== //
|
||
function showLoadingState() {
|
||
const loadingHTML = `
|
||
<div class="loading-state">
|
||
<div class="spinner"></div>
|
||
<p>데이터를 불러오는 중...</p>
|
||
</div>
|
||
`;
|
||
|
||
if (elements.workStatusContainer) {
|
||
elements.workStatusContainer.innerHTML = loadingHTML;
|
||
}
|
||
|
||
if (elements.workersContainer) {
|
||
elements.workersContainer.innerHTML = loadingHTML;
|
||
}
|
||
}
|
||
|
||
function showErrorState() {
|
||
const errorHTML = `
|
||
<div class="error-state">
|
||
<div class="error-icon">⚠️</div>
|
||
<h3>데이터를 불러올 수 없습니다</h3>
|
||
<p>네트워크 연결을 확인하고 다시 시도해주세요.</p>
|
||
<button class="btn btn-primary" onclick="loadDashboardData()">
|
||
<span>🔄</span>
|
||
다시 시도
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
if (elements.workStatusContainer) {
|
||
elements.workStatusContainer.innerHTML = errorHTML;
|
||
}
|
||
|
||
if (elements.workersContainer) {
|
||
elements.workersContainer.innerHTML = errorHTML;
|
||
}
|
||
}
|
||
|
||
// ========== 토스트 알림 ========== //
|
||
function showToast(message, type = 'info', duration = 3000) {
|
||
if (!elements.toastContainer) return;
|
||
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
|
||
const iconMap = {
|
||
success: '✅',
|
||
error: '❌',
|
||
warning: '⚠️',
|
||
info: 'ℹ️'
|
||
};
|
||
|
||
toast.innerHTML = `
|
||
<div class="toast-icon">${iconMap[type] || 'ℹ️'}</div>
|
||
<div class="toast-message">${message}</div>
|
||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
elements.toastContainer.appendChild(toast);
|
||
|
||
// 자동 제거
|
||
setTimeout(() => {
|
||
if (toast.parentElement) {
|
||
toast.remove();
|
||
}
|
||
}, duration);
|
||
}
|
||
|
||
// ========== 전역 함수 (HTML에서 호출) ========== //
|
||
window.loadDashboardData = loadDashboardData;
|
||
window.showToast = showToast;
|
||
window.updateSummaryCards = updateSummaryCards;
|
||
window.displayWorkers = displayWorkers;
|