/** * 일일 작업 보고서 컨트롤러 * * 작업 보고서 API 엔드포인트 핸들러 * * @author TK-FB-Project * @since 2025-12-11 */ const dailyWorkReportModel = require('../models/dailyWorkReportModel'); const dailyWorkReportService = require('../services/dailyWorkReportService'); const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors'); const { asyncHandler } = require('../middlewares/errorHandler'); const logger = require('../utils/logger'); /** * 작업보고서 생성 (V2 - Service Layer 사용) */ const createDailyWorkReport = asyncHandler(async (req, res) => { const reportData = { ...req.body, created_by: req.user?.user_id || req.user?.id, created_by_name: req.user?.name || req.user?.username || '알 수 없는 사용자' }; const result = await dailyWorkReportService.createDailyWorkReportService(reportData); res.status(201).json({ success: true, data: result, message: '작업보고서가 성공적으로 생성되었습니다' }); }); /** * 기여자별 요약 조회 */ const getContributorsSummary = asyncHandler(async (req, res) => { const { date, user_id } = req.query; if (!date || !user_id) { return res.status(400).json({ error: 'date와 user_id가 필요합니다.' }); } const data = await dailyWorkReportModel.getContributorsByDate(date, user_id); const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0); const result = { date, user_id, contributors: data, total_contributors: data.length, grand_total_hours: totalHours }; res.success(result, '기여자별 요약 조회 성공'); }); /** * 개인 누적 현황 조회 */ const getMyAccumulatedData = async (req, res) => { const { date, user_id } = req.query; const created_by = req.user?.user_id || req.user?.id; if (!date || !user_id) { return res.status(400).json({ error: 'date와 user_id가 필요합니다.', example: 'date=2024-06-16&user_id=1' }); } if (!created_by) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } try { const data = await dailyWorkReportModel.getMyAccumulatedHours(date, user_id, created_by); res.json({ date, user_id, created_by, my_data: data, timestamp: new Date().toISOString() }); } catch (err) { logger.error('개인 누적 현황 조회 오류:', err); res.status(500).json({ error: '개인 누적 현황 조회 중 오류가 발생했습니다.', details: err.message }); } }; /** * 개별 항목 삭제 (본인 작성분만) */ const removeMyEntry = async (req, res) => { const { id } = req.params; const deleted_by = req.user?.user_id || req.user?.id; if (!deleted_by) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } try { const result = await dailyWorkReportModel.removeSpecificEntry(id, deleted_by); res.json({ message: '항목이 성공적으로 삭제되었습니다.', id: id, deleted_by, timestamp: new Date().toISOString(), ...result }); } catch (err) { logger.error('개별 항목 삭제 오류:', err); res.status(500).json({ error: '항목 삭제 중 오류가 발생했습니다.', details: err.message }); } }; /** * 작업보고서 조회 (V2 - Service Layer 사용) */ const getDailyWorkReports = async (req, res) => { try { const userInfo = { user_id: req.user?.user_id || req.user?.id, role: req.user?.role || 'user' }; if (!userInfo.user_id) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } const reports = await dailyWorkReportService.getDailyWorkReportsService(req.query, userInfo); res.json(reports); } catch (error) { logger.error('작업보고서 조회 컨트롤러 오류:', error.message); res.status(400).json({ success: false, error: '작업보고서 조회에 실패했습니다.', details: error.message }); } }; /** * 날짜별 작업보고서 조회 (경로 파라미터 - 권한별 전체 조회 지원) */ const getDailyWorkReportsByDate = async (req, res) => { const { date } = req.params; const current_user_id = req.user?.user_id || req.user?.id; if (!current_user_id) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } try { const data = await dailyWorkReportModel.getByDate(date); // 임시로 모든 사용자에게 전체 조회 허용 res.json(data); } catch (err) { logger.error('날짜별 작업보고서 조회 오류:', err); res.status(500).json({ error: '작업보고서 조회 중 오류가 발생했습니다.', details: err.message }); } }; /** * 작업보고서 검색 (페이지네이션 포함) */ const searchWorkReports = async (req, res) => { const { start_date, end_date, user_id, project_id, work_status_id, page = 1, limit = 20 } = req.query; const created_by = req.user?.user_id || req.user?.id; if (!start_date || !end_date) { return res.status(400).json({ error: 'start_date와 end_date가 필요합니다.', example: 'start_date=2024-01-01&end_date=2024-01-31', optional: ['user_id', 'project_id', 'work_status_id', 'page', 'limit'] }); } if (!created_by) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } const searchParams = { start_date, end_date, user_id: user_id ? parseInt(user_id) : null, project_id: project_id ? parseInt(project_id) : null, work_status_id: work_status_id ? parseInt(work_status_id) : null, created_by, page: parseInt(page), limit: parseInt(limit) }; try { const data = await dailyWorkReportModel.searchWithDetails(searchParams); res.json(data); } catch (err) { logger.error('작업보고서 검색 오류:', err); res.status(500).json({ error: '작업보고서 검색 중 오류가 발생했습니다.', details: err.message }); } }; /** * 통계 조회 (V2 - Service Layer 사용) */ const getWorkReportStats = async (req, res) => { try { const statsData = await dailyWorkReportService.getStatisticsService(req.query); res.json(statsData); } catch (error) { logger.error('통계 조회 컨트롤러 오류:', error.message); res.status(400).json({ success: false, error: '통계 조회에 실패했습니다.', details: error.message }); } }; /** * 일일 근무 요약 조회 (V2 - Service Layer 사용) */ const getDailySummary = async (req, res) => { try { const summaryData = await dailyWorkReportService.getSummaryService(req.query); res.json(summaryData); } catch (error) { logger.error('일일 요약 조회 컨트롤러 오류:', error.message); res.status(400).json({ success: false, error: '일일 요약 조회에 실패했습니다.', details: error.message }); } }; /** * 월간 요약 조회 */ const getMonthlySummary = async (req, res) => { const { year, month } = req.query; if (!year || !month) { return res.status(400).json({ error: 'year와 month가 필요합니다.', example: 'year=2024&month=01', note: 'month는 01, 02, ..., 12 형식으로 입력하세요.' }); } try { const data = await dailyWorkReportModel.getMonthlySummary(year, month); res.json({ year: parseInt(year), month: parseInt(month), summary: data, total_entries: data.length, timestamp: new Date().toISOString() }); } catch (err) { logger.error('월간 요약 조회 오류:', err); res.status(500).json({ error: '월간 요약 조회 중 오류가 발생했습니다.', details: err.message }); } }; /** * 작업보고서 수정 (V2 - Service Layer 사용) */ const updateWorkReport = async (req, res) => { try { const { id: reportId } = req.params; const updateData = req.body; const userInfo = { user_id: req.user?.user_id || req.user?.id, role: req.user?.role || 'user' }; if (!userInfo.user_id) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } const result = await dailyWorkReportService.updateWorkReportService(reportId, updateData, userInfo); res.json({ success: true, timestamp: new Date().toISOString(), ...result }); } catch (error) { logger.error(`작업보고서 수정 컨트롤러 오류 (id: ${req.params.id}):`, error.message); const statusCode = error.statusCode || 400; res.status(statusCode).json({ success: false, error: '작업보고서 수정에 실패했습니다.', details: error.message }); } }; /** * 특정 작업보고서 삭제 (V2 - Service Layer 사용) * 권한: 그룹장(group_leader), 시스템(system), 관리자(admin)만 가능 */ const removeDailyWorkReport = async (req, res) => { try { const { id: reportId } = req.params; const userInfo = { user_id: req.user?.user_id || req.user?.id, access_level: req.user?.access_level || req.user?.role, }; if (!userInfo.user_id) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } // 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능 const allowedRoles = ['admin', 'system', 'group_leader']; if (!allowedRoles.includes(userInfo.access_level)) { return res.status(403).json({ error: '작업보고서 삭제 권한이 없습니다.', details: '그룹장 이상의 권한이 필요합니다.' }); } const result = await dailyWorkReportService.removeDailyWorkReportService(reportId, userInfo); res.json({ success: true, timestamp: new Date().toISOString(), ...result }); } catch (error) { logger.error(`작업보고서 삭제 컨트롤러 오류 (id: ${req.params.id}):`, error.message); const statusCode = error.statusCode || 400; res.status(statusCode).json({ success: false, error: '작업보고서 삭제에 실패했습니다.', details: error.message }); } }; /** * 작업자의 특정 날짜 전체 삭제 */ const removeDailyWorkReportByDateAndWorker = async (req, res) => { const { date, user_id } = req.params; const deleted_by = req.user?.user_id || req.user?.id; const access_level = req.user?.access_level || req.user?.role; if (!deleted_by) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } // 권한 체크: 그룹장, 시스템, 관리자만 삭제 가능 const allowedRoles = ['admin', 'system', 'group_leader']; if (!allowedRoles.includes(access_level)) { return res.status(403).json({ error: '작업보고서 삭제 권한이 없습니다.', details: '그룹장 이상의 권한이 필요합니다.' }); } try { const affectedRows = await dailyWorkReportModel.removeByDateAndWorker(date, user_id, deleted_by); if (affectedRows === 0) { return res.status(404).json({ error: '삭제할 작업보고서를 찾을 수 없습니다.', date: date, user_id: user_id }); } res.json({ message: `${date} 날짜의 작업자 ${user_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`, date, user_id, affected_rows: affectedRows, deleted_by, timestamp: new Date().toISOString() }); } catch (err) { logger.error('작업보고서 전체 삭제 오류:', err); res.status(500).json({ error: '작업보고서 삭제 중 오류가 발생했습니다.', details: err.message }); } }; /** * 마스터 데이터 조회 함수들 */ const getWorkTypes = async (req, res) => { try { const data = await dailyWorkReportModel.getAllWorkTypes(); res.json({ success: true, data: data, message: '작업 유형 조회 성공' }); } catch (err) { logger.error('작업 유형 조회 오류:', err); res.status(500).json({ success: false, error: { message: '작업 유형 조회 중 오류가 발생했습니다.', code: 'DATABASE_ERROR' } }); } }; const getWorkStatusTypes = async (req, res) => { try { const data = await dailyWorkReportModel.getAllWorkStatusTypes(); res.json(data); } catch (err) { logger.error('업무 상태 유형 조회 오류:', err); res.status(500).json({ error: '업무 상태 유형 조회 중 오류가 발생했습니다.', details: err.message }); } }; const getErrorTypes = async (req, res) => { try { const data = await dailyWorkReportModel.getAllErrorTypes(); res.json(data); } catch (err) { logger.error('에러 유형 조회 오류:', err); res.status(500).json({ error: '에러 유형 조회 중 오류가 발생했습니다.', details: err.message }); } }; // ========== 작업 유형 CRUD ========== /** * 작업 유형 생성 */ const createWorkType = asyncHandler(async (req, res) => { const { name, description, category } = req.body; if (!name) { return res.status(400).json({ error: '작업 유형 이름이 필요합니다.' }); } const result = await dailyWorkReportModel.createWorkType({ name, description, category }); res.created(result, '작업 유형이 성공적으로 생성되었습니다.'); }); /** * 작업 유형 수정 */ const updateWorkType = asyncHandler(async (req, res) => { const { id } = req.params; const { name, description, category } = req.body; if (!id) { return res.status(400).json({ error: '작업 유형 ID가 필요합니다.' }); } const result = await dailyWorkReportModel.updateWorkType(id, { name, description, category }); if (result.affectedRows === 0) { return res.status(404).json({ error: '수정할 작업 유형을 찾을 수 없습니다.' }); } res.success(result, '작업 유형이 성공적으로 수정되었습니다.'); }); /** * 작업 유형 삭제 */ const deleteWorkType = asyncHandler(async (req, res) => { const { id } = req.params; if (!id) { return res.status(400).json({ error: '작업 유형 ID가 필요합니다.' }); } const result = await dailyWorkReportModel.deleteWorkType(id); if (result.affectedRows === 0) { return res.status(404).json({ error: '삭제할 작업 유형을 찾을 수 없습니다.' }); } res.success(result, '작업 유형이 성공적으로 삭제되었습니다.'); }); // ========== 작업 상태 CRUD ========== /** * 작업 상태 생성 */ const createWorkStatus = asyncHandler(async (req, res) => { const { name, description, is_error } = req.body; if (!name) { return res.status(400).json({ error: '작업 상태 이름이 필요합니다.' }); } const result = await dailyWorkReportModel.createWorkStatus({ name, description, is_error }); res.created(result, '작업 상태가 성공적으로 생성되었습니다.'); }); /** * 작업 상태 수정 */ const updateWorkStatus = asyncHandler(async (req, res) => { const { id } = req.params; const { name, description, is_error } = req.body; if (!id) { return res.status(400).json({ error: '작업 상태 ID가 필요합니다.' }); } const result = await dailyWorkReportModel.updateWorkStatus(id, { name, description, is_error }); if (result.affectedRows === 0) { return res.status(404).json({ error: '수정할 작업 상태를 찾을 수 없습니다.' }); } res.success(result, '작업 상태가 성공적으로 수정되었습니다.'); }); /** * 작업 상태 삭제 */ const deleteWorkStatus = asyncHandler(async (req, res) => { const { id } = req.params; if (!id) { return res.status(400).json({ error: '작업 상태 ID가 필요합니다.' }); } const result = await dailyWorkReportModel.deleteWorkStatus(id); if (result.affectedRows === 0) { return res.status(404).json({ error: '삭제할 작업 상태를 찾을 수 없습니다.' }); } res.success(result, '작업 상태가 성공적으로 삭제되었습니다.'); }); // ========== 오류 유형 CRUD ========== /** * 오류 유형 생성 */ const createErrorType = asyncHandler(async (req, res) => { const { name, description, severity } = req.body; if (!name) { return res.status(400).json({ error: '오류 유형 이름이 필요합니다.' }); } const result = await dailyWorkReportModel.createErrorType({ name, description, severity }); res.created(result, '오류 유형이 성공적으로 생성되었습니다.'); }); /** * 오류 유형 수정 */ const updateErrorType = asyncHandler(async (req, res) => { const { id } = req.params; const { name, description, severity } = req.body; if (!id) { return res.status(400).json({ error: '오류 유형 ID가 필요합니다.' }); } const result = await dailyWorkReportModel.updateErrorType(id, { name, description, severity }); if (result.affectedRows === 0) { return res.status(404).json({ error: '수정할 오류 유형을 찾을 수 없습니다.' }); } res.success(result, '오류 유형이 성공적으로 수정되었습니다.'); }); /** * 오류 유형 삭제 */ const deleteErrorType = asyncHandler(async (req, res) => { const { id } = req.params; if (!id) { return res.status(400).json({ error: '오류 유형 ID가 필요합니다.' }); } const result = await dailyWorkReportModel.deleteErrorType(id); if (result.affectedRows === 0) { return res.status(404).json({ error: '삭제할 오류 유형을 찾을 수 없습니다.' }); } res.success(result, '오류 유형이 성공적으로 삭제되었습니다.'); }); /** * 누적 현황 조회 */ const getAccumulatedReports = async (req, res) => { const { date, user_id } = req.query; if (!date || !user_id) { return res.status(400).json({ error: 'date와 user_id가 필요합니다.', example: 'date=2024-06-16&user_id=1' }); } try { const data = await dailyWorkReportModel.getAccumulatedReportsByDate(date, user_id); res.json({ date, user_id, total_entries: data.length, accumulated_data: data, timestamp: new Date().toISOString() }); } catch (err) { logger.error('누적 현황 조회 오류:', err); res.status(500).json({ error: '누적 현황 조회 중 오류가 발생했습니다.', details: err.message }); } }; /** * TBM 배정 기반 작업보고서 생성 */ const createFromTbm = async (req, res) => { try { const { tbm_assignment_id, tbm_session_id, user_id, project_id, work_type_id, report_date, start_time, end_time, total_hours, error_hours, error_type_id, work_status_id } = req.body; // 필수 필드 검증 if (!tbm_assignment_id || !tbm_session_id || !user_id || !report_date || !total_hours) { return res.status(400).json({ success: false, message: '필수 필드가 누락되었습니다. (assignment_id, session_id, user_id, report_date, total_hours)' }); } // regular_hours 계산 const regular_hours = total_hours - (error_hours || 0); const reportData = { tbm_assignment_id, tbm_session_id, user_id, project_id, work_type_id, report_date, start_time, end_time, total_hours, error_hours: error_hours || 0, regular_hours, work_status_id: work_status_id || (error_hours > 0 ? 2 : 1), error_type_id, created_by: req.user.user_id }; const result = await dailyWorkReportModel.createFromTbmAssignment(reportData); res.status(201).json({ success: true, message: '작업보고서가 생성되었습니다.', data: result }); } catch (err) { logger.error('TBM 작업보고서 생성 오류:', err); res.status(500).json({ success: false, message: 'TBM 작업보고서 생성 중 오류가 발생했습니다.', error: err.message }); } }; // 모든 컨트롤러 함수 내보내기 module.exports = { // V2 핵심 CRUD 함수 createDailyWorkReport, getDailyWorkReports, updateWorkReport, removeDailyWorkReport, createFromTbm, // V2 통계 및 요약 함수 getWorkReportStats, getDailySummary, // 레거시 함수 (콜백 제거 완료) getAccumulatedReports, getContributorsSummary, getMyAccumulatedData, removeMyEntry, getDailyWorkReportsByDate, searchWorkReports, getMonthlySummary, removeDailyWorkReportByDateAndWorker, getWorkTypes, getWorkStatusTypes, getErrorTypes, // 마스터 데이터 CRUD createWorkType, updateWorkType, deleteWorkType, createWorkStatus, updateWorkStatus, deleteWorkStatus, createErrorType, updateErrorType, deleteErrorType };