## 백엔드 보안 수정 - 하드코딩된 비밀번호 및 JWT 시크릿 폴백 제거 - SQL Injection 방지를 위한 화이트리스트 검증 추가 - 인증 미적용 API 라우트에 requireAuth 미들웨어 적용 - CSRF 보호 미들웨어 구현 (csrf.js) - 파일 업로드 보안 유틸리티 추가 (fileUploadSecurity.js) - 비밀번호 정책 검증 유틸리티 추가 (passwordValidator.js) ## 프론트엔드 XSS 방지 - api-base.js에 전역 escapeHtml() 함수 추가 - 17개 주요 JS 파일에 escapeHtml 적용: - tbm.js, daily-patrol.js, daily-work-report.js - task-management.js, workplace-status.js - equipment-detail.js, equipment-management.js - issue-detail.js, issue-report.js - vacation-common.js, worker-management.js - safety-report-list.js, nonconformity-list.js - project-management.js, workplace-management.js ## 정리 - 백업 폴더 및 빈 파일 삭제 - SECURITY_GUIDE.md 문서 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
642 lines
21 KiB
JavaScript
642 lines
21 KiB
JavaScript
// 작업자 관리 페이지 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;
|