// 작업자 관리 페이지 JavaScript (부서 기반)
// 전역 변수
let departments = [];
let currentDepartmentId = null;
let allWorkers = [];
let filteredWorkers = [];
let currentEditingWorker = null;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
console.log('👥 작업자 관리 페이지 초기화 시작');
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 response = await window.apiCall('/departments');
const result = response;
if (result && result.success) {
departments = result.data;
renderDepartmentList();
updateParentDepartmentSelect();
console.log('✅ 부서 목록 로드 완료:', departments.length + '개');
} else if (Array.isArray(result)) {
departments = result;
renderDepartmentList();
updateParentDepartmentSelect();
}
} catch (error) {
console.error('부서 목록 로드 실패:', error);
showToast('부서 목록을 불러오는데 실패했습니다.', 'error');
}
}
// 부서 목록 렌더링
function renderDepartmentList() {
const container = document.getElementById('departmentList');
if (!container) return;
if (departments.length === 0) {
container.innerHTML = `
등록된 부서가 없습니다.
첫 부서 등록하기
`;
return;
}
container.innerHTML = departments.map(dept => {
const safeDeptId = parseInt(dept.department_id) || 0;
const safeDeptName = escapeHtml(dept.department_name || '-');
const workerCount = parseInt(dept.worker_count) || 0;
return `
${safeDeptName}
${workerCount}명
`;
}).join('');
}
// 부서 선택
async function selectDepartment(departmentId) {
currentDepartmentId = departmentId;
renderDepartmentList();
const dept = departments.find(d => d.department_id === departmentId);
document.getElementById('workerListTitle').textContent = `${dept.department_name} 작업자`;
document.getElementById('addWorkerBtn').style.display = 'inline-flex';
document.getElementById('workerToolbar').style.display = 'flex';
await loadWorkersByDepartment(departmentId);
}
// 상위 부서 선택 옵션 업데이트
function updateParentDepartmentSelect() {
const select = document.getElementById('parentDepartment');
if (!select) return;
const currentId = document.getElementById('departmentId')?.value;
select.innerHTML = '없음 (최상위 부서) ' +
departments
.filter(d => d.department_id !== parseInt(currentId))
.map(d => {
const safeDeptId = parseInt(d.department_id) || 0;
return `${escapeHtml(d.department_name || '-')} `;
})
.join('');
}
// 부서 모달 열기
function openDepartmentModal(departmentId = null) {
const modal = document.getElementById('departmentModal');
const title = document.getElementById('departmentModalTitle');
const form = document.getElementById('departmentForm');
const deleteBtn = document.getElementById('deleteDeptBtn');
updateParentDepartmentSelect();
if (departmentId) {
const dept = departments.find(d => d.department_id === departmentId);
title.textContent = '부서 수정';
deleteBtn.style.display = 'inline-flex';
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('isActiveDept').checked = dept.is_active !== 0 && dept.is_active !== false;
} else {
title.textContent = '새 부서 등록';
deleteBtn.style.display = 'none';
form.reset();
document.getElementById('departmentId').value = '';
document.getElementById('isActiveDept').checked = true;
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
setTimeout(() => {
document.getElementById('departmentName').focus();
}, 100);
}
// 부서 모달 닫기
function closeDepartmentModal() {
const modal = document.getElementById('departmentModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
}
}
// 부서 저장
async function saveDepartment() {
const departmentId = document.getElementById('departmentId').value;
const data = {
department_name: document.getElementById('departmentName').value.trim(),
parent_id: document.getElementById('parentDepartment').value || null,
description: document.getElementById('departmentDescription').value.trim(),
display_order: parseInt(document.getElementById('displayOrder').value) || 0,
is_active: document.getElementById('isActiveDept').checked
};
if (!data.department_name) {
showToast('부서명은 필수 입력 항목입니다.', 'error');
return;
}
try {
const url = departmentId ? `/departments/${departmentId}` : '/departments';
const method = departmentId ? 'PUT' : 'POST';
const response = await window.apiCall(url, method, data);
if (response && response.success) {
showToast(response.message || '부서가 저장되었습니다.', 'success');
closeDepartmentModal();
await loadDepartments();
} else {
throw new Error(response?.error || '저장 실패');
}
} catch (error) {
console.error('부서 저장 실패:', error);
showToast('부서 저장에 실패했습니다.', 'error');
}
}
// 부서 수정
function editDepartment(departmentId) {
openDepartmentModal(departmentId);
}
// 부서 삭제 확인
function confirmDeleteDepartment(departmentId) {
const dept = departments.find(d => d.department_id === departmentId);
if (!dept) return;
const workerCount = dept.worker_count || 0;
let message = `"${dept.department_name}" 부서를 삭제하시겠습니까?`;
if (workerCount > 0) {
message += `\n\n⚠️ 이 부서에는 ${workerCount}명의 작업자가 있습니다.\n삭제하면 작업자들의 부서 정보가 제거됩니다.`;
}
if (confirm(message)) {
deleteDepartment(departmentId);
}
}
// 부서 삭제
async function deleteDepartment(departmentId = null) {
const id = departmentId || document.getElementById('departmentId').value;
if (!id) return;
try {
const response = await window.apiCall(`/departments/${id}`, 'DELETE');
if (response && response.success) {
showToast('부서가 삭제되었습니다.', 'success');
closeDepartmentModal();
if (currentDepartmentId === parseInt(id)) {
currentDepartmentId = null;
document.getElementById('workerListTitle').textContent = '부서를 선택하세요';
document.getElementById('addWorkerBtn').style.display = 'none';
document.getElementById('workerToolbar').style.display = 'none';
document.getElementById('workerList').innerHTML = `
부서를 선택해주세요
왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.
`;
}
await loadDepartments();
} else {
throw new Error(response?.error || '삭제 실패');
}
} catch (error) {
console.error('부서 삭제 실패:', error);
showToast(error.message || '부서 삭제에 실패했습니다.', 'error');
}
}
// ============================================
// 작업자 관련 함수
// ============================================
// 부서별 작업자 로드
async function loadWorkersByDepartment(departmentId) {
try {
const response = await window.apiCall(`/departments/${departmentId}/workers`);
if (response && response.success) {
allWorkers = response.data;
filteredWorkers = [...allWorkers];
renderWorkerList();
console.log(`✅ ${departmentId} 부서 작업자 로드: ${allWorkers.length}명`);
} else if (Array.isArray(response)) {
allWorkers = response;
filteredWorkers = [...allWorkers];
renderWorkerList();
}
} catch (error) {
console.error('작업자 목록 로드 실패:', error);
showToast('작업자 목록을 불러오는데 실패했습니다.', 'error');
}
}
// 작업자 목록 렌더링
function renderWorkerList() {
const container = document.getElementById('workerList');
if (!container) return;
if (filteredWorkers.length === 0) {
container.innerHTML = `
작업자가 없습니다
"+ 작업자 추가" 버튼을 눌러 작업자를 등록하세요.
`;
return;
}
const tableHtml = `
이름
직책
상태
입사일
계정
관리
${filteredWorkers.map(worker => {
const jobTypeMap = {
'worker': '작업자',
'leader': '그룹장',
'admin': '관리자'
};
const safeJobType = ['worker', 'leader', 'admin'].includes(worker.job_type) ? worker.job_type : '';
const jobType = jobTypeMap[safeJobType] || escapeHtml(worker.job_type || '-');
const isInactive = worker.status === 'inactive';
const isResigned = worker.employment_status === 'resigned';
const hasAccount = worker.user_id !== null && worker.user_id !== undefined;
let statusClass = 'active';
let statusText = '현장직';
if (isResigned) {
statusClass = 'resigned';
statusText = '퇴사';
} else if (isInactive) {
statusClass = 'inactive';
statusText = '사무직';
}
const safeWorkerId = parseInt(worker.worker_id) || 0;
const safeWorkerName = escapeHtml(worker.worker_name || '');
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
return `
${firstChar}
${safeWorkerName}
${jobType}
${statusText}
${worker.join_date ? formatDate(worker.join_date) : '-'}
${hasAccount ? '🔐 연동' : '⚪ 없음'}
✏️
🗑️
`;
}).join('')}
`;
container.innerHTML = tableHtml;
}
// 작업자 필터링
function filterWorkers() {
const searchTerm = document.getElementById('workerSearch')?.value.toLowerCase().trim() || '';
const statusValue = document.getElementById('statusFilter')?.value || '';
filteredWorkers = allWorkers.filter(worker => {
// 검색 필터
const matchesSearch = !searchTerm ||
worker.worker_name.toLowerCase().includes(searchTerm) ||
(worker.job_type && worker.job_type.toLowerCase().includes(searchTerm));
// 상태 필터
let matchesStatus = true;
if (statusValue === 'active') {
matchesStatus = worker.status !== 'inactive' && worker.employment_status !== 'resigned';
} else if (statusValue === 'inactive') {
matchesStatus = worker.status === 'inactive';
} else if (statusValue === 'resigned') {
matchesStatus = worker.employment_status === 'resigned';
}
return matchesSearch && matchesStatus;
});
renderWorkerList();
}
// 작업자 모달 열기
function openWorkerModal(workerId = null) {
const modal = document.getElementById('workerModal');
const title = document.getElementById('workerModalTitle');
const deleteBtn = document.getElementById('deleteWorkerBtn');
if (!currentDepartmentId) {
showToast('먼저 부서를 선택해주세요.', 'error');
return;
}
if (workerId) {
const worker = allWorkers.find(w => w.worker_id === workerId);
if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error');
return;
}
currentEditingWorker = worker;
title.textContent = '작업자 정보 수정';
deleteBtn.style.display = 'inline-flex';
document.getElementById('workerId').value = worker.worker_id;
document.getElementById('workerName').value = worker.worker_name || '';
document.getElementById('jobType').value = worker.job_type || 'worker';
document.getElementById('joinDate').value = worker.join_date ? worker.join_date.split('T')[0] : '';
document.getElementById('salary').value = worker.salary || '';
document.getElementById('annualLeave').value = worker.annual_leave || 0;
document.getElementById('isActiveWorker').checked = worker.status !== 'inactive';
document.getElementById('createAccount').checked = worker.user_id !== null && worker.user_id !== undefined;
document.getElementById('isResigned').checked = worker.employment_status === 'resigned';
} else {
currentEditingWorker = null;
title.textContent = '새 작업자 등록';
deleteBtn.style.display = 'none';
document.getElementById('workerForm').reset();
document.getElementById('workerId').value = '';
document.getElementById('isActiveWorker').checked = true;
document.getElementById('createAccount').checked = false;
document.getElementById('isResigned').checked = false;
}
modal.style.display = 'flex';
document.body.style.overflow = 'hidden';
setTimeout(() => {
document.getElementById('workerName').focus();
}, 100);
}
// 작업자 모달 닫기
function closeWorkerModal() {
const modal = document.getElementById('workerModal');
if (modal) {
modal.style.display = 'none';
document.body.style.overflow = '';
currentEditingWorker = null;
}
}
// 작업자 편집
function editWorker(workerId) {
openWorkerModal(workerId);
}
// 작업자 저장
async function saveWorker() {
const workerId = document.getElementById('workerId').value;
const workerData = {
worker_name: document.getElementById('workerName').value.trim(),
job_type: document.getElementById('jobType').value || 'worker',
join_date: document.getElementById('joinDate').value || null,
salary: document.getElementById('salary').value || null,
annual_leave: document.getElementById('annualLeave').value || 0,
status: document.getElementById('isActiveWorker').checked ? 'active' : 'inactive',
employment_status: document.getElementById('isResigned').checked ? 'resigned' : 'employed',
create_account: document.getElementById('createAccount').checked,
department_id: currentDepartmentId
};
if (!workerData.worker_name) {
showToast('작업자명은 필수 입력 항목입니다.', 'error');
return;
}
try {
let response;
if (workerId) {
response = await window.apiCall(`/workers/${workerId}`, 'PUT', workerData);
} else {
response = await window.apiCall('/workers', 'POST', workerData);
}
if (response && (response.success || response.data)) {
const action = workerId ? '수정' : '등록';
showToast(`작업자가 성공적으로 ${action}되었습니다.`, 'success');
closeWorkerModal();
await loadDepartments();
await loadWorkersByDepartment(currentDepartmentId);
} else {
throw new Error(response?.message || '저장에 실패했습니다.');
}
} catch (error) {
console.error('작업자 저장 오류:', error);
showToast(error.message || '작업자 저장 중 오류가 발생했습니다.', 'error');
}
}
// 작업자 삭제 확인
function confirmDeleteWorker(workerId) {
const worker = allWorkers.find(w => w.worker_id === workerId);
if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error');
return;
}
const confirmMessage = `"${worker.worker_name}" 작업자를 삭제하시겠습니까?\n\n⚠️ 관련된 모든 데이터(작업보고서, 이슈 등)가 함께 삭제됩니다.`;
if (confirm(confirmMessage)) {
deleteWorkerById(workerId);
}
}
// 작업자 삭제 (모달에서)
function deleteWorker() {
if (currentEditingWorker) {
confirmDeleteWorker(currentEditingWorker.worker_id);
}
}
// 작업자 삭제 실행
async function deleteWorkerById(workerId) {
try {
const response = await window.apiCall(`/workers/${workerId}`, 'DELETE');
if (response && (response.success || response.message)) {
showToast('작업자가 삭제되었습니다.', 'success');
closeWorkerModal();
await loadDepartments();
await loadWorkersByDepartment(currentDepartmentId);
} else {
throw new Error(response?.error || '삭제 실패');
}
} catch (error) {
console.error('작업자 삭제 오류:', error);
showToast(error.message || '작업자 삭제에 실패했습니다.', 'error');
}
}
// ============================================
// 유틸리티 함수
// ============================================
// 날짜 포맷팅
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
// 토스트 메시지 표시
function showToast(message, type = 'info') {
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
Object.assign(toast.style, {
position: 'fixed',
top: '20px',
right: '20px',
padding: '12px 24px',
borderRadius: '8px',
color: 'white',
fontWeight: '500',
zIndex: '10000',
transform: 'translateX(100%)',
transition: 'transform 0.3s ease'
});
const colors = {
success: '#10b981',
error: '#ef4444',
warning: '#f59e0b',
info: '#3b82f6'
};
toast.style.backgroundColor = colors[type] || colors.info;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transform = 'translateX(0)';
}, 100);
setTimeout(() => {
toast.style.transform = 'translateX(100%)';
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 300);
}, 3000);
}
// ============================================
// 전역 함수 노출
// ============================================
window.openDepartmentModal = openDepartmentModal;
window.closeDepartmentModal = closeDepartmentModal;
window.saveDepartment = saveDepartment;
window.editDepartment = editDepartment;
window.deleteDepartment = deleteDepartment;
window.confirmDeleteDepartment = confirmDeleteDepartment;
window.selectDepartment = selectDepartment;
window.openWorkerModal = openWorkerModal;
window.closeWorkerModal = closeWorkerModal;
window.saveWorker = saveWorker;
window.editWorker = editWorker;
window.deleteWorker = deleteWorker;
window.confirmDeleteWorker = confirmDeleteWorker;
window.filterWorkers = filterWorkers;