// 근태 검증 관리 시스템 - API 통합 개선판 // ======================================== // API 설정 및 인증 (통합 방식) // ======================================== import { API, getAuthHeaders, apiCall } from '/js/api-config.js'; // ======================================== // 전역 변수 및 설정 // ======================================== let currentDate = new Date(); let selectedDate = null; let selectedDateWorkers = []; let currentFilter = 'all'; let editingWorker = null; // Rate Limiting 설정 - 더욱 엄격하게 const RATE_LIMIT = { maxConcurrent: 1, // 동시 최대 1개 요청만! delayBetweenRequests: 2000, // 요청 간 2초 딜레이 retryDelay: 5000, // 429 에러 시 5초 후 재시도 maxRetries: 1 // 최대 1번만 재시도 }; // 캐시 및 상태 관리 let dateStatusCache = new Map(); let requestQueue = []; let activeRequests = 0; let isProcessingQueue = false; // ======================================== // 캐시 및 성능 관리 유틸리티 // ======================================== /** * 캐시 상태 확인 */ function getCacheStatus() { return { cachedDates: dateStatusCache.size, activeRequests: activeRequests, queuedRequests: requestQueue.length, isProcessingQueue: isProcessingQueue }; } /** * 캐시 클리어 */ function clearCache() { dateStatusCache.clear(); console.log('📦 캐시가 클리어되었습니다.'); } /** * 성능 상태 UI 업데이트 */ function updatePerformanceUI() { const status = getCacheStatus(); const performanceEl = document.getElementById('performanceStatus'); if (performanceEl) { document.getElementById('activeReq').textContent = status.activeRequests; document.getElementById('cacheCount').textContent = status.cachedDates; document.getElementById('queueCount').textContent = status.queuedRequests; // 개발 환경에서만 표시 if (window.location.hostname === 'localhost' || window.location.hostname.includes('dev')) { performanceEl.classList.remove('hidden'); } } } /** * 성능 모니터링 (디버그용) */ function logPerformanceStatus() { const status = getCacheStatus(); console.log('📊 성능 상태:', status); updatePerformanceUI(); } // 개발 모드에서 성능 모니터링 (2초마다) if (window.location.hostname === 'localhost' || window.location.hostname.includes('dev')) { setInterval(logPerformanceStatus, 2000); } // ======================================== // 유틸리티 함수들 // ======================================== /** * 한국 시간 기준 날짜 문자열 반환 */ function getKoreaDateString(date = new Date()) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } /** * 현재 로그인한 사용자 정보 가져오기 */ function getCurrentUser() { try { const token = localStorage.getItem('sso_token'); if (!token) return null; const payloadBase64 = token.split('.')[1]; if (payloadBase64) { const payload = JSON.parse(atob(payloadBase64)); return payload; } } catch (error) { console.log('토큰에서 사용자 정보 추출 실패:', error); } try { const userInfo = localStorage.getItem('sso_user') || localStorage.getItem('userInfo'); if (userInfo) { return JSON.parse(userInfo); } } catch (error) { console.log('localStorage에서 사용자 정보 가져오기 실패:', error); } return null; } // ======================================== // Rate Limiting 및 Queue 시스템 // ======================================== /** * Rate Limited API 호출을 위한 Queue 처리 */ async function processRequestQueue() { if (isProcessingQueue || requestQueue.length === 0) return; isProcessingQueue = true; while (requestQueue.length > 0 && activeRequests < RATE_LIMIT.maxConcurrent) { const { resolve, reject, url, options, retryCount } = requestQueue.shift(); activeRequests++; try { const result = await makeRateLimitedRequest(url, options, retryCount); resolve(result); } catch (error) { reject(error); } finally { activeRequests--; updatePerformanceUI(); // UI 업데이트 // 요청 간 딜레이 if (requestQueue.length > 0) { await new Promise(resolve => setTimeout(resolve, RATE_LIMIT.delayBetweenRequests)); } } } isProcessingQueue = false; // 큐에 남은 요청이 있으면 다시 처리 if (requestQueue.length > 0) { setTimeout(processRequestQueue, RATE_LIMIT.delayBetweenRequests); } } /** * Rate Limiting이 적용된 실제 API 호출 */ async function makeRateLimitedRequest(url, options = {}, retryCount = 0) { const defaultOptions = { headers: getAuthHeaders() }; const finalOptions = { ...defaultOptions, ...options, headers: { ...defaultOptions.headers, ...options.headers } }; try { const response = await fetch(url, finalOptions); if (response.status === 401) { showMessage('인증이 만료되었습니다. 다시 로그인해주세요.', 'error'); localStorage.removeItem('sso_token'); setTimeout(() => { window.location.href = '/'; }, 2000); return; } if (response.status === 429) { // Rate Limit 에러 처리 if (retryCount < RATE_LIMIT.maxRetries) { console.log(`Rate limit 도달, ${RATE_LIMIT.retryDelay}ms 후 재시도 (${retryCount + 1}/${RATE_LIMIT.maxRetries})`); await new Promise(resolve => setTimeout(resolve, RATE_LIMIT.retryDelay * (retryCount + 1))); return makeRateLimitedRequest(url, options, retryCount + 1); } else { throw new Error('Rate limit exceeded - 요청이 너무 많습니다. 잠시 후 다시 시도해주세요.'); } } if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.error || errorData.message || `HTTP ${response.status}`); } return await response.json(); } catch (error) { console.error('API 호출 오류:', error); throw error; } } /** * API 호출 헬퍼 (Queue 시스템 사용) - 통합 apiCall 대신 사용 */ async function queuedApiCall(url, options = {}) { return new Promise((resolve, reject) => { requestQueue.push({ resolve, reject, url, options, retryCount: 0 }); updatePerformanceUI(); // UI 업데이트 processRequestQueue(); }); } // ======================================== // 메시지 및 UI 헬퍼 함수들 // ======================================== /** * 메시지 표시 */ function showMessage(message, type = 'info') { const container = document.getElementById('message-container'); container.innerHTML = `
${message}
`; if (type === 'success') { setTimeout(() => { hideMessage(); }, 5000); } } function hideMessage() { document.getElementById('message-container').innerHTML = ''; } /** * 로딩 상태 표시 */ function showLoadingState() { const workersList = document.getElementById('workersList'); workersList.innerHTML = `
작업자 데이터를 불러오는 중...
`; } /** * 오류 상태 표시 */ function showErrorState(message = '데이터를 불러오는 중 오류가 발생했습니다.') { const workersList = document.getElementById('workersList'); workersList.innerHTML = `
⚠️

데이터 로딩 오류

${message}

`; } // ======================================== // API 호출 함수들 (통합 설정 사용) // ======================================== /** * 특정 날짜의 작업 보고서를 가져옵니다 */ async function fetchWorkReports(date) { try { return await queuedApiCall(`${API}/workreports/date/${date}`); } catch (error) { console.error('WorkReports API 호출 실패:', error); return []; } } /** * 특정 날짜의 일일 작업 보고서를 가져옵니다 */ async function fetchDailyWorkReports(date) { try { return await queuedApiCall(`${API}/daily-work-reports/date/${date}`); } catch (error) { console.error('DailyWorkReports API 호출 실패:', error); return []; } } /** * 특정 작업자의 근무시간을 수정합니다 */ async function updateWorkerHours(workerId, date, newHours, reason = '') { try { const data = { user_id: workerId, report_date: date, work_hours: parseFloat(newHours), modification_reason: reason, modified_by: getCurrentUser()?.user_id || getCurrentUser()?.id }; return await queuedApiCall(`${API}/daily-work-reports/update-hours`, { method: 'PUT', body: JSON.stringify(data) }); } catch (error) { console.error('작업자 시간 수정 실패:', error); throw error; } } /** * 특정 작업자의 작업 데이터를 삭제합니다 */ async function deleteWorkerReport(workerId, date) { try { return await queuedApiCall(`${API}/daily-work-reports/worker/${workerId}/date/${date}`, { method: 'DELETE' }); } catch (error) { console.error('작업자 데이터 삭제 실패:', error); throw error; } } // ======================================== // 계산 및 검증 함수들 // ======================================== /** * 상태에 따른 예상 근무시간을 계산합니다 */ function calculateExpectedHours(status, overtime_hours = 0) { const baseHours = { 'normal': 8, // 정상 출근 'half_day': 4, // 반차 'early_leave': 4, // 조퇴 'quarter_day': 2, // 1/4 휴가 'vacation': 0, // 휴가 'sick_leave': 0 // 병가 }; return (baseHours[status] || 8) + (overtime_hours || 0); } /** * 작업자의 검증 상태를 계산합니다 */ function getValidationStatus(worker) { if (!worker.hasWorkReport || !worker.hasDailyReport) return 'missing'; if (Math.abs(worker.difference) > 0) return 'needs-review'; return 'normal'; } /** * 특정 날짜의 전체 상태를 계산합니다 (순차 호출) */ async function calculateDateStatus(dateStr) { // 캐시 확인 if (dateStatusCache.has(dateStr)) { return dateStatusCache.get(dateStr); } try { console.log(`📊 ${dateStr} 상태 계산 시작 - 순차 호출`); // 1단계: WorkReports 먼저 가져오기 console.log(`📝 1단계: WorkReports 조회 중...`); const workReports = await fetchWorkReports(dateStr); // 2초 대기 (서버 부하 방지) console.log(`⏳ 2초 대기 중... (서버 부하 방지)`); await new Promise(resolve => setTimeout(resolve, 2000)); // 2단계: DailyWorkReports 가져오기 console.log(`📊 2단계: DailyWorkReports 조회 중...`); const dailyReports = await fetchDailyWorkReports(dateStr); let status; if (workReports.length === 0 && dailyReports.length === 0) { status = 'no-data'; } else if (workReports.length === 0 || dailyReports.length === 0) { status = 'missing'; } else { const hasDiscrepancy = workReports.some(wr => { const dr = dailyReports.find(d => d.user_id === wr.user_id); if (!dr) return true; const expected = calculateExpectedHours(wr.status, wr.overtime_hours); return Math.abs(dr.work_hours - expected) > 0; }); status = hasDiscrepancy ? 'needs-review' : 'normal'; } // 캐시에 저장 dateStatusCache.set(dateStr, status); console.log(`✅ ${dateStr} 상태 계산 완료: ${status}`); return status; } catch (error) { console.error('날짜 상태 계산 오류:', error); // 에러 시 캐시하지 않고 기본값 반환 return 'no-data'; } } // ======================================== // 캘린더 관련 함수들 // ======================================== /** * 현재 월의 캘린더 데이터를 생성합니다 (온디맨드 로딩) */ function generateCalendarStructure() { const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1); const startDate = new Date(firstDay); startDate.setDate(startDate.getDate() - firstDay.getDay()); const calendar = []; const current = new Date(startDate); for (let week = 0; week < 6; week++) { const weekDays = []; for (let day = 0; day < 7; day++) { const dateStr = getKoreaDateString(current); const isCurrentMonth = current.getMonth() === month; weekDays.push({ date: new Date(current), dateStr, isCurrentMonth, status: 'no-data' // 모든 날짜를 no-data로 시작 }); current.setDate(current.getDate() + 1); } calendar.push(weekDays); } return calendar; } /** * 캘린더를 화면에 렌더링합니다 (자동 로딩 없음) */ async function renderCalendar() { try { showMessage('📅 캘린더를 표시합니다. 날짜를 클릭하면 순차적으로 상태를 확인할 수 있습니다.', 'success'); // 캘린더 구조만 렌더링 (API 호출 없음) const calendar = generateCalendarStructure(); const calendarGrid = document.getElementById('calendarGrid'); calendarGrid.innerHTML = ''; // 캘린더 날짜들을 생성 (상태 로딩 없음) calendar.flat().forEach((dateInfo) => { const button = document.createElement('button'); button.className = ` calendar-day ${dateInfo.isCurrentMonth ? 'text-gray-900 hover-enabled' : 'text-gray-400'} ${selectedDate === dateInfo.dateStr ? 'selected' : ''} no-data `; button.innerHTML = `
${dateInfo.date.getDate()} ${dateInfo.isCurrentMonth ? `
` : '' }
`; // 현재 월의 날짜만 클릭 가능 if (dateInfo.isCurrentMonth) { button.addEventListener('click', () => handleDateClick(dateInfo)); button.title = `${dateInfo.dateStr} - 클릭하여 상태 확인`; } else { button.disabled = true; button.style.cursor = 'not-allowed'; } calendarGrid.appendChild(button); }); // 월/년 표시 업데이트 const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; document.getElementById('currentMonthYear').textContent = `${currentDate.getFullYear()}년 ${monthNames[currentDate.getMonth()]}`; // 월간 요약 정보 초기화 document.getElementById('normalCount').textContent = '?'; document.getElementById('reviewCount').textContent = '?'; document.getElementById('missingCount').textContent = '?'; hideMessage(); } catch (error) { console.error('캘린더 렌더링 오류:', error); showMessage('캘린더 로딩 중 오류가 발생했습니다.', 'error'); } } /** * 특정 날짜의 상태만 로드하고 업데이트합니다 (순차 호출) */ async function loadAndUpdateDateStatus(dateStr, buttonElement) { try { // 로딩 상태 표시 buttonElement.classList.add('loading-state'); const statusDot = buttonElement.querySelector('.status-dot'); if (statusDot) { statusDot.style.background = '#3b82f6'; statusDot.style.opacity = '1'; statusDot.classList.add('pulse'); } // 진행 상황 표시 buttonElement.title = '1단계: WorkReports 조회 중...'; const status = await calculateDateStatus(dateStr); // 버튼 스타일 업데이트 buttonElement.classList.remove('loading-state', 'no-data'); buttonElement.classList.add(status); buttonElement.title = `${dateStr} - 상태: ${status}`; // 상태 점 업데이트 if (statusDot && status !== 'no-data') { statusDot.classList.remove('pulse'); statusDot.className = `status-dot ${ status === 'needs-review' ? 'warning' : status === 'missing' ? 'error' : status === 'normal' ? 'normal' : '' }`; } console.log(`✅ ${dateStr} 상태 로드 완료: ${status}`); } catch (error) { console.error(`❌ ${dateStr} 상태 로드 실패:`, error); buttonElement.classList.remove('loading-state'); buttonElement.classList.add('error-state'); buttonElement.title = `${dateStr} - 로드 실패: ${error.message}`; const statusDot = buttonElement.querySelector('.status-dot'); if (statusDot) { statusDot.style.background = '#ef4444'; statusDot.classList.remove('pulse'); } } } // ======================================== // 작업자 데이터 관련 함수들 // ======================================== /** * 특정 날짜의 모든 작업자 데이터를 조합합니다 (순차 호출) */ async function getWorkersForDate(dateStr) { try { console.log(`👥 ${dateStr} 작업자 데이터 조합 시작 - 순차 호출`); // 1단계: WorkReports 먼저 가져오기 console.log(`📝 1단계: WorkReports 조회 중...`); const workReports = await fetchWorkReports(dateStr); // 2초 대기 (서버 부하 방지) console.log(`⏳ 2초 대기 중... (서버 부하 방지)`); await new Promise(resolve => setTimeout(resolve, 2000)); // 2단계: DailyWorkReports 가져오기 console.log(`📊 2단계: DailyWorkReports 조회 중...`); const dailyReports = await fetchDailyWorkReports(dateStr); const workerMap = new Map(); // WorkReports 데이터 추가 workReports.forEach(wr => { workerMap.set(wr.user_id, { user_id: wr.user_id, worker_name: wr.worker_name, overtime_hours: wr.overtime_hours || 0, status: wr.status || 'normal', expected_hours: calculateExpectedHours(wr.status || 'normal', wr.overtime_hours), reported_hours: null, hasWorkReport: true, hasDailyReport: false }); }); // DailyReports 데이터 추가 dailyReports.forEach(dr => { if (workerMap.has(dr.user_id)) { const worker = workerMap.get(dr.user_id); worker.reported_hours = dr.work_hours; worker.hasDailyReport = true; } else { workerMap.set(dr.user_id, { user_id: dr.user_id, worker_name: dr.worker_name, overtime_hours: 0, status: 'normal', expected_hours: 8, reported_hours: dr.work_hours, hasWorkReport: false, hasDailyReport: true }); } }); const result = Array.from(workerMap.values()).map(worker => ({ ...worker, difference: worker.reported_hours !== null ? worker.reported_hours - worker.expected_hours : -worker.expected_hours, validationStatus: getValidationStatus(worker) })); console.log(`✅ ${dateStr} 작업자 데이터 조합 완료: ${result.length}명`); return result; } catch (error) { console.error('데이터 조합 오류:', error); return []; } } // ======================================== // 이벤트 핸들러 함수들 // ======================================== /** * 캘린더 날짜 클릭 이벤트 핸들러 (순차 호출) */ async function handleDateClick(dateInfo) { if (!dateInfo.isCurrentMonth) return; selectedDate = dateInfo.dateStr; // 선택된 날짜 표시 업데이트 document.querySelectorAll('.calendar-day').forEach(btn => { btn.classList.remove('selected'); }); const clickedButton = event.target.closest('.calendar-day'); if (clickedButton) { clickedButton.classList.add('selected'); // 해당 날짜의 상태가 아직 로드되지 않았다면 로드 if (clickedButton.classList.contains('no-data')) { showMessage(`📊 ${dateInfo.dateStr} 날짜의 상태를 확인하는 중... (순차 호출로 약 5초 소요)`, 'loading'); await loadAndUpdateDateStatus(dateInfo.dateStr, clickedButton); } } // 작업자 데이터 로드 (순차 호출) showLoadingState(); showMessage(`👥 ${dateInfo.dateStr} 작업자 데이터를 순차적으로 로드 중... (약 5초 소요)`, 'loading'); try { const workers = await getWorkersForDate(dateInfo.dateStr); selectedDateWorkers = workers; renderWorkersList(workers); showMessage(`✅ ${dateInfo.dateStr} 날짜의 데이터를 성공적으로 로드했습니다! (${workers.length}명)`, 'success'); } catch (error) { console.error('날짜별 데이터 로딩 오류:', error); showErrorState('해당 날짜의 데이터를 불러올 수 없습니다. 잠시 후 다시 시도해주세요.'); showMessage(`❌ ${dateInfo.dateStr} 데이터 로드 실패: ${error.message}`, 'error'); } } /** * 작업자 근무시간 수정 모달 열기 */ function openEditModal(worker) { editingWorker = worker; document.getElementById('editWorkerName').value = worker.worker_name; document.getElementById('editWorkerStatus').value = getStatusText(worker.status); document.getElementById('editWorkHours').value = worker.reported_hours || 0; document.getElementById('editReason').value = ''; document.getElementById('editModal').classList.remove('hidden'); } /** * 수정 모달 닫기 */ function closeEditModal() { editingWorker = null; document.getElementById('editModal').classList.add('hidden'); } /** * 수정된 작업 저장 */ async function saveEditedWork() { if (!editingWorker) return; try { const newHours = document.getElementById('editWorkHours').value; const reason = document.getElementById('editReason').value; if (!newHours || isNaN(newHours)) { showMessage('올바른 시간을 입력해주세요.', 'error'); return; } showMessage('수정 중...', 'loading'); await updateWorkerHours(editingWorker.user_id, selectedDate, newHours, reason); showMessage('✅ 근무시간이 성공적으로 수정되었습니다!', 'success'); closeEditModal(); // 데이터 새로고침 const workers = await getWorkersForDate(selectedDate); selectedDateWorkers = workers; renderWorkersList(workers); renderCalendar(); } catch (error) { console.error('수정 실패:', error); showMessage('수정 중 오류가 발생했습니다: ' + error.message, 'error'); } } /** * 작업자 데이터 삭제 */ async function deleteWorker(worker) { if (!confirm(`정말로 ${worker.worker_name}의 ${selectedDate} 작업 데이터를 삭제하시겠습니까?\n삭제된 데이터는 복구할 수 없습니다.`)) { return; } try { showMessage('삭제 중...', 'loading'); await deleteWorkerReport(worker.user_id, selectedDate); showMessage('✅ 작업 데이터가 성공적으로 삭제되었습니다!', 'success'); // 데이터 새로고침 const workers = await getWorkersForDate(selectedDate); selectedDateWorkers = workers; renderWorkersList(workers); renderCalendar(); } catch (error) { console.error('삭제 실패:', error); showMessage('삭제 중 오류가 발생했습니다: ' + error.message, 'error'); } } // ======================================== // UI 렌더링 함수들 // ======================================== /** * 상태 텍스트 반환 */ function getStatusText(status) { const statusMap = { 'normal': '정상출근', 'half_day': '반차', 'vacation': '휴가', 'early_leave': '조퇴', 'quarter_day': '1/4 휴가', 'sick_leave': '병가' }; return statusMap[status] || status; } /** * 상태 아이콘 반환 */ function getStatusIcon(status) { switch (status) { case 'normal': return '✅'; case 'needs-review': return '⚠️'; case 'missing': return '❌'; default: return '❓'; } } /** * 날짜 포맷팅 */ function formatDate(dateStr) { const date = new Date(dateStr); const month = date.getMonth() + 1; const day = date.getDate(); const weekdays = ['일', '월', '화', '수', '목', '금', '토']; const weekday = weekdays[date.getDay()]; return `${month}월 ${day}일 (${weekday})`; } /** * 작업자 리스트를 화면에 렌더링합니다 */ function renderWorkersList(workers) { const workersList = document.getElementById('workersList'); if (!selectedDate || workers.length === 0) { workersList.innerHTML = `
🔄

날짜를 클릭해주세요

캘린더에서 날짜를 클릭하면 해당 날짜의 작업자 검증 내역을 확인할 수 있습니다.
순차 호출 방식으로 안정적이지만 약 5초의 로딩 시간이 있습니다.

`; return; } // 필터링 적용 const filteredWorkers = workers.filter(worker => { if (currentFilter === 'all') return true; if (currentFilter === 'needsReview') return worker.validationStatus === 'needs-review'; if (currentFilter === 'normal') return worker.validationStatus === 'normal'; if (currentFilter === 'missing') return worker.validationStatus === 'missing'; return true; }); // HTML 생성 workersList.innerHTML = `

📋 작업자 검증 현황

${formatDate(selectedDate)}

${filteredWorkers.map(worker => `
${worker.worker_name.charAt(0)}
${worker.worker_name}
작업자 ID: ${worker.user_id}
${getStatusIcon(worker.validationStatus)}
그룹장 입력 ${worker.reported_hours !== null ? `${worker.reported_hours}시간` : '미입력'}
시스템 계산 ${worker.expected_hours}시간
근무 상태 ${getStatusText(worker.status)} ${worker.overtime_hours > 0 ? ` + 연장 ${worker.overtime_hours}시간` : ''}
${worker.difference !== 0 ? `
시간 차이 ${worker.difference > 0 ? '+' : ''}${worker.difference}시간
` : ''}
${worker.hasDailyReport ? ` ` : ''}
`).join('')}
${filteredWorkers.length === 0 ? `
🔍

해당 조건의 작업자가 없습니다

다른 필터를 선택하거나 전체 보기를 확인해주세요.
또는 해당 날짜에 등록된 작업자가 없을 수 있습니다.

` : ''}
`; // 필터 이벤트 리스너 추가 const filterSelect = document.getElementById('workerFilter'); filterSelect.value = currentFilter; filterSelect.addEventListener('change', (e) => { currentFilter = e.target.value; renderWorkersList(selectedDateWorkers); }); } // ======================================== // 초기화 및 이벤트 리스너 등록 // ======================================== /** * 페이지 로드 시 초기화 함수 */ async function init() { try { // 인증 확인 (api-config.js의 ensureAuthenticated 대신 직접 확인) const token = localStorage.getItem('sso_token'); if (!token || token === 'undefined') { showMessage('로그인이 필요합니다.', 'error'); localStorage.removeItem('sso_token'); setTimeout(() => { window.location.href = '/'; }, 2000); return; } // 월 이동 버튼 이벤트 리스너 document.getElementById('prevMonth').addEventListener('click', async () => { currentDate.setMonth(currentDate.getMonth() - 1); selectedDate = null; dateStatusCache.clear(); // 캐시 클리어 clearCache(); // 추가 캐시 클리어 await renderCalendar(); renderWorkersList([]); showMessage('📅 이전 달로 이동했습니다. 날짜를 클릭하여 순차적으로 데이터를 확인하세요.', 'success'); }); document.getElementById('nextMonth').addEventListener('click', async () => { currentDate.setMonth(currentDate.getMonth() + 1); selectedDate = null; dateStatusCache.clear(); // 캐시 클리어 clearCache(); // 추가 캐시 클리어 await renderCalendar(); renderWorkersList([]); showMessage('📅 다음 달로 이동했습니다. 날짜를 클릭하여 순차적으로 데이터를 확인하세요.', 'success'); }); // 모달 이벤트 리스너 document.getElementById('editModal').addEventListener('click', (e) => { if (e.target.id === 'editModal') { closeEditModal(); } }); // 초기 렌더링 await renderCalendar(); // 순차 호출 안내 메시지 showMessage('🔄 API 통합 적용 완료! 순차 호출 시스템으로 안정성이 개선되었습니다.', 'success'); // 전역 함수로 등록 window.openEditModal = openEditModal; window.closeEditModal = closeEditModal; window.saveEditedWork = saveEditedWork; window.deleteWorker = deleteWorker; console.log('✅ 근태 검증 관리 시스템 초기화 완료 (API 통합)'); console.log(`🔗 API 경로: ${API}`); console.log(`📊 설정: 동시 최대 ${RATE_LIMIT.maxConcurrent}개 요청, ${RATE_LIMIT.delayBetweenRequests}ms 딜레이`); console.log('🔄 API 호출 방식: 통합 설정 + 순차 호출'); console.log('🚫 429 에러 방지: 각 날짜당 최소 5초 간격'); } catch (error) { console.error('초기화 오류:', error); showMessage('시스템 초기화 중 오류가 발생했습니다.', 'error'); } } // 페이지 로드 시 초기화 document.addEventListener('DOMContentLoaded', init);