From 09f6756da7bda554cc7be1b8bbddf5f9d7dee7fe Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Thu, 11 Dec 2025 12:00:16 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20Phase=203.1=20-=20DailyWorkReport?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 작업 보고서 서비스와 컨트롤러를 새로운 에러 핸들링 및 로깅 시스템으로 업그레이드하여 코드 품질 및 유지보수성 향상 주요 변경사항: services/dailyWorkReportService.js: - 새로운 커스텀 에러 클래스 적용 * ValidationError: 유효성 검증 실패 * NotFoundError: 리소스를 찾을 수 없음 * DatabaseError: 데이터베이스 오류 - console.log → logger 유틸리티로 전환 * 구조화된 로깅 (context 포함) * 로그 레벨 분리 (info, warn, error) * 파일 로깅 지원 - 상세한 에러 컨텍스트 제공 * 필수 필드, 받은 값, 유효 범위 등 * 디버깅 및 문제 해결 용이성 향상 controllers/dailyWorkReportController.js: - 새로운 에러 클래스 import - asyncHandler 미들웨어 통일 - createDailyWorkReport 함수 간소화 * try-catch 제거 (asyncHandler가 처리) * 표준 JSON 응답 포맷 사용 개선 효과: - 에러 메시지 명확성 향상 - 로그 분석 및 모니터링 용이 - 일관된 에러 처리 패턴 - 테스트 가능성 향상 - 프로덕션 환경 파일 로깅 지원 파일 통계: - 2개 파일 수정 - +115 -65 (net +50 lines) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../controllers/dailyWorkReportController.js | 30 ++-- .../services/dailyWorkReportService.js | 150 +++++++++++------- 2 files changed, 115 insertions(+), 65 deletions(-) diff --git a/api.hyungi.net/controllers/dailyWorkReportController.js b/api.hyungi.net/controllers/dailyWorkReportController.js index ee8ee3d..7d9b78b 100644 --- a/api.hyungi.net/controllers/dailyWorkReportController.js +++ b/api.hyungi.net/controllers/dailyWorkReportController.js @@ -1,8 +1,17 @@ -// controllers/dailyWorkReportController.js - 권한별 전체 조회 지원 버전 +/** + * 일일 작업 보고서 컨트롤러 + * + * 작업 보고서 API 엔드포인트 핸들러 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ + const dailyWorkReportModel = require('../models/dailyWorkReportModel'); const dailyWorkReportService = require('../services/dailyWorkReportService'); -const { ApiError, asyncHandler, handleDatabaseError, handleNotFoundError } = require('../utils/errorHandler'); -const { validateSchema, schemas } = require('../utils/validator'); +const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors'); +const { asyncHandler } = require('../middlewares/errorHandler'); +const logger = require('../utils/logger'); /** * 📝 작업보고서 생성 (V2 - Service Layer 사용) @@ -14,16 +23,13 @@ const createDailyWorkReport = asyncHandler(async (req, res) => { created_by_name: req.user?.name || req.user?.username || '알 수 없는 사용자' }; - console.log('🔍 Controller에서 받은 데이터:', JSON.stringify(reportData, null, 2)); + const result = await dailyWorkReportService.createDailyWorkReportService(reportData); - try { - const result = await dailyWorkReportService.createDailyWorkReportService(reportData); - - res.created(result, '작업보고서가 성공적으로 생성되었습니다.'); - } catch (error) { - console.error('💥 작업보고서 생성 컨트롤러 오류:', error.message); - throw new ApiError('작업보고서 생성에 실패했습니다.', 400); - } + res.status(201).json({ + success: true, + data: result, + message: '작업보고서가 성공적으로 생성되었습니다' + }); }); /** diff --git a/api.hyungi.net/services/dailyWorkReportService.js b/api.hyungi.net/services/dailyWorkReportService.js index 574cf25..5b058a3 100644 --- a/api.hyungi.net/services/dailyWorkReportService.js +++ b/api.hyungi.net/services/dailyWorkReportService.js @@ -1,4 +1,15 @@ +/** + * 일일 작업 보고서 서비스 + * + * 작업 보고서 관련 비즈니스 로직 처리 + * + * @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'); /** * 일일 작업 보고서를 생성하는 비즈니스 로직을 처리합니다. @@ -10,36 +21,49 @@ const createDailyWorkReportService = async (reportData) => { // 1. 기본 유효성 검사 if (!report_date || !worker_id || !work_entries) { - throw new Error('필수 필드(report_date, worker_id, work_entries)가 누락되었습니다.'); + throw new ValidationError('필수 필드가 누락되었습니다', { + required: ['report_date', 'worker_id', 'work_entries'], + received: { report_date, worker_id, work_entries: !!work_entries } + }); } if (!Array.isArray(work_entries) || work_entries.length === 0) { - throw new Error('최소 하나의 작업 항목(work_entries)이 필요합니다.'); + throw new ValidationError('최소 하나의 작업 항목이 필요합니다'); } if (!created_by) { - throw new Error('사용자 인증 정보(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 Error(`작업 항목 ${i + 1}의 '${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 Error(`작업 항목 ${i + 1}이 에러 상태인 경우 '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 Error(`작업 항목 ${i + 1}의 작업 시간이 유효하지 않습니다 (0 초과 24 이하).`); + throw new ValidationError(`작업 시간이 유효하지 않습니다 (0 초과 24 이하)`, { + entry: i + 1, + work_hours: entry.work_hours, + valid_range: '0 < hours <= 24' + }); } } @@ -57,7 +81,7 @@ const createDailyWorkReportService = async (reportData) => { })) }; - console.log('📝 [Service] 작업보고서 생성 요청:', { + logger.info('작업보고서 생성 요청', { date: report_date, worker: worker_id, creator: created_by_name, @@ -67,15 +91,22 @@ const createDailyWorkReportService = async (reportData) => { // 4. 모델 함수 호출 try { const result = await dailyWorkReportModel.createReportEntries(modelData); - console.log('✅ [Service] 작업보고서 생성 성공:', result); + + logger.info('작업보고서 생성 성공', { + report_id: result.report_id, + entries_count: modelData.entries.length + }); + return { message: '작업보고서가 성공적으로 생성되었습니다.', ...result }; } catch (error) { - console.error('[Service] 작업보고서 생성 중 오류 발생:', error); - // 모델에서 발생한 오류를 그대로 상위로 전달 - throw error; + logger.error('작업보고서 생성 실패', { + error: error.message, + stack: error.stack + }); + throw new DatabaseError('작업보고서 생성 중 데이터베이스 오류가 발생했습니다'); } }; @@ -91,7 +122,10 @@ const getDailyWorkReportsService = async (queryParams, userInfo) => { // 날짜 또는 날짜 범위 중 하나는 필수 if (!date && (!start_date || !end_date)) { - throw new Error('조회를 위해 날짜(date) 또는 날짜 범위(start_date, end_date)가 필요합니다.'); + throw new ValidationError('날짜 또는 날짜 범위가 필요합니다', { + required: 'date OR (start_date AND end_date)', + received: { date, start_date, end_date } + }); } // 관리자 여부 확인 @@ -122,16 +156,15 @@ const getDailyWorkReportsService = async (queryParams, userInfo) => { } // created_by_user_id가 명시되지 않으면 모든 작성자의 데이터를 조회 - console.log('📊 [Service] 작업보고서 조회 요청:', { ...options, requester: current_user_id, isAdmin }); + logger.info('작업보고서 조회 요청', { ...options, requester: current_user_id, isAdmin }); try { - // 모델 함수 호출 const reports = await dailyWorkReportModel.getReportsWithOptions(options); - console.log(`✅ [Service] 작업보고서 ${reports.length}개 조회 성공`); + logger.info('작업보고서 조회 성공', { count: reports.length }); return reports; } catch (error) { - console.error('[Service] 작업보고서 조회 중 오류 발생:', error); - throw error; + logger.error('작업보고서 조회 실패', { error: error.message }); + throw new DatabaseError('작업보고서 조회 중 데이터베이스 오류가 발생했습니다'); } }; @@ -146,32 +179,36 @@ const updateWorkReportService = async (reportId, updateData, userInfo) => { const { user_id: updated_by } = userInfo; if (!reportId || !updateData || Object.keys(updateData).length === 0) { - throw new Error('수정을 위해 보고서 ID와 수정할 데이터가 필요합니다.'); + throw new ValidationError('보고서 ID와 수정할 데이터가 필요합니다', { + reportId, + hasUpdateData: !!updateData, + updateFieldsCount: updateData ? Object.keys(updateData).length : 0 + }); } const modelUpdateData = { ...updateData, updated_by_user_id: updated_by }; - console.log(`✏️ [Service] 작업보고서 수정 요청: id=${reportId}`); + logger.info('작업보고서 수정 요청', { reportId, updatedBy: updated_by }); try { const affectedRows = await dailyWorkReportModel.updateReportById(reportId, modelUpdateData); if (affectedRows === 0) { - // 에러를 발생시켜 컨트롤러에서 404 처리를 할 수 있도록 함 - const notFoundError = new Error('수정할 작업보고서를 찾을 수 없습니다.'); - notFoundError.statusCode = 404; - throw notFoundError; + throw new NotFoundError('수정할 작업보고서를 찾을 수 없습니다'); } - console.log(`✅ [Service] 작업보고서 수정 성공: id=${reportId}`); + logger.info('작업보고서 수정 성공', { reportId }); return { message: '작업보고서가 성공적으로 수정되었습니다.', report_id: reportId, affected_rows: affectedRows }; } catch (error) { - console.error(`[Service] 작업보고서 수정 중 오류 발생 (id: ${reportId}):`, error); - throw error; + if (error instanceof NotFoundError) { + throw error; + } + logger.error('작업보고서 수정 실패', { reportId, error: error.message }); + throw new DatabaseError('작업보고서 수정 중 데이터베이스 오류가 발생했습니다'); } }; @@ -185,30 +222,30 @@ const removeDailyWorkReportService = async (reportId, userInfo) => { const { user_id: deleted_by } = userInfo; if (!reportId) { - throw new Error('삭제를 위해 보고서 ID가 필요합니다.'); + throw new ValidationError('삭제할 보고서 ID가 필요합니다'); } - console.log(`🗑️ [Service] 작업보고서 삭제 요청: id=${reportId}`); + logger.info('작업보고서 삭제 요청', { reportId, deletedBy: deleted_by }); try { - // 모델 함수는 삭제 전 권한 검사를 위해 deleted_by 정보를 받을 수 있습니다 (현재 모델에서는 미사용). const affectedRows = await dailyWorkReportModel.removeReportById(reportId, deleted_by); if (affectedRows === 0) { - const notFoundError = new Error('삭제할 작업보고서를 찾을 수 없습니다.'); - notFoundError.statusCode = 404; - throw notFoundError; + throw new NotFoundError('삭제할 작업보고서를 찾을 수 없습니다'); } - console.log(`✅ [Service] 작업보고서 삭제 성공: id=${reportId}`); + logger.info('작업보고서 삭제 성공', { reportId }); return { message: '작업보고서가 성공적으로 삭제되었습니다.', report_id: reportId, affected_rows: affectedRows }; } catch (error) { - console.error(`[Service] 작업보고서 삭제 중 오류 발생 (id: ${reportId}):`, error); - throw error; + if (error instanceof NotFoundError) { + throw error; + } + logger.error('작업보고서 삭제 실패', { reportId, error: error.message }); + throw new DatabaseError('작업보고서 삭제 중 데이터베이스 오류가 발생했습니다'); } }; @@ -221,16 +258,18 @@ const getStatisticsService = async (queryParams) => { const { start_date, end_date } = queryParams; if (!start_date || !end_date) { - throw new Error('통계 조회를 위해 시작일(start_date)과 종료일(end_date)이 모두 필요합니다.'); + throw new ValidationError('시작일과 종료일이 모두 필요합니다', { + required: ['start_date', 'end_date'], + received: { start_date, end_date } + }); } - console.log(`📈 [Service] 통계 조회 요청: ${start_date} ~ ${end_date}`); + logger.info('작업보고서 통계 조회 요청', { start_date, end_date }); try { - // 모델의 getStatistics 함수가 Promise를 반환하도록 수정 필요 const statsData = await dailyWorkReportModel.getStatistics(start_date, end_date); - - console.log('✅ [Service] 통계 조회 성공'); + + logger.info('작업보고서 통계 조회 성공', { period: `${start_date} ~ ${end_date}` }); return { ...statsData, metadata: { @@ -239,8 +278,8 @@ const getStatisticsService = async (queryParams) => { } }; } catch (error) { - console.error('[Service] 통계 조회 중 오류 발생:', error); - throw error; + logger.error('통계 조회 실패', { error: error.message }); + throw new DatabaseError('통계 조회 중 데이터베이스 오류가 발생했습니다'); } }; @@ -253,22 +292,27 @@ const getSummaryService = async (queryParams) => { const { date, worker_id } = queryParams; if (!date && !worker_id) { - throw new Error('일일 또는 작업자별 요약 조회를 위해 날짜(date) 또는 작업자 ID(worker_id)가 필요합니다.'); + throw new ValidationError('날짜 또는 작업자 ID가 필요합니다', { + required: 'date OR worker_id', + received: { date, worker_id } + }); } try { if (date) { - console.log(`📊 [Service] 일일 요약 조회 요청: date=${date}`); - // 모델의 getSummaryByDate 함수가 Promise를 반환하도록 수정 필요 - return await dailyWorkReportModel.getSummaryByDate(date); + logger.info('일일 요약 조회 요청', { date }); + const result = await dailyWorkReportModel.getSummaryByDate(date); + logger.info('일일 요약 조회 성공', { date }); + return result; } else { // worker_id - console.log(`📊 [Service] 작업자별 요약 조회 요청: worker_id=${worker_id}`); - // 모델의 getSummaryByWorker 함수가 Promise를 반환하도록 수정 필요 - return await dailyWorkReportModel.getSummaryByWorker(worker_id); + logger.info('작업자별 요약 조회 요청', { worker_id }); + const result = await dailyWorkReportModel.getSummaryByWorker(worker_id); + logger.info('작업자별 요약 조회 성공', { worker_id }); + return result; } } catch (error) { - console.error('[Service] 요약 정보 조회 중 오류 발생:', error); - throw error; + logger.error('요약 정보 조회 실패', { error: error.message }); + throw new DatabaseError('요약 정보 조회 중 데이터베이스 오류가 발생했습니다'); } };