Files
TK-FB-Project/web-ui/js/vacation-allocation.js
Hyungi Ahn b6485e3140 feat: 대시보드 작업장 현황 지도 구현
- 실시간 작업장 현황을 지도로 시각화
- 작업장 관리 페이지에서 정의한 구역 정보 활용
- TBM 작업자 및 방문자 현황 표시

주요 변경사항:
- dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거)
- workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현
- modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가

시각화 방식:
- 인원 없음: 회색 테두리 + 작업장 이름
- 내부 작업자: 파란색 영역 + 인원 수
- 외부 방문자: 보라색 영역 + 인원 수
- 둘 다: 초록색 영역 + 총 인원 수

기술 구현:
- Canvas API 기반 사각형 영역 렌더링
- map-regions API를 통한 데이터 일관성 보장
- 클릭 이벤트로 상세 정보 모달 표시

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-29 15:46:47 +09:00

865 lines
26 KiB
JavaScript

/**
* vacation-allocation.js
* 휴가 발생 입력 페이지 로직
*/
import { API_BASE_URL } from './api-config.js';
// 전역 변수
let workers = [];
let vacationTypes = [];
let currentWorkerBalances = [];
/**
* 페이지 초기화
*/
document.addEventListener('DOMContentLoaded', async () => {
// 관리자 권한 체크
const user = JSON.parse(localStorage.getItem('user') || '{}');
console.log('Current user:', user);
console.log('Role ID:', user.role_id, 'Role:', user.role);
// role이 'Admin'이거나 role_id가 1 또는 2인 경우 허용
const isAdmin = user.role === 'Admin' || [1, 2].includes(user.role_id);
if (!isAdmin) {
console.error('Access denied. User:', user);
alert('관리자만 접근할 수 있습니다');
window.location.href = '/pages/dashboard.html';
return;
}
await loadInitialData();
initializeYearSelectors();
initializeTabNavigation();
initializeEventListeners();
});
/**
* 초기 데이터 로드
*/
async function loadInitialData() {
await Promise.all([
loadWorkers(),
loadVacationTypes()
]);
}
/**
* 작업자 목록 로드
*/
async function loadWorkers() {
try {
const token = localStorage.getItem('token');
console.log('Loading workers... Token:', token ? 'exists' : 'missing');
const response = await fetch(`${API_BASE_URL}/api/workers`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
console.log('Workers API Response status:', response.status);
if (!response.ok) {
const errorData = await response.json();
console.error('Workers API Error:', errorData);
throw new Error(errorData.message || '작업자 목록 로드 실패');
}
const result = await response.json();
console.log('Workers data:', result);
workers = result.data || [];
if (workers.length === 0) {
console.warn('No workers found in database');
showToast('등록된 작업자가 없습니다', 'warning');
return;
}
// 개별 입력 탭 - 작업자 셀렉트 박스
const selectWorker = document.getElementById('individualWorker');
workers.forEach(worker => {
const option = document.createElement('option');
option.value = worker.worker_id;
option.textContent = `${worker.worker_name} (${worker.employment_status === 'employed' ? '재직' : '퇴사'})`;
selectWorker.appendChild(option);
});
console.log(`Loaded ${workers.length} workers successfully`);
} catch (error) {
console.error('작업자 로드 오류:', error);
showToast(`작업자 목록을 불러오는데 실패했습니다: ${error.message}`, 'error');
}
}
/**
* 휴가 유형 목록 로드
*/
async function loadVacationTypes() {
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-types`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) throw new Error('휴가 유형 로드 실패');
const result = await response.json();
vacationTypes = result.data || [];
// 개별 입력 탭 - 휴가 유형 셀렉트 박스
const selectType = document.getElementById('individualVacationType');
vacationTypes.forEach(type => {
const option = document.createElement('option');
option.value = type.id;
option.textContent = `${type.type_name} ${type.is_special ? '(특별)' : ''}`;
selectType.appendChild(option);
});
// 특별 휴가 관리 탭 테이블 로드
loadSpecialTypesTable();
} catch (error) {
console.error('휴가 유형 로드 오류:', error);
showToast('휴가 유형을 불러오는데 실패했습니다', 'error');
}
}
/**
* 연도 셀렉터 초기화
*/
function initializeYearSelectors() {
const currentYear = new Date().getFullYear();
const yearSelectors = ['individualYear', 'bulkYear'];
yearSelectors.forEach(selectorId => {
const select = document.getElementById(selectorId);
for (let year = currentYear - 1; year <= currentYear + 2; year++) {
const option = document.createElement('option');
option.value = year;
option.textContent = `${year}`;
if (year === currentYear) {
option.selected = true;
}
select.appendChild(option);
}
});
}
/**
* 탭 네비게이션 초기화
*/
function initializeTabNavigation() {
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const tabName = button.dataset.tab;
switchTab(tabName);
});
});
}
/**
* 탭 전환
*/
function switchTab(tabName) {
// 탭 버튼 활성화
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
// 탭 콘텐츠 표시
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`tab-${tabName}`).classList.add('active');
}
/**
* 이벤트 리스너 초기화
*/
function initializeEventListeners() {
// === 탭 1: 개별 입력 ===
document.getElementById('individualWorker').addEventListener('change', loadWorkerBalances);
document.getElementById('autoCalculateBtn').addEventListener('click', autoCalculateAnnualLeave);
document.getElementById('individualSubmitBtn').addEventListener('click', submitIndividualVacation);
document.getElementById('individualResetBtn').addEventListener('click', resetIndividualForm);
// === 탭 2: 일괄 입력 ===
document.getElementById('bulkPreviewBtn').addEventListener('click', previewBulkAllocation);
document.getElementById('bulkSubmitBtn').addEventListener('click', submitBulkAllocation);
// === 탭 3: 특별 휴가 관리 ===
document.getElementById('addSpecialTypeBtn').addEventListener('click', () => openVacationTypeModal());
// 모달 닫기
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', closeModals);
});
// 모달 폼 제출
document.getElementById('vacationTypeForm').addEventListener('submit', submitVacationType);
document.getElementById('editBalanceForm').addEventListener('submit', submitEditBalance);
}
// =============================================================================
// 탭 1: 개별 입력
// =============================================================================
/**
* 작업자의 기존 휴가 잔액 로드
*/
async function loadWorkerBalances() {
const workerId = document.getElementById('individualWorker').value;
const year = document.getElementById('individualYear').value;
if (!workerId) {
document.getElementById('individualTableBody').innerHTML = `
<tr><td colspan="8" class="loading-state"><p>작업자를 선택하세요</p></td></tr>
`;
return;
}
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/worker/${workerId}/year/${year}`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) throw new Error('휴가 잔액 로드 실패');
const result = await response.json();
currentWorkerBalances = result.data || [];
updateWorkerBalancesTable();
} catch (error) {
console.error('휴가 잔액 로드 오류:', error);
showToast('휴가 잔액을 불러오는데 실패했습니다', 'error');
}
}
/**
* 작업자 휴가 잔액 테이블 업데이트
*/
function updateWorkerBalancesTable() {
const tbody = document.getElementById('individualTableBody');
if (currentWorkerBalances.length === 0) {
tbody.innerHTML = `
<tr><td colspan="8" class="loading-state"><p>등록된 휴가가 없습니다</p></td></tr>
`;
return;
}
tbody.innerHTML = currentWorkerBalances.map(balance => `
<tr>
<td>${balance.worker_name || '-'}</td>
<td>${balance.year}</td>
<td>${balance.type_name} ${balance.is_special ? '<span class="badge badge-info">특별</span>' : ''}</td>
<td>${balance.total_days}일</td>
<td>${balance.used_days}일</td>
<td>${balance.remaining_days}일</td>
<td>${balance.notes || '-'}</td>
<td class="action-buttons">
<button class="btn btn-sm btn-secondary btn-icon" onclick="window.editBalance(${balance.id})">✏️</button>
<button class="btn btn-sm btn-danger btn-icon" onclick="window.deleteBalance(${balance.id})">🗑️</button>
</td>
</tr>
`).join('');
}
/**
* 자동 계산 (연차만 해당)
*/
async function autoCalculateAnnualLeave() {
const workerId = document.getElementById('individualWorker').value;
const year = document.getElementById('individualYear').value;
const typeId = document.getElementById('individualVacationType').value;
if (!workerId) {
showToast('작업자를 선택하세요', 'warning');
return;
}
// 선택한 휴가 유형이 ANNUAL인지 확인
const selectedType = vacationTypes.find(t => t.id == typeId);
if (!selectedType || selectedType.type_code !== 'ANNUAL') {
showToast('연차(ANNUAL) 유형만 자동 계산이 가능합니다', 'warning');
return;
}
// 작업자의 입사일 조회
const worker = workers.find(w => w.worker_id == workerId);
if (!worker || !worker.hire_date) {
showToast('작업자의 입사일 정보가 없습니다', 'error');
return;
}
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/auto-calculate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
worker_id: workerId,
hire_date: worker.hire_date,
year: year
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '자동 계산 실패');
}
// 계산 결과 표시
const resultDiv = document.getElementById('autoCalculateResult');
resultDiv.innerHTML = `
<strong>자동 계산 완료</strong><br>
입사일: ${worker.hire_date}<br>
계산된 연차: ${result.data.calculated_days}일<br>
아래 "총 부여 일수"에 자동으로 입력됩니다.
`;
resultDiv.style.display = 'block';
// 폼에 자동 입력
document.getElementById('individualTotalDays').value = result.data.calculated_days;
document.getElementById('individualNotes').value = `근속년수 기반 자동 계산 (입사일: ${worker.hire_date})`;
showToast(result.message, 'success');
// 기존 데이터 새로고침
await loadWorkerBalances();
} catch (error) {
console.error('자동 계산 오류:', error);
showToast(error.message, 'error');
}
}
/**
* 개별 휴가 제출
*/
async function submitIndividualVacation() {
const workerId = document.getElementById('individualWorker').value;
const year = document.getElementById('individualYear').value;
const typeId = document.getElementById('individualVacationType').value;
const totalDays = document.getElementById('individualTotalDays').value;
const usedDays = document.getElementById('individualUsedDays').value || 0;
const notes = document.getElementById('individualNotes').value;
if (!workerId || !year || !typeId || !totalDays) {
showToast('필수 항목을 모두 입력하세요', 'warning');
return;
}
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-balances`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
worker_id: workerId,
vacation_type_id: typeId,
year: year,
total_days: parseFloat(totalDays),
used_days: parseFloat(usedDays),
notes: notes
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '저장 실패');
}
showToast('휴가가 등록되었습니다', 'success');
resetIndividualForm();
await loadWorkerBalances();
} catch (error) {
console.error('휴가 등록 오류:', error);
showToast(error.message, 'error');
}
}
/**
* 개별 입력 폼 초기화
*/
function resetIndividualForm() {
document.getElementById('individualVacationType').value = '';
document.getElementById('individualTotalDays').value = '';
document.getElementById('individualUsedDays').value = '0';
document.getElementById('individualNotes').value = '';
document.getElementById('autoCalculateResult').style.display = 'none';
}
/**
* 휴가 수정 (전역 함수로 노출)
*/
window.editBalance = function(balanceId) {
const balance = currentWorkerBalances.find(b => b.id === balanceId);
if (!balance) return;
document.getElementById('editBalanceId').value = balance.id;
document.getElementById('editTotalDays').value = balance.total_days;
document.getElementById('editUsedDays').value = balance.used_days;
document.getElementById('editNotes').value = balance.notes || '';
document.getElementById('editBalanceModal').classList.add('active');
};
/**
* 휴가 삭제 (전역 함수로 노출)
*/
window.deleteBalance = async function(balanceId) {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/${balanceId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '삭제 실패');
}
showToast('삭제되었습니다', 'success');
await loadWorkerBalances();
} catch (error) {
console.error('삭제 오류:', error);
showToast(error.message, 'error');
}
};
/**
* 휴가 수정 제출
*/
async function submitEditBalance(e) {
e.preventDefault();
const balanceId = document.getElementById('editBalanceId').value;
const totalDays = document.getElementById('editTotalDays').value;
const usedDays = document.getElementById('editUsedDays').value;
const notes = document.getElementById('editNotes').value;
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/${balanceId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
total_days: parseFloat(totalDays),
used_days: parseFloat(usedDays),
notes: notes
})
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '수정 실패');
}
showToast('수정되었습니다', 'success');
closeModals();
await loadWorkerBalances();
} catch (error) {
console.error('수정 오류:', error);
showToast(error.message, 'error');
}
}
// =============================================================================
// 탭 2: 일괄 입력
// =============================================================================
let bulkPreviewData = [];
/**
* 일괄 할당 미리보기
*/
async function previewBulkAllocation() {
const year = document.getElementById('bulkYear').value;
const employmentStatus = document.getElementById('bulkEmploymentStatus').value;
// 필터링된 작업자 목록
let targetWorkers = workers;
if (employmentStatus === 'employed') {
targetWorkers = workers.filter(w => w.employment_status === 'employed');
}
// ANNUAL 유형 찾기
const annualType = vacationTypes.find(t => t.type_code === 'ANNUAL');
if (!annualType) {
showToast('ANNUAL 휴가 유형이 없습니다', 'error');
return;
}
// 미리보기 데이터 생성
bulkPreviewData = targetWorkers.map(worker => {
const hireDate = worker.hire_date;
if (!hireDate) {
return {
worker_id: worker.worker_id,
worker_name: worker.worker_name,
hire_date: '-',
years_worked: '-',
calculated_days: 0,
reason: '입사일 정보 없음',
status: 'error'
};
}
const calculatedDays = calculateAnnualLeaveDays(hireDate, year);
const yearsWorked = calculateYearsWorked(hireDate, year);
return {
worker_id: worker.worker_id,
worker_name: worker.worker_name,
hire_date: hireDate,
years_worked: yearsWorked,
calculated_days: calculatedDays,
reason: getCalculationReason(yearsWorked, calculatedDays),
status: 'ready'
};
});
updateBulkPreviewTable();
document.getElementById('bulkPreviewSection').style.display = 'block';
document.getElementById('bulkSubmitBtn').disabled = false;
}
/**
* 연차 일수 계산 (한국 근로기준법)
*/
function calculateAnnualLeaveDays(hireDate, targetYear) {
const hire = new Date(hireDate);
const targetDate = new Date(targetYear, 0, 1);
const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12
+ (targetDate.getMonth() - hire.getMonth());
// 1년 미만: 월 1일
if (monthsDiff < 12) {
return Math.floor(monthsDiff);
}
// 1년 이상: 15일 기본 + 2년마다 1일 추가 (최대 25일)
const yearsWorked = Math.floor(monthsDiff / 12);
const additionalDays = Math.floor((yearsWorked - 1) / 2);
return Math.min(15 + additionalDays, 25);
}
/**
* 근속년수 계산
*/
function calculateYearsWorked(hireDate, targetYear) {
const hire = new Date(hireDate);
const targetDate = new Date(targetYear, 0, 1);
const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12
+ (targetDate.getMonth() - hire.getMonth());
return (monthsDiff / 12).toFixed(1);
}
/**
* 계산 근거 생성
*/
function getCalculationReason(yearsWorked, days) {
const years = parseFloat(yearsWorked);
if (years < 1) {
return `입사 ${Math.floor(years * 12)}개월 (월 1일)`;
}
if (days === 25) {
return '최대 25일 (근속 3년 이상)';
}
return `근속 ${Math.floor(years)}년 (15일 + ${days - 15}일)`;
}
/**
* 일괄 미리보기 테이블 업데이트
*/
function updateBulkPreviewTable() {
const tbody = document.getElementById('bulkPreviewTableBody');
tbody.innerHTML = bulkPreviewData.map(item => {
const statusBadge = item.status === 'error'
? '<span class="badge badge-error">오류</span>'
: '<span class="badge badge-success">준비</span>';
return `
<tr>
<td>${item.worker_name}</td>
<td>${item.hire_date}</td>
<td>${item.years_worked}년</td>
<td>${item.calculated_days}일</td>
<td>${item.reason}</td>
<td>${statusBadge}</td>
</tr>
`;
}).join('');
}
/**
* 일괄 할당 제출
*/
async function submitBulkAllocation() {
const year = document.getElementById('bulkYear').value;
// 오류가 없는 항목만 필터링
const validItems = bulkPreviewData.filter(item => item.status !== 'error' && item.calculated_days > 0);
if (validItems.length === 0) {
showToast('생성할 항목이 없습니다', 'warning');
return;
}
if (!confirm(`${validItems.length}명의 연차를 생성하시겠습니까?`)) {
return;
}
// ANNUAL 유형 찾기
const annualType = vacationTypes.find(t => t.type_code === 'ANNUAL');
let successCount = 0;
let failCount = 0;
for (const item of validItems) {
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/auto-calculate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
worker_id: item.worker_id,
hire_date: item.hire_date,
year: year
})
});
if (response.ok) {
successCount++;
} else {
failCount++;
}
} catch (error) {
failCount++;
}
}
showToast(`완료: ${successCount}건 성공, ${failCount}건 실패`, successCount > 0 ? 'success' : 'error');
// 미리보기 초기화
document.getElementById('bulkPreviewSection').style.display = 'none';
document.getElementById('bulkSubmitBtn').disabled = true;
bulkPreviewData = [];
}
// =============================================================================
// 탭 3: 특별 휴가 관리
// =============================================================================
/**
* 특별 휴가 유형 테이블 로드
*/
function loadSpecialTypesTable() {
const tbody = document.getElementById('specialTypesTableBody');
if (vacationTypes.length === 0) {
tbody.innerHTML = `
<tr><td colspan="7" class="loading-state"><p>등록된 휴가 유형이 없습니다</p></td></tr>
`;
return;
}
tbody.innerHTML = vacationTypes.map(type => `
<tr>
<td>${type.type_name}</td>
<td>${type.type_code}</td>
<td>${type.priority}</td>
<td>${type.is_special ? '<span class="badge badge-info">특별</span>' : '-'}</td>
<td>${type.is_system ? '<span class="badge badge-warning">시스템</span>' : '-'}</td>
<td>${type.description || '-'}</td>
<td class="action-buttons">
<button class="btn btn-sm btn-secondary btn-icon" onclick="window.editVacationType(${type.id})" ${type.is_system ? 'disabled' : ''}>✏️</button>
<button class="btn btn-sm btn-danger btn-icon" onclick="window.deleteVacationType(${type.id})" ${type.is_system ? 'disabled' : ''}>🗑️</button>
</td>
</tr>
`).join('');
}
/**
* 휴가 유형 모달 열기
*/
function openVacationTypeModal(typeId = null) {
const modal = document.getElementById('vacationTypeModal');
const form = document.getElementById('vacationTypeForm');
form.reset();
if (typeId) {
const type = vacationTypes.find(t => t.id === typeId);
if (!type) return;
document.getElementById('modalTitle').textContent = '휴가 유형 수정';
document.getElementById('modalTypeId').value = type.id;
document.getElementById('modalTypeName').value = type.type_name;
document.getElementById('modalTypeCode').value = type.type_code;
document.getElementById('modalPriority').value = type.priority;
document.getElementById('modalIsSpecial').checked = type.is_special === 1;
document.getElementById('modalDescription').value = type.description || '';
} else {
document.getElementById('modalTitle').textContent = '휴가 유형 추가';
document.getElementById('modalTypeId').value = '';
}
modal.classList.add('active');
}
/**
* 휴가 유형 수정 (전역 함수)
*/
window.editVacationType = function(typeId) {
openVacationTypeModal(typeId);
};
/**
* 휴가 유형 삭제 (전역 함수)
*/
window.deleteVacationType = async function(typeId) {
if (!confirm('정말 삭제하시겠습니까?')) return;
try {
const token = localStorage.getItem('token');
const response = await fetch(`${API_BASE_URL}/api/vacation-types/${typeId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '삭제 실패');
}
showToast('삭제되었습니다', 'success');
await loadVacationTypes();
} catch (error) {
console.error('삭제 오류:', error);
showToast(error.message, 'error');
}
};
/**
* 휴가 유형 제출
*/
async function submitVacationType(e) {
e.preventDefault();
const typeId = document.getElementById('modalTypeId').value;
const typeName = document.getElementById('modalTypeName').value;
const typeCode = document.getElementById('modalTypeCode').value;
const priority = document.getElementById('modalPriority').value;
const isSpecial = document.getElementById('modalIsSpecial').checked ? 1 : 0;
const description = document.getElementById('modalDescription').value;
const data = {
type_name: typeName,
type_code: typeCode.toUpperCase(),
priority: parseInt(priority),
is_special: isSpecial,
description: description
};
try {
const token = localStorage.getItem('token');
const url = typeId
? `${API_BASE_URL}/api/vacation-types/${typeId}`
: `${API_BASE_URL}/api/vacation-types`;
const response = await fetch(url, {
method: typeId ? 'PUT' : 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.message || '저장 실패');
}
showToast(typeId ? '수정되었습니다' : '추가되었습니다', 'success');
closeModals();
await loadVacationTypes();
} catch (error) {
console.error('저장 오류:', error);
showToast(error.message, 'error');
}
}
// =============================================================================
// 공통 함수
// =============================================================================
/**
* 모달 닫기
*/
function closeModals() {
document.querySelectorAll('.modal').forEach(modal => {
modal.classList.remove('active');
});
}
/**
* 토스트 메시지
*/
function showToast(message, type = 'info') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
container.removeChild(toast);
}, 300);
}, 3000);
}