feat: 알림 시스템 및 시설설비 관리 기능 구현

- 알림 시스템 구축 (navbar 알림 아이콘, 드롭다운)
- 알림 수신자 설정 기능 (계정관리 페이지)
- 시설설비 관리 페이지 추가 (수리 워크플로우)
- 수리 신청 → 접수 → 처리중 → 완료 상태 관리
- 사이드바 메뉴 구조 개선 (공장 관리 카테고리)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-04 15:56:57 +09:00
parent d1aec517a6
commit b8ccde7f17
24 changed files with 3204 additions and 9 deletions

View File

@@ -254,6 +254,114 @@
}
});
}
// 알림 버튼 이벤트
const notificationBtn = document.getElementById('notificationBtn');
const notificationDropdown = document.getElementById('notificationDropdown');
const notificationWrapper = document.getElementById('notificationWrapper');
if (notificationBtn && notificationDropdown) {
notificationBtn.addEventListener('click', (e) => {
e.stopPropagation();
notificationDropdown.classList.toggle('show');
});
document.addEventListener('click', (e) => {
if (notificationWrapper && !notificationWrapper.contains(e.target)) {
notificationDropdown.classList.remove('show');
}
});
}
}
// ===== 알림 로드 =====
async function loadNotifications() {
try {
const token = localStorage.getItem('token');
if (!token) return;
const response = await fetch(`${window.API_BASE_URL}/notifications/unread`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) return;
const result = await response.json();
if (result.success) {
const notifications = result.data || [];
updateNotificationBadge(notifications.length);
renderNotificationList(notifications);
}
} catch (error) {
console.warn('알림 로드 오류:', error.message);
}
}
function updateNotificationBadge(count) {
const badge = document.getElementById('notificationBadge');
const btn = document.getElementById('notificationBtn');
if (!badge) return;
if (count > 0) {
badge.textContent = count > 99 ? '99+' : count;
badge.style.display = 'flex';
btn?.classList.add('has-notifications');
} else {
badge.style.display = 'none';
btn?.classList.remove('has-notifications');
}
}
function renderNotificationList(notifications) {
const list = document.getElementById('notificationList');
if (!list) return;
if (notifications.length === 0) {
list.innerHTML = '<div class="notification-empty">새 알림이 없습니다.</div>';
return;
}
const icons = { repair: '🔧', safety: '⚠️', system: '📢', equipment: '🔩', maintenance: '🛠️' };
list.innerHTML = notifications.slice(0, 5).map(n => `
<div class="notification-item ${n.is_read ? '' : 'unread'}" data-id="${n.notification_id}" data-url="${n.link_url || ''}">
<div class="notification-item-icon ${n.type || 'repair'}">${icons[n.type] || '🔔'}</div>
<div class="notification-item-content">
<div class="notification-item-title">${escapeHtml(n.title)}</div>
<div class="notification-item-desc">${escapeHtml(n.message || '')}</div>
</div>
<div class="notification-item-time">${formatTimeAgo(n.created_at)}</div>
</div>
`).join('');
list.querySelectorAll('.notification-item').forEach(item => {
item.addEventListener('click', () => {
const url = item.dataset.url;
// 수리 알림은 클릭해도 읽음 처리 안함 (수리 처리 페이지에서 확인 처리해야 함)
window.location.href = url || '/pages/admin/notifications.html';
});
});
}
function formatTimeAgo(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return '방금 전';
if (diffMins < 60) return `${diffMins}분 전`;
if (diffHours < 24) return `${diffHours}시간 전`;
if (diffDays < 7) return `${diffDays}일 전`;
return date.toLocaleDateString('ko-KR', { month: 'short', day: 'numeric' });
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ===== 날짜/시간 업데이트 =====
@@ -387,6 +495,10 @@
// 8. 날씨 (백그라운드)
setTimeout(updateWeather, 100);
// 9. 알림 로드 (30초마다 갱신)
setTimeout(loadNotifications, 200);
setInterval(loadNotifications, 30000);
console.log('✅ app-init 완료');
}