// 근태 검증 관리 시스템 - 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('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('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('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 = {
worker_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.worker_id === wr.worker_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 = `