fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선

- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산
- 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader)
- 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql)
- synology_deployment 버전에도 동일 수정 적용
This commit is contained in:
Hyungi Ahn
2025-12-02 13:08:44 +09:00
parent beaffcad49
commit a9bce9d20b
419 changed files with 275129 additions and 394 deletions

534
web-ui/js/admin-settings.js Normal file
View File

@@ -0,0 +1,534 @@
// 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();
}
// ========== 사용자 정보 설정 ========== //
function setupUserInfo() {
const authData = getAuthData();
if (authData && authData.user) {
currentUser = authData.user;
// 사용자 이름 설정
if (elements.userName) {
elements.userName.textContent = currentUser.name || currentUser.username;
}
// 사용자 역할 설정
const roleMap = {
'admin': '관리자',
'system': '시스템 관리자',
'leader': '그룹장',
'user': '작업자'
};
if (elements.userRole) {
elements.userRole.textContent = roleMap[currentUser.role] || '작업자';
}
// 아바타 초기값 설정
if (elements.userInitial) {
const initial = (currentUser.name || currentUser.username).charAt(0);
elements.userInitial.textContent = initial;
}
console.log('👤 사용자 정보 설정 완료:', currentUser.name);
}
}
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>
<button class="action-btn toggle" onclick="toggleUserStatus(${user.user_id})">
${user.is_active ? '비활성화' : '활성화'}
</button>
<button class="action-btn delete" onclick="deleteUser(${user.user_id})">
삭제
</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;
}
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 = '사용자 정보 수정';
}
// 폼에 데이터 채우기
if (elements.userNameInput) elements.userNameInput.value = user.name || '';
if (elements.userIdInput) elements.userIdInput.value = user.username || '';
if (elements.userRoleSelect) elements.userRoleSelect.value = user.role || '';
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;
}
if (elements.userModal) {
elements.userModal.style.display = 'flex';
}
}
function closeUserModal() {
if (elements.userModal) {
elements.userModal.style.display = 'none';
}
currentEditingUser = null;
}
function deleteUser(userId) {
const user = users.find(u => u.user_id === userId);
if (!user) return;
currentEditingUser = user;
if (elements.deleteModal) {
elements.deleteModal.style.display = 'flex';
}
}
function closeDeleteModal() {
if (elements.deleteModal) {
elements.deleteModal.style.display = 'none';
}
currentEditingUser = null;
}
// ========== 사용자 CRUD ========== //
async function saveUser() {
try {
const formData = {
name: elements.userNameInput?.value,
username: elements.userIdInput?.value,
role: elements.userRoleSelect?.value,
email: elements.userEmailInput?.value,
phone: elements.userPhoneInput?.value
};
// 유효성 검사
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.deleteUser = deleteUser;
window.toggleUserStatus = toggleUserStatus;
window.closeUserModal = closeUserModal;
window.closeDeleteModal = closeDeleteModal;

View File

@@ -25,7 +25,7 @@ function filterMenuByRole(doc, userRole) {
selectors.forEach(({ role, selector }) => {
// 사용자가 해당 역할을 가지고 있지 않으면 메뉴 항목을 제거
if (userRole !== role) {
if (userRole !== role && userRole !== 'system') { // system 권한도 admin 메뉴 접근 가능
doc.querySelectorAll(selector).forEach(el => el.remove());
}
});
@@ -56,11 +56,7 @@ function populateUserInfo(doc, user) {
const dropdownIdEl = doc.getElementById('dropdown-user-id');
if (dropdownIdEl) dropdownIdEl.textContent = `@${user.username}`;
// Admin 버튼 표시 여부 결정 (admin 권한만)
const adminBtn = doc.getElementById('adminBtn');
if (adminBtn && user.role === 'admin') {
adminBtn.style.display = 'flex';
}
// Admin 버튼 제거됨
// System 버튼 표시 여부 결정 (system 권한만)
const systemBtn = doc.getElementById('systemBtn');
@@ -96,13 +92,7 @@ function setupNavbarEvents() {
});
}
// Admin 버튼 클릭 이벤트
const adminButton = document.getElementById('adminBtn');
if (adminButton) {
adminButton.addEventListener('click', () => {
window.location.href = '/pages/dashboard/admin.html';
});
}
// Admin 버튼 제거됨
// System 버튼 클릭 이벤트
const systemButton = document.getElementById('systemBtn');
@@ -111,6 +101,14 @@ function setupNavbarEvents() {
window.location.href = '/pages/dashboard/system.html';
});
}
// Dashboard 버튼 클릭 이벤트
const dashboardButton = document.querySelector('.dashboard-btn');
if (dashboardButton) {
dashboardButton.addEventListener('click', () => {
window.location.href = '/pages/dashboard/group-leader.html';
});
}
// 외부 클릭 시 드롭다운 닫기
document.addEventListener('click', (e) => {

View File

@@ -407,7 +407,6 @@ function displayWorkStatus() {
<span class="legend-item legend-vacation">휴가</span>
<span class="legend-item legend-partial">부분입력</span>
<span class="legend-item legend-incomplete">미입력</span>
<span class="legend-item legend-error">오류</span>
</div>
</div>
<div class="worker-status-rows">
@@ -621,11 +620,43 @@ function updateViewButtons(activeView) {
// ========== 관리자 권한 확인 ========== //
function checkAdminAccess() {
const adminElements = document.querySelectorAll('.admin-only');
const isAdmin = currentUser && ['admin', 'system'].includes(currentUser.access_level);
const isFullAdmin = currentUser && ['admin', 'system'].includes(currentUser.access_level);
const isGroupLeader = currentUser && currentUser.access_level === 'group_leader';
console.log(`🔐 권한 확인: 사용자=${currentUser?.username}, 역할=${currentUser.access_level}, 전체관리자=${isFullAdmin}, 그룹리더=${isGroupLeader}`);
adminElements.forEach(element => {
if (isAdmin) {
element.classList.add('visible');
const href = element.getAttribute('href');
// 작업 분석: 전체 관리자만 접근 가능
if (href && href.includes('work-analysis.html')) {
if (isFullAdmin) {
element.style.display = '';
element.classList.add('visible');
} else {
element.style.display = 'none';
element.classList.remove('visible');
}
}
// 작업 관리: 전체 관리자 + 그룹 리더 접근 가능
else if (href && href.includes('work-management.html')) {
if (isFullAdmin || isGroupLeader) {
element.style.display = '';
element.classList.add('visible');
} else {
element.style.display = 'none';
element.classList.remove('visible');
}
}
// 기타 관리자 전용 메뉴: 전체 관리자만 접근 가능
else {
if (isFullAdmin) {
element.style.display = '';
element.classList.add('visible');
} else {
element.style.display = 'none';
element.classList.remove('visible');
}
}
});
}

View File

@@ -14,7 +14,6 @@ document.addEventListener('DOMContentLoaded', function() {
initializePage();
loadStatistics();
loadRecentActivity();
});
// 페이지 초기화
@@ -186,63 +185,7 @@ function updateStatDisplay(elementId, value) {
}
}
// 최근 활동 로드
async function loadRecentActivity() {
try {
console.log('📋 최근 활동 로딩 시작');
// 임시 데이터 (실제로는 API에서 가져와야 함)
const activities = [
{
type: 'project',
icon: '📁',
title: '효성화학 에틸렌 탱크 건설공사 프로젝트가 수정되었습니다',
user: '김두수',
time: '2시간 전'
},
{
type: 'worker',
icon: '👥',
title: '새로운 작업자가 등록되었습니다',
user: '관리자',
time: '1일 전'
},
{
type: 'task',
icon: '📋',
title: '작업 유형이 업데이트되었습니다',
user: '김두수',
time: '2일 전'
}
];
renderActivityList(activities);
} catch (error) {
console.error('최근 활동 로딩 오류:', error);
}
}
// 활동 목록 렌더링
function renderActivityList(activities) {
const activityList = document.getElementById('activityList');
if (!activityList) return;
const activitiesHtml = activities.map(activity => `
<div class="activity-item">
<div class="activity-icon">${activity.icon}</div>
<div class="activity-content">
<div class="activity-title">${activity.title}</div>
<div class="activity-meta">
<span class="activity-user">${activity.user}</span>
<span class="activity-time">${activity.time}</span>
</div>
</div>
</div>
`).join('');
activityList.innerHTML = activitiesHtml;
}
// 최근 활동 관련 함수들 제거됨
// 페이지 네비게이션
function navigateToPage(url) {
@@ -317,4 +260,3 @@ function showToast(message, type = 'info') {
// 전역 함수로 노출
window.navigateToPage = navigateToPage;
window.loadRecentActivity = loadRecentActivity;