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 @@
'' +
'
0' +
'
' +
- '