From eb98bd79f692daca878e9d1372b132196a68802f Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 3 Nov 2025 11:41:35 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8D=98=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=B0=8F=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎨 ν•œκΈ€ 기반 λͺ¨λ˜ λ””μžμΈ μ‹œμŠ€ν…œ: - design-system.css: 포괄적인 λ””μžμΈ 토큰 및 μ»΄ν¬λ„ŒνŠΈ μ‹œμŠ€ν…œ - CSS λ³€μˆ˜ 기반 색상, νƒ€μ΄ν¬κ·Έλž˜ν”Ό, 간격, 그림자 체계 - λ°˜μ‘ν˜• κ·Έλ¦¬λ“œ, ν”Œλ ‰μŠ€ μœ ν‹Έλ¦¬ν‹°, μ• λ‹ˆλ©”μ΄μ…˜ μ‹œμŠ€ν…œ - μΉ΄λ“œ, λ²„νŠΌ, λ°°μ§€, μƒνƒœ ν‘œμ‹œκΈ° λ“± μž¬μ‚¬μš© κ°€λŠ₯ν•œ μ»΄ν¬λ„ŒνŠΈ πŸ“Š λͺ¨λ˜ λŒ€μ‹œλ³΄λ“œ κ΅¬ν˜„: - modern-dashboard.html: κΉ”λ”ν•˜κ³  직관적인 λŒ€μ‹œλ³΄λ“œ λ ˆμ΄μ•„μ›ƒ - μ‹€μ‹œκ°„ μ‹œκ°„ ν‘œμ‹œ, μ‚¬μš©μž ν”„λ‘œν•„ λ“œλ‘­λ‹€μš΄ - 4개 μš”μ•½ μΉ΄λ“œ: μž‘μ—…μž 수, μž‘μ—… μ‹œκ°„, ν”„λ‘œμ νŠΈ 수, 였λ₯˜ 건수 - ν”„λ‘œμ νŠΈλ³„ μž‘μ—… ν˜„ν™© μ‹œκ°ν™” - μž‘μ—…μž ν˜„ν™© μΉ΄λ“œ/리슀트 λ·° μ „ν™˜ κΈ°λŠ₯ πŸš€ κ³ κΈ‰ κΈ°λŠ₯: - modern-dashboard.js: ES6 λͺ¨λ“ˆ 기반 JavaScript - μ‹€μ‹œκ°„ 데이터 λ‘œλ”© 및 캐싱 - ν† μŠ€νŠΈ μ•Œλ¦Ό μ‹œμŠ€ν…œ - λ‘œλ”©/μ—λŸ¬ μƒνƒœ 처리 - λ°˜μ‘ν˜• λ””μžμΈ (λͺ¨λ°”일 μ΅œμ ν™”) ✨ μ‚¬μš©μž κ²½ν—˜ κ°œμ„ : - λΆ€λ“œλŸ¬μš΄ μ• λ‹ˆλ©”μ΄μ…˜ 및 ν˜Έλ²„ 효과 - 직관적인 μ•„μ΄μ½˜ 및 ν•œκΈ€ λ ˆμ΄λΈ” - μ ‘κ·Όμ„± κ³ λ € (ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜, 색상 λŒ€λΉ„) - μΌκ΄€λœ μ‹œκ°μ  계측 ꡬ쑰 μ ‘κ·Ό: http://localhost:20000/pages/dashboard/modern-dashboard.html --- web-ui/css/design-system.css | 452 ++++++++++ web-ui/css/modern-dashboard.css | 857 +++++++++++++++++++ web-ui/js/modern-dashboard.js | 519 +++++++++++ web-ui/pages/dashboard/modern-dashboard.html | 284 ++++++ 4 files changed, 2112 insertions(+) create mode 100644 web-ui/css/design-system.css create mode 100644 web-ui/css/modern-dashboard.css create mode 100644 web-ui/js/modern-dashboard.js create mode 100644 web-ui/pages/dashboard/modern-dashboard.html 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건 +

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

πŸ‘₯ μž‘μ—…μžλ³„ ν˜„ν™©

+
+ + +
+
+
+
+
+
+
+

μž‘μ—…μž 정보λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...

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