/** * 월별 작업자 상태 집계 컨트롤러 * * 월별 캘린더 및 작업자 상태 집계 API 엔드포인트 핸들러 * * @author TK-FB-Project * @since 2025-12-11 */ const MonthlyStatusModel = require('../models/monthlyStatusModel'); const { ValidationError, ForbiddenError, DatabaseError } = require('../utils/errors'); const { asyncHandler } = require('../middlewares/errorHandler'); const logger = require('../utils/logger'); /** * 월별 캘린더 데이터 조회 */ const getMonthlyCalendarData = asyncHandler(async (req, res) => { const { year, month } = req.query; if (!year || !month) { throw new ValidationError('연도(year)와 월(month)이 필요합니다', { required: ['year', 'month'], received: { year, month } }); } const yearNum = parseInt(year); const monthNum = parseInt(month); if (yearNum < 2020 || yearNum > 2030 || monthNum < 1 || monthNum > 12) { throw new ValidationError('유효하지 않은 연도 또는 월입니다', { received: { year: yearNum, month: monthNum } }); } logger.info('월별 캘린더 데이터 조회 요청', { year: yearNum, month: monthNum }); try { const summaryData = await MonthlyStatusModel.getMonthlySummary(yearNum, monthNum); // 날짜별 객체로 변환 const calendarData = {}; summaryData.forEach(day => { const dateKey = day.date.toISOString().split('T')[0]; calendarData[dateKey] = { totalWorkers: day.total_workers, workingWorkers: day.working_workers, hasIssues: day.has_issues, hasErrors: day.has_errors, hasOvertimeWarning: day.has_overtime_warning, incompleteWorkers: day.incomplete_workers, partialWorkers: day.partial_workers, errorWorkers: day.error_workers, overtimeWarningWorkers: day.overtime_warning_workers, totalHours: parseFloat(day.total_work_hours || 0), totalTasks: day.total_work_count, errorCount: day.total_error_count, lastUpdated: day.last_updated }; }); logger.info('월별 캘린더 데이터 조회 성공', { year: yearNum, month: monthNum, dayCount: Object.keys(calendarData).length }); res.json({ success: true, data: calendarData, message: `${year}년 ${month}월 캘린더 데이터를 성공적으로 조회했습니다` }); } catch (error) { logger.error('월별 캘린더 데이터 조회 실패', { year: yearNum, month: monthNum, error: error.message }); throw new DatabaseError('월별 캘린더 데이터 조회 중 오류가 발생했습니다'); } }); /** * 특정 날짜의 작업자별 상세 상태 조회 */ const getDailyWorkerDetails = asyncHandler(async (req, res) => { const { date } = req.query; if (!date) { throw new ValidationError('날짜(date)가 필요합니다', { required: ['date'], received: { date } }); } logger.info('일별 작업자 상세 조회 요청', { date }); try { const workerDetails = await MonthlyStatusModel.getDailyWorkerStatus(date); // 데이터 변환 const formattedData = workerDetails.map(worker => ({ userId: worker.user_id, workerName: worker.worker_name, jobType: worker.job_type, totalHours: parseFloat(worker.total_work_hours || 0), actualWorkHours: parseFloat(worker.actual_work_hours || 0), vacationHours: parseFloat(worker.vacation_hours || 0), totalWorkCount: worker.total_work_count, regularWorkCount: worker.regular_work_count, errorWorkCount: worker.error_work_count, status: worker.work_status, hasVacation: worker.has_vacation, hasError: worker.has_error, hasIssues: worker.has_issues, lastUpdated: worker.last_updated })); // 요약 정보 계산 const summary = { totalWorkers: formattedData.length, totalHours: formattedData.reduce((sum, w) => sum + w.totalHours, 0), totalTasks: formattedData.reduce((sum, w) => sum + w.totalWorkCount, 0), errorCount: formattedData.reduce((sum, w) => sum + w.errorWorkCount, 0) }; logger.info('일별 작업자 상세 조회 성공', { date, workerCount: formattedData.length, totalHours: summary.totalHours }); res.json({ success: true, data: { workers: formattedData, summary }, message: `${date} 작업자 상세 정보를 성공적으로 조회했습니다` }); } catch (error) { logger.error('일별 작업자 상세 조회 실패', { date, error: error.message }); throw new DatabaseError('일별 작업자 상세 조회 중 오류가 발생했습니다'); } }); /** * 월별 집계 재계산 (관리자용) */ const recalculateMonth = asyncHandler(async (req, res) => { const { year, month } = req.body; if (!year || !month) { throw new ValidationError('연도(year)와 월(month)이 필요합니다', { required: ['year', 'month'], received: { year, month } }); } // 관리자 권한 확인 if (req.user.role !== 'admin' && req.user.role !== 'system') { throw new ForbiddenError('관리자 권한이 필요합니다'); } logger.info('월별 집계 재계산 시작', { year, month, requestedBy: req.user.username }); try { const result = await MonthlyStatusModel.recalculateMonth(parseInt(year), parseInt(month)); logger.info('월별 집계 재계산 성공', { year, month, result }); res.json({ success: true, data: result, message: `${year}년 ${month}월 집계 재계산이 완료되었습니다` }); } catch (error) { logger.error('월별 집계 재계산 실패', { year, month, error: error.message }); throw new DatabaseError('월별 집계 재계산 중 오류가 발생했습니다'); } }); /** * 집계 테이블 상태 확인 (관리자용) */ const getStatusInfo = asyncHandler(async (req, res) => { // 관리자 권한 확인 if (req.user.role !== 'admin' && req.user.role !== 'system') { throw new ForbiddenError('관리자 권한이 필요합니다'); } logger.info('집계 테이블 상태 확인 요청', { requestedBy: req.user.username }); try { const statusInfo = await MonthlyStatusModel.getStatusInfo(); logger.info('집계 테이블 상태 확인 성공'); res.json({ success: true, data: statusInfo, message: '집계 테이블 상태 정보를 성공적으로 조회했습니다' }); } catch (error) { logger.error('집계 테이블 상태 확인 실패', { error: error.message }); throw new DatabaseError('집계 테이블 상태 확인 중 오류가 발생했습니다'); } }); module.exports = { getMonthlyCalendarData, getDailyWorkerDetails, recalculateMonth, getStatusInfo };