/** * 일일 작업 보고서 서비스 * * 작업 보고서 관련 비즈니스 로직 처리 * * @author TK-FB-Project * @since 2025-12-11 */ const dailyWorkReportModel = require('../models/dailyWorkReportModel'); const { ValidationError, NotFoundError, ForbiddenError, DatabaseError } = require('../utils/errors'); const logger = require('../utils/logger'); /** * 일일 작업 보고서를 생성하는 비즈니스 로직을 처리합니다. * @param {object} reportData - 컨트롤러에서 전달된 보고서 데이터 * @returns {Promise} 생성 결과 또는 에러 */ const createDailyWorkReportService = async (reportData) => { const { report_date, user_id, work_entries, created_by, created_by_name } = reportData; // 1. 기본 유효성 검사 if (!report_date || !user_id || !work_entries) { throw new ValidationError('필수 필드가 누락되었습니다', { required: ['report_date', 'user_id', 'work_entries'], received: { report_date, user_id, work_entries: !!work_entries } }); } if (!Array.isArray(work_entries) || work_entries.length === 0) { throw new ValidationError('최소 하나의 작업 항목이 필요합니다'); } if (!created_by) { throw new ValidationError('사용자 인증 정보가 없습니다'); } // 2. 작업 항목 유효성 검사 for (let i = 0; i < work_entries.length; i++) { const entry = work_entries[i]; const requiredFields = ['project_id', 'task_id', 'work_hours']; for (const field of requiredFields) { if (entry[field] === undefined || entry[field] === null || entry[field] === '') { throw new ValidationError(`작업 항목 ${i + 1}의 필수 필드가 누락되었습니다`, { entry: i + 1, missingField: field }); } } // is_error가 true일 때 error_type_code_id 필수 검사 if (entry.is_error === true && !entry.error_type_code_id) { throw new ValidationError(`에러 상태인 경우 에러 타입이 필요합니다`, { entry: i + 1, is_error: true }); } const hours = parseFloat(entry.work_hours); if (isNaN(hours) || hours <= 0 || hours > 24) { throw new ValidationError(`작업 시간이 유효하지 않습니다 (0 초과 24 이하)`, { entry: i + 1, work_hours: entry.work_hours, valid_range: '0 < hours <= 24' }); } } // 3. 모델에 전달할 데이터 준비 const modelData = { report_date, user_id: parseInt(user_id), entries: work_entries.map(entry => ({ project_id: entry.project_id, work_type_id: entry.task_id, // task_id를 work_type_id로 매핑 work_hours: parseFloat(entry.work_hours), work_status_id: entry.work_status_id, error_type_id: entry.error_type_id, created_by: created_by })) }; logger.info('작업보고서 생성 요청', { date: report_date, worker: user_id, creator: created_by_name, entries_count: modelData.entries.length }); // 4. 모델 함수 호출 try { const result = await dailyWorkReportModel.createReportEntries(modelData); logger.info('작업보고서 생성 성공', { report_id: result.report_id, entries_count: modelData.entries.length }); return { message: '작업보고서가 성공적으로 생성되었습니다.', ...result }; } catch (error) { logger.error('작업보고서 생성 실패', { error: error.message, stack: error.stack }); throw new DatabaseError('작업보고서 생성 중 데이터베이스 오류가 발생했습니다'); } }; /** * 사용자 권한과 요청 파라미터에 따라 일일 작업 보고서를 조회하는 비즈니스 로직을 처리합니다. * @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 * @param {object} userInfo - 요청을 보낸 사용자의 정보 (id, role 등) * @returns {Promise} 조회된 작업 보고서 배열 */ const getDailyWorkReportsService = async (queryParams, userInfo) => { const { date, start_date, end_date, user_id, created_by: requested_created_by, view_all } = queryParams; const { user_id: current_user_id, role } = userInfo; // 날짜 또는 날짜 범위 중 하나는 필수 if (!date && (!start_date || !end_date)) { throw new ValidationError('날짜 또는 날짜 범위가 필요합니다', { required: 'date OR (start_date AND end_date)', received: { date, start_date, end_date } }); } // 관리자 여부 확인 (대소문자 무시) const roleLower = (role || '').toLowerCase(); const isAdmin = roleLower === 'system' || roleLower === 'admin' || roleLower === 'system admin'; const canViewAll = isAdmin || view_all === 'true'; // 모델에 전달할 조회 옵션 객체 생성 const options = {}; if (date) { options.date = date; } else { options.start_date = start_date; options.end_date = end_date; } if (user_id) { options.user_id = parseInt(user_id); } // 최종적으로 필터링할 작성자 ID 결정 if (!canViewAll) { // 관리자가 아니면 자신의 데이터만 보거나, 명시적으로 요청된 자신의 ID만 허용 options.created_by_user_id = requested_created_by ? Math.min(requested_created_by, current_user_id) : current_user_id; } else if (requested_created_by) { // 관리자는 다른 사람의 데이터도 조회 가능 options.created_by_user_id = parseInt(requested_created_by); } // created_by_user_id가 명시되지 않으면 모든 작성자의 데이터를 조회 logger.info('작업보고서 조회 요청', { ...options, requester: current_user_id, isAdmin }); try { const reports = await dailyWorkReportModel.getReportsWithOptions(options); logger.info('작업보고서 조회 성공', { count: reports.length }); return reports; } catch (error) { logger.error('작업보고서 조회 실패', { error: error.message }); throw new DatabaseError('작업보고서 조회 중 데이터베이스 오류가 발생했습니다'); } }; /** * 특정 작업 보고서 항목을 수정하는 비즈니스 로직을 처리합니다. * @param {string} reportId - 수정할 보고서의 ID * @param {object} updateData - 수정할 데이터 * @param {object} userInfo - 요청을 보낸 사용자의 정보 * @returns {Promise} 수정 결과 */ const updateWorkReportService = async (reportId, updateData, userInfo) => { const { user_id: updated_by } = userInfo; if (!reportId || !updateData || Object.keys(updateData).length === 0) { throw new ValidationError('보고서 ID와 수정할 데이터가 필요합니다', { reportId, hasUpdateData: !!updateData, updateFieldsCount: updateData ? Object.keys(updateData).length : 0 }); } const modelUpdateData = { ...updateData, updated_by_user_id: updated_by }; logger.info('작업보고서 수정 요청', { reportId, updatedBy: updated_by }); try { const affectedRows = await dailyWorkReportModel.updateReportById(reportId, modelUpdateData); if (affectedRows === 0) { throw new NotFoundError('수정할 작업보고서를 찾을 수 없습니다'); } logger.info('작업보고서 수정 성공', { reportId }); return { message: '작업보고서가 성공적으로 수정되었습니다.', report_id: reportId, affected_rows: affectedRows }; } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('작업보고서 수정 실패', { reportId, error: error.message }); throw new DatabaseError('작업보고서 수정 중 데이터베이스 오류가 발생했습니다'); } }; /** * 특정 작업 보고서 항목을 삭제하는 비즈니스 로직을 처리합니다. * @param {string} reportId - 삭제할 보고서의 ID * @param {object} userInfo - 요청을 보낸 사용자의 정보 * @returns {Promise} 삭제 결과 */ const removeDailyWorkReportService = async (reportId, userInfo) => { const { user_id: deleted_by } = userInfo; if (!reportId) { throw new ValidationError('삭제할 보고서 ID가 필요합니다'); } logger.info('작업보고서 삭제 요청', { reportId, deletedBy: deleted_by }); try { const affectedRows = await dailyWorkReportModel.removeReportById(reportId, deleted_by); if (affectedRows === 0) { throw new NotFoundError('삭제할 작업보고서를 찾을 수 없습니다'); } logger.info('작업보고서 삭제 성공', { reportId }); return { message: '작업보고서가 성공적으로 삭제되었습니다.', report_id: reportId, affected_rows: affectedRows }; } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('작업보고서 삭제 실패', { reportId, error: error.message }); throw new DatabaseError('작업보고서 삭제 중 데이터베이스 오류가 발생했습니다'); } }; /** * 기간별 작업 보고서 통계를 조회하는 비즈니스 로직을 처리합니다. * @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (start_date, end_date) * @returns {Promise} 통계 데이터 */ const getStatisticsService = async (queryParams) => { const { start_date, end_date } = queryParams; if (!start_date || !end_date) { throw new ValidationError('시작일과 종료일이 모두 필요합니다', { required: ['start_date', 'end_date'], received: { start_date, end_date } }); } logger.info('작업보고서 통계 조회 요청', { start_date, end_date }); try { const statsData = await dailyWorkReportModel.getStatistics(start_date, end_date); logger.info('작업보고서 통계 조회 성공', { period: `${start_date} ~ ${end_date}` }); return { ...statsData, metadata: { period: `${start_date} ~ ${end_date}`, timestamp: new Date().toISOString() } }; } catch (error) { logger.error('통계 조회 실패', { error: error.message }); throw new DatabaseError('통계 조회 중 데이터베이스 오류가 발생했습니다'); } }; /** * 일일 또는 작업자별 작업 요약 정보를 조회하는 비즈니스 로직을 처리합니다. * @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 (date 또는 user_id) * @returns {Promise} 요약 데이터 */ const getSummaryService = async (queryParams) => { const { date, user_id } = queryParams; if (!date && !user_id) { throw new ValidationError('날짜 또는 작업자 ID가 필요합니다', { required: 'date OR user_id', received: { date, user_id } }); } try { if (date) { logger.info('일일 요약 조회 요청', { date }); const result = await dailyWorkReportModel.getSummaryByDate(date); logger.info('일일 요약 조회 성공', { date }); return result; } else { // user_id logger.info('작업자별 요약 조회 요청', { user_id }); const result = await dailyWorkReportModel.getSummaryByWorker(user_id); logger.info('작업자별 요약 조회 성공', { user_id }); return result; } } catch (error) { logger.error('요약 정보 조회 실패', { error: error.message }); throw new DatabaseError('요약 정보 조회 중 데이터베이스 오류가 발생했습니다'); } }; module.exports = { createDailyWorkReportService, getDailyWorkReportsService, updateWorkReportService, removeDailyWorkReportService, getStatisticsService, getSummaryService, };