// models/dailyWorkReportModel.js - 누적입력 방식 + 모든 기존 기능 포함 const { getDb } = require('../dbPool'); /** * 📋 마스터 데이터 조회 함수들 */ const getAllWorkTypes = async (callback) => { try { const db = await getDb(); const [rows] = await db.query('SELECT id, name, description, category, created_at, updated_at 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 id, name, description, is_error, created_at 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(); // issue_report_items에서 부적합(nonconformity) 타입의 항목만 조회 const [rows] = await db.query(` SELECT iri.item_id as id, iri.item_name as name, iri.description, iri.severity, irc.category_name as category, iri.display_order, iri.created_at FROM issue_report_items iri INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id WHERE irc.category_type = 'nonconformity' AND iri.is_active = TRUE ORDER BY irc.display_order, iri.display_order, iri.item_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(); // 5. 근태 기록 동기화 (추가) try { const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, report_date); } catch (syncErr) { console.error('근태 기록 동기화 실패:', syncErr); // 메인 트랜잭션은 성공했으므로 동기화 실패로 롤백하지 않음 (비동기 처리 또는 무시) } 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]); // 감사 로그 (삭제된 테이블이므로 콘솔 로그로 대체) console.log(`[삭제 로그] 작업자: ${entry.worker_name}, 프로젝트: ${entry.project_name}, 작업시간: ${entry.work_hours}시간, 삭제자: ${deleted_by}`); 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 쿼리 부분 * error_type_id는 issue_report_items 테이블 참조 (issue-categories.html에서 관리) */ 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, iri.item_name as error_type_name, irc.category_name as error_category_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 issue_report_items iri ON dwr.error_type_id = iri.item_id LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_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); // [Sync] 근태 기록 동기화 try { const [targetReport] = await db.query('SELECT worker_id, report_date FROM daily_work_reports WHERE id = ?', [id]); if (targetReport.length > 0) { const { worker_id, report_date } = targetReport[0]; const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, report_date); } } catch (syncErr) { console.error('근태 기록 동기화 실패 (Update):', syncErr); } 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(); // [Sync] 근태 기록 동기화 if (reportInfo.length > 0) { try { const { worker_id, report_date } = reportInfo[0]; const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, report_date); } catch (syncErr) { console.error('근태 기록 동기화 실패 (Delete):', syncErr); } } 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 id, report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_at, updated_at, created_by, updated_by 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(); // [Sync] 근태 기록 동기화 try { const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, date); } catch (syncErr) { console.error('근태 기록 동기화 실패 (Batch Delete):', syncErr); } 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, work_type_id, work_hours, work_status_id, error_type_id, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; for (const entry of entries) { const { project_id, work_type_id, work_hours, work_status_id, error_type_id, created_by } = entry; const [result] = await conn.query(sql, [ report_date, worker_id, project_id, work_type_id, work_hours, work_status_id || 1, error_type_id, created_by ]); insertedIds.push(result.insertId); } await conn.commit(); // [Sync] 근태 기록 동기화 try { const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, report_date); } catch (syncErr) { console.error('근태 기록 동기화 실패 (V2 Create):', syncErr); } 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 쿼리 (새로운 스키마 기준) * 주의: work_type_id 컬럼에는 실제로 task_id가 저장됨 * error_type_id는 issue_report_items 테이블 참조 (issue-categories.html에서 관리) */ const getSelectQueryV2 = () => ` 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, t.task_name, wt.name as work_type_name, wst.name as work_status_name, iri.item_name as error_type_name, irc.category_name as error_category_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.work_type_id = t.task_id LEFT JOIN work_types wt ON t.work_type_id = wt.id LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id LEFT JOIN issue_report_items iri ON dwr.error_type_id = iri.item_id LEFT JOIN issue_report_categories irc ON iri.category_id = irc.category_id LEFT JOIN users u ON dwr.created_by = u.user_id `; /** * [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); } else if (options.start_date && options.end_date) { whereConditions.push('dwr.report_date BETWEEN ? AND ?'); queryParams.push(options.start_date, options.end_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 = ?'); 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', 'work_type_id', 'work_hours', 'work_status_id', 'error_type_id']; const setClauses = []; const queryParams = []; for (const field of allowedFields) { if (updateData[field] !== undefined) { setClauses.push(`${field} = ?`); queryParams.push(updateData[field]); } } // updated_by는 항상 업데이트 if (updateData.updated_by_user_id) { setClauses.push('updated_by = ?'); queryParams.push(updateData.updated_by_user_id); } if (setClauses.length === 0) { throw new Error('수정할 데이터가 없습니다.'); } queryParams.push(reportId); // [Sync] 업데이트 전 정보 조회 (동기화를 위해) let targetInfo = null; try { const [rows] = await db.query('SELECT worker_id, report_date FROM daily_work_reports WHERE id = ?', [reportId]); if (rows.length > 0) targetInfo = rows[0]; } catch (e) { console.warn('Sync fetch failed', e); } const sql = `UPDATE daily_work_reports SET ${setClauses.join(', ')} WHERE id = ?`; try { const [result] = await db.query(sql, queryParams); // [Sync] 근태 기록 동기화 if (targetInfo) { try { const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(targetInfo.worker_id, targetInfo.report_date); } catch (syncErr) { console.error('근태 기록 동기화 실패 (V2 Update):', syncErr); } } 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 id, report_date, worker_id, project_id, work_type_id, work_status_id, error_type_id, work_hours, created_at, updated_at, created_by, updated_by FROM daily_work_reports WHERE id = ?', [reportId]); // 실제 삭제 작업 const [result] = await conn.query('DELETE FROM daily_work_reports WHERE id = ?', [reportId]); // 감사 로그 (삭제된 테이블이므로 콘솔 로그로 대체) if (reportInfo.length > 0 && deletedByUserId) { console.log(`[삭제 로그] 보고서 ID: ${reportId}, 삭제자: ${deletedByUserId}, 사유: Manual deletion by user`); } await conn.commit(); // [Sync] 근태 기록 동기화 if (reportInfo.length > 0) { try { const { worker_id, report_date } = reportInfo[0]; const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, report_date); } catch (syncErr) { console.error('근태 기록 동기화 실패 (V2 Delete):', syncErr); } } return result.affectedRows; } catch (err) { await conn.rollback(); console.error(`[Model] 작업 보고서 삭제 오류 (id: ${reportId}):`, err); throw new Error('데이터베이스에서 작업 보고서를 삭제하는 중 오류가 발생했습니다.'); } finally { conn.release(); } }; // ========== 마스터 데이터 CRUD 메서드들 ========== /** * 📝 작업 유형 생성 */ const createWorkType = async (data, callback) => { try { const db = await getDb(); const { name, description, category } = data; const [result] = await db.query( 'INSERT INTO work_types (name, description, category) VALUES (?, ?, ?)', [name, description, category] ); callback(null, { id: result.insertId, ...data }); } catch (err) { console.error('작업 유형 생성 오류:', err); callback(err); } }; /** * ✏️ 작업 유형 수정 */ const updateWorkType = async (id, data, callback) => { try { const db = await getDb(); const { name, description, category } = data; const [result] = await db.query( 'UPDATE work_types SET name = ?, description = ?, category = ? WHERE id = ?', [name, description, category, id] ); callback(null, result); } catch (err) { console.error('작업 유형 수정 오류:', err); callback(err); } }; /** * 🗑️ 작업 유형 삭제 */ const deleteWorkType = async (id, callback) => { try { const db = await getDb(); const [result] = await db.query('DELETE FROM work_types WHERE id = ?', [id]); callback(null, result); } catch (err) { console.error('작업 유형 삭제 오류:', err); callback(err); } }; /** * 📝 작업 상태 생성 */ const createWorkStatus = async (data, callback) => { try { const db = await getDb(); const { name, description, is_error } = data; const [result] = await db.query( 'INSERT INTO work_status_types (name, description, is_error) VALUES (?, ?, ?)', [name, description, is_error || 0] ); callback(null, { id: result.insertId, ...data }); } catch (err) { console.error('작업 상태 생성 오류:', err); callback(err); } }; /** * ✏️ 작업 상태 수정 */ const updateWorkStatus = async (id, data, callback) => { try { const db = await getDb(); const { name, description, is_error } = data; const [result] = await db.query( 'UPDATE work_status_types SET name = ?, description = ?, is_error = ? WHERE id = ?', [name, description, is_error || 0, id] ); callback(null, result); } catch (err) { console.error('작업 상태 수정 오류:', err); callback(err); } }; /** * 🗑️ 작업 상태 삭제 */ const deleteWorkStatus = async (id, callback) => { try { const db = await getDb(); const [result] = await db.query('DELETE FROM work_status_types WHERE id = ?', [id]); callback(null, result); } catch (err) { console.error('작업 상태 삭제 오류:', err); callback(err); } }; /** * 📝 오류 유형 생성 */ const createErrorType = async (data, callback) => { try { const db = await getDb(); const { name, description, severity } = data; const [result] = await db.query( 'INSERT INTO error_types (name, description, severity) VALUES (?, ?, ?)', [name, description, severity || 'medium'] ); callback(null, { id: result.insertId, ...data }); } catch (err) { console.error('오류 유형 생성 오류:', err); callback(err); } }; /** * ✏️ 오류 유형 수정 */ const updateErrorType = async (id, data, callback) => { try { const db = await getDb(); const { name, description, severity } = data; const [result] = await db.query( 'UPDATE error_types SET name = ?, description = ?, severity = ? WHERE id = ?', [name, description, severity || 'medium', id] ); callback(null, result); } catch (err) { console.error('오류 유형 수정 오류:', err); callback(err); } }; /** * 🗑️ 오류 유형 삭제 */ const deleteErrorType = async (id, callback) => { try { const db = await getDb(); const [result] = await db.query('DELETE FROM error_types WHERE id = ?', [id]); callback(null, result); } catch (err) { console.error('오류 유형 삭제 오류:', err); callback(err); } }; /** * TBM 기반 작업보고서 생성 및 TBM 세션 완료 처리 * @param {object} reportData - TBM 작업보고서 데이터 * @returns {Promise} 생성 결과 */ const createFromTbmAssignment = async (reportData) => { const { tbm_assignment_id, tbm_session_id, worker_id, project_id, work_type_id, report_date, start_time, end_time, total_hours, error_hours, regular_hours, work_status_id, error_type_id, created_by } = reportData; const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); // 1. 작업보고서 생성 const sql = ` INSERT INTO daily_work_reports (tbm_session_id, tbm_assignment_id, report_date, worker_id, project_id, work_type_id, start_time, end_time, work_hours, total_hours, regular_hours, error_hours, work_status_id, error_type_id, created_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW()) `; const [result] = await conn.query(sql, [ tbm_session_id, tbm_assignment_id, report_date, worker_id, project_id, work_type_id, start_time || null, end_time || null, total_hours, // work_hours는 TBM에서 total_hours와 동일 total_hours, regular_hours, error_hours || 0, work_status_id || 1, error_type_id || null, created_by ]); const reportId = result.insertId; // 2. TBM 세션의 모든 팀 배정이 작업보고서를 제출했는지 확인 const [assignmentCheck] = await conn.query(` SELECT COUNT(*) as total_assignments, COUNT(dwr.tbm_assignment_id) as completed_assignments FROM tbm_team_assignments ta LEFT JOIN daily_work_reports dwr ON ta.assignment_id = dwr.tbm_assignment_id WHERE ta.session_id = ? `, [tbm_session_id]); const { total_assignments, completed_assignments } = assignmentCheck[0]; // 3. 모든 팀원이 작업보고서를 제출했으면 TBM 세션을 완료로 표시 if (total_assignments === completed_assignments) { await conn.query(` UPDATE tbm_sessions SET status = 'completed', updated_at = NOW() WHERE session_id = ? `, [tbm_session_id]); } await conn.commit(); // 4. 근태 기록 동기화 try { const AttendanceModel = require('./attendanceModel'); await AttendanceModel.syncWithWorkReports(worker_id, report_date); } catch (syncErr) { console.error('근태 기록 동기화 실패 (TBM Report):', syncErr); } console.log(`[Model] TBM 작업보고서 생성 완료: report_id=${reportId}, session=${tbm_session_id}, assignment=${tbm_assignment_id}`); return { success: true, report_id: reportId, tbm_completed: total_assignments === completed_assignments, completion_status: `${completed_assignments}/${total_assignments} 작업 완료` }; } catch (err) { await conn.rollback(); console.error('[Model] TBM 작업보고서 생성 중 오류 발생:', err); throw new Error('TBM 작업보고서 생성 중 오류가 발생했습니다.'); } finally { conn.release(); } }; // 모든 함수 내보내기 (Promise 기반 함수 위주로 재구성) module.exports = { // 새로 추가된 V2 함수 (Promise 기반) createReportEntries, getReportsWithOptions, updateReportById, removeReportById, createFromTbmAssignment, // Promise 기반으로 리팩토링된 함수 getStatistics, getSummaryByDate, getSummaryByWorker, // 아직 리팩토링되지 않았지만 필요한 기존 함수들... // (점진적으로 아래 함수들도 Promise 기반으로 전환해야 함) getAllWorkTypes, getAllWorkStatusTypes, getAllErrorTypes, // 마스터 데이터 CRUD createWorkType, updateWorkType, deleteWorkType, createWorkStatus, updateWorkStatus, deleteWorkStatus, createErrorType, updateErrorType, deleteErrorType, createDailyReport, getMyAccumulatedHours, getAccumulatedReportsByDate, getContributorsByDate, removeSpecificEntry, getById, getByDate, getByDateAndCreator, getByWorker, getByDateAndWorker, getByRange, searchWithDetails, getMonthlySummary, updateById, removeById, removeByDateAndWorker, };