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

@@ -1074,3 +1074,190 @@ async function unlinkWorker() {
}
}
window.unlinkWorker = unlinkWorker;
// ========== 알림 수신자 관리 ========== //
let notificationRecipients = {};
let allUsersForRecipient = [];
let currentNotificationType = null;
const NOTIFICATION_TYPE_CONFIG = {
repair: { name: '설비 수리', icon: '🔧', description: '설비 수리 신청 시 알림을 받을 사용자' },
safety: { name: '안전 신고', icon: '⚠️', description: '안전 관련 신고 시 알림을 받을 사용자' },
nonconformity: { name: '부적합 신고', icon: '🚫', description: '부적합 사항 신고 시 알림을 받을 사용자' },
equipment: { name: '설비 관련', icon: '🔩', description: '설비 관련 알림을 받을 사용자' },
maintenance: { name: '정기점검', icon: '🛠️', description: '정기점검 알림을 받을 사용자' },
system: { name: '시스템', icon: '📢', description: '시스템 알림을 받을 사용자' }
};
// 알림 수신자 목록 로드
async function loadNotificationRecipients() {
try {
const response = await window.apiCall('/notification-recipients');
if (response.success) {
notificationRecipients = response.data || {};
renderNotificationTypeCards();
}
} catch (error) {
console.error('알림 수신자 로드 오류:', error);
}
}
// 알림 유형 카드 렌더링
function renderNotificationTypeCards() {
const container = document.getElementById('notificationTypeCards');
if (!container) return;
let html = '';
Object.keys(NOTIFICATION_TYPE_CONFIG).forEach(type => {
const config = NOTIFICATION_TYPE_CONFIG[type];
const recipients = notificationRecipients[type]?.recipients || [];
html += `
<div class="notification-type-card ${type}">
<div class="notification-type-header">
<div class="notification-type-title">
<span class="notification-type-icon">${config.icon}</span>
<span>${config.name}</span>
</div>
<button class="edit-recipients-btn" onclick="openRecipientModal('${type}')">
편집
</button>
</div>
<div class="recipient-list">
${recipients.length > 0
? recipients.map(r => `
<span class="recipient-tag">
<span class="tag-icon">👤</span>
${r.user_name || r.username}
</span>
`).join('')
: '<span class="no-recipients">지정된 수신자 없음</span>'
}
</div>
</div>
`;
});
container.innerHTML = html;
}
// 수신자 편집 모달 열기
async function openRecipientModal(notificationType) {
currentNotificationType = notificationType;
const config = NOTIFICATION_TYPE_CONFIG[notificationType];
// 모달 정보 업데이트
document.getElementById('recipientModalTitle').textContent = config.name + ' 알림 수신자';
document.getElementById('recipientModalDesc').textContent = config.description;
// 사용자 목록 로드 (users가 이미 로드되어 있으면 사용)
if (users.length === 0) {
await loadUsers();
}
allUsersForRecipient = users.filter(u => u.is_active);
// 현재 수신자 목록
const currentRecipients = notificationRecipients[notificationType]?.recipients || [];
const currentRecipientIds = currentRecipients.map(r => r.user_id);
// 사용자 목록 렌더링
renderRecipientUserList(currentRecipientIds);
// 검색 이벤트
const searchInput = document.getElementById('recipientSearchInput');
searchInput.value = '';
searchInput.oninput = (e) => {
renderRecipientUserList(currentRecipientIds, e.target.value);
};
// 모달 표시
document.getElementById('notificationRecipientModal').style.display = 'flex';
}
window.openRecipientModal = openRecipientModal;
// 수신자 사용자 목록 렌더링
function renderRecipientUserList(selectedIds, searchTerm = '') {
const container = document.getElementById('recipientUserList');
if (!container) return;
let filteredUsers = allUsersForRecipient;
if (searchTerm) {
const term = searchTerm.toLowerCase();
filteredUsers = filteredUsers.filter(u =>
(u.name && u.name.toLowerCase().includes(term)) ||
(u.username && u.username.toLowerCase().includes(term))
);
}
if (filteredUsers.length === 0) {
container.innerHTML = '<div style="padding: 2rem; text-align: center; color: #6c757d;">사용자가 없습니다</div>';
return;
}
container.innerHTML = filteredUsers.map(user => {
const isSelected = selectedIds.includes(user.user_id);
return `
<div class="recipient-user-item ${isSelected ? 'selected' : ''}" onclick="toggleRecipientUser(${user.user_id}, this)">
<input type="checkbox" ${isSelected ? 'checked' : ''} data-user-id="${user.user_id}">
<div class="user-avatar-small">${(user.name || user.username).charAt(0)}</div>
<div class="recipient-user-info">
<div class="recipient-user-name">${user.name || user.username}</div>
<div class="recipient-user-role">${getRoleName(user.role)}</div>
</div>
</div>
`;
}).join('');
}
// 수신자 토글
function toggleRecipientUser(userId, element) {
const checkbox = element.querySelector('input[type="checkbox"]');
checkbox.checked = !checkbox.checked;
element.classList.toggle('selected', checkbox.checked);
}
window.toggleRecipientUser = toggleRecipientUser;
// 수신자 모달 닫기
function closeRecipientModal() {
document.getElementById('notificationRecipientModal').style.display = 'none';
currentNotificationType = null;
}
window.closeRecipientModal = closeRecipientModal;
// 알림 수신자 저장
async function saveNotificationRecipients() {
if (!currentNotificationType) {
showToast('알림 유형이 선택되지 않았습니다.', 'error');
return;
}
try {
const checkboxes = document.querySelectorAll('#recipientUserList input[type="checkbox"]:checked');
const userIds = Array.from(checkboxes).map(cb => parseInt(cb.dataset.userId));
const response = await window.apiCall(`/notification-recipients/${currentNotificationType}`, 'PUT', {
user_ids: userIds
});
if (response.success) {
showToast('알림 수신자가 저장되었습니다.', 'success');
closeRecipientModal();
await loadNotificationRecipients();
} else {
throw new Error(response.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('알림 수신자 저장 오류:', error);
showToast(`저장 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
window.saveNotificationRecipients = saveNotificationRecipients;
// 초기화 시 알림 수신자 로드
const originalInitializePage = initializePage;
initializePage = async function() {
await originalInitializePage();
await loadNotificationRecipients();
};