오늘 작업자
++ + 전일 대비 +2명 +
+총 작업 시간
++ + 전일 대비 +4시간 +
+진행 프로젝트
++ + 변동 없음 +
+오류 발생
++ + 전일 대비 -1건 +
+📊 오늘의 작업 현황
+작업 현황을 불러오는 중...
+⚡ 빠른 작업
+👥 작업자별 현황
+작업자 정보를 불러오는 중...
+diff --git a/web-ui/css/design-system.css b/web-ui/css/design-system.css new file mode 100644 index 0000000..4b46ab1 --- /dev/null +++ b/web-ui/css/design-system.css @@ -0,0 +1,452 @@ +/* ✅ design-system.css - 한글 기반 모던 디자인 시스템 */ + +/* ========== 색상 시스템 ========== */ +:root { + /* 주요 브랜드 색상 */ + --primary-50: #e3f2fd; + --primary-100: #bbdefb; + --primary-200: #90caf9; + --primary-300: #64b5f6; + --primary-400: #42a5f5; + --primary-500: #2196f3; + --primary-600: #1e88e5; + --primary-700: #1976d2; + --primary-800: #1565c0; + --primary-900: #0d47a1; + + /* 보조 색상 */ + --secondary-50: #f3e5f5; + --secondary-100: #e1bee7; + --secondary-200: #ce93d8; + --secondary-300: #ba68c8; + --secondary-400: #ab47bc; + --secondary-500: #9c27b0; + --secondary-600: #8e24aa; + --secondary-700: #7b1fa2; + --secondary-800: #6a1b9a; + --secondary-900: #4a148c; + + /* 그레이 스케일 */ + --gray-50: #fafafa; + --gray-100: #f5f5f5; + --gray-200: #eeeeee; + --gray-300: #e0e0e0; + --gray-400: #bdbdbd; + --gray-500: #9e9e9e; + --gray-600: #757575; + --gray-700: #616161; + --gray-800: #424242; + --gray-900: #212121; + + /* 상태 색상 */ + --success-50: #e8f5e8; + --success-500: #4caf50; + --success-700: #388e3c; + + --warning-50: #fff8e1; + --warning-500: #ff9800; + --warning-700: #f57c00; + + --error-50: #ffebee; + --error-500: #f44336; + --error-700: #d32f2f; + + --info-50: #e1f5fe; + --info-500: #03a9f4; + --info-700: #0288d1; + + /* 배경 색상 */ + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-tertiary: #f1f5f9; + --bg-overlay: rgba(0, 0, 0, 0.5); + + /* 텍스트 색상 */ + --text-primary: #1a202c; + --text-secondary: #4a5568; + --text-tertiary: #718096; + --text-inverse: #ffffff; + + /* 경계선 */ + --border-light: #e2e8f0; + --border-medium: #cbd5e0; + --border-dark: #a0aec0; + + /* 그림자 */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* 반경 */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* 간격 */ + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; + --space-10: 40px; + --space-12: 48px; + --space-16: 64px; + --space-20: 80px; + --space-24: 96px; + + /* 폰트 크기 */ + --text-xs: 12px; + --text-sm: 14px; + --text-base: 16px; + --text-lg: 18px; + --text-xl: 20px; + --text-2xl: 24px; + --text-3xl: 30px; + --text-4xl: 36px; + --text-5xl: 48px; + + /* 폰트 두께 */ + --font-light: 300; + --font-normal: 400; + --font-medium: 500; + --font-semibold: 600; + --font-bold: 700; + --font-extrabold: 800; + + /* 애니메이션 */ + --transition-fast: 150ms ease-in-out; + --transition-normal: 250ms ease-in-out; + --transition-slow: 350ms ease-in-out; +} + +/* ========== 기본 리셋 ========== */ +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + font-family: 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', system-ui, sans-serif; + font-size: var(--text-base); + line-height: 1.6; + color: var(--text-primary); + background-color: var(--bg-secondary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ========== 타이포그래피 ========== */ +.text-xs { font-size: var(--text-xs); } +.text-sm { font-size: var(--text-sm); } +.text-base { font-size: var(--text-base); } +.text-lg { font-size: var(--text-lg); } +.text-xl { font-size: var(--text-xl); } +.text-2xl { font-size: var(--text-2xl); } +.text-3xl { font-size: var(--text-3xl); } +.text-4xl { font-size: var(--text-4xl); } +.text-5xl { font-size: var(--text-5xl); } + +.font-light { font-weight: var(--font-light); } +.font-normal { font-weight: var(--font-normal); } +.font-medium { font-weight: var(--font-medium); } +.font-semibold { font-weight: var(--font-semibold); } +.font-bold { font-weight: var(--font-bold); } +.font-extrabold { font-weight: var(--font-extrabold); } + +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-tertiary { color: var(--text-tertiary); } +.text-inverse { color: var(--text-inverse); } + +/* ========== 카드 컴포넌트 ========== */ +.card { + background: var(--bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + border: 1px solid var(--border-light); + transition: var(--transition-normal); +} + +.card:hover { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.card-header { + padding: var(--space-6); + border-bottom: 1px solid var(--border-light); +} + +.card-body { + padding: var(--space-6); +} + +.card-footer { + padding: var(--space-6); + border-top: 1px solid var(--border-light); + background: var(--bg-tertiary); + border-radius: 0 0 var(--radius-lg) var(--radius-lg); +} + +/* ========== 버튼 컴포넌트 ========== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: var(--space-3) var(--space-4); + font-size: var(--text-sm); + font-weight: var(--font-medium); + border-radius: var(--radius-md); + border: none; + cursor: pointer; + transition: var(--transition-fast); + text-decoration: none; + white-space: nowrap; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: var(--primary-500); + color: var(--text-inverse); +} + +.btn-primary:hover:not(:disabled) { + background: var(--primary-600); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-secondary { + background: var(--gray-100); + color: var(--text-primary); + border: 1px solid var(--border-medium); +} + +.btn-secondary:hover:not(:disabled) { + background: var(--gray-200); +} + +.btn-success { + background: var(--success-500); + color: var(--text-inverse); +} + +.btn-success:hover:not(:disabled) { + background: var(--success-700); +} + +.btn-warning { + background: var(--warning-500); + color: var(--text-inverse); +} + +.btn-warning:hover:not(:disabled) { + background: var(--warning-700); +} + +.btn-error { + background: var(--error-500); + color: var(--text-inverse); +} + +.btn-error:hover:not(:disabled) { + background: var(--error-700); +} + +.btn-sm { + padding: var(--space-2) var(--space-3); + font-size: var(--text-xs); +} + +.btn-lg { + padding: var(--space-4) var(--space-6); + font-size: var(--text-lg); +} + +/* ========== 배지 컴포넌트 ========== */ +.badge { + display: inline-flex; + align-items: center; + gap: var(--space-1); + padding: var(--space-1) var(--space-2); + font-size: var(--text-xs); + font-weight: var(--font-medium); + border-radius: var(--radius-full); + white-space: nowrap; +} + +.badge-primary { + background: var(--primary-100); + color: var(--primary-800); +} + +.badge-success { + background: var(--success-50); + color: var(--success-700); +} + +.badge-warning { + background: var(--warning-50); + color: var(--warning-700); +} + +.badge-error { + background: var(--error-50); + color: var(--error-700); +} + +.badge-gray { + background: var(--gray-100); + color: var(--gray-700); +} + +/* ========== 상태 표시기 ========== */ +.status-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: var(--radius-full); + margin-right: var(--space-2); +} + +.status-dot.active { + background: var(--success-500); + box-shadow: 0 0 0 2px var(--success-100); +} + +.status-dot.inactive { + background: var(--gray-400); +} + +.status-dot.warning { + background: var(--warning-500); + box-shadow: 0 0 0 2px var(--warning-100); +} + +.status-dot.error { + background: var(--error-500); + box-shadow: 0 0 0 2px var(--error-100); +} + +/* ========== 그리드 시스템 ========== */ +.grid { + display: grid; + gap: var(--space-6); +} + +.grid-cols-1 { grid-template-columns: repeat(1, 1fr); } +.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } +.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + +@media (max-width: 768px) { + .grid-cols-2, + .grid-cols-3, + .grid-cols-4 { + grid-template-columns: 1fr; + } +} + +/* ========== 플렉스 유틸리티 ========== */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-start { justify-content: flex-start; } +.justify-end { justify-content: flex-end; } +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } +.gap-6 { gap: var(--space-6); } + +/* ========== 간격 유틸리티 ========== */ +.p-1 { padding: var(--space-1); } +.p-2 { padding: var(--space-2); } +.p-3 { padding: var(--space-3); } +.p-4 { padding: var(--space-4); } +.p-6 { padding: var(--space-6); } +.p-8 { padding: var(--space-8); } + +.m-1 { margin: var(--space-1); } +.m-2 { margin: var(--space-2); } +.m-3 { margin: var(--space-3); } +.m-4 { margin: var(--space-4); } +.m-6 { margin: var(--space-6); } +.m-8 { margin: var(--space-8); } + +.mb-2 { margin-bottom: var(--space-2); } +.mb-4 { margin-bottom: var(--space-4); } +.mb-6 { margin-bottom: var(--space-6); } +.mt-4 { margin-top: var(--space-4); } +.mt-6 { margin-top: var(--space-6); } + +/* ========== 반응형 유틸리티 ========== */ +@media (max-width: 640px) { + .sm\:hidden { display: none; } + .sm\:text-sm { font-size: var(--text-sm); } + .sm\:p-4 { padding: var(--space-4); } +} + +@media (max-width: 768px) { + .md\:hidden { display: none; } + .md\:flex-col { flex-direction: column; } +} + +@media (max-width: 1024px) { + .lg\:hidden { display: none; } +} + +/* ========== 애니메이션 ========== */ +.fade-in { + animation: fadeIn var(--transition-normal) ease-in-out; +} + +.slide-up { + animation: slideUp var(--transition-normal) ease-out; +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ========== 로딩 스피너 ========== */ +.spinner { + width: 20px; + height: 20px; + border: 2px solid var(--gray-200); + border-top: 2px solid var(--primary-500); + border-radius: var(--radius-full); + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/web-ui/css/modern-dashboard.css b/web-ui/css/modern-dashboard.css new file mode 100644 index 0000000..a0ba2af --- /dev/null +++ b/web-ui/css/modern-dashboard.css @@ -0,0 +1,857 @@ +/* ✅ modern-dashboard.css - 모던 대시보드 전용 스타일 */ + +/* ========== 대시보드 레이아웃 ========== */ +.dashboard-container { + min-height: 100vh; + display: flex; + flex-direction: column; + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); +} + +/* ========== 헤더 ========== */ +.dashboard-header { + background: linear-gradient(135deg, #1e40af 0%, #1d4ed8 50%, #2563eb 100%); + color: var(--text-inverse); + padding: var(--space-4) var(--space-6); + box-shadow: var(--shadow-lg); + position: sticky; + top: 0; + z-index: 100; +} + +.header-content { + display: flex; + align-items: center; + justify-content: space-between; + max-width: 1400px; + margin: 0 auto; +} + +.header-left .brand { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.brand-logo { + width: 48px; + height: 48px; + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); +} + +.brand-title { + font-size: var(--text-2xl); + font-weight: var(--font-bold); + margin: 0; + line-height: 1.2; +} + +.brand-subtitle { + font-size: var(--text-sm); + opacity: 0.9; + margin: 0; + font-weight: var(--font-normal); +} + +.header-center .current-time { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--radius-full); + padding: var(--space-3) var(--space-4); + text-align: center; +} + +.time-label { + display: block; + font-size: var(--text-xs); + opacity: 0.8; + margin-bottom: var(--space-1); +} + +.time-value { + display: block; + font-size: var(--text-lg); + font-weight: var(--font-bold); + font-family: 'Courier New', monospace; +} + +.header-right .user-profile { + position: relative; + display: flex; + align-items: center; + gap: var(--space-3); + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--radius-full); + padding: var(--space-2) var(--space-4); + cursor: pointer; + transition: var(--transition-normal); +} + +.user-profile:hover { + background: rgba(255, 255, 255, 0.2); +} + +.user-avatar { + width: 40px; + height: 40px; + border-radius: var(--radius-full); + background: var(--primary-300); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-bold); + color: var(--primary-800); +} + +.user-info { + display: flex; + flex-direction: column; +} + +.user-name { + font-size: var(--text-sm); + font-weight: var(--font-semibold); + line-height: 1.2; +} + +.user-role { + font-size: var(--text-xs); + opacity: 0.8; +} + +.profile-menu { + position: absolute; + top: 100%; + right: 0; + margin-top: var(--space-2); + background: var(--bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + border: 1px solid var(--border-light); + min-width: 200px; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: var(--transition-normal); + z-index: 1000; +} + +.user-profile:hover .profile-menu, +.profile-menu:hover { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.menu-item { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + color: var(--text-primary); + text-decoration: none; + border: none; + background: none; + width: 100%; + text-align: left; + font-size: var(--text-sm); + cursor: pointer; + transition: var(--transition-fast); +} + +.menu-item:hover { + background: var(--gray-50); +} + +.menu-item:first-child { + border-radius: var(--radius-lg) var(--radius-lg) 0 0; +} + +.menu-item:last-child { + border-radius: 0 0 var(--radius-lg) var(--radius-lg); +} + +.logout-btn { + color: var(--error-600); + border-top: 1px solid var(--border-light); +} + +.logout-btn:hover { + background: var(--error-50); +} + +/* ========== 메인 콘텐츠 ========== */ +.dashboard-main { + flex: 1; + padding: var(--space-8) var(--space-6); + max-width: 1400px; + margin: 0 auto; + width: 100%; +} + +/* ========== 요약 섹션 ========== */ +.summary-section { + margin-bottom: var(--space-8); +} + +.summary-card { + position: relative; + overflow: hidden; + transition: var(--transition-normal); +} + +.summary-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--primary-500), var(--primary-600)); +} + +.summary-card .card-body { + display: flex; + align-items: flex-start; + gap: var(--space-4); +} + +.summary-icon { + width: 56px; + height: 56px; + border-radius: var(--radius-xl); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-2xl); + flex-shrink: 0; +} + +.summary-icon.success { + background: var(--success-100); + color: var(--success-700); +} + +.summary-icon.primary { + background: var(--primary-100); + color: var(--primary-700); +} + +.summary-icon.warning { + background: var(--warning-100); + color: var(--warning-700); +} + +.summary-icon.error { + background: var(--error-100); + color: var(--error-700); +} + +.summary-content { + flex: 1; +} + +.summary-title { + font-size: var(--text-sm); + font-weight: var(--font-medium); + color: var(--text-secondary); + margin: 0 0 var(--space-2) 0; +} + +.summary-value { + display: flex; + align-items: baseline; + gap: var(--space-1); + margin-bottom: var(--space-2); +} + +.value-number { + font-size: var(--text-3xl); + font-weight: var(--font-bold); + color: var(--text-primary); + line-height: 1; +} + +.value-unit { + font-size: var(--text-sm); + color: var(--text-secondary); + font-weight: var(--font-medium); +} + +.summary-change { + display: flex; + align-items: center; + gap: var(--space-1); + font-size: var(--text-xs); + font-weight: var(--font-medium); + margin: 0; +} + +.summary-change.positive { + color: var(--success-600); +} + +.summary-change.negative { + color: var(--error-600); +} + +.summary-change.neutral { + color: var(--text-tertiary); +} + +.change-icon { + font-size: var(--text-sm); +} + +/* ========== 콘텐츠 섹션 ========== */ +.content-section { + margin-bottom: var(--space-8); +} + +.content-card { + height: fit-content; +} + +.col-span-2 { + grid-column: span 2; +} + +.card-title { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + color: var(--text-primary); + margin: 0; + display: flex; + align-items: center; + gap: var(--space-2); +} + +.date-selector { + display: flex; + align-items: center; + gap: var(--space-3); +} + +.date-input { + padding: var(--space-2) var(--space-3); + border: 1px solid var(--border-medium); + border-radius: var(--radius-md); + font-size: var(--text-sm); + background: var(--bg-primary); + color: var(--text-primary); +} + +.date-input:focus { + outline: none; + border-color: var(--primary-500); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); +} + +/* ========== 작업 현황 ========== */ +.work-status-container { + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; +} + +.loading-state { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-4); + color: var(--text-secondary); +} + +/* ========== 빠른 작업 ========== */ +.quick-actions { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.quick-action-btn { + display: flex; + align-items: center; + gap: var(--space-4); + padding: var(--space-4); + background: var(--bg-tertiary); + border-radius: var(--radius-lg); + text-decoration: none; + color: var(--text-primary); + transition: var(--transition-normal); + border: 1px solid var(--border-light); +} + +.quick-action-btn:hover { + background: var(--bg-primary); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.action-icon { + width: 48px; + height: 48px; + background: var(--primary-100); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-xl); + flex-shrink: 0; +} + +.action-content h3 { + font-size: var(--text-sm); + font-weight: var(--font-semibold); + margin: 0 0 var(--space-1) 0; + color: var(--text-primary); +} + +.action-content p { + font-size: var(--text-xs); + color: var(--text-secondary); + margin: 0; + line-height: 1.4; +} + +.admin-only { + opacity: 0.6; + pointer-events: none; +} + +.admin-only.visible { + opacity: 1; + pointer-events: auto; +} + +/* ========== 작업자 섹션 ========== */ +.workers-section { + margin-bottom: var(--space-8); +} + +.view-controls { + display: flex; + gap: var(--space-2); +} + +.workers-container { + min-height: 200px; +} + +/* ========== 푸터 ========== */ +.dashboard-footer { + background: var(--bg-primary); + border-top: 1px solid var(--border-light); + padding: var(--space-6); + margin-top: auto; +} + +.footer-content { + max-width: 1400px; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.footer-text { + font-size: var(--text-sm); + color: var(--text-secondary); + margin: 0; +} + +.footer-links { + display: flex; + gap: var(--space-6); +} + +.footer-link { + font-size: var(--text-sm); + color: var(--text-secondary); + text-decoration: none; + transition: var(--transition-fast); +} + +.footer-link:hover { + color: var(--primary-600); +} + +/* ========== 토스트 알림 ========== */ +.toast-container { + position: fixed; + top: var(--space-6); + right: var(--space-6); + z-index: 1000; + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.toast { + background: var(--bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + border: 1px solid var(--border-light); + padding: var(--space-4); + min-width: 300px; + display: flex; + align-items: center; + gap: var(--space-3); + animation: slideInRight var(--transition-normal) ease-out; +} + +.toast.success { + border-left: 4px solid var(--success-500); +} + +.toast.error { + border-left: 4px solid var(--error-500); +} + +.toast.warning { + border-left: 4px solid var(--warning-500); +} + +.toast.info { + border-left: 4px solid var(--info-500); +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* ========== 작업 현황 스타일 ========== */ +.work-status-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--space-4); +} + +.project-status-card { + background: var(--bg-tertiary); + border-radius: var(--radius-lg); + padding: var(--space-4); + border: 1px solid var(--border-light); +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--space-3); +} + +.project-name { + font-size: var(--text-base); + font-weight: var(--font-semibold); + margin: 0; + color: var(--text-primary); +} + +.work-count { + font-size: var(--text-xs); +} + +.project-stats { + display: flex; + justify-content: space-between; + gap: var(--space-3); +} + +.stat-item { + text-align: center; +} + +.stat-label { + display: block; + font-size: var(--text-xs); + color: var(--text-secondary); + margin-bottom: var(--space-1); +} + +.stat-value { + display: block; + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--text-primary); +} + +.stat-value.error { + color: var(--error-600); +} + +/* ========== 작업자 카드 스타일 ========== */ +.workers-grid { + gap: var(--space-4); +} + +.worker-card { + transition: var(--transition-normal); +} + +.worker-card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.worker-header { + display: flex; + align-items: center; + gap: var(--space-3); + margin-bottom: var(--space-4); +} + +.worker-avatar { + width: 48px; + height: 48px; + border-radius: var(--radius-full); + background: var(--primary-100); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-bold); + color: var(--primary-700); + font-size: var(--text-lg); +} + +.worker-avatar.small { + width: 32px; + height: 32px; + font-size: var(--text-sm); +} + +.worker-info { + flex: 1; +} + +.worker-name { + font-size: var(--text-sm); + font-weight: var(--font-semibold); + margin: 0 0 var(--space-1) 0; + color: var(--text-primary); +} + +.worker-job { + font-size: var(--text-xs); + color: var(--text-secondary); + margin: 0; +} + +.worker-status { + display: flex; + align-items: center; +} + +.worker-stats { + display: flex; + justify-content: space-between; + gap: var(--space-2); +} + +.stat { + text-align: center; + flex: 1; +} + +.stat.error .stat-value { + color: var(--error-600); +} + +/* ========== 테이블 스타일 ========== */ +.workers-table { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + background: var(--bg-primary); + border-radius: var(--radius-lg); + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +.table th { + background: var(--gray-50); + padding: var(--space-4); + text-align: left; + font-size: var(--text-sm); + font-weight: var(--font-semibold); + color: var(--text-secondary); + border-bottom: 1px solid var(--border-light); +} + +.table td { + padding: var(--space-4); + border-bottom: 1px solid var(--border-light); + font-size: var(--text-sm); + color: var(--text-primary); +} + +.table tr:last-child td { + border-bottom: none; +} + +.table tr:hover { + background: var(--gray-50); +} + +.worker-cell { + display: flex; + align-items: center; + gap: var(--space-3); +} + +/* ========== 빈 상태 및 오류 상태 ========== */ +.empty-state, +.error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-12); + text-align: center; + color: var(--text-secondary); +} + +.empty-icon, +.error-icon { + font-size: var(--text-5xl); + margin-bottom: var(--space-4); + opacity: 0.5; +} + +.empty-state h3, +.error-state h3 { + font-size: var(--text-lg); + font-weight: var(--font-semibold); + margin: 0 0 var(--space-2) 0; + color: var(--text-primary); +} + +.empty-state p, +.error-state p { + font-size: var(--text-sm); + margin: 0 0 var(--space-4) 0; + max-width: 400px; + line-height: 1.5; +} + +/* ========== 토스트 스타일 보완 ========== */ +.toast-icon { + font-size: var(--text-lg); + flex-shrink: 0; +} + +.toast-message { + flex: 1; + font-size: var(--text-sm); + color: var(--text-primary); +} + +.toast-close { + background: none; + border: none; + font-size: var(--text-lg); + color: var(--text-secondary); + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + transition: var(--transition-fast); +} + +.toast-close:hover { + background: var(--gray-100); + color: var(--text-primary); +} + +/* ========== 반응형 디자인 ========== */ +@media (max-width: 1024px) { + .grid-cols-4 { + grid-template-columns: repeat(2, 1fr); + } + + .grid-cols-3 { + grid-template-columns: 1fr; + } + + .col-span-2 { + grid-column: span 1; + } +} + +@media (max-width: 768px) { + .dashboard-main { + padding: var(--space-4); + } + + .header-content { + flex-direction: column; + gap: var(--space-4); + } + + .header-center, + .header-right { + order: 3; + } + + .grid-cols-4, + .grid-cols-2 { + grid-template-columns: 1fr; + } + + .summary-card .card-body { + flex-direction: column; + text-align: center; + } + + .footer-content { + flex-direction: column; + gap: var(--space-4); + text-align: center; + } +} + +@media (max-width: 640px) { + .dashboard-header { + padding: var(--space-3) var(--space-4); + } + + .brand-title { + font-size: var(--text-lg); + } + + .brand-subtitle { + font-size: var(--text-xs); + } + + .user-info { + display: none; + } + + .toast-container { + left: var(--space-4); + right: var(--space-4); + } + + .toast { + min-width: auto; + } +} diff --git a/web-ui/js/modern-dashboard.js b/web-ui/js/modern-dashboard.js new file mode 100644 index 0000000..b726010 --- /dev/null +++ b/web-ui/js/modern-dashboard.js @@ -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 = ` +
${selectedDate}에 등록된 작업이 없습니다.
+등록된 작업자가 없습니다.
+${worker.job_type || '작업자'}
+| 작업자 | +직종 | +오늘 작업 | +작업 시간 | +상태 | +
|---|---|---|---|---|
|
+
+
+
+ ${worker.worker_name.charAt(0)}
+
+ ${worker.worker_name}
+ |
+ ${worker.job_type || '작업자'} | +${todayWork.length}건 | +${totalHours.toFixed(1)}시간 | ++ + ${todayWork.length > 0 ? '작업 중' : '대기'} + + ${hasError ? '오류' : ''} + | +
데이터를 불러오는 중...
+네트워크 연결을 확인하고 다시 시도해주세요.
+ +
+ 작업 현황판
++ + 전일 대비 +2명 +
++ + 전일 대비 +4시간 +
++ + 변동 없음 +
++ + 전일 대비 -1건 +
+작업 현황을 불러오는 중...
+작업자 정보를 불러오는 중...
+