feat: 대시보드 작업장 현황 지도 구현
- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -241,6 +241,11 @@ function renderUsersTable() {
|
||||
<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 toggle" onclick="toggleUserStatus(${user.user_id})">
|
||||
${user.is_active ? '비활성화' : '활성화'}
|
||||
</button>
|
||||
@@ -344,17 +349,25 @@ function openAddUserModal() {
|
||||
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 = user.role || '';
|
||||
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 || '';
|
||||
|
||||
@@ -403,24 +416,26 @@ async function saveUser() {
|
||||
const formData = {
|
||||
name: elements.userNameInput?.value,
|
||||
username: elements.userIdInput?.value,
|
||||
role: elements.userRoleSelect?.value,
|
||||
role: elements.userRoleSelect?.value, // HTML select value는 이미 'admin' 또는 'user'
|
||||
email: elements.userEmailInput?.value,
|
||||
phone: elements.userPhoneInput?.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) {
|
||||
// 수정
|
||||
@@ -429,17 +444,17 @@ async function saveUser() {
|
||||
// 생성
|
||||
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');
|
||||
@@ -532,3 +547,325 @@ window.deleteUser = deleteUser;
|
||||
window.toggleUserStatus = toggleUserStatus;
|
||||
window.closeUserModal = closeUserModal;
|
||||
window.closeDeleteModal = closeDeleteModal;
|
||||
|
||||
// ========== 페이지 권한 관리 ========== //
|
||||
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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 권한 체크박스 렌더링
|
||||
function renderPageAccessList(userRole) {
|
||||
const pageAccessList = document.getElementById('pageAccessList');
|
||||
const pageAccessGroup = document.getElementById('pageAccessGroup');
|
||||
|
||||
if (!pageAccessList || !pageAccessGroup) return;
|
||||
|
||||
// Admin 사용자는 권한 설정 불필요
|
||||
if (userRole === 'admin') {
|
||||
pageAccessGroup.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
pageAccessGroup.style.display = 'block';
|
||||
|
||||
// 카테고리별로 페이지 그룹화
|
||||
const pagesByCategory = {
|
||||
'work': [],
|
||||
'admin': [],
|
||||
'common': [],
|
||||
'profile': []
|
||||
};
|
||||
|
||||
allPages.forEach(page => {
|
||||
const category = page.category || 'common';
|
||||
if (pagesByCategory[category]) {
|
||||
pagesByCategory[category].push(page);
|
||||
}
|
||||
});
|
||||
|
||||
const categoryNames = {
|
||||
'common': '공통',
|
||||
'work': '작업',
|
||||
'admin': '관리',
|
||||
'profile': '프로필'
|
||||
};
|
||||
|
||||
// HTML 생성
|
||||
let html = '';
|
||||
|
||||
Object.keys(pagesByCategory).forEach(category => {
|
||||
const pages = pagesByCategory[category];
|
||||
if (pages.length === 0) return;
|
||||
|
||||
const catName = categoryNames[category] || category;
|
||||
html += '<div class="page-access-category">';
|
||||
html += '<div class="page-access-category-title">' + catName + '</div>';
|
||||
|
||||
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;
|
||||
|
||||
html += '<div class="page-access-item"><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="page-name">' + page.page_name + '</span>';
|
||||
html += '</label></div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
pageAccessList.innerHTML = html;
|
||||
}
|
||||
|
||||
// 페이지 권한 저장
|
||||
async function savePageAccess(userId) {
|
||||
try {
|
||||
const checkboxes = document.querySelectorAll('.page-access-checkbox:not([disabled])');
|
||||
const pageAccessData = [];
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
pageAccessData.push({
|
||||
page_id: parseInt(checkbox.dataset.pageId),
|
||||
can_access: checkbox.checked ? 1 : 0
|
||||
});
|
||||
});
|
||||
|
||||
console.log('📤 페이지 권한 저장:', userId, pageAccessData);
|
||||
|
||||
await apiCall(`/users/${userId}/page-access`, 'PUT', {
|
||||
pageAccess: pageAccessData
|
||||
});
|
||||
|
||||
console.log('✅ 페이지 권한 저장 완료');
|
||||
} catch (error) {
|
||||
console.error('❌ 페이지 권한 저장 오류:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// editUser 함수를 수정하여 페이지 권한 로드 추가
|
||||
const originalEditUser = window.editUser;
|
||||
window.editUser = async function(userId) {
|
||||
// 페이지 목록이 없으면 로드
|
||||
if (allPages.length === 0) {
|
||||
await loadAllPages();
|
||||
}
|
||||
|
||||
// 원래 editUser 함수 실행
|
||||
if (originalEditUser) {
|
||||
originalEditUser(userId);
|
||||
}
|
||||
|
||||
// 사용자의 페이지 권한 로드
|
||||
await loadUserPageAccess(userId);
|
||||
|
||||
// 사용자 정보 가져오기
|
||||
const user = users.find(u => u.user_id === userId);
|
||||
if (!user) return;
|
||||
|
||||
// 페이지 권한 체크박스 렌더링
|
||||
const roleToValueMap = {
|
||||
'Admin': 'admin',
|
||||
'System Admin': 'admin',
|
||||
'User': 'user',
|
||||
'Guest': 'user'
|
||||
};
|
||||
const userRole = roleToValueMap[user.role] || 'user';
|
||||
renderPageAccessList(userRole);
|
||||
};
|
||||
|
||||
// saveUser 함수를 수정하여 페이지 권한 저장 추가
|
||||
const originalSaveUser = window.saveUser;
|
||||
window.saveUser = async function() {
|
||||
try {
|
||||
// 원래 saveUser 함수 실행
|
||||
if (originalSaveUser) {
|
||||
await originalSaveUser();
|
||||
}
|
||||
|
||||
// 사용자 편집 시에만 페이지 권한 저장
|
||||
if (currentEditingUser && currentEditingUser.user_id) {
|
||||
const userRole = document.getElementById('userRole')?.value;
|
||||
|
||||
// Admin이 아닌 경우에만 페이지 권한 저장
|
||||
if (userRole !== 'admin') {
|
||||
await savePageAccess(currentEditingUser.user_id);
|
||||
}
|
||||
}
|
||||
|
||||
} 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;
|
||||
|
||||
// 카테고리별로 페이지 그룹화
|
||||
const pagesByCategory = {
|
||||
'work': [],
|
||||
'admin': [],
|
||||
'common': [],
|
||||
'profile': []
|
||||
};
|
||||
|
||||
allPages.forEach(page => {
|
||||
const category = page.category || 'common';
|
||||
if (pagesByCategory[category]) {
|
||||
pagesByCategory[category].push(page);
|
||||
}
|
||||
});
|
||||
|
||||
const categoryNames = {
|
||||
'common': '공통',
|
||||
'work': '작업',
|
||||
'admin': '관리',
|
||||
'profile': '프로필'
|
||||
};
|
||||
|
||||
// HTML 생성
|
||||
let html = '';
|
||||
|
||||
Object.keys(pagesByCategory).forEach(category => {
|
||||
const pages = pagesByCategory[category];
|
||||
if (pages.length === 0) return;
|
||||
|
||||
const catName = categoryNames[category] || category;
|
||||
html += '<div class="page-access-category">';
|
||||
html += '<div class="page-access-category-title">' + catName + '</div>';
|
||||
|
||||
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;
|
||||
|
||||
html += '<div class="page-access-item"><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="page-name">' + page.page_name + '</span>';
|
||||
html += '</label></div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
pageAccessList.innerHTML = html;
|
||||
}
|
||||
|
||||
// 페이지 권한 저장 (모달용)
|
||||
async function savePageAccessFromModal() {
|
||||
if (!currentPageAccessUser) {
|
||||
showToast('사용자 정보가 없습니다.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await savePageAccess(currentPageAccessUser.user_id);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user