From d154514fa07fb3b753329196c6a57bdad0aa0372 Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 28 Jul 2025 11:19:28 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EC=9D=BC=EC=9D=BC=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20CRUD=20API=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dailyWorkReportController의 조회, 수정, 삭제(CRUD) 로직을 Service와 Model로 분리 - Promise 기반의 async/await 구조로 비동기 코드 개선 - 새로운 DB 스키마 v2의 명명 규칙 적용 --- .../controllers/dailyWorkReportController.js | 246 ++++++------------ api.hyungi.net/models/dailyWorkReportModel.js | 162 +++++++++++- .../services/dailyWorkReportService.js | 129 +++++++++ 3 files changed, 363 insertions(+), 174 deletions(-) diff --git a/api.hyungi.net/controllers/dailyWorkReportController.js b/api.hyungi.net/controllers/dailyWorkReportController.js index 2227fd8..3dff628 100644 --- a/api.hyungi.net/controllers/dailyWorkReportController.js +++ b/api.hyungi.net/controllers/dailyWorkReportController.js @@ -182,110 +182,29 @@ const removeMyEntry = (req, res) => { }; /** - * 📊 작업보고서 조회 (권한별 전체 조회 지원 - 핵심 수정!) + * 📊 작업보고서 조회 (V2 - Service Layer 사용) */ -const getDailyWorkReports = (req, res) => { - const { date, worker_id, created_by: requested_created_by, view_all, admin, all, no_filter, ignore_created_by } = req.query; - const current_user_id = req.user?.user_id || req.user?.id; - const user_access_level = req.user?.access_level; +const getDailyWorkReports = async (req, res) => { + try { + const userInfo = { + user_id: req.user?.user_id || req.user?.id, + role: req.user?.role || 'user' // 기본값을 'user'로 설정하여 안전하게 처리 + }; - if (!current_user_id) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' - }); - } + if (!userInfo.user_id) { + return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); + } - // 🎯 권한별 필터링 로직 개선 - const isAdmin = user_access_level === 'system' || user_access_level === 'admin'; - const hasViewAllFlag = view_all === 'true' || admin === 'true' || all === 'true' || - no_filter === 'true' || ignore_created_by === 'true' || - requested_created_by === 'all' || requested_created_by === ''; - - const canViewAll = isAdmin || hasViewAllFlag; - - // 관리자가 아니고 전체 조회 플래그도 없으면 본인 작성분으로 제한 - let final_created_by = null; - if (!canViewAll) { - final_created_by = requested_created_by || current_user_id; - } else if (requested_created_by && requested_created_by !== 'all' && requested_created_by !== '') { - final_created_by = requested_created_by; - } + const reports = await dailyWorkReportService.getDailyWorkReportsService(req.query, userInfo); + + res.json(reports); - console.log('📊 작업보고서 조회 요청:', { - date, - worker_id, - requested_created_by, - current_user_id, - user_access_level, - isAdmin, - hasViewAllFlag, - canViewAll, - final_created_by - }); - - if (date && final_created_by) { - // 날짜 + 작성자별 조회 - dailyWorkReportModel.getByDateAndCreator(date, final_created_by, (err, data) => { - if (err) { - console.error('작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - console.log(`📊 날짜+작성자별 조회 결과: ${data.length}개`); - res.json(data); - }); - } else if (date && worker_id) { - // 날짜 + 작업자별 조회 - dailyWorkReportModel.getByDateAndWorker(date, worker_id, (err, data) => { - if (err) { - console.error('작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - // 🎯 권한별 필터링 - let finalData = data; - if (!canViewAll) { - finalData = data.filter(report => report.created_by === current_user_id); - console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`); - } else { - console.log(`📊 관리자/전체 조회 권한: ${data.length}개 전체 반환`); - } - - res.json(finalData); - }); - } else if (date) { - // 날짜별 조회 - dailyWorkReportModel.getByDate(date, (err, data) => { - if (err) { - console.error('작업보고서 조회 오류:', err); - return res.status(500).json({ - error: '작업보고서 조회 중 오류가 발생했습니다.', - details: err.message - }); - } - - // 🎯 권한별 필터링 - let finalData = data; - if (!canViewAll) { - finalData = data.filter(report => report.created_by === current_user_id); - console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`); - } else { - console.log(`📊 관리자/전체 조회 권한: ${data.length}개 전체 반환`); - } - - res.json(finalData); - }); - } else { + } catch (error) { + console.error('💥 작업보고서 조회 컨트롤러 오류:', error.message); res.status(400).json({ - error: '날짜(date) 파라미터가 필요합니다.', - example: 'date=2024-06-16', - optional: ['worker_id', 'created_by', 'view_all', 'admin', 'all'] + success: false, + error: '작업보고서 조회에 실패했습니다.', + details: error.message }); } }; @@ -498,94 +417,75 @@ const getMonthlySummary = (req, res) => { }; /** - * ✏️ 작업보고서 수정 + * ✏️ 작업보고서 수정 (V2 - Service Layer 사용) */ -const updateWorkReport = (req, res) => { - const { id } = req.params; - const updateData = req.body; - const updated_by = req.user?.user_id || req.user?.id; +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 (!updated_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' + 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 }); } - - updateData.updated_by = updated_by; - - console.log(`✏️ 작업보고서 수정 요청: id=${id}, 수정자=${updated_by}`); - - dailyWorkReportModel.updateById(id, updateData, (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: '수정할 작업보고서를 찾을 수 없습니다.', - id: id - }); - } - - console.log(`✅ 작업보고서 수정 완료: id=${id}`); - res.json({ - message: '작업보고서가 성공적으로 수정되었습니다.', - id: id, - affected_rows: affectedRows, - updated_by, - timestamp: new Date().toISOString() - }); - }); }; /** - * 🗑️ 특정 작업보고서 삭제 + * 🗑️ 특정 작업보고서 삭제 (V2 - Service Layer 사용) */ -const removeDailyWorkReport = (req, res) => { - const { id } = req.params; - const deleted_by = req.user?.user_id || req.user?.id; +const removeDailyWorkReport = async (req, res) => { + try { + const { id: reportId } = req.params; + const userInfo = { + user_id: req.user?.user_id || req.user?.id, + }; - if (!deleted_by) { - return res.status(401).json({ - error: '사용자 인증 정보가 없습니다.' + if (!userInfo.user_id) { + return res.status(401).json({ error: '사용자 인증 정보가 없습니다.' }); + } + + 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 }); } - - console.log(`🗑️ 작업보고서 삭제 요청: id=${id}, 삭제자=${deleted_by}`); - - dailyWorkReportModel.removeById(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: '삭제할 작업보고서를 찾을 수 없습니다.', - id: id - }); - } - - console.log(`✅ 작업보고서 삭제 완료: id=${id}`); - res.json({ - message: '작업보고서가 성공적으로 삭제되었습니다.', - id: id, - affected_rows: affectedRows, - deleted_by, - timestamp: new Date().toISOString() - }); - }); }; /** - * 🗑️ 작업자의 특정 날짜 전체 삭제 + * ��️ 작업자의 특정 날짜 전체 삭제 */ const removeDailyWorkReportByDateAndWorker = (req, res) => { const { date, worker_id } = req.params; diff --git a/api.hyungi.net/models/dailyWorkReportModel.js b/api.hyungi.net/models/dailyWorkReportModel.js index 67e5cd8..f3fe502 100644 --- a/api.hyungi.net/models/dailyWorkReportModel.js +++ b/api.hyungi.net/models/dailyWorkReportModel.js @@ -845,6 +845,163 @@ const createReportEntries = async ({ report_date, worker_id, entries }) => { } }; +/** + * [V2] 공통 SELECT 쿼리 (새로운 스키마 기준) + */ +const getSelectQueryV2 = () => ` + SELECT + dwr.report_id, + dwr.report_date, + dwr.worker_id, + dwr.project_id, + dwr.task_id, + dwr.work_hours, + dwr.is_error, + dwr.error_type_code_id, + dwr.created_by_user_id, + w.worker_name, + p.project_name, + t.task_name, + c.code_name as error_type_name, + u.name as created_by_name, + dwr.created_at + FROM daily_work_reports dwr + LEFT JOIN workers w ON dwr.worker_id = w.worker_id + LEFT JOIN projects p ON dwr.project_id = p.project_id + LEFT JOIN tasks t ON dwr.task_id = t.task_id + LEFT JOIN users u ON dwr.created_by_user_id = u.user_id + LEFT JOIN codes c ON dwr.error_type_code_id = c.code_id AND c.code_type_id = 'ERROR_TYPE' +`; + +/** + * [V2] 옵션 기반으로 작업 보고서를 조회합니다. (Promise 기반) + * @param {object} options - 조회 조건 (date, worker_id, created_by_user_id 등) + * @returns {Promise} 조회된 작업 보고서 배열 + */ +const getReportsWithOptions = async (options) => { + const db = await getDb(); + let whereConditions = []; + let queryParams = []; + + if (options.date) { + whereConditions.push('dwr.report_date = ?'); + queryParams.push(options.date); + } + if (options.worker_id) { + whereConditions.push('dwr.worker_id = ?'); + queryParams.push(options.worker_id); + } + if (options.created_by_user_id) { + whereConditions.push('dwr.created_by_user_id = ?'); + queryParams.push(options.created_by_user_id); + } + // 필요에 따라 다른 조건 추가 가능 (project_id 등) + + if (whereConditions.length === 0) { + throw new Error('조회 조건이 하나 이상 필요합니다.'); + } + + const whereClause = whereConditions.join(' AND '); + const sql = `${getSelectQueryV2()} WHERE ${whereClause} ORDER BY w.worker_name ASC, p.project_name ASC`; + + try { + const [rows] = await db.query(sql, queryParams); + return rows; + } catch (err) { + console.error('[Model] 작업 보고서 조회 오류:', err); + throw new Error('데이터베이스에서 작업 보고서를 조회하는 중 오류가 발생했습니다.'); + } +}; + +/** + * [V2] ID를 기준으로 특정 작업 보고서 항목을 수정합니다. (Promise 기반) + * @param {string} reportId - 수정할 보고서의 ID + * @param {object} updateData - 수정할 필드와 값 + * @returns {Promise} 영향을 받은 행의 수 + */ +const updateReportById = async (reportId, updateData) => { + const db = await getDb(); + + // 허용된 필드 목록 (보안 및 안정성) + const allowedFields = ['project_id', 'task_id', 'work_hours', 'is_error', 'error_type_code_id']; + const setClauses = []; + const queryParams = []; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + setClauses.push(`${field} = ?`); + queryParams.push(updateData[field]); + } + } + + // updated_by_user_id는 항상 업데이트 + if (updateData.updated_by_user_id) { + setClauses.push('updated_by_user_id = ?'); + queryParams.push(updateData.updated_by_user_id); + } + + if (setClauses.length === 0) { + throw new Error('수정할 데이터가 없습니다.'); + } + + queryParams.push(reportId); + + const sql = `UPDATE daily_work_reports SET ${setClauses.join(', ')} WHERE report_id = ?`; + + try { + const [result] = await db.query(sql, queryParams); + return result.affectedRows; + } catch (err) { + console.error(`[Model] 작업 보고서 수정 오류 (id: ${reportId}):`, err); + throw new Error('데이터베이스에서 작업 보고서를 수정하는 중 오류가 발생했습니다.'); + } +}; + +/** + * [V2] ID를 기준으로 특정 작업 보고서를 삭제합니다. (Promise 기반) + * @param {string} reportId - 삭제할 보고서 ID + * @param {number} deletedByUserId - 삭제를 수행하는 사용자 ID + * @returns {Promise} 영향을 받은 행의 수 + */ +const removeReportById = async (reportId, deletedByUserId) => { + const db = await getDb(); + const conn = await db.getConnection(); + + try { + await conn.beginTransaction(); + + // 감사 로그를 위해 삭제 전 정보 조회 + const [reportInfo] = await conn.query('SELECT * FROM daily_work_reports WHERE report_id = ?', [reportId]); + + // 실제 삭제 작업 + const [result] = await conn.query('DELETE FROM daily_work_reports WHERE report_id = ?', [reportId]); + + // 감사 로그 추가 (삭제된 항목이 있고, 삭제자가 명시된 경우) + if (reportInfo.length > 0 && deletedByUserId) { + try { + await conn.query( + `INSERT INTO work_report_audit_log (action, report_id, old_values, changed_by, change_reason) VALUES (?, ?, ?, ?, ?)`, + ['DELETE', reportId, JSON.stringify(reportInfo[0]), deletedByUserId, 'Manual deletion by user'] + ); + } catch (auditErr) { + console.warn('감사 로그 추가 실패:', auditErr.message); + // 감사 로그 실패가 전체 트랜잭션을 롤백시키지는 않음 + } + } + + await conn.commit(); + return result.affectedRows; + + } catch (err) { + await conn.rollback(); + console.error(`[Model] 작업 보고서 삭제 오류 (id: ${reportId}):`, err); + throw new Error('데이터베이스에서 작업 보고서를 삭제하는 중 오류가 발생했습니다.'); + } finally { + conn.release(); + } +}; + + // 모든 함수 내보내기 (기존 기능 + 누적 기능) module.exports = { // 📋 마스터 데이터 @@ -880,5 +1037,8 @@ module.exports = { getStatistics, // 새로 추가된 V2 함수 - createReportEntries + createReportEntries, + getReportsWithOptions, + updateReportById, + removeReportById }; \ No newline at end of file diff --git a/api.hyungi.net/services/dailyWorkReportService.js b/api.hyungi.net/services/dailyWorkReportService.js index f08ec6f..9986ba4 100644 --- a/api.hyungi.net/services/dailyWorkReportService.js +++ b/api.hyungi.net/services/dailyWorkReportService.js @@ -79,6 +79,135 @@ const createDailyWorkReportService = async (reportData) => { } }; +/** + * 사용자 권한과 요청 파라미터에 따라 일일 작업 보고서를 조회하는 비즈니스 로직을 처리합니다. + * @param {object} queryParams - 컨트롤러에서 전달된 쿼리 파라미터 + * @param {object} userInfo - 요청을 보낸 사용자의 정보 (id, role 등) + * @returns {Promise} 조회된 작업 보고서 배열 + */ +const getDailyWorkReportsService = async (queryParams, userInfo) => { + const { date, worker_id, created_by: requested_created_by, view_all } = queryParams; + const { user_id: current_user_id, role } = userInfo; + + if (!date) { + throw new Error('조회를 위해 날짜(date)는 필수입니다.'); + } + + // 관리자 여부 확인 + const isAdmin = role === 'system' || role === 'admin'; + const canViewAll = isAdmin || view_all === 'true'; + + // 모델에 전달할 조회 옵션 객체 생성 + const options = { date }; + + if (worker_id) { + options.worker_id = parseInt(worker_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가 명시되지 않으면 모든 작성자의 데이터를 조회 + + console.log('📊 [Service] 작업보고서 조회 요청:', { ...options, requester: current_user_id, isAdmin }); + + try { + // 모델 함수 호출 + const reports = await dailyWorkReportModel.getReportsWithOptions(options); + console.log(`✅ [Service] 작업보고서 ${reports.length}개 조회 성공`); + return reports; + } catch (error) { + console.error('[Service] 작업보고서 조회 중 오류 발생:', error); + throw error; + } +}; + +/** + * 특정 작업 보고서 항목을 수정하는 비즈니스 로직을 처리합니다. + * @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 Error('수정을 위해 보고서 ID와 수정할 데이터가 필요합니다.'); + } + + const modelUpdateData = { ...updateData, updated_by_user_id: updated_by }; + + console.log(`✏️ [Service] 작업보고서 수정 요청: id=${reportId}`); + + try { + const affectedRows = await dailyWorkReportModel.updateReportById(reportId, modelUpdateData); + + if (affectedRows === 0) { + // 에러를 발생시켜 컨트롤러에서 404 처리를 할 수 있도록 함 + const notFoundError = new Error('수정할 작업보고서를 찾을 수 없습니다.'); + notFoundError.statusCode = 404; + throw notFoundError; + } + + console.log(`✅ [Service] 작업보고서 수정 성공: id=${reportId}`); + return { + message: '작업보고서가 성공적으로 수정되었습니다.', + report_id: reportId, + affected_rows: affectedRows + }; + } catch (error) { + console.error(`[Service] 작업보고서 수정 중 오류 발생 (id: ${reportId}):`, error); + throw error; + } +}; + +/** + * 특정 작업 보고서 항목을 삭제하는 비즈니스 로직을 처리합니다. + * @param {string} reportId - 삭제할 보고서의 ID + * @param {object} userInfo - 요청을 보낸 사용자의 정보 + * @returns {Promise} 삭제 결과 + */ +const removeDailyWorkReportService = async (reportId, userInfo) => { + const { user_id: deleted_by } = userInfo; + + if (!reportId) { + throw new Error('삭제를 위해 보고서 ID가 필요합니다.'); + } + + console.log(`🗑️ [Service] 작업보고서 삭제 요청: id=${reportId}`); + + try { + // 모델 함수는 삭제 전 권한 검사를 위해 deleted_by 정보를 받을 수 있습니다 (현재 모델에서는 미사용). + const affectedRows = await dailyWorkReportModel.removeReportById(reportId, deleted_by); + + if (affectedRows === 0) { + const notFoundError = new Error('삭제할 작업보고서를 찾을 수 없습니다.'); + notFoundError.statusCode = 404; + throw notFoundError; + } + + console.log(`✅ [Service] 작업보고서 삭제 성공: id=${reportId}`); + return { + message: '작업보고서가 성공적으로 삭제되었습니다.', + report_id: reportId, + affected_rows: affectedRows + }; + } catch (error) { + console.error(`[Service] 작업보고서 삭제 중 오류 발생 (id: ${reportId}):`, error); + throw error; + } +}; + + module.exports = { createDailyWorkReportService, + getDailyWorkReportsService, + updateWorkReportService, + removeDailyWorkReportService, }; \ No newline at end of file