Files
TK-FB-Project/web-ui/js/admin-settings.js
Hyungi Ahn b8ccde7f17 feat: 알림 시스템 및 시설설비 관리 기능 구현
- 알림 시스템 구축 (navbar 알림 아이콘, 드롭다운)
- 알림 수신자 설정 기능 (계정관리 페이지)
- 시설설비 관리 페이지 추가 (수리 워크플로우)
- 수리 신청 → 접수 → 처리중 → 완료 상태 관리
- 사이드바 메뉴 구조 개선 (공장 관리 카테고리)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 15:56:57 +09:00

1264 lines
40 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// admin-settings.js - 관리자 설정 페이지
// 전역 변수
let currentUser = null;
let users = [];
let filteredUsers = [];
let currentEditingUser = null;
// DOM 요소
const elements = {
// 시간
timeValue: document.getElementById('timeValue'),
// 사용자 정보
userName: document.getElementById('userName'),
userRole: document.getElementById('userRole'),
userInitial: document.getElementById('userInitial'),
// 검색 및 필터
userSearch: document.getElementById('userSearch'),
filterButtons: document.querySelectorAll('.filter-btn'),
// 테이블
usersTableBody: document.getElementById('usersTableBody'),
emptyState: document.getElementById('emptyState'),
// 버튼
addUserBtn: document.getElementById('addUserBtn'),
saveUserBtn: document.getElementById('saveUserBtn'),
confirmDeleteBtn: document.getElementById('confirmDeleteBtn'),
// 모달
userModal: document.getElementById('userModal'),
deleteModal: document.getElementById('deleteModal'),
modalTitle: document.getElementById('modalTitle'),
// 폼
userForm: document.getElementById('userForm'),
userNameInput: document.getElementById('userName'),
userIdInput: document.getElementById('userId'),
userPasswordInput: document.getElementById('userPassword'),
userRoleSelect: document.getElementById('userRole'),
userEmailInput: document.getElementById('userEmail'),
userPhoneInput: document.getElementById('userPhone'),
passwordGroup: document.getElementById('passwordGroup'),
// 토스트
toastContainer: document.getElementById('toastContainer')
};
// ========== 초기화 ========== //
document.addEventListener('DOMContentLoaded', async () => {
console.log('🔧 관리자 설정 페이지 초기화 시작');
try {
await initializePage();
console.log('✅ 관리자 설정 페이지 초기화 완료');
} catch (error) {
console.error('❌ 페이지 초기화 오류:', error);
showToast('페이지를 불러오는 중 오류가 발생했습니다.', 'error');
}
});
async function initializePage() {
// 이벤트 리스너 설정
setupEventListeners();
// 사용자 목록 로드
await loadUsers();
}
// ========== 사용자 정보 설정 ========== //
// navbar/sidebar는 app-init.js에서 공통 처리
function setupUserInfo() {
const authData = getAuthData();
if (authData && authData.user) {
currentUser = authData.user;
console.log('👤 사용자 정보 로드 완료:', currentUser.name, currentUser.role);
}
}
function getAuthData() {
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
return {
token,
user: user ? JSON.parse(user) : null
};
}
// ========== 시간 업데이트 ========== //
function updateCurrentTime() {
const now = new Date();
const timeString = now.toLocaleTimeString('ko-KR', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
if (elements.timeValue) {
elements.timeValue.textContent = timeString;
}
}
// ========== 이벤트 리스너 ========== //
function setupEventListeners() {
// 검색
if (elements.userSearch) {
elements.userSearch.addEventListener('input', handleSearch);
}
// 필터 버튼
elements.filterButtons.forEach(btn => {
btn.addEventListener('click', handleFilter);
});
// 사용자 추가 버튼
if (elements.addUserBtn) {
elements.addUserBtn.addEventListener('click', openAddUserModal);
}
// 사용자 저장 버튼
if (elements.saveUserBtn) {
elements.saveUserBtn.addEventListener('click', saveUser);
}
// 삭제 확인 버튼
if (elements.confirmDeleteBtn) {
elements.confirmDeleteBtn.addEventListener('click', confirmDeleteUser);
}
// 로그아웃 버튼
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', handleLogout);
}
// 프로필 드롭다운
const userProfile = document.getElementById('userProfile');
const profileMenu = document.getElementById('profileMenu');
if (userProfile && profileMenu) {
userProfile.addEventListener('click', (e) => {
e.stopPropagation();
profileMenu.style.display = profileMenu.style.display === 'block' ? 'none' : 'block';
});
document.addEventListener('click', () => {
profileMenu.style.display = 'none';
});
}
}
// ========== 사용자 관리 ========== //
async function loadUsers() {
try {
console.log('👥 사용자 목록 로딩...');
// 실제 API에서 사용자 데이터 가져오기
const response = await window.apiCall('/users');
users = Array.isArray(response) ? response : (response.data || []);
console.log(`✅ 사용자 ${users.length}명 로드 완료`);
// 필터링된 사용자 목록 초기화
filteredUsers = [...users];
// 테이블 렌더링
renderUsersTable();
} catch (error) {
console.error('❌ 사용자 목록 로딩 오류:', error);
showToast('사용자 목록을 불러오는 중 오류가 발생했습니다.', 'error');
users = [];
filteredUsers = [];
renderUsersTable();
}
}
function renderUsersTable() {
if (!elements.usersTableBody) return;
if (filteredUsers.length === 0) {
elements.usersTableBody.innerHTML = '';
if (elements.emptyState) {
elements.emptyState.style.display = 'block';
}
return;
}
if (elements.emptyState) {
elements.emptyState.style.display = 'none';
}
elements.usersTableBody.innerHTML = filteredUsers.map(user => `
<tr>
<td>
<div class="user-info">
<div class="user-avatar-small">${(user.name || user.username).charAt(0)}</div>
<div class="user-details">
<h4>${user.name || user.username}</h4>
<p>${user.email || '이메일 없음'}</p>
</div>
</div>
</td>
<td><strong>${user.username}</strong></td>
<td>
<span class="role-badge ${user.role}">
${getRoleIcon(user.role)} ${getRoleName(user.role)}
</span>
</td>
<td>
<span class="status-badge ${user.is_active ? 'active' : 'inactive'}">
${user.is_active ? '활성' : '비활성'}
</span>
</td>
<td>${formatDate(user.last_login) || '로그인 기록 없음'}</td>
<td>
<div class="action-buttons">
<button class="action-btn edit" onclick="editUser(${user.user_id})">
수정
</button>
${user.role !== 'Admin' && user.role !== 'admin' ? `
<button class="action-btn permissions" onclick="managePageAccess(${user.user_id})">
권한
</button>
` : ''}
<button class="action-btn reset-pw" onclick="resetPassword(${user.user_id}, '${user.username}')" title="비밀번호 000000으로 초기화">
비번초기화
</button>
<button class="action-btn ${user.is_active ? 'deactivate' : 'activate'}" onclick="toggleUserStatus(${user.user_id})">
${user.is_active ? '비활성화' : '활성화'}
</button>
<button class="action-btn delete danger" onclick="permanentDeleteUser(${user.user_id}, '${user.username}')" title="영구 삭제 (복구 불가)">
삭제
</button>
</div>
</td>
</tr>
`).join('');
}
function getRoleIcon(role) {
const icons = {
admin: '👑',
leader: '👨‍💼',
user: '👤'
};
return icons[role] || '👤';
}
function getRoleName(role) {
const names = {
admin: '관리자',
leader: '그룹장',
user: '작업자'
};
return names[role] || '작업자';
}
function formatDate(dateString) {
if (!dateString) return null;
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
// ========== 검색 및 필터링 ========== //
function handleSearch(e) {
const searchTerm = e.target.value.toLowerCase();
filteredUsers = users.filter(user => {
return (user.name && user.name.toLowerCase().includes(searchTerm)) ||
(user.username && user.username.toLowerCase().includes(searchTerm)) ||
(user.email && user.email.toLowerCase().includes(searchTerm));
});
renderUsersTable();
}
function handleFilter(e) {
const filterType = e.target.dataset.filter;
// 활성 버튼 변경
elements.filterButtons.forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
// 필터링
if (filterType === 'all') {
filteredUsers = [...users];
} else {
filteredUsers = users.filter(user => user.role === filterType);
}
renderUsersTable();
}
// ========== 모달 관리 ========== //
function openAddUserModal() {
currentEditingUser = null;
if (elements.modalTitle) {
elements.modalTitle.textContent = '새 사용자 추가';
}
// 폼 초기화
if (elements.userForm) {
elements.userForm.reset();
}
// 비밀번호 필드 표시
if (elements.passwordGroup) {
elements.passwordGroup.style.display = 'block';
}
if (elements.userPasswordInput) {
elements.userPasswordInput.required = true;
}
// 작업자 연결 섹션 숨기기 (새 사용자 추가 시)
const workerLinkGroup = document.getElementById('workerLinkGroup');
if (workerLinkGroup) {
workerLinkGroup.style.display = 'none';
}
if (elements.userModal) {
elements.userModal.style.display = 'flex';
}
}
function editUser(userId) {
const user = users.find(u => u.user_id === userId);
if (!user) return;
currentEditingUser = user;
if (elements.modalTitle) {
elements.modalTitle.textContent = '사용자 정보 수정';
}
// 역할 이름을 HTML select option value로 변환
const roleToValueMap = {
'Admin': 'admin',
'System Admin': 'admin',
'User': 'user',
'Guest': 'user'
};
// 폼에 데이터 채우기
if (elements.userNameInput) elements.userNameInput.value = user.name || '';
if (elements.userIdInput) elements.userIdInput.value = user.username || '';
if (elements.userRoleSelect) elements.userRoleSelect.value = roleToValueMap[user.role] || 'user';
if (elements.userEmailInput) elements.userEmailInput.value = user.email || '';
if (elements.userPhoneInput) elements.userPhoneInput.value = user.phone || '';
// 비밀번호 필드 숨기기 (수정 시에는 선택사항)
if (elements.passwordGroup) {
elements.passwordGroup.style.display = 'none';
}
if (elements.userPasswordInput) {
elements.userPasswordInput.required = false;
}
// 작업자 연결 섹션 표시 (수정 시에만)
const workerLinkGroup = document.getElementById('workerLinkGroup');
if (workerLinkGroup) {
workerLinkGroup.style.display = 'block';
updateLinkedWorkerDisplay(user);
}
if (elements.userModal) {
elements.userModal.style.display = 'flex';
}
}
function closeUserModal() {
if (elements.userModal) {
elements.userModal.style.display = 'none';
}
currentEditingUser = null;
}
// 영구 삭제 (Hard Delete)
async function permanentDeleteUser(userId, username) {
if (!confirm(`⚠️ 경고: "${username}" 사용자를 영구 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다!\n관련된 모든 데이터(로그인 기록, 권한 설정 등)도 함께 삭제됩니다.`)) {
return;
}
// 이중 확인
if (!confirm(`정말로 "${username}"을(를) 영구 삭제하시겠습니까?\n\n[확인]을 누르면 즉시 삭제됩니다.`)) {
return;
}
try {
const response = await window.apiCall(`/users/${userId}/permanent`, 'DELETE');
if (response.success) {
showToast(`"${username}" 사용자가 영구 삭제되었습니다.`, 'success');
await loadUsers();
} else {
throw new Error(response.message || '사용자 삭제에 실패했습니다.');
}
} catch (error) {
console.error('사용자 영구 삭제 오류:', error);
showToast(`사용자 삭제 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
function closeDeleteModal() {
if (elements.deleteModal) {
elements.deleteModal.style.display = 'none';
}
currentEditingUser = null;
}
// ========== 비밀번호 초기화 ========== //
async function resetPassword(userId, username) {
if (!confirm(`${username} 사용자의 비밀번호를 000000으로 초기화하시겠습니까?`)) {
return;
}
try {
const response = await window.apiCall(`/users/${userId}/reset-password`, 'POST');
if (response.success) {
showToast(`${username}의 비밀번호가 000000으로 초기화되었습니다.`, 'success');
} else {
showToast(response.message || '비밀번호 초기화에 실패했습니다.', 'error');
}
} catch (error) {
console.error('비밀번호 초기화 오류:', error);
showToast('비밀번호 초기화 중 오류가 발생했습니다.', 'error');
}
}
window.resetPassword = resetPassword;
// ========== 사용자 CRUD ========== //
async function saveUser() {
try {
const formData = {
name: elements.userNameInput?.value,
username: elements.userIdInput?.value,
role: elements.userRoleSelect?.value, // HTML select value는 이미 'admin' 또는 'user'
email: elements.userEmailInput?.value
};
console.log('저장할 데이터:', formData);
// 유효성 검사
if (!formData.name || !formData.username || !formData.role) {
showToast('필수 항목을 모두 입력해주세요.', 'error');
return;
}
// 비밀번호 처리
if (!currentEditingUser && elements.userPasswordInput?.value) {
formData.password = elements.userPasswordInput.value;
} else if (currentEditingUser && elements.userPasswordInput?.value) {
formData.password = elements.userPasswordInput.value;
}
let response;
if (currentEditingUser) {
// 수정
response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', formData);
} else {
// 생성
response = await window.apiCall('/users', 'POST', formData);
}
if (response.success || response.user_id) {
const action = currentEditingUser ? '수정' : '생성';
showToast(`사용자가 성공적으로 ${action}되었습니다.`, 'success');
closeUserModal();
await loadUsers();
} else {
throw new Error(response.message || '사용자 저장에 실패했습니다.');
}
} catch (error) {
console.error('사용자 저장 오류:', error);
showToast(`사용자 저장 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
async function confirmDeleteUser() {
if (!currentEditingUser) return;
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'DELETE');
if (response.success) {
showToast('사용자가 성공적으로 삭제되었습니다.', 'success');
closeDeleteModal();
await loadUsers();
} else {
throw new Error(response.message || '사용자 삭제에 실패했습니다.');
}
} catch (error) {
console.error('사용자 삭제 오류:', error);
showToast(`사용자 삭제 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
async function toggleUserStatus(userId) {
try {
const user = users.find(u => u.user_id === userId);
if (!user) return;
const newStatus = !user.is_active;
const response = await window.apiCall(`/users/${userId}/status`, 'PUT', { is_active: newStatus });
if (response.success) {
const action = newStatus ? '활성화' : '비활성화';
showToast(`사용자가 성공적으로 ${action}되었습니다.`, 'success');
await loadUsers();
} else {
throw new Error(response.message || '사용자 상태 변경에 실패했습니다.');
}
} catch (error) {
console.error('사용자 상태 변경 오류:', error);
showToast(`사용자 상태 변경 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
// ========== 로그아웃 ========== //
function handleLogout() {
if (confirm('로그아웃하시겠습니까?')) {
localStorage.clear();
window.location.href = '/index.html';
}
}
// ========== 토스트 알림 ========== //
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 = `
<div class="toast-icon">${iconMap[type] || ''}</div>
<div class="toast-message">${message}</div>
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
`;
elements.toastContainer.appendChild(toast);
// 자동 제거
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, duration);
}
// ========== 전역 함수 (HTML에서 호출) ========== //
window.editUser = editUser;
window.toggleUserStatus = toggleUserStatus;
window.closeUserModal = closeUserModal;
window.closeDeleteModal = closeDeleteModal;
window.permanentDeleteUser = permanentDeleteUser;
// ========== 페이지 권한 관리 ========== //
let allPages = [];
let userPageAccess = [];
// 모든 페이지 목록 로드
async function loadAllPages() {
try {
const response = await apiCall('/pages');
allPages = response.data || response || [];
console.log('📄 페이지 목록 로드:', allPages.length, '개');
} catch (error) {
console.error('❌ 페이지 목록 로드 오류:', error);
allPages = [];
}
}
// 사용자의 페이지 권한 로드
async function loadUserPageAccess(userId) {
try {
const response = await apiCall(`/users/${userId}/page-access`);
userPageAccess = response.data?.pageAccess || [];
console.log(`👤 사용자 ${userId} 페이지 권한 로드:`, userPageAccess.length, '개');
} catch (error) {
console.error('❌ 사용자 페이지 권한 로드 오류:', error);
userPageAccess = [];
}
}
// 페이지 권한 저장
async function savePageAccess(userId, containerId = null) {
try {
// 특정 컨테이너가 지정되면 그 안에서만 체크박스 선택
const container = containerId ? document.getElementById(containerId) : document;
const checkboxes = container.querySelectorAll('.page-access-checkbox:not([disabled])');
// 중복 page_id 제거 (Map 사용)
const pageAccessMap = new Map();
checkboxes.forEach(checkbox => {
const pageId = parseInt(checkbox.dataset.pageId);
pageAccessMap.set(pageId, {
page_id: pageId,
can_access: checkbox.checked ? 1 : 0
});
});
const pageAccessData = Array.from(pageAccessMap.values());
console.log('📤 페이지 권한 저장:', userId, pageAccessData);
await apiCall(`/users/${userId}/page-access`, 'PUT', {
pageAccess: pageAccessData
});
console.log('✅ 페이지 권한 저장 완료');
} catch (error) {
console.error('❌ 페이지 권한 저장 오류:', error);
throw error;
}
}
// ========== 페이지 권한 관리 모달 ========== //
let currentPageAccessUser = null;
// 페이지 권한 관리 모달 열기
async function managePageAccess(userId) {
try {
// 페이지 목록이 없으면 로드
if (allPages.length === 0) {
await loadAllPages();
}
// 사용자 정보 가져오기
const user = users.find(u => u.user_id === userId);
if (!user) {
showToast('사용자를 찾을 수 없습니다.', 'error');
return;
}
currentPageAccessUser = user;
// 사용자의 페이지 권한 로드
await loadUserPageAccess(userId);
// 모달 정보 업데이트
const userName = user.name || user.username;
document.getElementById('pageAccessModalTitle').textContent = userName + ' - 페이지 권한 관리';
document.getElementById('pageAccessUserName').textContent = userName;
document.getElementById('pageAccessUserRole').textContent = getRoleName(user.role);
document.getElementById('pageAccessUserAvatar').textContent = userName.charAt(0);
// 페이지 권한 체크박스 렌더링
renderPageAccessModalList();
// 모달 표시
document.getElementById('pageAccessModal').style.display = 'flex';
} catch (error) {
console.error('❌ 페이지 권한 관리 모달 오류:', error);
showToast('페이지 권한 관리를 열 수 없습니다.', 'error');
}
}
// 페이지 권한 모달 닫기
function closePageAccessModal() {
document.getElementById('pageAccessModal').style.display = 'none';
currentPageAccessUser = null;
}
// 페이지 권한 체크박스 렌더링 (모달용) - 폴더 구조 형태
function renderPageAccessModalList() {
const pageAccessList = document.getElementById('pageAccessModalList');
if (!pageAccessList) return;
// 폴더 구조 정의 (page_key 패턴 기준)
const folderStructure = {
'dashboard': { name: '대시보드', icon: '📊', pages: [] },
'work': { name: '작업 관리', icon: '📋', pages: [] },
'safety': { name: '안전 관리', icon: '🛡️', pages: [] },
'attendance': { name: '근태 관리', icon: '📅', pages: [] },
'admin': { name: '시스템 관리', icon: '⚙️', pages: [] },
'profile': { name: '내 정보', icon: '👤', pages: [] }
};
// 페이지를 폴더별로 분류
allPages.forEach(page => {
const pageKey = page.page_key || '';
if (pageKey === 'dashboard') {
folderStructure['dashboard'].pages.push(page);
} else if (pageKey.startsWith('work.')) {
folderStructure['work'].pages.push(page);
} else if (pageKey.startsWith('safety.')) {
folderStructure['safety'].pages.push(page);
} else if (pageKey.startsWith('attendance.')) {
folderStructure['attendance'].pages.push(page);
} else if (pageKey.startsWith('admin.')) {
folderStructure['admin'].pages.push(page);
} else if (pageKey.startsWith('profile.')) {
folderStructure['profile'].pages.push(page);
}
});
// HTML 생성 - 폴더 트리 형태
let html = '<div class="folder-tree">';
Object.keys(folderStructure).forEach(folderKey => {
const folder = folderStructure[folderKey];
if (folder.pages.length === 0) return;
const folderId = 'folder-' + folderKey;
html += '<div class="folder-group">';
html += '<div class="folder-header" onclick="toggleFolder(\'' + folderId + '\')">';
html += '<span class="folder-icon">' + folder.icon + '</span>';
html += '<span class="folder-name">' + folder.name + '</span>';
html += '<span class="folder-count">(' + folder.pages.length + ')</span>';
html += '<span class="folder-toggle" id="toggle-' + folderId + '">▼</span>';
html += '</div>';
html += '<div class="folder-content" id="' + folderId + '">';
folder.pages.forEach(page => {
// 프로필과 대시보드는 모든 사용자가 접근 가능
const isAlwaysAccessible = page.page_key === 'dashboard' || page.page_key.startsWith('profile.');
const isChecked = userPageAccess.find(p => p.page_id === page.id && p.can_access === 1) || isAlwaysAccessible;
// 파일명만 추출 (page_key에서)
const fileName = page.page_key.split('.').pop() || page.page_key;
html += '<div class="page-item">';
html += '<label class="page-label">';
html += '<input type="checkbox" class="page-access-checkbox" ';
html += 'data-page-id="' + page.id + '" ';
html += 'data-page-key="' + page.page_key + '" ';
html += (isChecked ? 'checked ' : '');
html += (isAlwaysAccessible ? 'disabled ' : '');
html += '>';
html += '<span class="file-icon">📄</span>';
html += '<span class="page-name">' + page.page_name + '</span>';
if (isAlwaysAccessible) {
html += '<span class="always-access-badge">기본</span>';
}
html += '</label>';
html += '</div>';
});
html += '</div>'; // folder-content
html += '</div>'; // folder-group
});
html += '</div>'; // folder-tree
pageAccessList.innerHTML = html;
}
// 폴더 접기/펼치기
function toggleFolder(folderId) {
const content = document.getElementById(folderId);
const toggle = document.getElementById('toggle-' + folderId);
if (content && toggle) {
const isExpanded = content.style.display !== 'none';
content.style.display = isExpanded ? 'none' : 'block';
toggle.textContent = isExpanded ? '▶' : '▼';
}
}
window.toggleFolder = toggleFolder;
// 페이지 권한 저장 (모달용)
async function savePageAccessFromModal() {
if (!currentPageAccessUser) {
showToast('사용자 정보가 없습니다.', 'error');
return;
}
try {
// 모달 컨테이너 지정
await savePageAccess(currentPageAccessUser.user_id, 'pageAccessModalList');
showToast('페이지 권한이 저장되었습니다.', 'success');
// 캐시 삭제 (사용자가 다시 로그인하거나 페이지 새로고침 필요)
localStorage.removeItem('userPageAccess');
closePageAccessModal();
} catch (error) {
console.error('❌ 페이지 권한 저장 오류:', error);
showToast('페이지 권한 저장에 실패했습니다.', 'error');
}
}
// 전역 함수로 등록
window.managePageAccess = managePageAccess;
window.closePageAccessModal = closePageAccessModal;
// 저장 버튼 이벤트 리스너
document.addEventListener('DOMContentLoaded', () => {
const saveBtn = document.getElementById('savePageAccessBtn');
if (saveBtn) {
saveBtn.addEventListener('click', savePageAccessFromModal);
}
});
// ========== 작업자 연결 기능 ========== //
let departments = [];
let selectedWorkerId = null;
// 연결된 작업자 정보 표시 업데이트
function updateLinkedWorkerDisplay(user) {
const linkedWorkerInfo = document.getElementById('linkedWorkerInfo');
if (!linkedWorkerInfo) return;
if (user.worker_id && user.worker_name) {
linkedWorkerInfo.innerHTML = `
<span class="worker-badge">
<span class="worker-name">👤 ${user.worker_name}</span>
${user.department_name ? `<span class="dept-name">(${user.department_name})</span>` : ''}
</span>
`;
} else {
linkedWorkerInfo.innerHTML = '<span class="no-worker">연결된 작업자 없음</span>';
}
}
// 작업자 선택 모달 열기
async function openWorkerSelectModal() {
if (!currentEditingUser) {
showToast('사용자 정보가 없습니다.', 'error');
return;
}
selectedWorkerId = currentEditingUser.worker_id || null;
// 부서 목록 로드
await loadDepartmentsForSelect();
// 모달 표시
document.getElementById('workerSelectModal').style.display = 'flex';
}
window.openWorkerSelectModal = openWorkerSelectModal;
// 작업자 선택 모달 닫기
function closeWorkerSelectModal() {
document.getElementById('workerSelectModal').style.display = 'none';
selectedWorkerId = null;
}
window.closeWorkerSelectModal = closeWorkerSelectModal;
// 부서 목록 로드
async function loadDepartmentsForSelect() {
try {
const response = await window.apiCall('/departments');
departments = response.data || response || [];
renderDepartmentList();
} catch (error) {
console.error('부서 목록 로드 실패:', error);
showToast('부서 목록을 불러오는데 실패했습니다.', 'error');
}
}
// 부서 목록 렌더링
function renderDepartmentList() {
const container = document.getElementById('departmentList');
if (!container) return;
if (departments.length === 0) {
container.innerHTML = '<div class="empty-message">등록된 부서가 없습니다</div>';
return;
}
container.innerHTML = departments.map(dept => `
<div class="department-item" data-dept-id="${dept.department_id}" onclick="selectDepartment(${dept.department_id})">
<span class="dept-icon">📁</span>
<span class="dept-name">${dept.department_name}</span>
<span class="dept-count">${dept.worker_count || 0}명</span>
</div>
`).join('');
}
// 부서 선택
async function selectDepartment(departmentId) {
// 활성 상태 업데이트
document.querySelectorAll('.department-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`.department-item[data-dept-id="${departmentId}"]`)?.classList.add('active');
// 해당 부서의 작업자 목록 로드
await loadWorkersForSelect(departmentId);
}
window.selectDepartment = selectDepartment;
// 부서별 작업자 목록 로드
async function loadWorkersForSelect(departmentId) {
try {
const response = await window.apiCall(`/departments/${departmentId}/workers`);
const workers = response.data || response || [];
renderWorkerListForSelect(workers);
} catch (error) {
console.error('작업자 목록 로드 실패:', error);
showToast('작업자 목록을 불러오는데 실패했습니다.', 'error');
}
}
// 작업자 목록 렌더링 (선택용)
function renderWorkerListForSelect(workers) {
const container = document.getElementById('workerListForSelect');
if (!container) return;
if (workers.length === 0) {
container.innerHTML = '<div class="empty-message">이 부서에 작업자가 없습니다</div>';
return;
}
// 이미 다른 계정에 연결된 작업자 확인을 위해 users 배열 사용
const linkedWorkerIds = users
.filter(u => u.worker_id && u.user_id !== currentEditingUser?.user_id)
.map(u => u.worker_id);
container.innerHTML = workers.map(worker => {
const isSelected = selectedWorkerId === worker.worker_id;
const isLinkedToOther = linkedWorkerIds.includes(worker.worker_id);
const linkedUser = isLinkedToOther ? users.find(u => u.worker_id === worker.worker_id) : null;
return `
<div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}"
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.worker_id}, '${worker.worker_name}')`}">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<div class="worker-info">
<div class="worker-name">${worker.worker_name}</div>
<div class="worker-role">${getJobTypeName(worker.job_type)}</div>
</div>
${isLinkedToOther ? `<span class="already-linked">${linkedUser?.username} 연결됨</span>` : ''}
<div class="select-indicator">${isSelected ? '✓' : ''}</div>
</div>
`;
}).join('');
}
// 직책 한글 변환
function getJobTypeName(jobType) {
const names = {
leader: '그룹장',
worker: '작업자',
admin: '관리자'
};
return names[jobType] || jobType || '-';
}
// 작업자 선택
async function selectWorker(workerId, workerName) {
selectedWorkerId = workerId;
// UI 업데이트
document.querySelectorAll('.worker-select-item').forEach(item => {
item.classList.remove('selected');
item.querySelector('.select-indicator').textContent = '';
});
const selectedItem = document.querySelector(`.worker-select-item[onclick*="${workerId}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
selectedItem.querySelector('.select-indicator').textContent = '✓';
}
// 서버에 저장
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: workerId
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.worker_id = workerId;
currentEditingUser.worker_name = workerName;
// 부서 정보도 업데이트
const dept = departments.find(d =>
document.querySelector(`.department-item.active`)?.dataset.deptId == d.department_id
);
if (dept) {
currentEditingUser.department_name = dept.department_name;
}
// users 배열 업데이트
const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id);
if (userIndex !== -1) {
users[userIndex] = { ...users[userIndex], ...currentEditingUser };
}
// 표시 업데이트
updateLinkedWorkerDisplay(currentEditingUser);
showToast(`${workerName} 작업자가 연결되었습니다.`, 'success');
closeWorkerSelectModal();
} else {
throw new Error(response.message || '작업자 연결에 실패했습니다.');
}
} catch (error) {
console.error('작업자 연결 오류:', error);
showToast(`작업자 연결 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
window.selectWorker = selectWorker;
// 작업자 연결 해제
async function unlinkWorker() {
if (!currentEditingUser) {
showToast('사용자 정보가 없습니다.', 'error');
return;
}
if (!currentEditingUser.worker_id) {
showToast('연결된 작업자가 없습니다.', 'warning');
closeWorkerSelectModal();
return;
}
if (!confirm('작업자 연결을 해제하시겠습니까?')) {
return;
}
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: null
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.worker_id = null;
currentEditingUser.worker_name = null;
currentEditingUser.department_name = null;
// users 배열 업데이트
const userIndex = users.findIndex(u => u.user_id === currentEditingUser.user_id);
if (userIndex !== -1) {
users[userIndex] = { ...users[userIndex], worker_id: null, worker_name: null, department_name: null };
}
// 표시 업데이트
updateLinkedWorkerDisplay(currentEditingUser);
showToast('작업자 연결이 해제되었습니다.', 'success');
closeWorkerSelectModal();
} else {
throw new Error(response.message || '연결 해제에 실패했습니다.');
}
} catch (error) {
console.error('작업자 연결 해제 오류:', error);
showToast(`연결 해제 중 오류가 발생했습니다: ${error.message}`, 'error');
}
}
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();
};