feat: 일일순회점검 시스템 구축 및 관리 기능 개선

- 일일순회점검 시스템 신규 구현
  - DB 테이블: patrol_checklist_items, daily_patrol_sessions, patrol_check_records, workplace_items, item_types
  - API: /api/patrol/* 엔드포인트
  - 프론트엔드: 지도 기반 작업장 점검 UI

- 설비 관리 기능 개선
  - 구매 관련 필드 추가 (구매일, 가격, 공급업체 등)
  - 설비 코드 자동 생성 (TKP-XXX 형식)

- 작업장 관리 개선
  - 레이아웃 이미지 업로드 기능
  - 마커 위치 저장 기능

- 부서 관리 기능 추가
- 사이드바 네비게이션 카테고리 재구성
- 이미지 401 오류 수정 (정적 파일 경로 처리)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-04 11:41:41 +09:00
parent 2e9d24faf2
commit 90d3e32992
101 changed files with 17421 additions and 7047 deletions

View File

@@ -70,34 +70,12 @@ async function initializePage() {
}
// ========== 사용자 정보 설정 ========== //
// navbar/sidebar는 app-init.js에서 공통 처리
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);
console.log('👤 사용자 정보 로드 완료:', currentUser.name, currentUser.role);
}
}
@@ -249,10 +227,10 @@ function renderUsersTable() {
<button class="action-btn reset-pw" onclick="resetPassword(${user.user_id}, '${user.username}')" title="비밀번호 000000으로 초기화">
비번초기화
</button>
<button class="action-btn toggle" onclick="toggleUserStatus(${user.user_id})">
<button class="action-btn ${user.is_active ? 'deactivate' : 'activate'}" onclick="toggleUserStatus(${user.user_id})">
${user.is_active ? '비활성화' : '활성화'}
</button>
<button class="action-btn delete" onclick="deleteUser(${user.user_id})">
<button class="action-btn delete danger" onclick="permanentDeleteUser(${user.user_id}, '${user.username}')" title="영구 삭제 (복구 불가)">
삭제
</button>
</div>
@@ -325,25 +303,31 @@ function handleFilter(e) {
// ========== 모달 관리 ========== //
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';
}
@@ -373,16 +357,23 @@ function editUser(userId) {
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';
}
@@ -395,14 +386,29 @@ function closeUserModal() {
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';
// 영구 삭제 (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');
}
}
@@ -566,10 +572,10 @@ function showToast(message, type = 'info', duration = 3000) {
// ========== 전역 함수 (HTML에서 호출) ========== //
window.editUser = editUser;
window.deleteUser = deleteUser;
window.toggleUserStatus = toggleUserStatus;
window.closeUserModal = closeUserModal;
window.closeDeleteModal = closeDeleteModal;
window.permanentDeleteUser = permanentDeleteUser;
// ========== 페이지 권한 관리 ========== //
let allPages = [];
@@ -599,75 +605,6 @@ async function loadUserPageAccess(userId) {
}
}
// 페이지 권한 체크박스 렌더링
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, containerId = null) {
@@ -701,61 +638,6 @@ async function savePageAccess(userId, containerId = null) {
}
}
// 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;
}
};
@@ -806,66 +688,106 @@ function closePageAccessModal() {
currentPageAccessUser = null;
}
// 페이지 권한 체크박스 렌더링 (모달용)
// 페이지 권한 체크박스 렌더링 (모달용) - 폴더 구조 형태
function renderPageAccessModalList() {
const pageAccessList = document.getElementById('pageAccessModalList');
if (!pageAccessList) return;
// 카테고리별로 페이지 그룹화
const pagesByCategory = {
'work': [],
'admin': [],
'common': [],
'profile': []
// 폴더 구조 정의 (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 category = page.category || 'common';
if (pagesByCategory[category]) {
pagesByCategory[category].push(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);
}
});
const categoryNames = {
'common': '공통',
'work': '작업',
'admin': '관리',
'profile': '프로필'
};
// HTML 생성 - 폴더 트리 형태
let html = '<div class="folder-tree">';
// HTML 생성
let html = '';
Object.keys(folderStructure).forEach(folderKey => {
const folder = folderStructure[folderKey];
if (folder.pages.length === 0) return;
Object.keys(pagesByCategory).forEach(category => {
const pages = pagesByCategory[category];
if (pages.length === 0) return;
const folderId = 'folder-' + folderKey;
const catName = categoryNames[category] || category;
html += '<div class="page-access-category">';
html += '<div class="page-access-category-title">' + catName + '</div>';
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>';
pages.forEach(page => {
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;
html += '<div class="page-access-item"><label>';
// 파일명만 추출 (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>';
html += '</label></div>';
if (isAlwaysAccessible) {
html += '<span class="always-access-badge">기본</span>';
}
html += '</label>';
html += '</div>';
});
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) {
@@ -899,3 +821,256 @@ document.addEventListener('DOMContentLoaded', () => {
saveBtn.addEventListener('click', savePageAccessFromModal);
}
});
// ========== 작업자 연결 기능 ========== //
let departments = [];
let selectedWorkerId = null;
// 연결된 작업자 정보 표시 업데이트
function updateLinkedWorkerDisplay(user) {
const linkedWorkerInfo = document.getElementById('linkedWorkerInfo');
if (!linkedWorkerInfo) return;
if (user.worker_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;
}
selectedWorkerId = currentEditingUser.worker_id || null;
// 부서 목록 로드
await loadDepartmentsForSelect();
// 모달 표시
document.getElementById('workerSelectModal').style.display = 'flex';
}
window.openWorkerSelectModal = openWorkerSelectModal;
// 작업자 선택 모달 닫기
function closeWorkerSelectModal() {
document.getElementById('workerSelectModal').style.display = 'none';
selectedWorkerId = 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 linkedWorkerIds = users
.filter(u => u.worker_id && u.user_id !== currentEditingUser?.user_id)
.map(u => u.worker_id);
container.innerHTML = workers.map(worker => {
const isSelected = selectedWorkerId === worker.worker_id;
const isLinkedToOther = linkedWorkerIds.includes(worker.worker_id);
const linkedUser = isLinkedToOther ? users.find(u => u.worker_id === worker.worker_id) : null;
return `
<div class="worker-select-item ${isSelected ? 'selected' : ''} ${isLinkedToOther ? 'disabled' : ''}"
onclick="${isLinkedToOther ? '' : `selectWorker(${worker.worker_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(workerId, workerName) {
selectedWorkerId = workerId;
// 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*="${workerId}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
selectedItem.querySelector('.select-indicator').textContent = '✓';
}
// 서버에 저장
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: workerId
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.worker_id = workerId;
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.worker_id) {
showToast('연결된 작업자가 없습니다.', 'warning');
closeWorkerSelectModal();
return;
}
if (!confirm('작업자 연결을 해제하시겠습니까?')) {
return;
}
try {
const response = await window.apiCall(`/users/${currentEditingUser.user_id}`, 'PUT', {
worker_id: null
});
if (response.success) {
// currentEditingUser 업데이트
currentEditingUser.worker_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], worker_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;

58
web-ui/js/api-base.js Normal file
View File

@@ -0,0 +1,58 @@
// /js/api-base.js
// API 기본 설정 (비모듈 - 빠른 로딩용)
(function() {
'use strict';
const API_PORT = 20005;
const API_PATH = '/api';
function getApiBaseUrl() {
const hostname = window.location.hostname;
const protocol = window.location.protocol;
return `${protocol}//${hostname}:${API_PORT}${API_PATH}`;
}
// 전역 API 설정
const apiUrl = getApiBaseUrl();
window.API_BASE_URL = apiUrl;
window.API = apiUrl; // 이전 호환성
// 인증 헤더 생성
window.getAuthHeaders = function() {
const token = localStorage.getItem('token');
return {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
};
};
// API 호출 헬퍼 (기존 시그니처 유지: endpoint, method, data)
// JSON 파싱하여 반환
window.apiCall = async function(endpoint, method = 'GET', data = null) {
const url = `${window.API_BASE_URL}${endpoint}`;
const config = {
method: method,
headers: window.getAuthHeaders()
};
if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) {
config.body = JSON.stringify(data);
}
const response = await fetch(url, config);
// 401 Unauthorized 처리
if (response.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/index.html';
throw new Error('인증이 만료되었습니다.');
}
// JSON 파싱하여 반환
return response.json();
};
console.log('✅ API 설정 완료:', window.API_BASE_URL);
})();

470
web-ui/js/app-init.js Normal file
View File

@@ -0,0 +1,470 @@
// /js/app-init.js
// 앱 초기화 - 인증, 네비바, 사이드바를 한 번에 로드
// 모든 페이지에서 이 하나의 스크립트만 로드하면 됨
(function() {
'use strict';
// ===== 캐시 설정 =====
const CACHE_DURATION = 10 * 60 * 1000; // 10분
const COMPONENT_CACHE_PREFIX = 'component_';
// ===== 인증 함수 =====
function isLoggedIn() {
const token = localStorage.getItem('token');
return token && token !== 'undefined' && token !== 'null';
}
function getUser() {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
}
function clearAuthData() {
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('userPageAccess');
}
// ===== 페이지 권한 캐시 =====
let pageAccessPromise = null;
async function getPageAccess(currentUser) {
if (!currentUser || !currentUser.user_id) return null;
// 캐시 확인
const cached = localStorage.getItem('userPageAccess');
if (cached) {
try {
const cacheData = JSON.parse(cached);
if (Date.now() - cacheData.timestamp < CACHE_DURATION) {
return cacheData.pages;
}
} catch (e) {
localStorage.removeItem('userPageAccess');
}
}
// 이미 로딩 중이면 기존 Promise 반환
if (pageAccessPromise) return pageAccessPromise;
// 새로운 API 호출
pageAccessPromise = (async () => {
try {
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) return null;
const data = await response.json();
const pages = data.data.pageAccess || [];
localStorage.setItem('userPageAccess', JSON.stringify({
pages: pages,
timestamp: Date.now()
}));
return pages;
} catch (error) {
console.error('페이지 권한 조회 오류:', error);
return null;
} finally {
pageAccessPromise = null;
}
})();
return pageAccessPromise;
}
async function getAccessiblePageKeys(currentUser) {
const pages = await getPageAccess(currentUser);
if (!pages) return [];
return pages.filter(p => p.can_access === 1).map(p => p.page_key);
}
// ===== 현재 페이지 키 추출 =====
function getCurrentPageKey() {
const path = window.location.pathname;
if (!path.startsWith('/pages/')) return null;
const pagePath = path.substring(7).replace('.html', '');
return pagePath.replace(/\//g, '.');
}
// ===== 컴포넌트 로더 =====
async function loadComponent(name, selector, processor) {
const container = document.querySelector(selector);
if (!container) return;
const paths = {
'navbar': '/components/navbar.html',
'sidebar-nav': '/components/sidebar-nav.html'
};
const componentPath = paths[name];
if (!componentPath) return;
try {
const cacheKey = COMPONENT_CACHE_PREFIX + name;
let html = sessionStorage.getItem(cacheKey);
if (!html) {
const response = await fetch(componentPath);
if (!response.ok) throw new Error('컴포넌트 로드 실패');
html = await response.text();
try { sessionStorage.setItem(cacheKey, html); } catch (e) {}
}
if (processor) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
await processor(doc);
container.innerHTML = doc.body.innerHTML;
} else {
container.innerHTML = html;
}
} catch (error) {
console.error(`컴포넌트 로드 오류 (${name}):`, error);
}
}
// ===== 네비바 처리 =====
const ROLE_NAMES = {
'system admin': '시스템 관리자',
'admin': '관리자',
'leader': '그룹장',
'user': '작업자',
'support': '지원팀',
'default': '사용자'
};
async function processNavbar(doc, currentUser, accessiblePageKeys) {
const userRole = (currentUser.role || '').toLowerCase();
const isAdmin = userRole === 'admin' || userRole === 'system admin';
if (isAdmin) {
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
} else {
doc.querySelectorAll('[data-page-key]').forEach(item => {
const pageKey = item.getAttribute('data-page-key');
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return;
if (!accessiblePageKeys.includes(pageKey)) item.remove();
});
doc.querySelectorAll('.admin-only').forEach(el => el.remove());
}
// 사용자 정보 표시
const displayName = currentUser.name || currentUser.username;
const roleName = ROLE_NAMES[userRole] || ROLE_NAMES.default;
const setElementText = (id, text) => {
const el = doc.getElementById(id);
if (el) el.textContent = text;
};
setElementText('userName', displayName);
setElementText('userRole', roleName);
setElementText('userInitial', displayName.charAt(0));
}
// ===== 사이드바 처리 =====
async function processSidebar(doc, currentUser, accessiblePageKeys) {
const userRole = (currentUser.role || '').toLowerCase();
const accessLevel = (currentUser.access_level || '').toLowerCase();
// role 또는 access_level로 관리자 확인
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
accessLevel === 'admin' || accessLevel === 'system';
if (isAdmin) {
doc.querySelectorAll('.admin-only').forEach(el => el.classList.add('visible'));
} else {
doc.querySelectorAll('[data-page-key]').forEach(item => {
const pageKey = item.getAttribute('data-page-key');
if (pageKey === 'dashboard' || pageKey.startsWith('profile.')) return;
if (!accessiblePageKeys.includes(pageKey)) item.style.display = 'none';
});
doc.querySelectorAll('.nav-category.admin-only').forEach(el => el.remove());
}
// 현재 페이지 하이라이트
const currentPath = window.location.pathname;
doc.querySelectorAll('.nav-item').forEach(item => {
const href = item.getAttribute('href');
if (href && currentPath.includes(href.replace(/^\//, ''))) {
item.classList.add('active');
const category = item.closest('.nav-category');
if (category) category.classList.add('expanded');
}
});
// 저장된 상태 복원
const isCollapsed = localStorage.getItem('sidebarCollapsed') === 'true';
const sidebar = doc.querySelector('.sidebar-nav');
if (isCollapsed && sidebar) {
sidebar.classList.add('collapsed');
document.body.classList.add('sidebar-collapsed');
}
const expandedCategories = JSON.parse(localStorage.getItem('sidebarExpanded') || '[]');
expandedCategories.forEach(category => {
const el = doc.querySelector(`[data-category="${category}"]`);
if (el) el.classList.add('expanded');
});
}
// ===== 사이드바 이벤트 설정 =====
function setupSidebarEvents() {
const sidebar = document.getElementById('sidebarNav');
const toggle = document.getElementById('sidebarToggle');
if (!sidebar || !toggle) return;
toggle.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
document.body.classList.toggle('sidebar-collapsed');
localStorage.setItem('sidebarCollapsed', sidebar.classList.contains('collapsed'));
});
sidebar.querySelectorAll('.nav-category-header').forEach(header => {
header.addEventListener('click', () => {
const category = header.closest('.nav-category');
category.classList.toggle('expanded');
const expanded = [];
sidebar.querySelectorAll('.nav-category.expanded').forEach(cat => {
const name = cat.getAttribute('data-category');
if (name) expanded.push(name);
});
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded));
});
});
}
// ===== 네비바 이벤트 설정 =====
function setupNavbarEvents() {
const logoutButton = document.getElementById('logoutBtn');
if (logoutButton) {
logoutButton.addEventListener('click', () => {
if (confirm('로그아웃 하시겠습니까?')) {
clearAuthData();
window.location.href = '/index.html';
}
});
}
}
// ===== 날짜/시간 업데이트 =====
function updateDateTime() {
const now = new Date();
const timeEl = document.getElementById('timeValue');
if (timeEl) timeEl.textContent = now.toLocaleTimeString('ko-KR', { hour12: false });
const dateEl = document.getElementById('dateValue');
if (dateEl) {
const days = ['일', '월', '화', '수', '목', '토'];
dateEl.textContent = `${now.getMonth() + 1}${now.getDate()}일 (${days[now.getDay()]})`;
}
}
// ===== 날씨 업데이트 =====
const WEATHER_ICONS = { clear: '☀️', rain: '🌧️', snow: '❄️', heat: '🔥', cold: '🥶', wind: '💨', fog: '🌫️', dust: '😷', cloudy: '⛅', overcast: '☁️' };
const WEATHER_NAMES = { clear: '맑음', rain: '비', snow: '눈', heat: '폭염', cold: '한파', wind: '강풍', fog: '안개', dust: '미세먼지', cloudy: '구름많음', overcast: '흐림' };
async function updateWeather() {
try {
const token = localStorage.getItem('token');
if (!token) return;
// 캐시 확인
const cached = sessionStorage.getItem('weatherCache');
let result;
if (cached) {
const cacheData = JSON.parse(cached);
if (Date.now() - cacheData.timestamp < 5 * 60 * 1000) {
result = cacheData.data;
}
}
if (!result) {
const response = await fetch(`${window.API_BASE_URL}/tbm/weather/current`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) return;
result = await response.json();
sessionStorage.setItem('weatherCache', JSON.stringify({ data: result, timestamp: Date.now() }));
}
if (result.success && result.data) {
const { temperature, conditions } = result.data;
const tempEl = document.getElementById('weatherTemp');
if (tempEl && temperature != null) tempEl.textContent = `${Math.round(temperature)}°C`;
const iconEl = document.getElementById('weatherIcon');
const descEl = document.getElementById('weatherDesc');
if (conditions && conditions.length > 0) {
const primary = conditions[0];
if (iconEl) iconEl.textContent = WEATHER_ICONS[primary] || '🌤️';
if (descEl) descEl.textContent = WEATHER_NAMES[primary] || '맑음';
}
}
} catch (error) {
console.warn('날씨 정보 로드 실패');
}
}
// ===== 메인 초기화 =====
async function init() {
console.log('🚀 app-init 시작');
// 1. 인증 확인
if (!isLoggedIn()) {
clearAuthData();
window.location.href = '/index.html';
return;
}
const currentUser = getUser();
if (!currentUser || !currentUser.username) {
clearAuthData();
window.location.href = '/index.html';
return;
}
console.log('✅ 인증 확인:', currentUser.username);
const userRole = (currentUser.role || '').toLowerCase();
const accessLevel = (currentUser.access_level || '').toLowerCase();
// role 또는 access_level로 관리자 확인
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
accessLevel === 'admin' || accessLevel === 'system';
// 2. 페이지 접근 권한 체크 (Admin은 건너뛰기)
let accessiblePageKeys = [];
if (!isAdmin) {
const pageKey = getCurrentPageKey();
if (pageKey && pageKey !== 'dashboard' && !pageKey.startsWith('profile.')) {
accessiblePageKeys = await getAccessiblePageKeys(currentUser);
if (!accessiblePageKeys.includes(pageKey)) {
alert('이 페이지에 접근할 권한이 없습니다.');
window.location.href = '/pages/dashboard.html';
return;
}
}
}
// 3. 사이드바 컨테이너 생성 (없으면)
let sidebarContainer = document.getElementById('sidebar-container');
if (!sidebarContainer) {
sidebarContainer = document.createElement('div');
sidebarContainer.id = 'sidebar-container';
document.body.prepend(sidebarContainer);
console.log('📦 사이드바 컨테이너 생성됨');
}
// 4. 네비바와 사이드바 동시 로드
console.log('📥 컴포넌트 로딩 시작');
await Promise.all([
loadComponent('navbar', '#navbar-container', (doc) => processNavbar(doc, currentUser, accessiblePageKeys)),
loadComponent('sidebar-nav', '#sidebar-container', (doc) => processSidebar(doc, currentUser, accessiblePageKeys))
]);
console.log('✅ 컴포넌트 로딩 완료');
// 5. 이벤트 설정
setupNavbarEvents();
setupSidebarEvents();
document.body.classList.add('has-sidebar');
// 6. 페이지 전환 로딩 인디케이터 설정
setupPageTransitionLoader();
// 7. 날짜/시간 (비동기)
updateDateTime();
setInterval(updateDateTime, 1000);
// 8. 날씨 (백그라운드)
setTimeout(updateWeather, 100);
console.log('✅ app-init 완료');
}
// ===== 페이지 전환 로딩 인디케이터 =====
function setupPageTransitionLoader() {
// 로딩 바 스타일 추가
const style = document.createElement('style');
style.textContent = `
#page-loader {
position: fixed;
top: 0;
left: 0;
width: 0;
height: 3px;
background: linear-gradient(90deg, #3b82f6, #60a5fa);
z-index: 99999;
transition: width 0.3s ease;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
}
#page-loader.loading {
width: 70%;
}
#page-loader.done {
width: 100%;
opacity: 0;
transition: width 0.2s ease, opacity 0.3s ease 0.2s;
}
body.page-loading {
cursor: wait;
}
body.page-loading * {
pointer-events: none;
}
`;
document.head.appendChild(style);
// 로딩 바 엘리먼트 생성
const loader = document.createElement('div');
loader.id = 'page-loader';
document.body.appendChild(loader);
// 모든 내부 링크에 클릭 이벤트 추가
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href) return;
// 외부 링크, 해시 링크, javascript: 링크 제외
if (href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript:')) return;
// 새 탭 링크 제외
if (link.target === '_blank') return;
// 로딩 시작
loader.classList.remove('done');
loader.classList.add('loading');
document.body.classList.add('page-loading');
});
// 페이지 떠날 때 완료 표시
window.addEventListener('beforeunload', () => {
const loader = document.getElementById('page-loader');
if (loader) {
loader.classList.remove('loading');
loader.classList.add('done');
}
});
}
// DOMContentLoaded 시 실행
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 전역 노출 (필요시)
window.appInit = { getUser, clearAuthData, isLoggedIn };
})();

View File

@@ -48,38 +48,9 @@ function updateCurrentTime() {
}
// 사용자 정보 업데이트
// navbar/sidebar는 app-init.js에서 공통 처리
function updateUserInfo() {
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
const finalUserInfo = {
worker_name: userInfo.worker_name || authUser.username || authUser.worker_name,
job_type: userInfo.job_type || authUser.role || authUser.job_type,
username: authUser.username || userInfo.username
};
const userNameElement = document.getElementById('userName');
const userRoleElement = document.getElementById('userRole');
const userInitialElement = document.getElementById('userInitial');
if (userNameElement) {
userNameElement.textContent = finalUserInfo.worker_name || '사용자';
}
if (userRoleElement) {
const roleMap = {
'leader': '그룹장',
'worker': '작업자',
'admin': '관리자',
'system': '시스템 관리자'
};
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type || '작업자';
}
if (userInitialElement) {
const name = finalUserInfo.worker_name || '사용자';
userInitialElement.textContent = name.charAt(0);
}
// app-init.js가 navbar 사용자 정보를 처리
}
// 프로필 메뉴 설정

View File

@@ -1,6 +1,38 @@
// /js/component-loader.js
import { config } from './config.js';
// 캐시 버전 (컴포넌트 변경 시 증가)
const CACHE_VERSION = 'v1';
/**
* 컴포넌트 HTML을 캐시에서 가져오거나 fetch
*/
async function getComponentHtml(componentName, componentPath) {
const cacheKey = `component_${componentName}_${CACHE_VERSION}`;
// 캐시에서 먼저 확인
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
return cached;
}
// 캐시 없으면 fetch
const response = await fetch(componentPath);
if (!response.ok) {
throw new Error(`컴포넌트 파일을 불러올 수 없습니다: ${response.statusText}`);
}
const htmlText = await response.text();
// 캐시에 저장
try {
sessionStorage.setItem(cacheKey, htmlText);
} catch (e) {
// sessionStorage 용량 초과 시 무시
}
return htmlText;
}
/**
* 공용 HTML 컴포넌트를 페이지의 특정 위치에 동적으로 로드합니다.
* @param {string} componentName - 로드할 컴포넌트의 이름 (e.g., 'sidebar', 'navbar'). config.js의 components 객체에 정의된 키와 일치해야 합니다.
@@ -23,20 +55,16 @@ export async function loadComponent(componentName, containerSelector, domProcess
}
try {
const response = await fetch(componentPath);
if (!response.ok) {
throw new Error(`컴포넌트 파일을 불러올 수 없습니다: ${response.statusText}`);
}
const htmlText = await response.text();
const htmlText = await getComponentHtml(componentName, componentPath);
if (domProcessor) {
// 1. 텍스트를 가상 DOM으로 파싱
const parser = new DOMParser();
const doc = parser.parseFromString(htmlText, 'text/html');
// 2. DOM 프로세서(콜백)를 실행하여 DOM 조작
await domProcessor(doc);
// 3. 조작된 HTML을 실제 DOM에 삽입
container.innerHTML = doc.body.innerHTML;
} else {

732
web-ui/js/daily-patrol.js Normal file
View File

@@ -0,0 +1,732 @@
// daily-patrol.js - 일일순회점검 페이지 JavaScript
// 전역 상태
let currentSession = null;
let categories = []; // 공장(대분류) 목록
let workplaces = []; // 작업장 목록
let checklistItems = []; // 체크리스트 항목
let checkRecords = {}; // 체크 기록 (workplace_id -> records)
let selectedWorkplace = null;
let itemTypes = []; // 물품 유형
let workplaceItems = []; // 현재 작업장 물품
let isItemEditMode = false;
// 이미지 URL 헬퍼 함수 (정적 파일용 - /api 경로 제외)
function getImageUrl(path) {
if (!path) return '';
// 이미 http로 시작하면 그대로 반환
if (path.startsWith('http')) return path;
// API_BASE_URL에서 /api 제거하여 정적 파일 서버 URL 생성
// /uploads 경로는 인증 없이 접근 가능한 정적 파일 경로
const staticUrl = window.API_BASE_URL.replace(/\/api$/, '');
return staticUrl + path;
}
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
await waitForAxiosConfig();
initializePage();
});
// axios 설정 대기
function waitForAxiosConfig() {
return new Promise((resolve) => {
const check = setInterval(() => {
if (axios.defaults.baseURL) {
clearInterval(check);
resolve();
}
}, 50);
setTimeout(() => {
clearInterval(check);
resolve();
}, 5000);
});
}
// 페이지 초기화
async function initializePage() {
// 오늘 날짜 설정
const today = new Date().toISOString().slice(0, 10);
document.getElementById('patrolDate').value = today;
// 시간대 버튼 이벤트
document.querySelectorAll('.patrol-time-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.patrol-time-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// 데이터 로드
await Promise.all([
loadCategories(),
loadItemTypes(),
loadTodayStatus()
]);
}
// 공장(대분류) 목록 로드
async function loadCategories() {
try {
const response = await axios.get('/workplaces/categories');
if (response.data.success) {
categories = response.data.data;
const select = document.getElementById('categorySelect');
select.innerHTML = '<option value="">공장 선택...</option>' +
categories.map(c => `<option value="${c.category_id}">${c.category_name}</option>`).join('');
}
} catch (error) {
console.error('공장 목록 로드 실패:', error);
}
}
// 물품 유형 로드
async function loadItemTypes() {
try {
const response = await axios.get('/patrol/item-types');
if (response.data.success) {
itemTypes = response.data.data;
renderItemTypesSelect();
renderItemsLegend();
}
} catch (error) {
console.error('물품 유형 로드 실패:', error);
}
}
// 오늘 점검 현황 로드
async function loadTodayStatus() {
try {
const response = await axios.get('/patrol/today-status');
if (response.data.success) {
renderTodayStatus(response.data.data);
}
} catch (error) {
console.error('오늘 현황 로드 실패:', error);
}
}
// 오늘 점검 현황 렌더링
function renderTodayStatus(statusList) {
const container = document.getElementById('todayStatusSummary');
if (!statusList || statusList.length === 0) {
container.innerHTML = `
<div class="status-card">
<div class="status-label">오전</div>
<div class="status-value pending">미점검</div>
</div>
<div class="status-card">
<div class="status-label">오후</div>
<div class="status-value pending">미점검</div>
</div>
`;
return;
}
const morning = statusList.find(s => s.patrol_time === 'morning');
const afternoon = statusList.find(s => s.patrol_time === 'afternoon');
container.innerHTML = `
<div class="status-card">
<div class="status-label">오전</div>
<div class="status-value ${morning?.status === 'completed' ? 'completed' : 'pending'}">
${morning ? (morning.status === 'completed' ? '완료' : '진행중') : '미점검'}
</div>
${morning ? `<div class="status-sub">${morning.inspector_name || ''}</div>` : ''}
</div>
<div class="status-card">
<div class="status-label">오후</div>
<div class="status-value ${afternoon?.status === 'completed' ? 'completed' : 'pending'}">
${afternoon ? (afternoon.status === 'completed' ? '완료' : '진행중') : '미점검'}
</div>
${afternoon ? `<div class="status-sub">${afternoon.inspector_name || ''}</div>` : ''}
</div>
`;
}
// 순회점검 시작
async function startPatrol() {
const patrolDate = document.getElementById('patrolDate').value;
const patrolTime = document.querySelector('.patrol-time-btn.active')?.dataset.time;
const categoryId = document.getElementById('categorySelect').value;
if (!patrolDate || !patrolTime || !categoryId) {
alert('점검 일자, 시간대, 공장을 모두 선택해주세요.');
return;
}
try {
// 세션 생성 또는 조회
const response = await axios.post('/patrol/sessions', {
patrol_date: patrolDate,
patrol_time: patrolTime,
category_id: categoryId
});
if (response.data.success) {
currentSession = response.data.data;
currentSession.patrol_date = patrolDate;
currentSession.patrol_time = patrolTime;
currentSession.category_id = categoryId;
// 작업장 목록 로드
await loadWorkplaces(categoryId);
// 체크리스트 항목 로드
await loadChecklistItems(categoryId);
// 점검 영역 표시
document.getElementById('patrolArea').style.display = 'block';
renderSessionInfo();
renderWorkplaceMap();
// 시작 버튼 비활성화
document.getElementById('startPatrolBtn').textContent = '점검 진행중...';
document.getElementById('startPatrolBtn').disabled = true;
}
} catch (error) {
console.error('순회점검 시작 실패:', error);
alert('순회점검을 시작할 수 없습니다.');
}
}
// 작업장 목록 로드
async function loadWorkplaces(categoryId) {
try {
const response = await axios.get(`/workplaces?category_id=${categoryId}`);
if (response.data.success) {
workplaces = response.data.data;
}
} catch (error) {
console.error('작업장 목록 로드 실패:', error);
}
}
// 체크리스트 항목 로드
async function loadChecklistItems(categoryId) {
try {
const response = await axios.get(`/patrol/checklist?category_id=${categoryId}`);
if (response.data.success) {
checklistItems = response.data.data.items;
}
} catch (error) {
console.error('체크리스트 항목 로드 실패:', error);
}
}
// 세션 정보 렌더링
function renderSessionInfo() {
const container = document.getElementById('sessionInfo');
const category = categories.find(c => c.category_id == currentSession.category_id);
const checkedCount = Object.values(checkRecords).flat().filter(r => r.is_checked).length;
const totalCount = workplaces.length * checklistItems.length;
const progress = totalCount > 0 ? Math.round(checkedCount / totalCount * 100) : 0;
container.innerHTML = `
<div class="session-info">
<div class="session-info-item">
<span class="session-info-label">점검일자</span>
<span class="session-info-value">${formatDate(currentSession.patrol_date)}</span>
</div>
<div class="session-info-item">
<span class="session-info-label">시간대</span>
<span class="session-info-value">${currentSession.patrol_time === 'morning' ? '오전' : '오후'}</span>
</div>
<div class="session-info-item">
<span class="session-info-label">공장</span>
<span class="session-info-value">${category?.category_name || ''}</span>
</div>
</div>
<div class="session-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<span class="progress-text">${progress}%</span>
</div>
`;
}
// 작업장 지도/목록 렌더링
function renderWorkplaceMap() {
const mapContainer = document.getElementById('patrolMapContainer');
const listContainer = document.getElementById('workplaceListContainer');
const category = categories.find(c => c.category_id == currentSession.category_id);
// 지도 이미지가 있으면 지도 표시
if (category?.layout_image) {
mapContainer.innerHTML = `<img src="${getImageUrl(category.layout_image)}" alt="${category.category_name} 지도">`;
mapContainer.style.display = 'block';
listContainer.style.display = 'none';
// 작업장 마커 추가
workplaces.forEach(wp => {
if (wp.x_percent && wp.y_percent) {
const marker = document.createElement('div');
marker.className = 'workplace-marker';
marker.style.left = `${wp.x_percent}%`;
marker.style.top = `${wp.y_percent}%`;
marker.textContent = wp.workplace_name;
marker.dataset.workplaceId = wp.workplace_id;
marker.onclick = () => selectWorkplace(wp.workplace_id);
// 점검 상태에 따른 스타일
const records = checkRecords[wp.workplace_id];
if (records && records.some(r => r.is_checked)) {
marker.classList.add(records.every(r => r.is_checked) ? 'completed' : 'in-progress');
}
mapContainer.appendChild(marker);
}
});
} else {
// 지도 없으면 카드 목록으로 표시
mapContainer.style.display = 'none';
listContainer.style.display = 'grid';
listContainer.innerHTML = workplaces.map(wp => {
const records = checkRecords[wp.workplace_id];
const isCompleted = records && records.length > 0 && records.every(r => r.is_checked);
const isInProgress = records && records.some(r => r.is_checked);
return `
<div class="workplace-card ${isCompleted ? 'completed' : ''} ${selectedWorkplace?.workplace_id === wp.workplace_id ? 'selected' : ''}"
data-workplace-id="${wp.workplace_id}"
onclick="selectWorkplace(${wp.workplace_id})">
<div class="workplace-card-name">${wp.workplace_name}</div>
<div class="workplace-card-status">
${isCompleted ? '점검완료' : (isInProgress ? '점검중' : '미점검')}
</div>
</div>
`;
}).join('');
}
}
// 작업장 선택
async function selectWorkplace(workplaceId) {
selectedWorkplace = workplaces.find(w => w.workplace_id === workplaceId);
// 마커/카드 선택 상태 업데이트
document.querySelectorAll('.workplace-marker, .workplace-card').forEach(el => {
el.classList.remove('selected');
if (el.dataset.workplaceId == workplaceId) {
el.classList.add('selected');
}
});
// 기존 체크 기록 로드
if (!checkRecords[workplaceId]) {
try {
const response = await axios.get(`/patrol/sessions/${currentSession.session_id}/records?workplace_id=${workplaceId}`);
if (response.data.success) {
checkRecords[workplaceId] = response.data.data;
}
} catch (error) {
console.error('체크 기록 로드 실패:', error);
checkRecords[workplaceId] = [];
}
}
// 체크리스트 렌더링
renderChecklist(workplaceId);
// 물품 현황 로드 및 표시
await loadWorkplaceItems(workplaceId);
// 액션 버튼 표시
document.getElementById('checklistActions').style.display = 'flex';
}
// 체크리스트 렌더링
function renderChecklist(workplaceId) {
const header = document.getElementById('checklistHeader');
const content = document.getElementById('checklistContent');
const workplace = workplaces.find(w => w.workplace_id === workplaceId);
header.innerHTML = `
<h3>${workplace?.workplace_name || ''} 체크리스트</h3>
<p class="checklist-subtitle">각 항목을 점검하고 체크해주세요</p>
`;
// 카테고리별 그룹화
const grouped = {};
checklistItems.forEach(item => {
if (!grouped[item.check_category]) {
grouped[item.check_category] = [];
}
grouped[item.check_category].push(item);
});
const records = checkRecords[workplaceId] || [];
content.innerHTML = Object.entries(grouped).map(([category, items]) => `
<div class="checklist-category">
<div class="checklist-category-title">${getCategoryName(category)}</div>
${items.map(item => {
const record = records.find(r => r.check_item_id === item.item_id);
const isChecked = record?.is_checked;
const checkResult = record?.check_result;
return `
<div class="check-item ${isChecked ? 'checked' : ''}"
data-item-id="${item.item_id}"
onclick="toggleCheckItem(${workplaceId}, ${item.item_id})">
<div class="check-item-checkbox">
${isChecked ? '&#10003;' : ''}
</div>
<div class="check-item-content">
<div class="check-item-text">
${item.check_item}
${item.is_required ? '<span class="check-item-required">*</span>' : ''}
</div>
${isChecked ? `
<div class="check-result-selector" onclick="event.stopPropagation()">
<button class="check-result-btn good ${checkResult === 'good' ? 'active' : ''}"
onclick="setCheckResult(${workplaceId}, ${item.item_id}, 'good')">양호</button>
<button class="check-result-btn warning ${checkResult === 'warning' ? 'active' : ''}"
onclick="setCheckResult(${workplaceId}, ${item.item_id}, 'warning')">주의</button>
<button class="check-result-btn bad ${checkResult === 'bad' ? 'active' : ''}"
onclick="setCheckResult(${workplaceId}, ${item.item_id}, 'bad')">불량</button>
</div>
` : ''}
</div>
</div>
`;
}).join('')}
</div>
`).join('');
}
// 카테고리명 변환
function getCategoryName(code) {
const names = {
'SAFETY': '안전',
'ORGANIZATION': '정리정돈',
'EQUIPMENT': '설비',
'ENVIRONMENT': '환경'
};
return names[code] || code;
}
// 체크 항목 토글
function toggleCheckItem(workplaceId, itemId) {
if (!checkRecords[workplaceId]) {
checkRecords[workplaceId] = [];
}
const records = checkRecords[workplaceId];
const existingIndex = records.findIndex(r => r.check_item_id === itemId);
if (existingIndex >= 0) {
records[existingIndex].is_checked = !records[existingIndex].is_checked;
if (!records[existingIndex].is_checked) {
records[existingIndex].check_result = null;
}
} else {
records.push({
check_item_id: itemId,
is_checked: true,
check_result: 'good',
note: null
});
}
renderChecklist(workplaceId);
renderWorkplaceMap();
renderSessionInfo();
}
// 체크 결과 설정
function setCheckResult(workplaceId, itemId, result) {
const records = checkRecords[workplaceId];
const record = records.find(r => r.check_item_id === itemId);
if (record) {
record.check_result = result;
renderChecklist(workplaceId);
}
}
// 임시 저장
async function saveChecklistDraft() {
if (!selectedWorkplace) return;
try {
const records = checkRecords[selectedWorkplace.workplace_id] || [];
await axios.post(`/patrol/sessions/${currentSession.session_id}/records/batch`, {
workplace_id: selectedWorkplace.workplace_id,
records: records
});
alert('임시 저장되었습니다.');
} catch (error) {
console.error('임시 저장 실패:', error);
alert('저장에 실패했습니다.');
}
}
// 저장 후 다음
async function saveChecklist() {
if (!selectedWorkplace) return;
try {
const records = checkRecords[selectedWorkplace.workplace_id] || [];
await axios.post(`/patrol/sessions/${currentSession.session_id}/records/batch`, {
workplace_id: selectedWorkplace.workplace_id,
records: records
});
// 다음 미점검 작업장으로 이동
const currentIndex = workplaces.findIndex(w => w.workplace_id === selectedWorkplace.workplace_id);
const nextWorkplace = workplaces.slice(currentIndex + 1).find(w => {
const records = checkRecords[w.workplace_id];
return !records || records.length === 0 || !records.every(r => r.is_checked);
});
if (nextWorkplace) {
selectWorkplace(nextWorkplace.workplace_id);
} else {
alert('모든 작업장 점검이 완료되었습니다!');
}
} catch (error) {
console.error('저장 실패:', error);
alert('저장에 실패했습니다.');
}
}
// 순회점검 완료
async function completePatrol() {
if (!currentSession) return;
// 미점검 작업장 확인
const uncheckedCount = workplaces.filter(w => {
const records = checkRecords[w.workplace_id];
return !records || records.length === 0;
}).length;
if (uncheckedCount > 0) {
if (!confirm(`아직 ${uncheckedCount}개 작업장이 미점검 상태입니다. 그래도 완료하시겠습니까?`)) {
return;
}
}
try {
const notes = document.getElementById('patrolNotes').value;
if (notes) {
await axios.patch(`/patrol/sessions/${currentSession.session_id}/notes`, { notes });
}
await axios.patch(`/patrol/sessions/${currentSession.session_id}/complete`);
alert('순회점검이 완료되었습니다.');
location.reload();
} catch (error) {
console.error('순회점검 완료 실패:', error);
alert('순회점검 완료에 실패했습니다.');
}
}
// ==================== 물품 현황 ====================
// 작업장 물품 로드
async function loadWorkplaceItems(workplaceId) {
try {
const response = await axios.get(`/patrol/workplaces/${workplaceId}/items`);
if (response.data.success) {
workplaceItems = response.data.data;
renderItemsSection(workplaceId);
}
} catch (error) {
console.error('물품 로드 실패:', error);
workplaceItems = [];
}
}
// 물품 섹션 렌더링
function renderItemsSection(workplaceId) {
const section = document.getElementById('itemsSection');
const workplace = workplaces.find(w => w.workplace_id === workplaceId);
const container = document.getElementById('itemsMapContainer');
document.getElementById('selectedWorkplaceName').textContent = workplace?.workplace_name || '';
// 작업장 레이아웃 이미지가 있으면 표시
if (workplace?.layout_image) {
container.innerHTML = `<img src="${getImageUrl(workplace.layout_image)}" alt="${workplace.workplace_name}">`;
// 물품 마커 추가
workplaceItems.forEach(item => {
if (item.x_percent && item.y_percent) {
const marker = document.createElement('div');
marker.className = `item-marker ${item.item_type}`;
marker.style.left = `${item.x_percent}%`;
marker.style.top = `${item.y_percent}%`;
marker.style.width = `${item.width_percent || 5}%`;
marker.style.height = `${item.height_percent || 5}%`;
marker.innerHTML = item.icon || getItemTypeIcon(item.item_type);
marker.title = `${item.item_name || item.type_name} (${item.quantity}개)`;
marker.dataset.itemId = item.item_id;
marker.onclick = () => openItemModal(item);
container.appendChild(marker);
}
});
} else {
container.innerHTML = '<p style="padding: 2rem; text-align: center; color: #64748b;">작업장 레이아웃 이미지가 없습니다.</p>';
}
section.style.display = 'block';
}
// 물품 유형 아이콘
function getItemTypeIcon(typeCode) {
const icons = {
'container': '📦',
'plate': '🔲',
'material': '🧱',
'tool': '🔧',
'other': '📍'
};
return icons[typeCode] || '📍';
}
// 물품 유형 셀렉트 렌더링
function renderItemTypesSelect() {
const select = document.getElementById('itemType');
if (!select) return;
select.innerHTML = itemTypes.map(t =>
`<option value="${t.type_code}">${t.icon} ${t.type_name}</option>`
).join('');
}
// 물품 범례 렌더링
function renderItemsLegend() {
const container = document.getElementById('itemsLegend');
if (!container) return;
container.innerHTML = itemTypes.map(t => `
<div class="item-legend-item">
<div class="item-legend-icon" style="background: ${t.color}20; border: 1px solid ${t.color};">
${t.icon}
</div>
<span>${t.type_name}</span>
</div>
`).join('');
}
// 편집 모드 토글
function toggleItemEditMode() {
isItemEditMode = !isItemEditMode;
document.getElementById('itemEditModeText').textContent = isItemEditMode ? '편집모드 종료' : '편집모드';
if (isItemEditMode) {
// 지도 클릭으로 물품 추가
const container = document.getElementById('itemsMapContainer');
container.style.cursor = 'crosshair';
container.onclick = (e) => {
if (e.target === container || e.target.tagName === 'IMG') {
const rect = container.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width * 100).toFixed(2);
const y = ((e.clientY - rect.top) / rect.height * 100).toFixed(2);
openItemModal(null, x, y);
}
};
} else {
const container = document.getElementById('itemsMapContainer');
container.style.cursor = 'default';
container.onclick = null;
}
}
// 물품 모달 열기
function openItemModal(item = null, x = null, y = null) {
const modal = document.getElementById('itemModal');
const title = document.getElementById('itemModalTitle');
const deleteBtn = document.getElementById('deleteItemBtn');
if (item) {
title.textContent = '물품 수정';
document.getElementById('itemId').value = item.item_id;
document.getElementById('itemType').value = item.item_type;
document.getElementById('itemName').value = item.item_name || '';
document.getElementById('itemQuantity').value = item.quantity || 1;
deleteBtn.style.display = 'inline-block';
} else {
title.textContent = '물품 추가';
document.getElementById('itemForm').reset();
document.getElementById('itemId').value = '';
document.getElementById('itemId').dataset.x = x;
document.getElementById('itemId').dataset.y = y;
deleteBtn.style.display = 'none';
}
modal.style.display = 'flex';
}
// 물품 모달 닫기
function closeItemModal() {
document.getElementById('itemModal').style.display = 'none';
}
// 물품 저장
async function saveItem() {
if (!selectedWorkplace) return;
const itemId = document.getElementById('itemId').value;
const data = {
item_type: document.getElementById('itemType').value,
item_name: document.getElementById('itemName').value,
quantity: parseInt(document.getElementById('itemQuantity').value) || 1,
patrol_session_id: currentSession?.session_id
};
// 새 물품일 경우 위치 추가
if (!itemId) {
data.x_percent = parseFloat(document.getElementById('itemId').dataset.x);
data.y_percent = parseFloat(document.getElementById('itemId').dataset.y);
data.width_percent = 5;
data.height_percent = 5;
}
try {
if (itemId) {
await axios.put(`/patrol/items/${itemId}`, data);
} else {
await axios.post(`/patrol/workplaces/${selectedWorkplace.workplace_id}/items`, data);
}
closeItemModal();
await loadWorkplaceItems(selectedWorkplace.workplace_id);
} catch (error) {
console.error('물품 저장 실패:', error);
alert('물품 저장에 실패했습니다.');
}
}
// 물품 삭제
async function deleteItem() {
const itemId = document.getElementById('itemId').value;
if (!itemId) return;
if (!confirm('이 물품을 삭제하시겠습니까?')) return;
try {
await axios.delete(`/patrol/items/${itemId}`);
closeItemModal();
await loadWorkplaceItems(selectedWorkplace.workplace_id);
} catch (error) {
console.error('물품 삭제 실패:', error);
alert('물품 삭제에 실패했습니다.');
}
}
// 유틸리티 함수
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('ko-KR', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' });
}
// ESC 키로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeItemModal();
}
});

View File

@@ -1738,8 +1738,8 @@ async function loadData() {
async function loadWorkers() {
try {
console.log('Workers API 호출 중... (통합 API 사용)');
// 모든 작업자 1000명까지 조회
const data = await window.apiCall(`${window.API}/workers?limit=1000`);
// 생산팀 소속 작업자만 조회
const data = await window.apiCall(`/workers?limit=1000&department_id=1`);
const allWorkers = Array.isArray(data) ? data : (data.data || data.workers || []);
// 작업 보고서에 표시할 작업자만 필터링
@@ -1760,7 +1760,7 @@ async function loadWorkers() {
async function loadProjects() {
try {
console.log('Projects API 호출 중... (활성 프로젝트만)');
const data = await window.apiCall(`${window.API}/projects/active/list`);
const data = await window.apiCall(`/projects/active/list`);
projects = Array.isArray(data) ? data : (data.data || data.projects || []);
console.log('✅ 활성 프로젝트 로드 성공:', projects.length);
} catch (error) {
@@ -1771,7 +1771,7 @@ async function loadProjects() {
async function loadWorkTypes() {
try {
const data = await window.apiCall(`${window.API}/daily-work-reports/work-types`);
const data = await window.apiCall(`/daily-work-reports/work-types`);
if (Array.isArray(data) && data.length > 0) {
workTypes = data;
console.log('✅ 작업 유형 API 사용 (통합 설정)');
@@ -1790,7 +1790,7 @@ async function loadWorkTypes() {
async function loadWorkStatusTypes() {
try {
const data = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`);
const data = await window.apiCall(`/daily-work-reports/work-status-types`);
if (Array.isArray(data) && data.length > 0) {
workStatusTypes = data;
console.log('✅ 업무 상태 유형 API 사용 (통합 설정)');
@@ -1809,7 +1809,7 @@ async function loadWorkStatusTypes() {
async function loadErrorTypes() {
// 레거시 에러 유형 로드 (호환성)
try {
const data = await window.apiCall(`${window.API}/daily-work-reports/error-types`);
const data = await window.apiCall(`/daily-work-reports/error-types`);
if (Array.isArray(data) && data.length > 0) {
errorTypes = data;
}
@@ -2268,7 +2268,7 @@ async function saveWorkReport() {
console.log('🔄 전송 데이터 JSON:', JSON.stringify(requestData, null, 2));
try {
const result = await window.apiCall(`${window.API}/daily-work-reports`, 'POST', requestData);
const result = await window.apiCall(`/daily-work-reports`, 'POST', requestData);
console.log('✅ 저장 성공:', result);
totalSaved++;
@@ -2370,7 +2370,7 @@ async function loadTodayWorkers() {
console.log(`🔒 본인 입력분만 조회 (통합 API): ${API}/daily-work-reports?${queryParams}`);
const rawData = await window.apiCall(`${window.API}/daily-work-reports?${queryParams}`);
const rawData = await window.apiCall(`/daily-work-reports?${queryParams}`);
console.log('📊 당일 작업 데이터 (통합 API):', rawData);
let data = [];
@@ -2505,7 +2505,7 @@ async function editWorkItem(workId) {
// 1. 기존 데이터 조회 (통합 API 사용)
showMessage('작업 정보를 불러오는 중... (통합 API)', 'loading');
const workData = await window.apiCall(`${window.API}/daily-work-reports/${workId}`);
const workData = await window.apiCall(`/daily-work-reports/${workId}`);
console.log('수정할 작업 데이터 (통합 API):', workData);
// 2. 수정 모달 표시
@@ -2644,7 +2644,7 @@ async function saveEditedWork() {
showMessage('작업을 수정하는 중... (통합 API)', 'loading');
const result = await window.apiCall(`${window.API}/daily-work-reports/${editingWorkId}`, {
const result = await window.apiCall(`/daily-work-reports/${editingWorkId}`, {
method: 'PUT',
body: JSON.stringify(updateData)
});
@@ -2673,7 +2673,7 @@ async function deleteWorkItem(workId) {
showMessage('작업을 삭제하는 중... (통합 API)', 'loading');
// 개별 항목 삭제 API 호출 (본인 작성분만 삭제 가능) - 통합 API 사용
const result = await window.apiCall(`${window.API}/daily-work-reports/my-entry/${workId}`, {
const result = await window.apiCall(`/daily-work-reports/my-entry/${workId}`, {
method: 'DELETE'
});

View File

@@ -0,0 +1,339 @@
// department-management.js
// 부서 관리 페이지 JavaScript
let departments = [];
let selectedDepartmentId = null;
let selectedWorkers = new Set();
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
await waitForApiConfig();
await loadDepartments();
});
// API 설정 로드 대기
async function waitForApiConfig() {
let retryCount = 0;
while (!window.apiCall && retryCount < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
retryCount++;
}
if (!window.apiCall) {
console.error('API 설정 로드 실패');
}
}
// 부서 목록 로드
async function loadDepartments() {
try {
const result = await window.apiCall('/departments');
if (result.success) {
departments = result.data;
renderDepartmentList();
updateMoveToDepartmentSelect();
}
} catch (error) {
console.error('부서 목록 로드 실패:', error);
}
}
// 부서 목록 렌더링
function renderDepartmentList() {
const container = document.getElementById('departmentList');
if (departments.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 2rem; color: #9ca3af;">
등록된 부서가 없습니다.<br>
<button class="btn btn-primary btn-sm" style="margin-top: 1rem;" onclick="openDepartmentModal()">
첫 부서 등록하기
</button>
</div>
`;
return;
}
container.innerHTML = departments.map(dept => `
<div class="department-item ${selectedDepartmentId === dept.department_id ? 'active' : ''}"
onclick="selectDepartment(${dept.department_id})">
<div class="department-info">
<span class="department-name">${dept.department_name}</span>
<span class="department-count">${dept.worker_count || 0}명</span>
</div>
<div class="department-actions" onclick="event.stopPropagation()">
<button class="btn-icon" onclick="editDepartment(${dept.department_id})" title="수정">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button class="btn-icon danger" onclick="deleteDepartment(${dept.department_id})" title="삭제">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</div>
`).join('');
}
// 부서 선택
async function selectDepartment(departmentId) {
selectedDepartmentId = departmentId;
selectedWorkers.clear();
updateBulkActions();
renderDepartmentList();
const dept = departments.find(d => d.department_id === departmentId);
document.getElementById('workerListTitle').textContent = `${dept.department_name} 작업자`;
document.getElementById('addWorkerBtn').style.display = 'inline-flex';
await loadWorkers(departmentId);
}
// 부서별 작업자 로드
async function loadWorkers(departmentId) {
try {
const result = await window.apiCall(`/departments/${departmentId}/workers`);
if (result.success) {
renderWorkerList(result.data);
}
} catch (error) {
console.error('작업자 목록 로드 실패:', error);
}
}
// 작업자 목록 렌더링
function renderWorkerList(workers) {
const container = document.getElementById('workerList');
if (workers.length === 0) {
container.innerHTML = `
<div style="text-align: center; padding: 2rem; color: #9ca3af;">
이 부서에 소속된 작업자가 없습니다.
</div>
`;
return;
}
container.innerHTML = workers.map(worker => `
<div class="worker-card ${selectedWorkers.has(worker.worker_id) ? 'selected' : ''}"
onclick="toggleWorkerSelection(${worker.worker_id})">
<div class="worker-info-row">
<input type="checkbox" ${selectedWorkers.has(worker.worker_id) ? 'checked' : ''}
onclick="event.stopPropagation(); toggleWorkerSelection(${worker.worker_id})">
<div class="worker-avatar">${worker.worker_name.charAt(0)}</div>
<div class="worker-details">
<span class="worker-name">${worker.worker_name}</span>
<span class="worker-job">${getJobTypeName(worker.job_type)}</span>
</div>
</div>
</div>
`).join('');
}
// 직책 한글 변환
function getJobTypeName(jobType) {
const names = {
leader: '그룹장',
worker: '작업자',
admin: '관리자'
};
return names[jobType] || jobType || '-';
}
// 작업자 선택 토글
function toggleWorkerSelection(workerId) {
if (selectedWorkers.has(workerId)) {
selectedWorkers.delete(workerId);
} else {
selectedWorkers.add(workerId);
}
updateBulkActions();
// 선택 상태 업데이트
const card = document.querySelector(`.worker-card[onclick*="${workerId}"]`);
if (card) {
card.classList.toggle('selected', selectedWorkers.has(workerId));
const checkbox = card.querySelector('input[type="checkbox"]');
if (checkbox) checkbox.checked = selectedWorkers.has(workerId);
}
}
// 일괄 작업 영역 업데이트
function updateBulkActions() {
const bulkActions = document.getElementById('bulkActions');
const selectedCount = document.getElementById('selectedCount');
if (selectedWorkers.size > 0) {
bulkActions.classList.add('visible');
selectedCount.textContent = selectedWorkers.size;
} else {
bulkActions.classList.remove('visible');
}
}
// 이동 대상 부서 선택 업데이트
function updateMoveToDepartmentSelect() {
const select = document.getElementById('moveToDepartment');
select.innerHTML = '<option value="">부서 이동...</option>' +
departments.map(d => `<option value="${d.department_id}">${d.department_name}</option>`).join('');
}
// 선택한 작업자 이동
async function moveSelectedWorkers() {
const targetDepartmentId = document.getElementById('moveToDepartment').value;
if (!targetDepartmentId) {
alert('이동할 부서를 선택하세요.');
return;
}
if (selectedWorkers.size === 0) {
alert('이동할 작업자를 선택하세요.');
return;
}
if (parseInt(targetDepartmentId) === selectedDepartmentId) {
alert('같은 부서로는 이동할 수 없습니다.');
return;
}
try {
const result = await window.apiCall('/departments/move-workers', 'POST', {
workerIds: Array.from(selectedWorkers),
departmentId: parseInt(targetDepartmentId)
});
if (result.success) {
alert(result.message);
selectedWorkers.clear();
updateBulkActions();
document.getElementById('moveToDepartment').value = '';
await loadDepartments();
await loadWorkers(selectedDepartmentId);
} else {
alert(result.error || '이동 실패');
}
} catch (error) {
console.error('작업자 이동 실패:', error);
alert('작업자 이동에 실패했습니다.');
}
}
// 부서 모달 열기
function openDepartmentModal(departmentId = null) {
const modal = document.getElementById('departmentModal');
const title = document.getElementById('departmentModalTitle');
const form = document.getElementById('departmentForm');
// 상위 부서 선택 옵션 업데이트
const parentSelect = document.getElementById('parentDepartment');
parentSelect.innerHTML = '<option value="">없음 (최상위 부서)</option>' +
departments
.filter(d => d.department_id !== departmentId)
.map(d => `<option value="${d.department_id}">${d.department_name}</option>`)
.join('');
if (departmentId) {
const dept = departments.find(d => d.department_id === departmentId);
title.textContent = '부서 수정';
document.getElementById('departmentId').value = dept.department_id;
document.getElementById('departmentName').value = dept.department_name;
document.getElementById('parentDepartment').value = dept.parent_id || '';
document.getElementById('departmentDescription').value = dept.description || '';
document.getElementById('displayOrder').value = dept.display_order || 0;
document.getElementById('isActive').checked = dept.is_active;
} else {
title.textContent = '새 부서 등록';
form.reset();
document.getElementById('departmentId').value = '';
document.getElementById('isActive').checked = true;
}
modal.classList.add('show');
}
// 부서 모달 닫기
function closeDepartmentModal() {
document.getElementById('departmentModal').classList.remove('show');
}
// 부서 저장
async function saveDepartment(event) {
event.preventDefault();
const departmentId = document.getElementById('departmentId').value;
const data = {
department_name: document.getElementById('departmentName').value,
parent_id: document.getElementById('parentDepartment').value || null,
description: document.getElementById('departmentDescription').value,
display_order: parseInt(document.getElementById('displayOrder').value) || 0,
is_active: document.getElementById('isActive').checked
};
try {
const url = departmentId ? `/departments/${departmentId}` : '/departments';
const method = departmentId ? 'PUT' : 'POST';
const result = await window.apiCall(url, method, data);
if (result.success) {
alert(result.message);
closeDepartmentModal();
await loadDepartments();
} else {
alert(result.error || '저장 실패');
}
} catch (error) {
console.error('부서 저장 실패:', error);
alert('부서 저장에 실패했습니다.');
}
}
// 부서 수정
function editDepartment(departmentId) {
openDepartmentModal(departmentId);
}
// 부서 삭제
async function deleteDepartment(departmentId) {
const dept = departments.find(d => d.department_id === departmentId);
if (!confirm(`"${dept.department_name}" 부서를 삭제하시겠습니까?\n\n소속 작업자가 있거나 하위 부서가 있으면 삭제할 수 없습니다.`)) {
return;
}
try {
const result = await window.apiCall(`/departments/${departmentId}`, 'DELETE');
if (result.success) {
alert('부서가 삭제되었습니다.');
if (selectedDepartmentId === departmentId) {
selectedDepartmentId = null;
document.getElementById('workerListTitle').textContent = '부서를 선택하세요';
document.getElementById('addWorkerBtn').style.display = 'none';
document.getElementById('workerList').innerHTML = `
<div style="text-align: center; padding: 2rem; color: #9ca3af;">
왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.
</div>
`;
}
await loadDepartments();
} else {
alert(result.error || '삭제 실패');
}
} catch (error) {
console.error('부서 삭제 실패:', error);
alert('부서 삭제에 실패했습니다.');
}
}
// 작업자 추가 모달 (작업자 관리 페이지로 이동)
function openAddWorkerModal() {
alert('작업자 관리 페이지에서 작업자를 등록한 후 이 페이지에서 부서를 배정하세요.');
// window.location.href = '/pages/admin/workers.html';
}

View File

@@ -2,13 +2,13 @@
// 설비 관리 페이지 JavaScript
let equipments = [];
let allEquipments = []; // 필터링 전 전체 데이터
let workplaces = [];
let equipmentTypes = [];
let currentEquipment = null;
// 페이지 로드 시 초기화
document.addEventListener('DOMContentLoaded', async () => {
// axios 설정이 완료될 때까지 대기
await waitForAxiosConfig();
await loadInitialData();
});
@@ -22,11 +22,10 @@ function waitForAxiosConfig() {
resolve();
}
}, 50);
// 최대 5초 대기
setTimeout(() => {
clearInterval(check);
if (!axios.defaults.baseURL) {
console.error('⚠️ Axios 설정 시간 초과');
console.error('Axios 설정 시간 초과');
}
resolve();
}, 5000);
@@ -52,7 +51,9 @@ async function loadEquipments() {
try {
const response = await axios.get('/equipments');
if (response.data.success) {
equipments = response.data.data;
allEquipments = response.data.data;
equipments = [...allEquipments];
renderStats();
renderEquipmentList();
}
} catch (error) {
@@ -71,7 +72,6 @@ async function loadWorkplaces() {
}
} catch (error) {
console.error('작업장 목록 로드 실패:', error);
throw error;
}
}
@@ -85,26 +85,69 @@ async function loadEquipmentTypes() {
}
} catch (error) {
console.error('설비 유형 로드 실패:', error);
// 실패해도 계속 진행 (유형이 없을 수 있음)
}
}
// 통계 렌더링
function renderStats() {
const container = document.getElementById('statsSection');
if (!container) return;
const totalCount = allEquipments.length;
const activeCount = allEquipments.filter(e => e.status === 'active').length;
const maintenanceCount = allEquipments.filter(e => e.status === 'maintenance').length;
const inactiveCount = allEquipments.filter(e => e.status === 'inactive').length;
const totalValue = allEquipments.reduce((sum, e) => sum + (Number(e.purchase_price) || 0), 0);
const avgValue = totalCount > 0 ? totalValue / totalCount : 0;
container.innerHTML = `
<div class="eq-stat-card highlight">
<div class="eq-stat-label">전체 설비</div>
<div class="eq-stat-value">${totalCount}대</div>
<div class="eq-stat-sub">총 자산가치 ${formatPriceShort(totalValue)}</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">활성</div>
<div class="eq-stat-value" style="color: #16a34a;">${activeCount}대</div>
<div class="eq-stat-sub">${totalCount > 0 ? Math.round(activeCount / totalCount * 100) : 0}%</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">정비중</div>
<div class="eq-stat-value" style="color: #d97706;">${maintenanceCount}대</div>
<div class="eq-stat-sub">${totalCount > 0 ? Math.round(maintenanceCount / totalCount * 100) : 0}%</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">비활성</div>
<div class="eq-stat-value" style="color: #dc2626;">${inactiveCount}대</div>
<div class="eq-stat-sub">${totalCount > 0 ? Math.round(inactiveCount / totalCount * 100) : 0}%</div>
</div>
<div class="eq-stat-card">
<div class="eq-stat-label">평균 구입가</div>
<div class="eq-stat-value">${formatPriceShort(avgValue)}</div>
<div class="eq-stat-sub">설비당 평균</div>
</div>
`;
}
// 작업장 필터 채우기
function populateWorkplaceFilters() {
const filterWorkplace = document.getElementById('filterWorkplace');
const modalWorkplace = document.getElementById('workplaceId');
const workplaceOptions = workplaces.map(w =>
`<option value="${w.workplace_id}">${w.category_name} - ${w.workplace_name}</option>`
`<option value="${w.workplace_id}">${w.category_name ? w.category_name + ' - ' : ''}${w.workplace_name}</option>`
).join('');
filterWorkplace.innerHTML = '<option value="">전체</option>' + workplaceOptions;
modalWorkplace.innerHTML = '<option value="">선택 안함</option>' + workplaceOptions;
if (filterWorkplace) filterWorkplace.innerHTML = '<option value="">전체</option>' + workplaceOptions;
if (modalWorkplace) modalWorkplace.innerHTML = '<option value="">선택 안함</option>' + workplaceOptions;
}
// 설비 유형 필터 채우기
function populateTypeFilter() {
const filterType = document.getElementById('filterType');
if (!filterType) return;
const typeOptions = equipmentTypes.map(type =>
`<option value="${type}">${type}</option>`
).join('');
@@ -117,7 +160,7 @@ function renderEquipmentList() {
if (equipments.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="eq-empty-state">
<p>등록된 설비가 없습니다.</p>
<button class="btn btn-primary" onclick="openEquipmentModal()">설비 추가하기</button>
</div>
@@ -126,47 +169,56 @@ function renderEquipmentList() {
}
const tableHTML = `
<table class="data-table">
<thead>
<tr>
<th>설비 코드</th>
<th>설비명</th>
<th>유형</th>
<th>작업장</th>
<th>제조사</th>
<th>모델명</th>
<th>상태</th>
<th>관리</th>
</tr>
</thead>
<tbody>
${equipments.map(equipment => `
<div class="eq-result-count">
<span>검색 결과 <strong>${equipments.length}건</strong></span>
</div>
<div class="eq-table-wrapper">
<table class="eq-table">
<thead>
<tr>
<td><strong>${equipment.equipment_code}</strong></td>
<td>${equipment.equipment_name}</td>
<td>${equipment.equipment_type || '-'}</td>
<td>${equipment.workplace_name || '-'}</td>
<td>${equipment.manufacturer || '-'}</td>
<td>${equipment.model_name || '-'}</td>
<td>
<span class="status-badge status-${equipment.status}">
${getStatusText(equipment.status)}
</span>
</td>
<td>
<div class="action-buttons">
<button class="btn-small btn-primary" onclick="editEquipment(${equipment.equipment_id})" title="수정">
✏️
</button>
<button class="btn-small btn-danger" onclick="deleteEquipment(${equipment.equipment_id})" title="삭제">
🗑️
</button>
</div>
</td>
<th>관리번호</th>
<th>설비명</th>
<th>모델명</th>
<th>규격</th>
<th>제조사</th>
<th>구입처</th>
<th style="text-align:right">구입가격</th>
<th>구입일자</th>
<th>상태</th>
<th style="width:80px">관리</th>
</tr>
`).join('')}
</tbody>
</table>
</thead>
<tbody>
${equipments.map(eq => `
<tr>
<td class="eq-col-code">${eq.equipment_code || '-'}</td>
<td class="eq-col-name" title="${eq.equipment_name || ''}">${eq.equipment_name || '-'}</td>
<td class="eq-col-model" title="${eq.model_name || ''}">${eq.model_name || '-'}</td>
<td class="eq-col-spec" title="${eq.specifications || ''}">${eq.specifications || '-'}</td>
<td>${eq.manufacturer || '-'}</td>
<td>${eq.supplier || '-'}</td>
<td class="eq-col-price">${eq.purchase_price ? formatPrice(eq.purchase_price) : '-'}</td>
<td class="eq-col-date">${eq.installation_date ? formatDate(eq.installation_date) : '-'}</td>
<td>
<span class="eq-status eq-status-${eq.status}">
${getStatusText(eq.status)}
</span>
</td>
<td>
<div class="eq-actions">
<button class="eq-btn-action eq-btn-edit" onclick="editEquipment(${eq.equipment_id})" title="수정">
✏️
</button>
<button class="eq-btn-action eq-btn-delete" onclick="deleteEquipment(${eq.equipment_id})" title="삭제">
🗑️
</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = tableHTML;
@@ -179,7 +231,32 @@ function getStatusText(status) {
'maintenance': '정비중',
'inactive': '비활성'
};
return statusMap[status] || status;
return statusMap[status] || status || '-';
}
// 가격 포맷팅 (전체)
function formatPrice(price) {
if (!price) return '-';
return Number(price).toLocaleString('ko-KR') + '원';
}
// 가격 포맷팅 (축약)
function formatPriceShort(price) {
if (!price) return '0원';
const num = Number(price);
if (num >= 100000000) {
return (num / 100000000).toFixed(1).replace(/\.0$/, '') + '억원';
} else if (num >= 10000) {
return (num / 10000).toFixed(0) + '만원';
}
return num.toLocaleString('ko-KR') + '원';
}
// 날짜 포맷팅
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('ko-KR', { year: 'numeric', month: '2-digit', day: '2-digit' });
}
// 필터링
@@ -189,38 +266,28 @@ function filterEquipments() {
const statusFilter = document.getElementById('filterStatus').value;
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
// API에서 필터링된 데이터를 가져오는 것이 더 효율적이지만,
// 클라이언트 측에서도 필터링을 적용합니다.
let filtered = [...equipments];
equipments = allEquipments.filter(e => {
if (workplaceFilter && e.workplace_id != workplaceFilter) return false;
if (typeFilter && e.equipment_type !== typeFilter) return false;
if (statusFilter && e.status !== statusFilter) return false;
if (searchTerm) {
const searchFields = [
e.equipment_name,
e.equipment_code,
e.manufacturer,
e.supplier,
e.model_name
].map(f => (f || '').toLowerCase());
if (!searchFields.some(f => f.includes(searchTerm))) return false;
}
return true;
});
if (workplaceFilter) {
filtered = filtered.filter(e => e.workplace_id == workplaceFilter);
}
if (typeFilter) {
filtered = filtered.filter(e => e.equipment_type === typeFilter);
}
if (statusFilter) {
filtered = filtered.filter(e => e.status === statusFilter);
}
if (searchTerm) {
filtered = filtered.filter(e =>
e.equipment_name.toLowerCase().includes(searchTerm) ||
e.equipment_code.toLowerCase().includes(searchTerm)
);
}
// 임시로 equipments를 필터링된 것으로 교체하고 렌더링
const originalEquipments = equipments;
equipments = filtered;
renderEquipmentList();
equipments = originalEquipments;
}
// 설비 추가 모달 열기
function openEquipmentModal(equipmentId = null) {
async function openEquipmentModal(equipmentId = null) {
currentEquipment = equipmentId;
const modal = document.getElementById('equipmentModal');
const modalTitle = document.getElementById('modalTitle');
@@ -234,30 +301,51 @@ function openEquipmentModal(equipmentId = null) {
loadEquipmentData(equipmentId);
} else {
modalTitle.textContent = '설비 추가';
// 새 설비일 경우 다음 관리번호 자동 생성
await loadNextEquipmentCode();
}
modal.style.display = 'flex';
}
// 다음 관리번호 로드
async function loadNextEquipmentCode() {
try {
console.log('📋 다음 관리번호 조회 중...');
const response = await axios.get('/equipments/next-code');
console.log('📋 다음 관리번호 응답:', response.data);
if (response.data.success) {
document.getElementById('equipmentCode').value = response.data.data.next_code;
console.log('✅ 다음 관리번호 설정:', response.data.data.next_code);
}
} catch (error) {
console.error('❌ 다음 관리번호 조회 실패:', error);
console.error('❌ 에러 상세:', error.response?.data || error.message);
// 오류 시 기본값으로 빈 값 유지 (사용자가 직접 입력)
}
}
// 설비 데이터 로드 (수정용)
async function loadEquipmentData(equipmentId) {
try {
const response = await axios.get(`/equipments/${equipmentId}`);
if (response.data.success) {
const equipment = response.data.data;
const eq = response.data.data;
document.getElementById('equipmentId').value = equipment.equipment_id;
document.getElementById('equipmentCode').value = equipment.equipment_code;
document.getElementById('equipmentName').value = equipment.equipment_name;
document.getElementById('equipmentType').value = equipment.equipment_type || '';
document.getElementById('workplaceId').value = equipment.workplace_id || '';
document.getElementById('manufacturer').value = equipment.manufacturer || '';
document.getElementById('modelName').value = equipment.model_name || '';
document.getElementById('serialNumber').value = equipment.serial_number || '';
document.getElementById('installationDate').value = equipment.installation_date ? equipment.installation_date.split('T')[0] : '';
document.getElementById('equipmentStatus').value = equipment.status || 'active';
document.getElementById('specifications').value = equipment.specifications || '';
document.getElementById('notes').value = equipment.notes || '';
document.getElementById('equipmentId').value = eq.equipment_id;
document.getElementById('equipmentCode').value = eq.equipment_code || '';
document.getElementById('equipmentName').value = eq.equipment_name || '';
document.getElementById('equipmentType').value = eq.equipment_type || '';
document.getElementById('workplaceId').value = eq.workplace_id || '';
document.getElementById('manufacturer').value = eq.manufacturer || '';
document.getElementById('supplier').value = eq.supplier || '';
document.getElementById('purchasePrice').value = eq.purchase_price || '';
document.getElementById('modelName').value = eq.model_name || '';
document.getElementById('serialNumber').value = eq.serial_number || '';
document.getElementById('installationDate').value = eq.installation_date ? eq.installation_date.split('T')[0] : '';
document.getElementById('equipmentStatus').value = eq.status || 'active';
document.getElementById('specifications').value = eq.specifications || '';
document.getElementById('notes').value = eq.notes || '';
}
} catch (error) {
console.error('설비 데이터 로드 실패:', error);
@@ -280,6 +368,8 @@ async function saveEquipment() {
equipment_type: document.getElementById('equipmentType').value.trim() || null,
workplace_id: document.getElementById('workplaceId').value || null,
manufacturer: document.getElementById('manufacturer').value.trim() || null,
supplier: document.getElementById('supplier').value.trim() || null,
purchase_price: document.getElementById('purchasePrice').value || null,
model_name: document.getElementById('modelName').value.trim() || null,
serial_number: document.getElementById('serialNumber').value.trim() || null,
installation_date: document.getElementById('installationDate').value || null,
@@ -288,9 +378,8 @@ async function saveEquipment() {
notes: document.getElementById('notes').value.trim() || null
};
// 유효성 검사
if (!equipmentData.equipment_code) {
alert('설비 코드를 입력해주세요.');
alert('관리번호를 입력해주세요.');
return;
}
@@ -302,10 +391,8 @@ async function saveEquipment() {
try {
let response;
if (equipmentId) {
// 수정
response = await axios.put(`/equipments/${equipmentId}`, equipmentData);
} else {
// 추가
response = await axios.post('/equipments', equipmentData);
}
@@ -313,11 +400,11 @@ async function saveEquipment() {
alert(equipmentId ? '설비가 수정되었습니다.' : '설비가 추가되었습니다.');
closeEquipmentModal();
await loadEquipments();
await loadEquipmentTypes(); // 새로운 유형이 추가될 수 있으므로
await loadEquipmentTypes();
}
} catch (error) {
console.error('설비 저장 실패:', error);
if (error.response && error.response.data && error.response.data.message) {
if (error.response?.data?.message) {
alert(error.response.data.message);
} else {
alert('설비 저장 중 오류가 발생했습니다.');
@@ -332,7 +419,7 @@ function editEquipment(equipmentId) {
// 설비 삭제
async function deleteEquipment(equipmentId) {
const equipment = equipments.find(e => e.equipment_id === equipmentId);
const equipment = allEquipments.find(e => e.equipment_id === equipmentId);
if (!equipment) return;
if (!confirm(`'${equipment.equipment_name}' 설비를 삭제하시겠습니까?`)) {
@@ -359,7 +446,7 @@ document.addEventListener('keydown', (e) => {
});
// 모달 외부 클릭 시 닫기
document.getElementById('equipmentModal').addEventListener('click', (e) => {
document.getElementById('equipmentModal')?.addEventListener('click', (e) => {
if (e.target.id === 'equipmentModal') {
closeEquipmentModal();
}

View File

@@ -5,12 +5,13 @@ import { config } from './config.js';
// 역할 이름을 한글로 변환하는 맵
const ROLE_NAMES = {
admin: '관리자',
system: '시스템 관리자',
leader: '그룹장',
user: '작업자',
support: '지원팀',
default: '사용자',
'system admin': '시스템 관리자',
'admin': '관리자',
'system': '시스템 관리자',
'leader': '그룹장',
'user': '작업자',
'support': '지원팀',
'default': '사용자',
};
/**

View File

@@ -12,7 +12,10 @@ async function processSidebarDom(doc) {
if (!currentUser) return;
const userRole = (currentUser.role || '').toLowerCase();
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system';
const accessLevel = (currentUser.access_level || '').toLowerCase();
// role 또는 access_level로 관리자 확인
const isAdmin = userRole === 'admin' || userRole === 'system admin' || userRole === 'system' ||
accessLevel === 'admin' || accessLevel === 'system';
// 1. 관리자 전용 메뉴 표시/숨김
if (isAdmin) {
@@ -164,6 +167,21 @@ function setupSidebarEvents() {
localStorage.setItem('sidebarExpanded', JSON.stringify(expanded));
});
});
// 링크 프리페치 - 마우스 올리면 미리 로드
const prefetchedUrls = new Set();
sidebar.querySelectorAll('a.nav-item').forEach(link => {
link.addEventListener('mouseenter', () => {
const href = link.getAttribute('href');
if (href && !prefetchedUrls.has(href) && !href.startsWith('#')) {
prefetchedUrls.add(href);
const prefetchLink = document.createElement('link');
prefetchLink.rel = 'prefetch';
prefetchLink.href = href;
document.head.appendChild(prefetchLink);
}
}, { once: true });
});
}
/**

View File

@@ -108,36 +108,12 @@ async function initializeDashboard() {
}
// ========== 사용자 정보 설정 ========== //
// navbar/sidebar는 app-init.js에서 공통 처리하므로 여기서는 currentUser만 설정
function setupUserInfo() {
const authData = getAuthData();
if (authData && authData.user) {
currentUser = authData.user;
// Navbar 컴포넌트가 사용자 정보를 처리하므로 여기서는 currentUser만 설정
// 사용자 이름 설정 (navbar 컴포넌트가 없는 경우에만)
if (elements.userName) {
elements.userName.textContent = currentUser.name || currentUser.username;
}
// 사용자 역할 설정 (navbar 컴포넌트가 없는 경우에만)
if (elements.userRole) {
const roleMap = {
'admin': '관리자',
'system': '시스템 관리자',
'group_leader': '그룹장',
'leader': '그룹장',
'user': '작업자'
};
elements.userRole.textContent = roleMap[currentUser.role] || '작업자';
}
// 아바타 초기값 설정 (navbar 컴포넌트가 없는 경우에만)
if (elements.userInitial) {
const initial = (currentUser.name || currentUser.username).charAt(0);
elements.userInitial.textContent = initial;
}
console.log('👤 사용자 정보 설정 완료:', currentUser.name);
console.log('👤 사용자 정보 로드 완료:', currentUser.name, currentUser.role);
}
}

View File

@@ -0,0 +1,119 @@
// /js/page-access-cache.js
// 페이지 권한 캐시 - 중복 API 호출 방지
const CACHE_KEY = 'userPageAccess';
const CACHE_DURATION = 10 * 60 * 1000; // 10분
// 진행 중인 API 호출 Promise (중복 방지)
let fetchPromise = null;
/**
* 페이지 접근 권한 데이터 가져오기 (캐시 우선)
* @param {object} currentUser - 현재 사용자 객체
* @returns {Promise<Array>} 접근 가능한 페이지 목록
*/
export async function getPageAccess(currentUser) {
if (!currentUser || !currentUser.user_id) {
return null;
}
// 1. 캐시 확인
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
try {
const cacheData = JSON.parse(cached);
if (Date.now() - cacheData.timestamp < CACHE_DURATION) {
return cacheData.pages;
}
} catch (e) {
localStorage.removeItem(CACHE_KEY);
}
}
// 2. 이미 API 호출 중이면 기존 Promise 반환
if (fetchPromise) {
return fetchPromise;
}
// 3. 새로운 API 호출
fetchPromise = (async () => {
try {
const response = await fetch(`${window.API_BASE_URL}/users/${currentUser.user_id}/page-access`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
});
if (!response.ok) {
console.error('페이지 권한 조회 실패:', response.status);
return null;
}
const data = await response.json();
const accessiblePages = data.data.pageAccess || [];
// 캐시 저장
localStorage.setItem(CACHE_KEY, JSON.stringify({
pages: accessiblePages,
timestamp: Date.now()
}));
return accessiblePages;
} catch (error) {
console.error('페이지 권한 조회 오류:', error);
return null;
} finally {
fetchPromise = null;
}
})();
return fetchPromise;
}
/**
* 특정 페이지에 대한 접근 권한 확인
* @param {string} pageKey - 페이지 키
* @param {object} currentUser - 현재 사용자 객체
* @returns {Promise<boolean>}
*/
export async function hasPageAccess(pageKey, currentUser) {
// Admin은 모든 페이지 접근 가능
if (currentUser.role === 'Admin' || currentUser.role === 'System Admin') {
return true;
}
// 대시보드, 프로필은 모든 사용자 접근 가능
if (pageKey === 'dashboard' || (pageKey && pageKey.startsWith('profile.'))) {
return true;
}
const pages = await getPageAccess(currentUser);
if (!pages) return false;
const pageAccess = pages.find(p => p.page_key === pageKey);
return pageAccess && pageAccess.can_access === 1;
}
/**
* 접근 가능한 페이지 키 목록 반환
* @param {object} currentUser
* @returns {Promise<string[]>}
*/
export async function getAccessiblePageKeys(currentUser) {
const pages = await getPageAccess(currentUser);
if (!pages) return [];
return pages
.filter(p => p.can_access === 1)
.map(p => p.page_key);
}
/**
* 캐시 초기화
*/
export function clearPageAccessCache() {
localStorage.removeItem(CACHE_KEY);
fetchPromise = null;
}

View File

@@ -1,338 +0,0 @@
// page-access-management.js - 페이지 권한 관리
// 전역 변수
let allUsers = [];
let allPages = [];
let currentUserId = null;
let currentFilter = 'all';
// DOM이 로드되면 초기화
document.addEventListener('DOMContentLoaded', async () => {
console.log('🚀 페이지 권한 관리 시스템 초기화');
// API 함수가 로드될 때까지 대기
let retryCount = 0;
while (!window.apiCall && retryCount < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
retryCount++;
}
if (!window.apiCall) {
showToast('시스템을 초기화할 수 없습니다. 페이지를 새로고침해주세요.', 'error');
return;
}
// 이벤트 리스너 설정
setupEventListeners();
// 데이터 로드
await loadInitialData();
});
// 이벤트 리스너 설정
function setupEventListeners() {
// 필터 버튼
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
currentFilter = e.target.dataset.filter;
filterUsers();
});
});
// 저장 버튼
const saveBtn = document.getElementById('savePageAccessBtn');
if (saveBtn) {
saveBtn.addEventListener('click', savePageAccess);
}
}
// 초기 데이터 로드
async function loadInitialData() {
try {
// 페이지 목록 로드
const pagesResponse = await window.apiCall('/pages');
if (pagesResponse && pagesResponse.success) {
allPages = pagesResponse.data;
console.log('✅ 페이지 목록 로드:', allPages.length + '개');
}
// 사용자 목록 로드 - 계정이 있는 작업자만
const workersResponse = await window.apiCall('/workers?limit=1000');
if (workersResponse) {
const workers = Array.isArray(workersResponse) ? workersResponse : (workersResponse.data || []);
// user_id가 있고 활성 상태인 작업자만 필터링
const usersWithAccounts = workers.filter(w => w.user_id && w.is_active);
// 각 사용자의 페이지 권한 수 조회
allUsers = await Promise.all(usersWithAccounts.map(async (worker) => {
try {
const accessResponse = await window.apiCall(`/users/${worker.user_id}/page-access`);
const grantedPagesCount = accessResponse && accessResponse.success
? accessResponse.data.pageAccess.filter(p => p.can_access).length
: 0;
return {
user_id: worker.user_id,
username: worker.username || 'N/A',
name: worker.name || worker.worker_name,
role_name: worker.role_name || 'User',
worker_name: worker.worker_name,
worker_id: worker.worker_id,
granted_pages_count: grantedPagesCount
};
} catch (error) {
console.error(`권한 조회 오류 (user_id: ${worker.user_id}):`, error);
return {
...worker,
granted_pages_count: 0
};
}
}));
console.log('✅ 사용자 목록 로드:', allUsers.length + '명');
displayUsers();
}
} catch (error) {
console.error('❌ 데이터 로드 오류:', error);
showToast('데이터를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
// 사용자 목록 표시
function displayUsers() {
const tbody = document.getElementById('usersTableBody');
const emptyState = document.getElementById('emptyState');
if (allUsers.length === 0) {
tbody.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
const filteredUsers = filterUsersByStatus();
if (filteredUsers.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; padding: 2rem; color: #6b7280;">
<p>필터 조건에 맞는 사용자가 없습니다.</p>
</td>
</tr>
`;
return;
}
tbody.innerHTML = filteredUsers.map(user => `
<tr>
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #3b82f6, #2563eb); color: white; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 0.875rem;">
${(user.name || user.username).charAt(0)}
</div>
<span style="font-weight: 600;">${user.name || user.username}</span>
</div>
</td>
<td>${user.username}</td>
<td>
<span class="badge ${user.role_name === 'Admin' ? 'badge-warning' : 'badge-info'}">
${user.role_name}
</span>
</td>
<td>${user.worker_name || '-'}</td>
<td>
<span style="font-weight: 600; color: ${user.granted_pages_count > 0 ? '#16a34a' : '#6b7280'};">
${user.granted_pages_count}
</span>
<span style="color: #9ca3af;"> / ${allPages.length}개</span>
</td>
<td>
<button class="btn btn-sm btn-primary" onclick="openPageAccessModal(${user.user_id})">
권한 설정
</button>
</td>
</tr>
`).join('');
}
// 사용자 필터링
function filterUsersByStatus() {
if (currentFilter === 'all') {
return allUsers;
} else if (currentFilter === 'with-access') {
return allUsers.filter(u => u.granted_pages_count > 0);
} else if (currentFilter === 'no-access') {
return allUsers.filter(u => u.granted_pages_count === 0);
}
return allUsers;
}
function filterUsers() {
displayUsers();
}
// 페이지 권한 설정 모달 열기
async function openPageAccessModal(userId) {
currentUserId = userId;
const user = allUsers.find(u => u.user_id === userId);
if (!user) {
showToast('사용자 정보를 찾을 수 없습니다.', 'error');
return;
}
// 모달 열기
document.getElementById('pageAccessModal').style.display = 'flex';
document.body.style.overflow = 'hidden';
// 사용자 정보 표시
document.getElementById('modalUserInitial').textContent = (user.name || user.username).charAt(0);
document.getElementById('modalUserName').textContent = user.name || user.username;
document.getElementById('modalUsername').textContent = user.username;
document.getElementById('modalWorkerName').textContent = user.worker_name || '작업자 정보 없음';
// 페이지 목록 로드
try {
const response = await window.apiCall(`/users/${userId}/page-access`);
if (response && response.success) {
const pageAccess = response.data.pageAccess;
renderPageList(pageAccess);
} else {
showToast('페이지 권한 정보를 불러올 수 없습니다.', 'error');
}
} catch (error) {
console.error('페이지 권한 조회 오류:', error);
showToast('페이지 권한 정보를 불러오는 중 오류가 발생했습니다.', 'error');
}
}
// 페이지 목록 렌더링
function renderPageList(pageAccess) {
const container = document.getElementById('pageListContainer');
// 카테고리별로 그룹화
const grouped = {};
pageAccess.forEach(page => {
const category = page.category || 'common';
if (!grouped[category]) {
grouped[category] = [];
}
grouped[category].push(page);
});
const categoryNames = {
'dashboard': '대시보드',
'management': '관리',
'common': '공통',
'admin': '관리자',
'work': '작업',
'guest': '게스트'
};
container.innerHTML = Object.keys(grouped).map(category => `
<div style="margin-bottom: 1rem;">
<div style="font-weight: 600; font-size: 0.875rem; color: #6b7280; padding: 0.5rem; background: #f9fafb; border-radius: 0.375rem; margin-bottom: 0.5rem;">
${categoryNames[category] || category}
</div>
${grouped[category].map(page => `
<div style="padding: 0.75rem; border-bottom: 1px solid #f3f4f6; display: flex; align-items: center; justify-content: space-between;">
<label style="display: flex; align-items: center; gap: 0.75rem; cursor: pointer; flex: 1;">
<input
type="checkbox"
class="page-checkbox"
data-page-id="${page.page_id}"
${page.can_access || page.is_default ? 'checked' : ''}
${page.is_default ? 'disabled' : ''}
style="width: 18px; height: 18px; cursor: pointer;"
/>
<div style="flex: 1;">
<div style="font-weight: 500; color: #111827;">${page.page_name}</div>
<div style="font-size: 0.75rem; color: #9ca3af;">${page.page_path}</div>
</div>
</label>
${page.is_default ? '<span style="font-size: 0.75rem; color: #16a34a; font-weight: 600;">기본 권한</span>' : ''}
</div>
`).join('')}
</div>
`).join('');
}
// 페이지 권한 저장
async function savePageAccess() {
if (!currentUserId) return;
const checkboxes = document.querySelectorAll('.page-checkbox:not([disabled]):checked');
const pageIds = Array.from(checkboxes).map(cb => parseInt(cb.dataset.pageId));
try {
document.getElementById('savePageAccessBtn').disabled = true;
document.getElementById('savePageAccessBtn').textContent = '저장 중...';
const response = await window.apiCall(
`/users/${currentUserId}/page-access`,
'POST',
{ pageIds, canAccess: true }
);
if (response && response.success) {
showToast('페이지 권한이 저장되었습니다.', 'success');
closePageAccessModal();
await loadInitialData(); // 목록 새로고침
} else {
throw new Error(response.error || '저장에 실패했습니다.');
}
} catch (error) {
console.error('페이지 권한 저장 오류:', error);
showToast('페이지 권한 저장 중 오류가 발생했습니다.', 'error');
} finally {
document.getElementById('savePageAccessBtn').disabled = false;
document.getElementById('savePageAccessBtn').textContent = '저장';
}
}
// 모달 닫기
function closePageAccessModal() {
document.getElementById('pageAccessModal').style.display = 'none';
document.body.style.overflow = 'auto';
currentUserId = null;
}
// 토스트 알림
function showToast(message, type = 'info', duration = 3000) {
const container = document.getElementById('toastContainer');
if (!container) 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>
`;
container.appendChild(toast);
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, duration);
}
// 전역 함수로 export
window.openPageAccessModal = openPageAccessModal;
window.closePageAccessModal = closePageAccessModal;

View File

@@ -49,39 +49,9 @@ function updateCurrentTime() {
}
}
// 사용자 정보 업데이트
// navbar/sidebar는 app-init.js에서 공통 처리
function updateUserInfo() {
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
const finalUserInfo = {
worker_name: userInfo.worker_name || authUser.username || authUser.worker_name,
job_type: userInfo.job_type || authUser.role || authUser.job_type,
username: authUser.username || userInfo.username
};
const userNameElement = document.getElementById('userName');
const userRoleElement = document.getElementById('userRole');
const userInitialElement = document.getElementById('userInitial');
if (userNameElement) {
userNameElement.textContent = finalUserInfo.worker_name || '사용자';
}
if (userRoleElement) {
const roleMap = {
'leader': '그룹장',
'worker': '작업자',
'admin': '관리자',
'system': '시스템 관리자'
};
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type || '작업자';
}
if (userInitialElement) {
const name = finalUserInfo.worker_name || '사용자';
userInitialElement.textContent = name.charAt(0);
}
// app-init.js가 navbar 사용자 정보를 처리
}
// 프로필 메뉴 설정

View File

@@ -118,8 +118,8 @@ async function loadInitialData() {
currentUser = userInfo;
console.log('👤 로그인 사용자:', currentUser, 'worker_id:', currentUser?.worker_id);
// 작업자 목록 로드
const workersResponse = await window.apiCall('/workers?limit=1000');
// 작업자 목록 로드 (생산팀 소속만)
const workersResponse = await window.apiCall('/workers?limit=1000&department_id=1');
if (workersResponse) {
allWorkers = Array.isArray(workersResponse) ? workersResponse : (workersResponse.data || []);
// 활성 상태인 작업자만 필터링
@@ -185,7 +185,7 @@ function switchTbmTab(tabName) {
currentTab = tabName;
// 탭 버튼 활성화 상태 변경
document.querySelectorAll('.tab-btn').forEach(btn => {
document.querySelectorAll('.tbm-tab-btn').forEach(btn => {
if (btn.dataset.tab === tabName) {
btn.classList.add('active');
} else {
@@ -194,7 +194,7 @@ function switchTbmTab(tabName) {
});
// 탭 컨텐츠 표시 변경
document.querySelectorAll('.code-tab-content').forEach(content => {
document.querySelectorAll('.tbm-tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}-tab`).classList.add('active');
@@ -409,20 +409,33 @@ function displayTbmGroupedByDate() {
const displayDate = `${parseInt(month)}${parseInt(day)}`;
return `
<div class="date-group">
<div class="date-group-header ${isToday ? 'today' : ''}">
<span class="date-group-date">${displayDate}</span>
<span class="date-group-day">${dayName}요일${isToday ? ' (오늘)' : ''}</span>
<span class="date-group-count">${sessions.length}</span>
<div class="tbm-date-group" data-date="${date}">
<div class="tbm-date-header ${isToday ? 'today' : ''}" onclick="toggleDateGroup('${date}')">
<span class="tbm-date-toggle">&#9660;</span>
<span class="tbm-date-title">${displayDate}</span>
<span class="tbm-date-day">${dayName}요일</span>
${isToday ? '<span class="tbm-today-badge">오늘</span>' : ''}
<span class="tbm-date-count">${sessions.length}건</span>
</div>
<div class="date-group-grid">
${sessions.map(session => createSessionCard(session)).join('')}
<div class="tbm-date-content">
<div class="tbm-date-grid">
${sessions.map(session => createSessionCard(session)).join('')}
</div>
</div>
</div>
`;
}).join('');
}
// 날짜 그룹 토글
function toggleDateGroup(date) {
const group = document.querySelector(`.tbm-date-group[data-date="${date}"]`);
if (group) {
group.classList.toggle('collapsed');
}
}
window.toggleDateGroup = toggleDateGroup;
/**
* 더 많은 날짜 로드
*/
@@ -474,73 +487,66 @@ function displayTbmSessions() {
// TBM 세션 카드 생성 (공통)
function createSessionCard(session) {
const statusBadge = {
'draft': '<span class="badge" style="background: #fef3c7; color: #92400e;">진행중</span>',
'completed': '<span class="badge" style="background: #dcfce7; color: #166534;">완료</span>',
'cancelled': '<span class="badge" style="background: #fee2e2; color: #991b1b;">취소</span>'
'draft': '<span class="tbm-card-status draft">진행중</span>',
'completed': '<span class="tbm-card-status completed">완료</span>',
'cancelled': '<span class="tbm-card-status cancelled">취소</span>'
}[session.status] || '';
// 작업 책임자 표시 (leader_name이 있으면 표시, 없으면 created_by_name 표시)
const leaderDisplay = session.leader_name
? `${session.leader_name} (${session.leader_job_type || '작업자'})`
: `${session.created_by_name || '작업 책임자'} (관리자)`;
const leaderName = session.leader_name || session.created_by_name || '작업 책임자';
const leaderRole = session.leader_name
? (session.leader_job_type || '작업자')
: '관리자';
return `
<div class="project-card" style="cursor: pointer;" onclick="viewTbmSession(${session.session_id})">
<div class="project-header">
<div>
<h3 class="project-name" style="font-size: 1rem; margin-bottom: 0.25rem;">
${leaderDisplay}
</h3>
<p style="font-size: 0.75rem; color: #6b7280; margin: 0;">
${formatDate(session.session_date)}
</p>
<div class="tbm-session-card" onclick="viewTbmSession(${session.session_id})">
<div class="tbm-card-header">
<div class="tbm-card-header-top">
<div>
<h3 class="tbm-card-leader">
${leaderName}
<span class="tbm-card-leader-role">${leaderRole}</span>
</h3>
</div>
${statusBadge}
</div>
${statusBadge}
</div>
<div class="project-info" style="margin-top: 1rem;">
<div class="info-item">
<span class="info-label">프로젝트</span>
<span class="info-value">${session.project_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">공정</span>
<span class="info-value">${session.work_type_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">작업</span>
<span class="info-value">${session.task_name || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">작업 장소</span>
<span class="info-value">${session.work_location || '-'}</span>
</div>
<div class="info-item">
<span class="info-label">팀원 수</span>
<span class="info-value">${session.team_member_count || 0}명</span>
</div>
<div class="info-item">
<span class="info-label">시작 시간</span>
<span class="info-value">${session.start_time || '-'}</span>
<div class="tbm-card-date">
<span>&#128197;</span>
${formatDate(session.session_date)} ${session.start_time ? '| ' + session.start_time : ''}
</div>
</div>
${session.work_description ? `
<div style="margin-top: 0.75rem; padding: 0.75rem; background: #f9fafb; border-radius: 0.375rem; font-size: 0.875rem; color: #374151;">
${session.work_description}
<div class="tbm-card-body">
<div class="tbm-card-info-grid">
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">프로젝트</span>
<span class="tbm-card-info-value">${session.project_name || '-'}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">공정</span>
<span class="tbm-card-info-value">${session.work_type_name || '-'}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">작업장</span>
<span class="tbm-card-info-value">${session.work_location || '-'}</span>
</div>
<div class="tbm-card-info-item">
<span class="tbm-card-info-label">팀원</span>
<span class="tbm-card-info-value">${session.team_member_count || 0}명</span>
</div>
</div>
</div>
${session.status === 'draft' ? `
<div class="tbm-card-footer">
<button class="tbm-btn tbm-btn-primary tbm-btn-sm" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})">
&#128101; 팀 구성
</button>
<button class="tbm-btn tbm-btn-secondary tbm-btn-sm" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})">
&#10003; 안전 체크
</button>
</div>
` : ''}
<div style="margin-top: 1rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
${session.status === 'draft' ? `
<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); openTeamCompositionModal(${session.session_id})" style="flex: 1; min-width: 100px;">
👥 팀 구성
</button>
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); openSafetyCheckModal(${session.session_id})" style="flex: 1; min-width: 100px;">
✅ 안전 체크
</button>
` : ''}
</div>
</div>
`;
}
@@ -550,23 +556,33 @@ function openNewTbmModal() {
currentSessionId = null;
workerTaskList = []; // 작업자 목록 초기화
document.getElementById('modalTitle').textContent = '새 TBM 시작';
document.getElementById('modalTitle').innerHTML = '<span>&#128221;</span> 새 TBM 시작';
document.getElementById('sessionId').value = '';
document.getElementById('tbmForm').reset();
const today = getTodayKST();
document.getElementById('sessionDate').value = today;
// 날짜 표시 업데이트
const [year, month, day] = today.split('-');
const dayNames = ['일', '월', '화', '수', '목', '금', '토'];
const dateObj = new Date(today);
const dayName = dayNames[dateObj.getDay()];
const sessionDateDisplay = document.getElementById('sessionDateDisplay');
if (sessionDateDisplay) {
sessionDateDisplay.textContent = `${year}${parseInt(month)}${parseInt(day)}일 (${dayName})`;
}
// 입력자 자동 설정 (readonly)
if (currentUser && currentUser.worker_id) {
const worker = allWorkers.find(w => w.worker_id === currentUser.worker_id);
if (worker) {
document.getElementById('leaderName').value = worker.worker_name;
document.getElementById('leaderName').textContent = worker.worker_name;
document.getElementById('leaderId').value = worker.worker_id;
}
} else if (currentUser && currentUser.name) {
// 관리자: 이름만 표시
document.getElementById('leaderName').value = currentUser.name;
document.getElementById('leaderName').textContent = currentUser.name;
document.getElementById('leaderId').value = '';
}

View File

@@ -54,39 +54,9 @@ function updateCurrentTime() {
}
}
// 사용자 정보 업데이트
// 사용자 정보 업데이트 - navbar/sidebar는 app-init.js에서 공통 처리
function updateUserInfo() {
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
const finalUserInfo = {
worker_name: userInfo.worker_name || authUser.username || authUser.worker_name,
job_type: userInfo.job_type || authUser.role || authUser.job_type,
username: authUser.username || userInfo.username
};
const userNameElement = document.getElementById('userName');
const userRoleElement = document.getElementById('userRole');
const userInitialElement = document.getElementById('userInitial');
if (userNameElement) {
userNameElement.textContent = finalUserInfo.worker_name || '사용자';
}
if (userRoleElement) {
const roleMap = {
'leader': '그룹장',
'worker': '작업자',
'admin': '관리자',
'system': '시스템 관리자'
};
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type || '작업자';
}
if (userInitialElement) {
const name = finalUserInfo.worker_name || '사용자';
userInitialElement.textContent = name.charAt(0);
}
// app-init.js가 navbar 사용자 정보를 처리하므로 여기서는 아무것도 하지 않음
}
// 프로필 메뉴 설정

View File

@@ -48,39 +48,9 @@ function updateCurrentTime() {
}
}
// 사용자 정보 업데이트
// navbar/sidebar는 app-init.js에서 공통 처리
function updateUserInfo() {
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
const finalUserInfo = {
worker_name: userInfo.worker_name || authUser.username || authUser.worker_name,
job_type: userInfo.job_type || authUser.role || authUser.job_type,
username: authUser.username || userInfo.username
};
const userNameElement = document.getElementById('userName');
const userRoleElement = document.getElementById('userRole');
const userInitialElement = document.getElementById('userInitial');
if (userNameElement) {
userNameElement.textContent = finalUserInfo.worker_name || '사용자';
}
if (userRoleElement) {
const roleMap = {
'leader': '그룹장',
'worker': '작업자',
'admin': '관리자',
'system': '시스템 관리자'
};
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type || '작업자';
}
if (userInitialElement) {
const name = finalUserInfo.worker_name || '사용자';
userInitialElement.textContent = name.charAt(0);
}
// app-init.js가 navbar 사용자 정보를 처리
}
// 프로필 메뉴 설정

View File

@@ -788,57 +788,9 @@ function updateCurrentTime() {
}
}
// 사용자 정보 업데이트 함수
// navbar/sidebar는 app-init.js에서 공통 처리
function updateUserInfo() {
// auth-check.js에서 사용하는 'user' 키와 기존 'userInfo' 키 모두 확인
let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
let authUser = JSON.parse(localStorage.getItem('user') || '{}');
console.log('👤 localStorage userInfo:', userInfo);
console.log('👤 localStorage user (auth):', authUser);
// 두 소스에서 사용자 정보 통합
const finalUserInfo = {
worker_name: userInfo.worker_name || authUser.username || authUser.worker_name,
job_type: userInfo.job_type || authUser.role || authUser.job_type,
username: authUser.username || userInfo.username
};
console.log('👤 최종 사용자 정보:', finalUserInfo);
const userNameElement = document.getElementById('userName');
const userRoleElement = document.getElementById('userRole');
const userInitialElement = document.getElementById('userInitial');
if (userNameElement) {
if (finalUserInfo.worker_name) {
userNameElement.textContent = finalUserInfo.worker_name;
} else {
userNameElement.textContent = '사용자';
}
}
if (userRoleElement) {
if (finalUserInfo.job_type) {
// role을 한글로 변환
const roleMap = {
'leader': '그룹장',
'worker': '작업자',
'admin': '관리자'
};
userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type;
} else {
userRoleElement.textContent = '작업자';
}
}
if (userInitialElement) {
if (finalUserInfo.worker_name) {
userInitialElement.textContent = finalUserInfo.worker_name.charAt(0);
} else {
userInitialElement.textContent = '사';
}
}
// app-init.js가 navbar 사용자 정보를 처리
}
// 페이지 초기화 개선

View File

@@ -179,7 +179,7 @@ async function loadInitialData() {
async function loadWorkerInfo() {
try {
const response = await window.apiCall(`${window.API}/workers/${currentWorkerId}`);
const response = await window.apiCall(`/workers/${currentWorkerId}`);
const worker = response.data || response;
document.getElementById('workerJob').textContent = worker.job_type || '작업자';
} catch (error) {
@@ -189,7 +189,7 @@ async function loadWorkerInfo() {
async function loadExistingWork() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`);
const response = await window.apiCall(`/daily-work-reports?date=${selectedDate}&worker_id=${currentWorkerId}`);
existingWork = Array.isArray(response) ? response : (response.data || []);
console.log(`✅ 기존 작업 ${existingWork.length}건 로드 완료`);
} catch (error) {
@@ -200,7 +200,7 @@ async function loadExistingWork() {
async function loadProjects() {
try {
const response = await window.apiCall(`${window.API}/projects/active/list`);
const response = await window.apiCall(`/projects/active/list`);
projects = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('프로젝트 로드 오류:', error);
@@ -210,7 +210,7 @@ async function loadProjects() {
async function loadWorkTypes() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports/work-types`);
const response = await window.apiCall(`/daily-work-reports/work-types`);
workTypes = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('작업 유형 로드 오류:', error);
@@ -220,7 +220,7 @@ async function loadWorkTypes() {
async function loadWorkStatusTypes() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports/work-status-types`);
const response = await window.apiCall(`/daily-work-reports/work-status-types`);
workStatusTypes = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('작업 상태 유형 로드 오류:', error);
@@ -230,7 +230,7 @@ async function loadWorkStatusTypes() {
async function loadErrorTypes() {
try {
const response = await window.apiCall(`${window.API}/daily-work-reports/error-types`);
const response = await window.apiCall(`/daily-work-reports/error-types`);
errorTypes = Array.isArray(response) ? response : (response.data || []);
} catch (error) {
console.error('에러 유형 로드 오류:', error);
@@ -407,7 +407,7 @@ async function saveNewWork() {
created_by: currentUser?.user_id || 1
};
const response = await window.apiCall(`${window.API}/daily-work-reports`, 'POST', workData);
const response = await window.apiCall(`/daily-work-reports`, 'POST', workData);
showMessage('작업이 성공적으로 저장되었습니다.', 'success');
@@ -437,7 +437,7 @@ async function deleteWork(workId) {
try {
showMessage('작업을 삭제하는 중...', 'loading');
await window.apiCall(`${window.API}/daily-work-reports/${workId}`, {
await window.apiCall(`/daily-work-reports/${workId}`, {
method: 'DELETE'
});
@@ -486,7 +486,7 @@ async function handleVacationProcess(vacationType) {
created_by: currentUser?.user_id || 1
};
const response = await window.apiCall(`${window.API}/daily-work-reports`, {
const response = await window.apiCall(`/daily-work-reports`, {
method: 'POST',
body: JSON.stringify(vacationWork)
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff