Files
tk-factory-services/system1-factory/web/js/admin-settings.js
Hyungi Ahn abd7564e6b refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)
sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거,
department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러,
4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:13:10 +09:00

1234 lines
40 KiB
JavaScript

// 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('sso_token');
const user = localStorage.getItem('sso_user');
return {
token,
user: user ? JSON.parse(user) : null
};
}
// ========== 시간 업데이트 ========== //
function updateCurrentTime() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
if (elements.timeValue) {
elements.timeValue.textContent = `${hours}${minutes}${seconds}`;
}
}
// ========== 이벤트 리스너 ========== //
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();
if (window.clearSSOAuth) window.clearSSOAuth();
window.location.href = window.getLoginUrl ? window.getLoginUrl() : '/login';
}
}
// showToast → api-base.js 전역 사용
// ========== 전역 함수 (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 selectedUserId = null;
// 연결된 작업자 정보 표시 업데이트
function updateLinkedWorkerDisplay(user) {
const linkedWorkerInfo = document.getElementById('linkedWorkerInfo');
if (!linkedWorkerInfo) return;
if (user.user_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;
}
selectedUserId = currentEditingUser.user_id || null;
// 부서 목록 로드
await loadDepartmentsForSelect();
// 모달 표시
document.getElementById('workerSelectModal').style.display = 'flex';
}
window.openWorkerSelectModal = openWorkerSelectModal;
// 작업자 선택 모달 닫기
function closeWorkerSelectModal() {
document.getElementById('workerSelectModal').style.display = 'none';
selectedUserId = 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 linkedUserIds = users
.filter(u => u.user_id && u.user_id !== currentEditingUser?.user_id)
.map(u => u.user_id);
container.innerHTML = workers.map(worker => {
const isSelected = selectedUserId === worker.user_id;
const isLinkedToOther = linkedUserIds.includes(worker.user_id);
const linkedUser = isLinkedToOther ? users.find(u => u.user_id === worker.user_id) : null;
return `
<div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}"
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.user_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(userId, workerName) {
selectedUserId = userId;
// 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*="${userId}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
selectedItem.querySelector('.select-indicator').textContent = '✓';
}
// 서버에 저장
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
user_id: userId
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.user_id = userId;
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.user_id) {
showToast('연결된 작업자가 없습니다.', 'warning');
closeWorkerSelectModal();
return;
}
if (!confirm('작업자 연결을 해제하시겠습니까?')) {
return;
}
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
user_id: null
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.user_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], user_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();
};