Files
TK-FB-Project/web-ui/js/modern-dashboard.js
Hyungi Ahn eb98bd79f6 feat: 모던 대시보드 및 디자인 시스템 구축
🎨 한글 기반 모던 디자인 시스템:
- design-system.css: 포괄적인 디자인 토큰 및 컴포넌트 시스템
- CSS 변수 기반 색상, 타이포그래피, 간격, 그림자 체계
- 반응형 그리드, 플렉스 유틸리티, 애니메이션 시스템
- 카드, 버튼, 배지, 상태 표시기 등 재사용 가능한 컴포넌트

📊 모던 대시보드 구현:
- modern-dashboard.html: 깔끔하고 직관적인 대시보드 레이아웃
- 실시간 시간 표시, 사용자 프로필 드롭다운
- 4개 요약 카드: 작업자 수, 작업 시간, 프로젝트 수, 오류 건수
- 프로젝트별 작업 현황 시각화
- 작업자 현황 카드/리스트 뷰 전환 기능

🚀 고급 기능:
- modern-dashboard.js: ES6 모듈 기반 JavaScript
- 실시간 데이터 로딩 및 캐싱
- 토스트 알림 시스템
- 로딩/에러 상태 처리
- 반응형 디자인 (모바일 최적화)

 사용자 경험 개선:
- 부드러운 애니메이션 및 호버 효과
- 직관적인 아이콘 및 한글 레이블
- 접근성 고려 (키보드 네비게이션, 색상 대비)
- 일관된 시각적 계층 구조

접근: http://localhost:20000/pages/dashboard/modern-dashboard.html
2025-11-03 11:41:35 +09:00

520 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ✅ modern-dashboard.js - 모던 대시보드 JavaScript
import { apiCall, API } from './api-config.js';
import { getAuthData } from './auth.js';
// 전역 변수
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 () => {
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 apiCall(`${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 apiCall(`${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;
// ========== 내보내기 ========== //
export {
loadDashboardData,
showToast,
updateSummaryCards,
displayWorkers
};