/** * 작업 보고서 관리 서비스 * * 작업 보고서 CRUD 및 조회 관련 비즈니스 로직 처리 * * @author TK-FB-Project * @since 2025-12-11 */ const workReportModel = require('../models/workReportModel'); const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors'); const logger = require('../utils/logger'); const { getDb } = require('../dbPool'); /** * 작업 보고서 생성 (단일 또는 다중) */ const createWorkReportService = async (reportData) => { const reports = Array.isArray(reportData) ? reportData : [reportData]; if (reports.length === 0) { throw new ValidationError('보고서 데이터가 필요합니다'); } logger.info('작업 보고서 생성 요청', { count: reports.length }); const workReport_ids = []; try { for (const report of reports) { const id = await new Promise((resolve, reject) => { workReportModel.create(report, (err, insertId) => { if (err) reject(err); else resolve(insertId); }); }); workReport_ids.push(id); } logger.info('작업 보고서 생성 성공', { count: workReport_ids.length, ids: workReport_ids }); return { workReport_ids }; } catch (error) { logger.error('작업 보고서 생성 실패', { count: reports.length, error: error.message }); throw new DatabaseError('작업 보고서 생성 중 오류가 발생했습니다'); } }; /** * 날짜별 작업 보고서 조회 */ const getWorkReportsByDateService = async (date) => { if (!date) { throw new ValidationError('날짜가 필요합니다', { required: ['date'], received: { date } }); } logger.info('작업 보고서 날짜별 조회 요청', { date }); try { const rows = await new Promise((resolve, reject) => { workReportModel.getAllByDate(date, (err, data) => { if (err) reject(err); else resolve(data); }); }); logger.info('작업 보고서 조회 성공', { date, count: rows.length }); return rows; } catch (error) { logger.error('작업 보고서 조회 실패', { date, error: error.message }); throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다'); } }; /** * 기간별 작업 보고서 조회 */ const getWorkReportsInRangeService = async (start, end) => { if (!start || !end) { throw new ValidationError('시작일과 종료일이 필요합니다', { required: ['start', 'end'], received: { start, end } }); } logger.info('작업 보고서 기간별 조회 요청', { start, end }); try { const rows = await new Promise((resolve, reject) => { workReportModel.getByRange(start, end, (err, data) => { if (err) reject(err); else resolve(data); }); }); logger.info('작업 보고서 조회 성공', { start, end, count: rows.length }); return rows; } catch (error) { logger.error('작업 보고서 조회 실패', { start, end, error: error.message }); throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다'); } }; /** * 단일 작업 보고서 조회 */ const getWorkReportByIdService = async (id) => { if (!id) { throw new ValidationError('보고서 ID가 필요합니다'); } logger.info('작업 보고서 조회 요청', { report_id: id }); try { const row = await new Promise((resolve, reject) => { workReportModel.getById(id, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (!row) { logger.warn('작업 보고서를 찾을 수 없음', { report_id: id }); throw new NotFoundError('작업 보고서를 찾을 수 없습니다'); } logger.info('작업 보고서 조회 성공', { report_id: id }); return row; } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('작업 보고서 조회 실패', { report_id: id, error: error.message }); throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다'); } }; /** * 작업 보고서 수정 */ const updateWorkReportService = async (id, updateData) => { if (!id) { throw new ValidationError('보고서 ID가 필요합니다'); } logger.info('작업 보고서 수정 요청', { report_id: id }); try { const changes = await new Promise((resolve, reject) => { workReportModel.update(id, updateData, (err, affectedRows) => { if (err) reject(err); else resolve(affectedRows); }); }); if (changes === 0) { logger.warn('작업 보고서를 찾을 수 없거나 변경사항 없음', { report_id: id }); throw new NotFoundError('작업 보고서를 찾을 수 없습니다'); } logger.info('작업 보고서 수정 성공', { report_id: id, changes }); return { changes }; } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('작업 보고서 수정 실패', { report_id: id, error: error.message }); throw new DatabaseError('작업 보고서 수정 중 오류가 발생했습니다'); } }; /** * 작업 보고서 삭제 */ const removeWorkReportService = async (id) => { if (!id) { throw new ValidationError('보고서 ID가 필요합니다'); } logger.info('작업 보고서 삭제 요청', { report_id: id }); try { const changes = await new Promise((resolve, reject) => { workReportModel.remove(id, (err, affectedRows) => { if (err) reject(err); else resolve(affectedRows); }); }); if (changes === 0) { logger.warn('작업 보고서를 찾을 수 없음', { report_id: id }); throw new NotFoundError('작업 보고서를 찾을 수 없습니다'); } logger.info('작업 보고서 삭제 성공', { report_id: id, changes }); return { changes }; } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('작업 보고서 삭제 실패', { report_id: id, error: error.message }); throw new DatabaseError('작업 보고서 삭제 중 오류가 발생했습니다'); } }; /** * 월간 요약 조회 */ const getSummaryService = async (year, month) => { if (!year || !month) { throw new ValidationError('연도와 월이 필요합니다', { required: ['year', 'month'], received: { year, month } }); } const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`; const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`; logger.info('작업 보고서 월간 요약 조회 요청', { year, month, start, end }); try { const rows = await new Promise((resolve, reject) => { workReportModel.getByRange(start, end, (err, data) => { if (err) reject(err); else resolve(data); }); }); if (!rows || rows.length === 0) { logger.warn('월간 요약 데이터 없음', { year, month }); throw new NotFoundError('해당 기간의 작업 보고서가 없습니다'); } logger.info('작업 보고서 월간 요약 조회 성공', { year, month, count: rows.length }); return rows; } catch (error) { if (error instanceof NotFoundError) { throw error; } logger.error('작업 보고서 월간 요약 조회 실패', { year, month, error: error.message }); throw new DatabaseError('월간 요약 조회 중 오류가 발생했습니다'); } }; // ========== 부적합 원인 관리 서비스 ========== /** * 작업 보고서의 부적합 원인 목록 조회 */ const getReportDefectsService = async (reportId) => { const db = await getDb(); try { const [rows] = await db.execute(` SELECT d.defect_id, d.report_id, d.error_type_id, d.defect_hours, d.note, d.created_at, et.name as error_type_name, et.severity FROM work_report_defects d JOIN error_types et ON d.error_type_id = et.id WHERE d.report_id = ? ORDER BY d.created_at `, [reportId]); return rows; } catch (error) { logger.error('부적합 원인 조회 실패', { reportId, error: error.message }); throw new DatabaseError('부적합 원인 조회 중 오류가 발생했습니다'); } }; /** * 부적합 원인 저장 (전체 교체) */ const saveReportDefectsService = async (reportId, defects) => { const db = await getDb(); try { await db.query('START TRANSACTION'); // 기존 부적합 원인 삭제 await db.execute('DELETE FROM work_report_defects WHERE report_id = ?', [reportId]); // 새 부적합 원인 추가 if (defects && defects.length > 0) { for (const defect of defects) { await db.execute(` INSERT INTO work_report_defects (report_id, error_type_id, defect_hours, note) VALUES (?, ?, ?, ?) `, [reportId, defect.error_type_id, defect.defect_hours || 0, defect.note || null]); } } // 총 부적합 시간 계산 및 daily_work_reports 업데이트 const totalErrorHours = defects ? defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0) : 0; await db.execute(` UPDATE daily_work_reports SET error_hours = ?, error_type_id = ?, work_status_id = ? WHERE id = ? `, [ totalErrorHours, defects && defects.length > 0 ? defects[0].error_type_id : null, totalErrorHours > 0 ? 2 : 1, reportId ]); await db.query('COMMIT'); logger.info('부적합 원인 저장 성공', { reportId, count: defects?.length || 0 }); return { success: true, count: defects?.length || 0, totalErrorHours }; } catch (error) { await db.query('ROLLBACK'); logger.error('부적합 원인 저장 실패', { reportId, error: error.message }); throw new DatabaseError('부적합 원인 저장 중 오류가 발생했습니다'); } }; /** * 부적합 원인 추가 (단일) */ const addReportDefectService = async (reportId, defectData) => { const db = await getDb(); try { const [result] = await db.execute(` INSERT INTO work_report_defects (report_id, error_type_id, defect_hours, note) VALUES (?, ?, ?, ?) `, [reportId, defectData.error_type_id, defectData.defect_hours || 0, defectData.note || null]); // 총 부적합 시간 업데이트 await updateTotalErrorHours(reportId); logger.info('부적합 원인 추가 성공', { reportId, defectId: result.insertId }); return { success: true, defect_id: result.insertId }; } catch (error) { logger.error('부적합 원인 추가 실패', { reportId, error: error.message }); throw new DatabaseError('부적합 원인 추가 중 오류가 발생했습니다'); } }; /** * 부적합 원인 삭제 */ const removeReportDefectService = async (defectId) => { const db = await getDb(); try { // report_id 먼저 조회 const [defect] = await db.execute('SELECT report_id FROM work_report_defects WHERE defect_id = ?', [defectId]); if (defect.length === 0) { throw new NotFoundError('부적합 원인을 찾을 수 없습니다'); } const reportId = defect[0].report_id; // 삭제 await db.execute('DELETE FROM work_report_defects WHERE defect_id = ?', [defectId]); // 총 부적합 시간 업데이트 await updateTotalErrorHours(reportId); logger.info('부적합 원인 삭제 성공', { defectId, reportId }); return { success: true }; } catch (error) { if (error instanceof NotFoundError) throw error; logger.error('부적합 원인 삭제 실패', { defectId, error: error.message }); throw new DatabaseError('부적합 원인 삭제 중 오류가 발생했습니다'); } }; /** * 총 부적합 시간 업데이트 헬퍼 */ const updateTotalErrorHours = async (reportId) => { const db = await getDb(); const [result] = await db.execute(` SELECT COALESCE(SUM(defect_hours), 0) as total FROM work_report_defects WHERE report_id = ? `, [reportId]); const totalErrorHours = result[0].total || 0; // 첫 번째 부적합 원인의 error_type_id를 대표값으로 사용 const [firstDefect] = await db.execute(` SELECT error_type_id FROM work_report_defects WHERE report_id = ? ORDER BY created_at LIMIT 1 `, [reportId]); await db.execute(` UPDATE daily_work_reports SET error_hours = ?, error_type_id = ?, work_status_id = ? WHERE id = ? `, [ totalErrorHours, firstDefect.length > 0 ? firstDefect[0].error_type_id : null, totalErrorHours > 0 ? 2 : 1, reportId ]); }; module.exports = { createWorkReportService, getWorkReportsByDateService, getWorkReportsInRangeService, getWorkReportByIdService, updateWorkReportService, removeWorkReportService, getSummaryService, // 부적합 원인 관리 getReportDefectsService, saveReportDefectsService, addReportDefectService, removeReportDefectService };