Files
tk-factory-services/system1-factory/web/js/worker-management.js
Hyungi Ahn abd7564e6b refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)
sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거,
department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러,
4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:13:10 +09:00

586 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 작업자 관리 페이지 JavaScript (부서 기반)
// 전역 변수
let departments = [];
let currentDepartmentId = null;
let allWorkers = [];
let filteredWorkers = [];
let currentEditingWorker = null;
// 페이지 초기화
document.addEventListener('DOMContentLoaded', async () => {
console.log('👥 작업자 관리 페이지 초기화 시작');
await waitForApi();
await loadDepartments();
});
// waitForApi → api-base.js 전역 사용
// ============================================
// 부서 관련 함수
// ============================================
// 부서 목록 로드
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 = `
<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 => {
const safeDeptId = parseInt(dept.department_id) || 0;
const safeDeptName = escapeHtml(dept.department_name || '-');
const workerCount = parseInt(dept.worker_count) || 0;
return `
<div class="department-item ${currentDepartmentId === dept.department_id ? 'active' : ''}"
onclick="selectDepartment(${safeDeptId})">
<div class="department-info">
<span class="department-name">${safeDeptName}</span>
<span class="department-count">${workerCount}명</span>
</div>
<div class="department-actions" onclick="event.stopPropagation()">
<button class="btn-icon" onclick="editDepartment(${safeDeptId})" title="수정">
<svg width="14" height="14" 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="confirmDeleteDepartment(${safeDeptId})" title="삭제">
<svg width="14" height="14" 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) {
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 = '<option value="">없음 (최상위 부서)</option>' +
departments
.filter(d => d.department_id !== parseInt(currentId))
.map(d => {
const safeDeptId = parseInt(d.department_id) || 0;
return `<option value="${safeDeptId}">${escapeHtml(d.department_name || '-')}</option>`;
})
.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 = `
<div class="empty-state">
<h4>부서를 선택해주세요</h4>
<p>왼쪽에서 부서를 선택하면 해당 부서의 작업자가 표시됩니다.</p>
</div>
`;
}
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 = `
<div class="empty-state">
<h4>작업자가 없습니다</h4>
<p>"+ 작업자 추가" 버튼을 눌러 작업자를 등록하세요.</p>
</div>
`;
return;
}
const tableHtml = `
<table class="workers-table">
<thead>
<tr>
<th>이름</th>
<th>직책</th>
<th>상태</th>
<th>입사일</th>
<th>계정</th>
<th style="width: 100px;">관리</th>
</tr>
</thead>
<tbody>
${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.user_id) || 0;
const safeWorkerName = escapeHtml(worker.worker_name || '');
const firstChar = safeWorkerName ? safeWorkerName.charAt(0) : '?';
return `
<tr style="${isResigned ? 'opacity: 0.6;' : ''}">
<td>
<div class="worker-name-cell">
<div class="worker-avatar">${firstChar}</div>
<span style="font-weight: 500;">${safeWorkerName}</span>
</div>
</td>
<td>${jobType}</td>
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
<td>${worker.join_date ? formatDate(worker.join_date) : '-'}</td>
<td>
<span class="account-badge ${hasAccount ? 'has-account' : 'no-account'}">
${hasAccount ? '🔐 연동' : '⚪ 없음'}
</span>
</td>
<td>
<div style="display: flex; gap: 0.25rem; justify-content: center;">
<button class="btn-icon" onclick="editWorker(${safeWorkerId})" title="수정"></button>
<button class="btn-icon danger" onclick="confirmDeleteWorker(${safeWorkerId})" title="삭제">🗑</button>
</div>
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
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.user_id === workerId);
if (!worker) {
showToast('작업자를 찾을 수 없습니다.', 'error');
return;
}
currentEditingWorker = worker;
title.textContent = '작업자 정보 수정';
deleteBtn.style.display = 'inline-flex';
document.getElementById('workerId').value = worker.user_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.user_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.user_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'
});
}
// showToast → api-base.js 전역 사용
// ============================================
// 전역 함수 노출
// ============================================
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;