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:
Hyungi Ahn
2026-01-29 15:46:47 +09:00
parent e1227a69fe
commit b6485e3140
87 changed files with 17509 additions and 698 deletions

View File

@@ -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);
}
});