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>
1234 lines
40 KiB
JavaScript
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();
|
|
};
|