/** * 일일 작업 보고서 컨트롤러 * * 작업 보고서 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, worker_id } = req.query; if (!date || !worker_id) { throw new ApiError('date와 worker_id가 필요합니다.', 400); } console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`); try { const data = await new Promise((resolve, reject) => { dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => { if (err) reject(err); else resolve(data); }); }); const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0); console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`); const result = { date, worker_id, contributors: data, total_contributors: data.length, grand_total_hours: totalHours }; res.success(result, '기여자별 요약 조회 성공'); } catch (err) { handleDatabaseError(err, '기여자별 요약 조회'); } }); /** * 📊 개인 누적 현황 조회 (새로운 기능) */ const getMyAccumulatedData = (req, res) => { const { date, worker_id } = req.query; const created_by = req.user?.user_id || req.user?.id; if (!date || !worker_id) { return res.status(400).json({ error: 'date와 worker_id가 필요합니다.', example: 'date=2024-06-16&worker_id=1' }); } if (!created_by) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`); dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => { if (err) { console.error('개인 누적 현황 조회 오류:', err); return res.status(500).json({ error: '개인 누적 현황 조회 중 오류가 발생했습니다.', details: err.message }); } console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`); res.json({ date, worker_id, created_by, my_data: data, timestamp: new Date().toISOString() }); }); }; /** * 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능) */ const removeMyEntry = (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: '사용자 인증 정보가 없습니다.' }); } console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`); dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => { if (err) { console.error('개별 항목 삭제 오류:', err); return res.status(500).json({ error: '항목 삭제 중 오류가 발생했습니다.', details: err.message }); } console.log(`✅ 개별 항목 삭제 완료: id=${id}`); res.json({ message: '항목이 성공적으로 삭제되었습니다.', id: id, deleted_by, timestamp: new Date().toISOString(), ...result }); }); }; /** * 📊 작업보고서 조회 (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' // 기본값을 'user'로 설정하여 안전하게 처리 }; if (!userInfo.user_id) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } const reports = await dailyWorkReportService.getDailyWorkReportsService(req.query, userInfo); res.json(reports); } catch (error) { console.error('💥 작업보고서 조회 컨트롤러 오류:', error.message); res.status(400).json({ success: false, error: '작업보고서 조회에 실패했습니다.', details: error.message }); } }; /** * 📊 날짜별 작업보고서 조회 (경로 파라미터 - 권한별 전체 조회 지원) */ const getDailyWorkReportsByDate = (req, res) => { const { date } = req.params; const current_user_id = req.user?.user_id || req.user?.id; const user_access_level = req.user?.access_level; const user_job_type = req.user?.job_type; if (!current_user_id) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } const isAdmin = user_access_level === 'system' || user_access_level === 'admin' || user_access_level === 'leader' || user_job_type === 'leader'; console.log(`📊 날짜별 조회 (경로): date=${date}, user=${current_user_id}, 권한=${user_access_level}, 직책=${user_job_type}, 관리자=${isAdmin}`); console.log(`🔍 사용자 정보 상세:`, req.user); dailyWorkReportModel.getByDate(date, (err, data) => { if (err) { console.error('날짜별 작업보고서 조회 오류:', err); return res.status(500).json({ error: '작업보고서 조회 중 오류가 발생했습니다.', details: err.message }); } // 🎯 권한별 필터링 (임시로 비활성화) let finalData = data; console.log(`📊 임시로 모든 사용자에게 전체 조회 허용: ${data.length}개`); console.log(`📊 권한 정보: access_level=${user_access_level}, job_type=${user_job_type}, isAdmin=${isAdmin}`); // if (!isAdmin) { // finalData = data.filter(report => report.created_by === current_user_id); // console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`); // } else { // console.log(`📊 관리자 권한으로 전체 조회: ${data.length}개`); // } res.json(finalData); }); }; /** * 🔍 작업보고서 검색 (페이지네이션 포함) */ const searchWorkReports = (req, res) => { const { start_date, end_date, worker_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: ['worker_id', 'project_id', 'work_status_id', 'page', 'limit'] }); } if (!created_by) { return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); } const searchParams = { start_date, end_date, worker_id: worker_id ? parseInt(worker_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) }; console.log('🔍 작업보고서 검색 요청:', searchParams); dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => { if (err) { console.error('작업보고서 검색 오류:', err); return res.status(500).json({ error: '작업보고서 검색 중 오류가 발생했습니다.', details: err.message }); } console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`); res.json(data); }); }; /** * 📈 통계 조회 (V2 - Service Layer 사용) */ const getWorkReportStats = async (req, res) => { try { const statsData = await dailyWorkReportService.getStatisticsService(req.query); res.json(statsData); } catch (error) { console.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) { console.error('💥 일일 요약 조회 컨트롤러 오류:', error.message); res.status(400).json({ success: false, error: '일일 요약 조회에 실패했습니다.', details: error.message }); } }; /** * 📅 월간 요약 조회 */ const getMonthlySummary = (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 형식으로 입력하세요.' }); } console.log(`📅 월간 요약 조회: ${year}-${month}`); dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => { if (err) { console.error('월간 요약 조회 오류:', err); return res.status(500).json({ error: '월간 요약 조회 중 오류가 발생했습니다.', details: err.message }); } res.json({ year: parseInt(year), month: parseInt(month), summary: data, total_entries: data.length, timestamp: new Date().toISOString() }); }); }; /** * ✏️ 작업보고서 수정 (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) { console.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) { console.error(`💥 작업보고서 삭제 컨트롤러 오류 (id: ${req.params.id}):`, error.message); const statusCode = error.statusCode || 400; res.status(statusCode).json({ success: false, error: '작업보고서 삭제에 실패했습니다.', details: error.message }); } }; /** * ��️ 작업자의 특정 날짜 전체 삭제 */ const removeDailyWorkReportByDateAndWorker = (req, res) => { const { date, worker_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: '그룹장 이상의 권한이 필요합니다.' }); } console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`); dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => { if (err) { console.error('작업보고서 전체 삭제 오류:', err); return res.status(500).json({ error: '작업보고서 삭제 중 오류가 발생했습니다.', details: err.message }); } if (affectedRows === 0) { return res.status(404).json({ error: '삭제할 작업보고서를 찾을 수 없습니다.', date: date, worker_id: worker_id }); } console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}개`); res.json({ message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`, date, worker_id, affected_rows: affectedRows, deleted_by, timestamp: new Date().toISOString() }); }); }; /** * 📋 마스터 데이터 조회 함수들 */ const getWorkTypes = (req, res) => { console.log('📋 작업 유형 조회 요청'); dailyWorkReportModel.getAllWorkTypes((err, data) => { if (err) { console.error('작업 유형 조회 오류:', err); return res.status(500).json({ success: false, error: { message: '작업 유형 조회 중 오류가 발생했습니다.', code: 'DATABASE_ERROR' } }); } console.log(`📋 작업 유형 조회 결과: ${data.length}개`); res.json({ success: true, data: data, message: '작업 유형 조회 성공' }); }); }; const getWorkStatusTypes = (req, res) => { console.log('📋 업무 상태 유형 조회 요청'); dailyWorkReportModel.getAllWorkStatusTypes((err, data) => { if (err) { console.error('업무 상태 유형 조회 오류:', err); return res.status(500).json({ error: '업무 상태 유형 조회 중 오류가 발생했습니다.', details: err.message }); } console.log(`📋 업무 상태 유형 조회 결과: ${data.length}개`); res.json(data); }); }; const getErrorTypes = (req, res) => { console.log('📋 에러 유형 조회 요청'); dailyWorkReportModel.getAllErrorTypes((err, data) => { if (err) { console.error('에러 유형 조회 오류:', err); return res.status(500).json({ error: '에러 유형 조회 중 오류가 발생했습니다.', details: err.message }); } console.log(`📋 에러 유형 조회 결과: ${data.length}개`); res.json(data); }); }; // ========== 작업 유형 CRUD ========== /** * 📝 작업 유형 생성 */ const createWorkType = asyncHandler(async (req, res) => { const { name, description, category } = req.body; if (!name) { throw new ApiError('작업 유형 이름이 필요합니다.', 400); } console.log('📝 작업 유형 생성:', { name, description, category }); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.createWorkType({ name, description, category }, (err, data) => { if (err) reject(err); else resolve(data); }); }); res.created(result, '작업 유형이 성공적으로 생성되었습니다.'); } catch (err) { handleDatabaseError(err, '작업 유형 생성'); } }); /** * ✏️ 작업 유형 수정 */ const updateWorkType = asyncHandler(async (req, res) => { const { id } = req.params; const { name, description, category } = req.body; if (!id) { throw new ApiError('작업 유형 ID가 필요합니다.', 400); } console.log('✏️ 작업 유형 수정:', { id, name, description, category }); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.updateWorkType(id, { name, description, category }, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (result.affectedRows === 0) { throw new ApiError('수정할 작업 유형을 찾을 수 없습니다.', 404); } res.success(result, '작업 유형이 성공적으로 수정되었습니다.'); } catch (err) { handleDatabaseError(err, '작업 유형 수정'); } }); /** * 🗑️ 작업 유형 삭제 */ const deleteWorkType = asyncHandler(async (req, res) => { const { id } = req.params; if (!id) { throw new ApiError('작업 유형 ID가 필요합니다.', 400); } console.log('🗑️ 작업 유형 삭제:', id); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.deleteWorkType(id, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (result.affectedRows === 0) { throw new ApiError('삭제할 작업 유형을 찾을 수 없습니다.', 404); } res.success(result, '작업 유형이 성공적으로 삭제되었습니다.'); } catch (err) { handleDatabaseError(err, '작업 유형 삭제'); } }); // ========== 작업 상태 CRUD ========== /** * 📝 작업 상태 생성 */ const createWorkStatus = asyncHandler(async (req, res) => { const { name, description, is_error } = req.body; if (!name) { throw new ApiError('작업 상태 이름이 필요합니다.', 400); } console.log('📝 작업 상태 생성:', { name, description, is_error }); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.createWorkStatus({ name, description, is_error }, (err, data) => { if (err) reject(err); else resolve(data); }); }); res.created(result, '작업 상태가 성공적으로 생성되었습니다.'); } catch (err) { handleDatabaseError(err, '작업 상태 생성'); } }); /** * ✏️ 작업 상태 수정 */ const updateWorkStatus = asyncHandler(async (req, res) => { const { id } = req.params; const { name, description, is_error } = req.body; if (!id) { throw new ApiError('작업 상태 ID가 필요합니다.', 400); } console.log('✏️ 작업 상태 수정:', { id, name, description, is_error }); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.updateWorkStatus(id, { name, description, is_error }, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (result.affectedRows === 0) { throw new ApiError('수정할 작업 상태를 찾을 수 없습니다.', 404); } res.success(result, '작업 상태가 성공적으로 수정되었습니다.'); } catch (err) { handleDatabaseError(err, '작업 상태 수정'); } }); /** * 🗑️ 작업 상태 삭제 */ const deleteWorkStatus = asyncHandler(async (req, res) => { const { id } = req.params; if (!id) { throw new ApiError('작업 상태 ID가 필요합니다.', 400); } console.log('🗑️ 작업 상태 삭제:', id); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.deleteWorkStatus(id, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (result.affectedRows === 0) { throw new ApiError('삭제할 작업 상태를 찾을 수 없습니다.', 404); } res.success(result, '작업 상태가 성공적으로 삭제되었습니다.'); } catch (err) { handleDatabaseError(err, '작업 상태 삭제'); } }); // ========== 오류 유형 CRUD ========== /** * 📝 오류 유형 생성 */ const createErrorType = asyncHandler(async (req, res) => { const { name, description, severity } = req.body; if (!name) { throw new ApiError('오류 유형 이름이 필요합니다.', 400); } console.log('📝 오류 유형 생성:', { name, description, severity }); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.createErrorType({ name, description, severity }, (err, data) => { if (err) reject(err); else resolve(data); }); }); res.created(result, '오류 유형이 성공적으로 생성되었습니다.'); } catch (err) { handleDatabaseError(err, '오류 유형 생성'); } }); /** * ✏️ 오류 유형 수정 */ const updateErrorType = asyncHandler(async (req, res) => { const { id } = req.params; const { name, description, severity } = req.body; if (!id) { throw new ApiError('오류 유형 ID가 필요합니다.', 400); } console.log('✏️ 오류 유형 수정:', { id, name, description, severity }); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.updateErrorType(id, { name, description, severity }, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (result.affectedRows === 0) { throw new ApiError('수정할 오류 유형을 찾을 수 없습니다.', 404); } res.success(result, '오류 유형이 성공적으로 수정되었습니다.'); } catch (err) { handleDatabaseError(err, '오류 유형 수정'); } }); /** * 🗑️ 오류 유형 삭제 */ const deleteErrorType = asyncHandler(async (req, res) => { const { id } = req.params; if (!id) { throw new ApiError('오류 유형 ID가 필요합니다.', 400); } console.log('🗑️ 오류 유형 삭제:', id); try { const result = await new Promise((resolve, reject) => { dailyWorkReportModel.deleteErrorType(id, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (result.affectedRows === 0) { throw new ApiError('삭제할 오류 유형을 찾을 수 없습니다.', 404); } res.success(result, '오류 유형이 성공적으로 삭제되었습니다.'); } catch (err) { handleDatabaseError(err, '오류 유형 삭제'); } }); /** * 📊 누적 현황 조회 */ const getAccumulatedReports = (req, res) => { const { date, worker_id } = req.query; if (!date || !worker_id) { return res.status(400).json({ error: 'date와 worker_id가 필요합니다.', example: 'date=2024-06-16&worker_id=1' }); } console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`); dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => { if (err) { console.error('누적 현황 조회 오류:', err); return res.status(500).json({ error: '누적 현황 조회 중 오류가 발생했습니다.', details: err.message }); } console.log(`📊 누적 현황 조회 결과: ${data.length}개`); res.json({ date, worker_id, total_entries: data.length, accumulated_data: data, timestamp: new Date().toISOString() }); }); }; /** * TBM 배정 기반 작업보고서 생성 */ const createFromTbm = async (req, res) => { try { const { tbm_assignment_id, tbm_session_id, worker_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 || !worker_id || !report_date || !total_hours) { return res.status(400).json({ success: false, message: '필수 필드가 누락되었습니다. (assignment_id, session_id, worker_id, report_date, total_hours)' }); } // regular_hours 계산 const regular_hours = total_hours - (error_hours || 0); const reportData = { tbm_assignment_id, tbm_session_id, worker_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_hours가 있으면 상태 2 (부적합) 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) { console.error('TBM 작업보고서 생성 오류:', err); console.error('Error stack:', err.stack); res.status(500).json({ success: false, message: 'TBM 작업보고서 생성 중 오류가 발생했습니다.', error: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined }); } }; // 모든 컨트롤러 함수 내보내기 (리팩토링된 함수 위주로 재구성) 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 };