feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
641
deploy/tkfb-package/web-ui/js/worker-management.js
Normal file
641
deploy/tkfb-package/web-ui/js/worker-management.js
Normal file
@@ -0,0 +1,641 @@
|
||||
// 작업자 관리 페이지 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 = `
|
||||
<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.worker_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.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;
|
||||
Reference in New Issue
Block a user