// models/dailyWorkReportModel.js - 누적입력 방식 + 모든 기존 기능 포함 const { getDb } = require('../dbPool'); /** * 📋 마스터 데이터 조회 함수들 */ const getAllWorkTypes = async (callback) => { try { const db = await getDb(); const [rows] = await db.query('SELECT * FROM work_types ORDER BY name ASC'); callback(null, rows); } catch (err) { console.error('작업 유형 조회 오류:', err); callback(err); } }; const getAllWorkStatusTypes = async (callback) => { try { const db = await getDb(); const [rows] = await db.query('SELECT * FROM work_status_types ORDER BY id ASC'); callback(null, rows); } catch (err) { console.error('업무 상태 유형 조회 오류:', err); callback(err); } }; const getAllErrorTypes = async (callback) => { try { const db = await getDb(); const [rows] = await db.query('SELECT * FROM error_types ORDER BY name ASC'); callback(null, rows); } catch (err) { console.error('에러 유형 조회 오류:', err); callback(err); } }; /** * 🔄 누적 추가 전용 함수 (createDailyReport 대체) - 절대 삭제 안함! */ const createDailyReport = async (reportData, callback) => { const { report_date, worker_id, work_entries, created_by, created_by_name, total_hours } = reportData; const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); console.log(`📝 ${created_by_name}이 ${report_date} ${worker_id}번 작업자에게 데이터 추가 중...`); // ✅ 수정된 쿼리 (테이블 alias 추가): const [existingReports] = await conn.query( `SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours FROM daily_work_reports dwr LEFT JOIN users u ON dwr.created_by = u.user_id WHERE dwr.report_date = ? AND dwr.worker_id = ? GROUP BY dwr.created_by`, [report_date, worker_id] ); console.log('기존 데이터 (삭제하지 않음):', existingReports); // 2. ✅ 삭제 없이 새로운 데이터만 추가! const insertedIds = []; for (const entry of work_entries) { const { project_id, work_type_id, work_status_id, error_type_id, work_hours } = entry; const [insertResult] = await conn.query( `INSERT INTO daily_work_reports (report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())`, [report_date, worker_id, project_id, work_type_id, work_status_id || 1, error_type_id || null, work_hours, created_by] ); insertedIds.push(insertResult.insertId); } // ✅ 수정된 쿼리: const [finalReports] = await conn.query( `SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours FROM daily_work_reports dwr LEFT JOIN users u ON dwr.created_by = u.user_id WHERE dwr.report_date = ? AND dwr.worker_id = ? GROUP BY dwr.created_by`, [report_date, worker_id] ); const grandTotal = finalReports.reduce((sum, report) => sum + parseFloat(report.total_hours || 0), 0); const myTotal = finalReports.find(r => r.created_by === created_by)?.total_hours || 0; console.log('최종 결과:'); finalReports.forEach(report => { console.log(` - ${report.created_by_name}: ${report.total_hours}시간 (${report.count}개 항목)`); }); console.log(` 📊 총합: ${grandTotal}시간`); // 4. 감사 로그 추가 try { await conn.query( `INSERT INTO work_report_audit_log (action, report_id, new_values, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, ?, NOW())`, [ 'ADD_ACCUMULATE', insertedIds[0] || null, JSON.stringify({ report_date, worker_id, work_entries_count: work_entries.length, added_hours: total_hours, my_total: myTotal, grand_total: grandTotal, contributors: finalReports.map(r => ({ name: r.created_by_name, hours: r.total_hours })) }), created_by, `누적 추가 by ${created_by_name} - 삭제 없음` ] ); } catch (auditErr) { console.warn('감사 로그 추가 실패:', auditErr.message); } await conn.commit(); callback(null, { success: true, inserted_count: insertedIds.length, deleted_count: 0, // 항상 0 (삭제 안함) action: 'accumulated', message: `${created_by_name}이 ${total_hours}시간 추가했습니다. (개인 총 ${myTotal}시간, 전체 총 ${grandTotal}시간)`, final_summary: { my_total: parseFloat(myTotal), grand_total: grandTotal, total_contributors: finalReports.length, contributors: finalReports } }); } catch (err) { await conn.rollback(); console.error('작업보고서 누적 추가 오류:', err); callback(err); } finally { conn.release(); } }; /** * 📊 특정 날짜 + 작업자 + 작성자의 누적 현황 조회 */ const getMyAccumulatedHours = async (date, worker_id, created_by, callback) => { try { const db = await getDb(); const sql = ` SELECT SUM(work_hours) as my_total_hours, COUNT(*) as my_entry_count, GROUP_CONCAT( CONCAT(p.project_name, ':', work_hours, 'h') ORDER BY created_at ) as my_entries FROM daily_work_reports dwr LEFT JOIN projects p ON dwr.project_id = p.project_id WHERE dwr.report_date = ? AND dwr.worker_id = ? AND dwr.created_by = ? `; const [rows] = await db.query(sql, [date, worker_id, created_by]); callback(null, rows[0] || { my_total_hours: 0, my_entry_count: 0, my_entries: null }); } catch (err) { console.error('개인 누적 현황 조회 오류:', err); callback(err); } }; /** * 📊 누적 현황 조회 - 날짜+작업자별 (모든 기여자) */ const getAccumulatedReportsByDate = async (date, worker_id, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + ` WHERE dwr.report_date = ? AND dwr.worker_id = ? ORDER BY dwr.created_by, dwr.created_at ASC `; const [rows] = await db.query(sql, [date, worker_id]); callback(null, rows); } catch (err) { console.error('누적 현황 조회 오류:', err); callback(err); } }; /** * 📊 기여자별 요약 조회 */ const getContributorsByDate = async (date, worker_id, callback) => { try { const db = await getDb(); const sql = ` SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as entry_count, SUM(dwr.work_hours) as total_hours, MIN(dwr.created_at) as first_entry, MAX(dwr.created_at) as last_entry, GROUP_CONCAT( CONCAT(p.project_name, ':', dwr.work_hours, 'h') ORDER BY dwr.created_at SEPARATOR ', ' ) as entry_details FROM daily_work_reports dwr LEFT JOIN users u ON dwr.created_by = u.user_id LEFT JOIN projects p ON dwr.project_id = p.project_id WHERE dwr.report_date = ? AND dwr.worker_id = ? GROUP BY dwr.created_by ORDER BY total_hours DESC, first_entry ASC `; const [rows] = await db.query(sql, [date, worker_id]); callback(null, rows); } catch (err) { console.error('기여자별 요약 조회 오류:', err); callback(err); } }; /** * 🗑️ 특정 작업 항목만 삭제 (개별 삭제) */ const removeSpecificEntry = async (entry_id, deleted_by, callback) => { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); // 삭제 전 정보 확인 const [entryInfo] = await conn.query( `SELECT dwr.*, w.worker_name, p.project_name, u.name as created_by_name 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 users u ON dwr.created_by = u.user_id WHERE dwr.id = ?`, [entry_id] ); if (entryInfo.length === 0) { await conn.rollback(); return callback(new Error('삭제할 항목을 찾을 수 없습니다.')); } const entry = entryInfo[0]; // 권한 확인: 본인이 작성한 것만 삭제 가능 if (entry.created_by !== deleted_by) { await conn.rollback(); return callback(new Error('본인이 작성한 항목만 삭제할 수 있습니다.')); } // 개별 항목 삭제 const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [entry_id]); // 감사 로그 try { await conn.query( `INSERT INTO work_report_audit_log (action, report_id, old_values, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, ?, NOW())`, [ 'DELETE_SINGLE', entry_id, JSON.stringify({ worker_name: entry.worker_name, project_name: entry.project_name, work_hours: entry.work_hours, report_date: entry.report_date }), deleted_by, `개별 항목 삭제` ] ); } catch (auditErr) { console.warn('감사 로그 추가 실패:', auditErr.message); } await conn.commit(); callback(null, { success: true, deleted_entry: { worker_name: entry.worker_name, project_name: entry.project_name, work_hours: entry.work_hours } }); } catch (err) { await conn.rollback(); console.error('개별 항목 삭제 오류:', err); callback(err); } finally { conn.release(); } }; /** * 공통 SELECT 쿼리 부분 */ const getSelectQuery = () => ` SELECT dwr.id, dwr.report_date, dwr.worker_id, dwr.project_id, dwr.work_type_id, dwr.work_status_id, dwr.error_type_id, dwr.work_hours, dwr.created_by, w.worker_name, p.project_name, wt.name as work_type_name, wst.name as work_status_name, et.name as error_type_name, u.name as created_by_name, dwr.created_at, dwr.updated_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 work_types wt ON dwr.work_type_id = wt.id LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id LEFT JOIN error_types et ON dwr.error_type_id = et.id LEFT JOIN users u ON dwr.created_by = u.user_id `; /** * 7. ID로 작업보고서 조회 */ const getById = async (id, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + 'WHERE dwr.id = ?'; const [rows] = await db.query(sql, [id]); callback(null, rows[0] || null); } catch (err) { console.error('ID로 작업보고서 조회 오류:', err); callback(err); } }; /** * 8. 일일 작업보고서 조회 (날짜별) */ const getByDate = async (date, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + ` WHERE dwr.report_date = ? ORDER BY w.worker_name ASC, p.project_name ASC, dwr.id ASC `; const [rows] = await db.query(sql, [date]); callback(null, rows); } catch (err) { console.error('날짜별 작업보고서 조회 오류:', err); callback(err); } }; /** * 9. 일일 작업보고서 조회 (날짜 + 작성자별) */ const getByDateAndCreator = async (date, created_by, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + ` WHERE dwr.report_date = ? AND dwr.created_by = ? ORDER BY w.worker_name ASC, p.project_name ASC, dwr.id ASC `; const [rows] = await db.query(sql, [date, created_by]); callback(null, rows); } catch (err) { console.error('날짜+작성자별 작업보고서 조회 오류:', err); callback(err); } }; /** * 10. 일일 작업보고서 조회 (작업자별) */ const getByWorker = async (worker_id, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + ` WHERE dwr.worker_id = ? ORDER BY dwr.report_date DESC, dwr.id ASC `; const [rows] = await db.query(sql, [worker_id]); callback(null, rows); } catch (err) { console.error('작업자별 작업보고서 조회 오류:', err); callback(err); } }; /** * 11. 일일 작업보고서 조회 (날짜 + 작업자) */ const getByDateAndWorker = async (date, worker_id, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + ` WHERE dwr.report_date = ? AND dwr.worker_id = ? ORDER BY dwr.id ASC `; const [rows] = await db.query(sql, [date, worker_id]); callback(null, rows); } catch (err) { console.error('날짜+작업자별 작업보고서 조회 오류:', err); callback(err); } }; /** * 12. 기간별 조회 */ const getByRange = async (start_date, end_date, callback) => { try { const db = await getDb(); const sql = getSelectQuery() + ` WHERE dwr.report_date BETWEEN ? AND ? ORDER BY dwr.report_date DESC, w.worker_name ASC, dwr.id ASC `; const [rows] = await db.query(sql, [start_date, end_date]); callback(null, rows); } catch (err) { console.error('기간별 작업보고서 조회 오류:', err); callback(err); } }; /** * 13. 상세 검색 (페이지네이션 포함) */ const searchWithDetails = async (params, callback) => { const { start_date, end_date, worker_id, project_id, work_status_id, created_by, page, limit } = params; try { const db = await getDb(); // 조건 구성 let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let queryParams = [start_date, end_date]; if (worker_id) { whereConditions.push('dwr.worker_id = ?'); queryParams.push(worker_id); } if (project_id) { whereConditions.push('dwr.project_id = ?'); queryParams.push(project_id); } if (work_status_id) { whereConditions.push('dwr.work_status_id = ?'); queryParams.push(work_status_id); } if (created_by) { whereConditions.push('dwr.created_by = ?'); queryParams.push(created_by); } const whereClause = whereConditions.join(' AND '); // 총 개수 조회 const countQuery = ` SELECT COUNT(*) as total FROM daily_work_reports dwr WHERE ${whereClause} `; const [countResult] = await db.query(countQuery, queryParams); const total = countResult[0].total; // 데이터 조회 (JOIN 포함) const offset = (page - 1) * limit; const dataQuery = getSelectQuery() + ` WHERE ${whereClause} ORDER BY dwr.report_date DESC, w.worker_name ASC, dwr.created_at DESC LIMIT ? OFFSET ? `; const dataParams = [...queryParams, limit, offset]; const [rows] = await db.query(dataQuery, dataParams); callback(null, { reports: rows, total }); } catch (err) { console.error('상세 검색 오류:', err); callback(err); } }; /** * 14. 일일 근무 요약 조회 (날짜별) */ const getSummaryByDate = async (date, callback) => { try { const db = await getDb(); const sql = ` SELECT dwr.worker_id, w.worker_name, dwr.report_date, SUM(dwr.work_hours) as total_hours, COUNT(*) as work_entries_count, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id WHERE dwr.report_date = ? GROUP BY dwr.worker_id, dwr.report_date ORDER BY w.worker_name ASC `; const [rows] = await db.query(sql, [date]); callback(null, rows); } catch (err) { console.error('일일 근무 요약 조회 오류:', err); callback(err); } }; /** * 15. 일일 근무 요약 조회 (작업자별) */ const getSummaryByWorker = async (worker_id, callback) => { try { const db = await getDb(); const sql = ` SELECT dwr.report_date, dwr.worker_id, w.worker_name, SUM(dwr.work_hours) as total_hours, COUNT(*) as work_entries_count, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count FROM daily_work_reports dwr LEFT JOIN workers w ON dwr.worker_id = w.worker_id WHERE dwr.worker_id = ? GROUP BY dwr.report_date, dwr.worker_id ORDER BY dwr.report_date DESC `; const [rows] = await db.query(sql, [worker_id]); callback(null, rows); } catch (err) { console.error('작업자별 근무 요약 조회 오류:', err); callback(err); } }; /** * 16. 월간 요약 */ const getMonthlySummary = async (year, month, callback) => { try { const db = await getDb(); const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`; const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`; const sql = ` SELECT dwr.report_date, dwr.worker_id, w.worker_name, SUM(dwr.work_hours) as total_work_hours, COUNT(DISTINCT dwr.project_id) as project_count, COUNT(*) as work_entries_count, SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as error_count, GROUP_CONCAT(DISTINCT p.project_name ORDER BY p.project_name) as projects, GROUP_CONCAT(DISTINCT wt.name ORDER BY wt.name) as work_types 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 work_types wt ON dwr.work_type_id = wt.id WHERE dwr.report_date BETWEEN ? AND ? GROUP BY dwr.report_date, dwr.worker_id ORDER BY dwr.report_date DESC, w.worker_name ASC `; const [rows] = await db.query(sql, [start, end]); callback(null, rows); } catch (err) { console.error('월간 요약 조회 오류:', err); callback(err); } }; /** * 17. 작업보고서 수정 */ const updateById = async (id, updateData, callback) => { try { const db = await getDb(); const setFields = []; const values = []; if (updateData.work_hours !== undefined) { setFields.push('work_hours = ?'); values.push(updateData.work_hours); } if (updateData.work_status_id !== undefined) { setFields.push('work_status_id = ?'); values.push(updateData.work_status_id); } if (updateData.error_type_id !== undefined) { setFields.push('error_type_id = ?'); values.push(updateData.error_type_id); } if (updateData.project_id !== undefined) { setFields.push('project_id = ?'); values.push(updateData.project_id); } if (updateData.work_type_id !== undefined) { setFields.push('work_type_id = ?'); values.push(updateData.work_type_id); } setFields.push('updated_at = NOW()'); if (updateData.updated_by) { setFields.push('updated_by = ?'); values.push(updateData.updated_by); } values.push(id); const sql = `UPDATE daily_work_reports SET ${setFields.join(', ')} WHERE id = ?`; const [result] = await db.query(sql, values); callback(null, result.affectedRows); } catch (err) { console.error('작업보고서 수정 오류:', err); callback(err); } }; /** * 18. 특정 작업보고서 삭제 */ const removeById = async (id, deletedBy, callback) => { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); // 삭제 전 정보 저장 (감사 로그용) const [reportInfo] = await conn.query('SELECT * FROM daily_work_reports WHERE id = ?', [id]); // 작업보고서 삭제 const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [id]); // 감사 로그 추가 if (reportInfo.length > 0 && deletedBy) { try { await conn.query( `INSERT INTO work_report_audit_log (action, report_id, old_values, changed_by, change_reason, created_at) VALUES ('DELETE', ?, ?, ?, 'Manual deletion', NOW())`, [id, JSON.stringify(reportInfo[0]), deletedBy] ); } catch (auditErr) { console.warn('감사 로그 추가 실패:', auditErr.message); } } await conn.commit(); callback(null, result.affectedRows); } catch (err) { await conn.rollback(); console.error('작업보고서 삭제 오류:', err); callback(err); } finally { conn.release(); } }; /** * 19. 작업자의 특정 날짜 전체 삭제 */ const removeByDateAndWorker = async (date, worker_id, deletedBy, callback) => { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); // 삭제 전 정보 저장 (감사 로그용) const [reportInfos] = await conn.query( 'SELECT * FROM daily_work_reports WHERE report_date = ? AND worker_id = ?', [date, worker_id] ); // 작업보고서 삭제 const [result] = await conn.query( 'DELETE FROM daily_work_reports WHERE report_date = ? AND worker_id = ?', [date, worker_id] ); // 감사 로그 추가 if (reportInfos.length > 0 && deletedBy) { try { await conn.query( `INSERT INTO work_report_audit_log (action, old_values, changed_by, change_reason, created_at) VALUES ('DELETE_BATCH', ?, ?, 'Batch deletion by date and worker', NOW())`, [JSON.stringify({ deleted_reports: reportInfos, count: reportInfos.length }), deletedBy] ); } catch (auditErr) { console.warn('감사 로그 추가 실패:', auditErr.message); } } await conn.commit(); callback(null, result.affectedRows); } catch (err) { await conn.rollback(); console.error('작업보고서 전체 삭제 오류:', err); callback(err); } finally { conn.release(); } }; /** * 20. 통계 조회 (Promise 기반) */ const getStatistics = async (start_date, end_date) => { try { const db = await getDb(); const overallSql = ` SELECT COUNT(*) as total_reports, SUM(work_hours) as total_hours, COUNT(DISTINCT worker_id) as unique_workers, COUNT(DISTINCT project_id) as unique_projects FROM daily_work_reports WHERE report_date BETWEEN ? AND ? `; const [overallRows] = await db.query(overallSql, [start_date, end_date]); const dailyStatsSql = ` SELECT report_date, SUM(work_hours) as daily_hours, COUNT(DISTINCT worker_id) as daily_workers FROM daily_work_reports WHERE report_date BETWEEN ? AND ? GROUP BY report_date ORDER BY report_date DESC `; const [dailyStats] = await db.query(dailyStatsSql, [start_date, end_date]); return { overall: overallRows[0], daily_breakdown: dailyStats }; } catch (err) { console.error('통계 조회 오류:', err); throw new Error('데이터베이스에서 통계 정보를 조회하는 중 오류가 발생했습니다.'); } }; /** * [V2] 여러 작업 보고서 항목을 트랜잭션으로 생성합니다. (Promise 기반) * @param {object} modelData - 서비스 레이어에서 전달된 데이터 * @returns {Promise} 삽입된 항목의 ID 배열과 개수 */ const createReportEntries = async ({ report_date, worker_id, entries }) => { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); const insertedIds = []; const sql = ` INSERT INTO daily_work_reports (report_date, worker_id, project_id, task_id, work_hours, is_error, error_type_code_id, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; for (const entry of entries) { const { project_id, task_id, work_hours, is_error, error_type_code_id, created_by_user_id } = entry; const [result] = await conn.query(sql, [ report_date, worker_id, project_id, task_id, work_hours, is_error, error_type_code_id, created_by_user_id ]); insertedIds.push(result.insertId); } await conn.commit(); console.log(`[Model] ${insertedIds.length}개 작업 항목 생성 완료.`); return { inserted_ids: insertedIds, inserted_count: insertedIds.length }; } catch (err) { await conn.rollback(); console.error('[Model] 작업 보고서 생성 중 오류 발생:', err); // 여기서 발생한 에러는 서비스 레이어로 전파됩니다. throw new Error('데이터베이스에 작업 보고서를 생성하는 중 오류가 발생했습니다.'); } finally { conn.release(); } }; /** * [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(); } }; // 모든 함수 내보내기 (Promise 기반 함수 위주로 재구성) module.exports = { // 새로 추가된 V2 함수 (Promise 기반) createReportEntries, getReportsWithOptions, updateReportById, removeReportById, // Promise 기반으로 리팩토링된 함수 getStatistics, getSummaryByDate, getSummaryByWorker, // 아직 리팩토링되지 않았지만 필요한 기존 함수들... // (점진적으로 아래 함수들도 Promise 기반으로 전환해야 함) getAllWorkTypes, getAllWorkStatusTypes, getAllErrorTypes, createDailyReport, getMyAccumulatedHours, getAccumulatedReportsByDate, getContributorsByDate, removeSpecificEntry, getById, getByDate, getByDateAndCreator, getByWorker, getByDateAndWorker, getByRange, searchWithDetails, getMonthlySummary, updateById, removeById, removeByDateAndWorker, };