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}에 등록된 작업이 없습니다.

+
+ `; + return; + } + + // 프로젝트별 작업 현황 그룹화 + const projectGroups = groupWorkDataByProject(); + + elements.workStatusContainer.innerHTML = ` +
+ ${Object.entries(projectGroups).map(([projectName, works]) => ` +
+
+

📁 ${projectName}

+ ${works.length}건 +
+
+
+ 총 시간 + ${works.reduce((sum, w) => sum + parseFloat(w.work_hours || 0), 0).toFixed(1)}h +
+
+ 작업자 + ${new Set(works.map(w => w.worker_id)).size}명 +
+
+ 오류 + ${works.filter(w => w.work_status_id === 2).length}건 +
+
+
+ `).join('')} +
+ `; +} + +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 = ` +
+
👥
+

작업자 데이터가 없습니다

+

등록된 작업자가 없습니다.

+
+ `; + return; + } + + if (viewType === 'list') { + displayWorkersAsList(workers); + } else { + displayWorkersAsCards(workers); + } +} + +function displayWorkersAsCards(workers) { + elements.workersContainer.innerHTML = ` +
+ ${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 ` +
+
+
+
+ ${worker.worker_name.charAt(0)} +
+
+

${worker.worker_name}

+

${worker.job_type || '작업자'}

+
+
+ +
+
+
+
+ 오늘 작업 + ${todayWork.length}건 +
+
+ 작업 시간 + ${totalHours.toFixed(1)}h +
+ ${hasError ? ` +
+ 오류 + ⚠️ +
+ ` : ''} +
+
+
+ `; + }).join('')} +
+ `; +} + +function displayWorkersAsList(workers) { + elements.workersContainer.innerHTML = ` +
+ + + + + + + + + + + + ${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 ` + + + + + + + + `; + }).join('')} + +
작업자직종오늘 작업작업 시간상태
+
+
+ ${worker.worker_name.charAt(0)} +
+ ${worker.worker_name} +
+
${worker.job_type || '작업자'}${todayWork.length}건${totalHours.toFixed(1)}시간 + + ${todayWork.length > 0 ? '작업 중' : '대기'} + + ${hasError ? '오류' : ''} +
+
+ `; +} + +// ========== 뷰 버튼 업데이트 ========== // +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 = ` +
+
+

데이터를 불러오는 중...

+
+ `; + + if (elements.workStatusContainer) { + elements.workStatusContainer.innerHTML = loadingHTML; + } + + if (elements.workersContainer) { + elements.workersContainer.innerHTML = loadingHTML; + } +} + +function showErrorState() { + const errorHTML = ` +
+
⚠️
+

데이터를 불러올 수 없습니다

+

네트워크 연결을 확인하고 다시 시도해주세요.

+ +
+ `; + + 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 = ` +
${iconMap[type] || 'ℹ️'}
+
${message}
+ + `; + + elements.toastContainer.appendChild(toast); + + // 자동 제거 + setTimeout(() => { + if (toast.parentElement) { + toast.remove(); + } + }, duration); +} + +// ========== 전역 함수 (HTML에서 호출) ========== // +window.loadDashboardData = loadDashboardData; +window.showToast = showToast; + +// ========== 내보내기 ========== // +export { + loadDashboardData, + showToast, + updateSummaryCards, + displayWorkers +}; diff --git a/web-ui/pages/dashboard/modern-dashboard.html b/web-ui/pages/dashboard/modern-dashboard.html new file mode 100644 index 0000000..2bfc4ab --- /dev/null +++ b/web-ui/pages/dashboard/modern-dashboard.html @@ -0,0 +1,284 @@ + + + + + + 작업 현황판 | 테크니컬코리아 + + + + + + + + + + + + + +
+ + +
+
+
+
+ +
+

테크니컬코리아

+

작업 현황판

+
+
+
+ +
+
+ 현재 시각 + --:--:-- +
+
+ +
+ +
+
+
+ + +
+ + +
+
+ + +
+
+
+ 👥 +
+
+

오늘 작업자

+
+ - + +
+

+ + 전일 대비 +2명 +

+
+
+
+ + +
+
+
+ +
+
+

총 작업 시간

+
+ - + 시간 +
+

+ + 전일 대비 +4시간 +

+
+
+
+ + +
+
+
+ 📁 +
+
+

진행 프로젝트

+
+ - + +
+

+ + 변동 없음 +

+
+
+
+ + +
+
+
+ ⚠️ +
+
+

오류 발생

+
+ - + +
+

+ + 전일 대비 -1건 +

+
+
+
+ +
+
+ + +
+ +
+ + +
+
+
+
+

👥 작업자별 현황

+
+ + +
+
+
+
+
+
+
+

작업자 정보를 불러오는 중...

+
+
+
+
+
+ +
+ + + + +
+ + +
+ + +