feat: 모던 대시보드 및 디자인 시스템 구축

🎨 한글 기반 모던 디자인 시스템:
- design-system.css: 포괄적인 디자인 토큰 및 컴포넌트 시스템
- CSS 변수 기반 색상, 타이포그래피, 간격, 그림자 체계
- 반응형 그리드, 플렉스 유틸리티, 애니메이션 시스템
- 카드, 버튼, 배지, 상태 표시기 등 재사용 가능한 컴포넌트

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

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

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

접근: http://localhost:20000/pages/dashboard/modern-dashboard.html
This commit is contained in:
Hyungi Ahn
2025-11-03 11:41:35 +09:00
parent a3da93adc2
commit eb98bd79f6
4 changed files with 2112 additions and 0 deletions

View File

@@ -0,0 +1,519 @@
// ✅ 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
};