From 86312c1af79accc521de3357108539e9b3ba1af2 Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Fri, 13 Mar 2026 19:39:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20=EB=8C=80=EC=8B=9C=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20UI=20=EA=B0=9C=EC=84=A0=20=E2=80=94=20=ED=99=98?= =?UTF-8?q?=EC=98=81=20=EC=9D=B8=EC=82=AC=20+=20=ED=98=84=ED=99=A9=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20+=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A: 시간대별 환영 인사, 날짜, 날씨 API 연동 - B: 오늘 현황 숫자카드 (출근/작업/이슈) — 권한 기반 동적 표시 - C: 시스템 카드에 설명 텍스트 추가 - fix: notification-bell 드롭다운 position:fixed + 스크롤 시 닫기 Co-Authored-By: Claude Opus 4.6 --- gateway/html/dashboard.html | 218 ++++++++++++++++++++++- gateway/html/shared/notification-bell.js | 28 ++- 2 files changed, 236 insertions(+), 10 deletions(-) diff --git a/gateway/html/dashboard.html b/gateway/html/dashboard.html index 0ed4e96..9606890 100644 --- a/gateway/html/dashboard.html +++ b/gateway/html/dashboard.html @@ -241,6 +241,52 @@ box-shadow: 0 1px 3px rgba(0,0,0,0.08); } + /* ===== Welcome Section ===== */ + .welcome-section { + padding: 24px; + margin-bottom: 24px; + background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%); + border-radius: 12px; + } + .welcome-greeting { + font-size: 20px; + font-weight: 600; + color: #1e3a5f; + } + .welcome-meta { + font-size: 14px; + color: #6b7280; + margin-top: 6px; + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + /* ===== Stats Cards ===== */ + .stats-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + @media (min-width: 640px) { + .stats-grid { grid-template-columns: repeat(3, 1fr); } + } + .stat-card { + background: white; + border-radius: 10px; + padding: 18px 14px; + text-align: center; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); + border: 1px solid #e5e7eb; + } + .stat-icon { font-size: 22px; margin-bottom: 4px; } + .stat-label { font-size: 12px; color: #6b7280; font-weight: 500; } + .stat-value { font-size: 28px; font-weight: 700; margin-top: 4px; } + + /* ===== Card desc ===== */ + .card-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } + .card-desc { font-size: 11px; color: #9ca3af; font-weight: 400; line-height: 1.3; } + .footer { text-align: center; padding: 16px; @@ -280,6 +326,16 @@
+ + @@ -433,13 +489,13 @@ // ===== Card Definitions ===== var SYSTEM_CARDS = [ - { id: 'factory', name: '공장관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard.html', pageKey: 's1.dashboard', color: '#1a56db' }, - { id: 'report_sys', name: '신고', icon: '\uD83D\uDEA8', subdomain: 'tkreport', accessKey: 'system2', color: '#dc2626' }, - { id: 'quality', name: '부적합관리', icon: '\uD83D\uDCCA', subdomain: 'tkqc', pageKey: 'issues_dashboard', color: '#059669' }, - { id: 'purchase', name: '구매관리', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', pageKey: 'purchasing_schedule', color: '#d97706' }, - { id: 'safety', name: '안전관리', icon: '\uD83D\uDD27', subdomain: 'tksafety', pageKey: 'safety_visit_management', color: '#7c3aed' }, - { id: 'support', name: '행정지원', icon: '\uD83C\uDFE2', subdomain: 'tksupport', comingSoon: true, color: '#6b7280' }, - { id: 'admin', name: '통합관리', icon: '\u2699\uFE0F', subdomain: 'tkuser', pageKey: 'tkuser.users', color: '#0891b2' } + { id: 'factory', name: '공장관리', desc: '작업장 현황, TBM, 설비관리', icon: '\uD83C\uDFED', subdomain: 'tkfb', path: '/pages/dashboard.html', pageKey: 's1.dashboard', color: '#1a56db' }, + { id: 'report_sys', name: '신고', desc: '사건·사고 신고 접수', icon: '\uD83D\uDEA8', subdomain: 'tkreport', accessKey: 'system2', color: '#dc2626' }, + { id: 'quality', name: '부적합관리', desc: '부적합 이슈 추적·처리', icon: '\uD83D\uDCCA', subdomain: 'tkqc', pageKey: 'issues_dashboard', color: '#059669' }, + { id: 'purchase', name: '구매관리', desc: '자재 구매, 일용직 관리', icon: '\uD83D\uDED2', subdomain: 'tkpurchase', pageKey: 'purchasing_schedule', color: '#d97706' }, + { id: 'safety', name: '안전관리', desc: '안전 점검, 방문 관리', icon: '\uD83E\uDDBA', subdomain: 'tksafety', pageKey: 'safety_visit_management', color: '#7c3aed' }, + { id: 'support', name: '행정지원', desc: '전사 행정 업무 지원', icon: '\uD83C\uDFE2', subdomain: 'tksupport', comingSoon: true, color: '#6b7280' }, + { id: 'admin', name: '통합관리', desc: '사용자·권한 관리', icon: '\u2699\uFE0F', subdomain: 'tkuser', pageKey: 'tkuser.users', color: '#0891b2' } ]; // ===== Rendering ===== @@ -485,7 +541,18 @@ nameSpan.appendChild(badge); } - a.appendChild(nameSpan); + if (card.desc && isSystem) { + var textDiv = document.createElement('div'); + textDiv.className = 'card-text'; + textDiv.appendChild(nameSpan); + var descSpan = document.createElement('span'); + descSpan.className = 'card-desc'; + descSpan.textContent = card.desc; + textDiv.appendChild(descSpan); + a.appendChild(textDiv); + } else { + a.appendChild(nameSpan); + } return a; } @@ -541,11 +608,146 @@ if (systemAccess.system3 !== false) allowed.add('issues_dashboard'); } + // A: Welcome section + showWelcome(user, token); + + // B: Today stats + loadTodayStats(token, allowed); + // Render banners + system cards loadBanners(token, allowed); renderSection('systemSection', 'systemGrid', SYSTEM_CARDS, allowed, systemAccess, true); } + // ===== A: Welcome ===== + function getGreeting() { + var h = new Date().getHours(); + if (h >= 6 && h < 12) return '좋은 아침입니다'; + if (h >= 12 && h < 18) return '좋은 오후입니다'; + return '좋은 저녁입니다'; + } + + function getWeatherIcon(sky, precip) { + var p = Number(precip); + if (p === 1 || p === 2 || p === 4 || p === 5 || p === 6) return '\uD83C\uDF27\uFE0F'; + if (p === 3 || p === 7) return '\u2744\uFE0F'; + if (sky === 'overcast') return '\u2601\uFE0F'; + if (sky === 'cloudy') return '\u26C5'; + return '\u2600\uFE0F'; + } + + function getSkyLabel(sky) { + if (sky === 'clear') return '맑음'; + if (sky === 'cloudy') return '구름많음'; + if (sky === 'overcast') return '흐림'; + return ''; + } + + function showWelcome(user, token) { + var section = document.getElementById('welcomeSection'); + var name = user.name || user.username; + document.getElementById('welcomeGreeting').textContent = getGreeting() + ', ' + name + '님'; + + var now = new Date(); + var days = ['일','월','화','수','목','금','토']; + var dateStr = now.getFullYear() + '년 ' + (now.getMonth()+1) + '월 ' + now.getDate() + '일 (' + days[now.getDay()] + ')'; + document.getElementById('welcomeDate').textContent = dateStr; + + section.style.display = ''; + + // Weather (async, hide on failure) + var weatherEl = document.getElementById('welcomeWeather'); + weatherEl.textContent = ''; + fetch('/api/tbm/weather/current', { headers: { 'Authorization': 'Bearer ' + token } }) + .then(function(r) { return r.ok ? r.json() : Promise.reject(); }) + .then(function(json) { + var d = json.data; + if (!d) return; + var icon = getWeatherIcon(d.skyCondition, d.precipitationType); + var label = getSkyLabel(d.skyCondition); + var temp = d.temperature != null ? Math.round(d.temperature) + '\u00B0C' : ''; + weatherEl.textContent = '| ' + icon + ' ' + label + ' ' + temp; + }) + .catch(function() { weatherEl.textContent = ''; }); + } + + // ===== B: Today Stats ===== + function loadTodayStats(token, allowed) { + var today = new Date().toISOString().slice(0, 10); + var cards = []; + var fetches = []; + + // Attendance + if (allowed.has('s1.attendance.daily_status') || allowed.has('s1.attendance.vacation_management') || allowed.has('s1.dashboard')) { + var idx = cards.length; + cards.push({ icon: '\uD83D\uDC77', label: '출근', value: '\u2013', color: '#1a56db', visible: true }); + fetches.push( + fetch('/api/attendance/daily-status?date=' + today, { headers: { 'Authorization': 'Bearer ' + token } }) + .then(function(r) { return r.ok ? r.json() : Promise.reject(); }) + .then(function(json) { + var data = json.data; + if (Array.isArray(data)) { + cards[idx].value = data.filter(function(d) { return d.status !== 'vacation'; }).length + '명'; + } + }) + .catch(function() { cards[idx].visible = false; }) + ); + } + + // Work reports + if (allowed.has('s1.dashboard')) { + var idx2 = cards.length; + cards.push({ icon: '\uD83D\uDD27', label: '작업', value: '\u2013', color: '#059669', visible: true }); + fetches.push( + fetch('/api/work-analysis/dashboard?start=' + today + '&end=' + today, { headers: { 'Authorization': 'Bearer ' + token } }) + .then(function(r) { return r.ok ? r.json() : Promise.reject(); }) + .then(function(json) { + var stats = json.data && json.data.stats; + if (stats) { + cards[idx2].value = (stats.totalReports || 0) + '건'; + } + }) + .catch(function() { cards[idx2].visible = false; }) + ); + } + + // Issues + var idx3 = cards.length; + cards.push({ icon: '\u26A0\uFE0F', label: '이슈', value: '\u2013', color: '#dc2626', visible: true }); + fetches.push( + fetch('/api/work-issues/stats/summary', { headers: { 'Authorization': 'Bearer ' + token } }) + .then(function(r) { return r.ok ? r.json() : Promise.reject(); }) + .then(function(json) { + var data = json.data; + if (data && data.openCount != null) { + cards[idx3].value = data.openCount + '건'; + } else if (data && data.total != null) { + cards[idx3].value = data.total + '건'; + } + }) + .catch(function() { cards[idx3].visible = false; }) + ); + + if (fetches.length === 0) return; + + Promise.allSettled(fetches).then(function() { + var visible = cards.filter(function(c) { return c.visible; }); + if (visible.length === 0) return; + + var grid = document.getElementById('statsGrid'); + grid.innerHTML = ''; + visible.forEach(function(c) { + var div = document.createElement('div'); + div.className = 'stat-card'; + div.innerHTML = '
' + c.icon + '
' + + '
' + c.label + '
' + + '
' + c.value + '
'; + grid.appendChild(div); + }); + document.getElementById('statsSection').style.display = ''; + }); + } + // ===== Login ===== async function handleLogin(e) { e.preventDefault(); diff --git a/gateway/html/shared/notification-bell.js b/gateway/html/shared/notification-bell.js index 811940c..ea4129a 100644 --- a/gateway/html/shared/notification-bell.js +++ b/gateway/html/shared/notification-bell.js @@ -63,7 +63,7 @@ '' + '' + '
' + - '