// models/tbmTransferModel.js - TBM 작업자 이동 모델 const { getDb } = require('../dbPool'); const TbmTransferModel = { /** * 작업자 이동 실행 (보내기/빼오기) * 트랜잭션: source work_hours 업데이트 + dest INSERT + 로그 INSERT */ async createTransfer(transferData) { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); const { transfer_type, user_id, source_session_id, dest_session_id, hours, initiated_by, transfer_date, project_id, work_type_id, task_id, workplace_category_id, workplace_id } = transferData; // 1. source 세션에서 해당 작업자의 work_hours 업데이트 const [sourceRows] = await conn.query( 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?', [source_session_id, user_id] ); if (sourceRows.length === 0) { await conn.rollback(); return { success: false, message: '원본 세션에서 해당 작업자를 찾을 수 없습니다.' }; } const currentHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours); const newSourceHours = currentHours - parseFloat(hours); if (newSourceHours < 0) { await conn.rollback(); return { success: false, message: '이동 시간이 현재 배정 시간보다 큽니다.' }; } await conn.query( 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?', [newSourceHours, source_session_id, user_id] ); // 2. dest 세션에 작업자 INSERT (이미 있으면 work_hours 증가) const [destRows] = await conn.query( 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?', [dest_session_id, user_id] ); if (destRows.length > 0) { const existingHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours); await conn.query( 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?', [existingHours + parseFloat(hours), dest_session_id, user_id] ); } else { await conn.query( `INSERT INTO tbm_team_assignments (session_id, user_id, work_hours, project_id, work_type_id, task_id, workplace_category_id, workplace_id, is_present) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)`, [dest_session_id, user_id, parseFloat(hours), project_id || null, work_type_id || null, task_id || null, workplace_category_id || null, workplace_id || null] ); } // 3. tbm_transfers에 로그 INSERT const [logResult] = await conn.query( `INSERT INTO tbm_transfers (transfer_date, user_id, source_session_id, dest_session_id, hours, transfer_type, initiated_by) VALUES (?, ?, ?, ?, ?, ?, ?)`, [transfer_date, user_id, source_session_id, dest_session_id, parseFloat(hours), transfer_type, initiated_by] ); // 4. 합계 시간 검증 (경고만, 차단 안함) const [totalRows] = await conn.query( `SELECT SUM(COALESCE(work_hours, 8)) as total_hours FROM tbm_team_assignments WHERE user_id = ? AND session_id IN (SELECT session_id FROM tbm_sessions WHERE session_date = ?)`, [user_id, transfer_date] ); const totalHours = totalRows[0] ? parseFloat(totalRows[0].total_hours) : 0; await conn.commit(); const result = { success: true, transfer_id: logResult.insertId, total_hours: totalHours }; if (totalHours > 8) { result.warning = `해당 작업자의 당일 합계가 ${totalHours}h입니다 (8h 초과).`; } return result; } catch (err) { try { await conn.rollback(); } catch (e) {} throw err; } finally { conn.release(); } }, /** * 이동 취소 (원복) */ async cancelTransfer(transferId) { const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); // 1. 이동 로그 조회 const [transfers] = await conn.query( 'SELECT * FROM tbm_transfers WHERE transfer_id = ?', [transferId] ); if (transfers.length === 0) { await conn.rollback(); return { success: false, message: '이동 기록을 찾을 수 없습니다.' }; } const t = transfers[0]; // 2. dest 세션에서 작업자 work_hours 감소 (또는 삭제) const [destRows] = await conn.query( 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?', [t.dest_session_id, t.user_id] ); if (destRows.length > 0) { const destHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours); const newDestHours = destHours - parseFloat(t.hours); if (newDestHours <= 0) { await conn.query( 'DELETE FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?', [t.dest_session_id, t.user_id] ); } else { await conn.query( 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?', [newDestHours, t.dest_session_id, t.user_id] ); } } // 3. source 세션에서 작업자 work_hours 복원 const [sourceRows] = await conn.query( 'SELECT assignment_id, work_hours FROM tbm_team_assignments WHERE session_id = ? AND user_id = ?', [t.source_session_id, t.user_id] ); if (sourceRows.length > 0) { const sourceHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours); const restoredHours = sourceHours + parseFloat(t.hours); await conn.query( 'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND user_id = ?', [restoredHours >= 8 ? null : restoredHours, t.source_session_id, t.user_id] ); } // 4. 이동 로그 삭제 await conn.query('DELETE FROM tbm_transfers WHERE transfer_id = ?', [transferId]); await conn.commit(); return { success: true }; } catch (err) { try { await conn.rollback(); } catch (e) {} throw err; } finally { conn.release(); } }, /** * 당일 이동 내역 조회 */ async getTransfersByDate(date) { const db = await getDb(); const sql = ` SELECT t.*, w.worker_name, w.job_type, sl.worker_name as source_leader_name, dl.worker_name as dest_leader_name, u.name as initiated_by_name FROM tbm_transfers t INNER JOIN workers w ON t.user_id = w.user_id LEFT JOIN tbm_sessions ss ON t.source_session_id = ss.session_id LEFT JOIN workers sl ON ss.leader_id = sl.user_id LEFT JOIN tbm_sessions ds ON t.dest_session_id = ds.session_id LEFT JOIN workers dl ON ds.leader_id = dl.user_id LEFT JOIN sso_users u ON t.initiated_by = u.user_id WHERE t.transfer_date = ? ORDER BY t.created_at DESC `; const [rows] = await db.query(sql, [date]); return rows; }, /** * 당일 전 작업자 배정 현황 조회 */ async getWorkerAssignmentsByDate(date) { const db = await getDb(); // 1. 해당 날짜의 모든 배정 가져오기 const [assignments] = await db.query(` SELECT ta.user_id, ta.session_id, ta.work_hours, w.worker_name, w.job_type, s.leader_id, lw.worker_name as leader_name, s.status as session_status FROM tbm_team_assignments ta INNER JOIN tbm_sessions s ON ta.session_id = s.session_id INNER JOIN workers w ON ta.user_id = w.user_id LEFT JOIN workers lw ON s.leader_id = lw.user_id WHERE s.session_date = ? ORDER BY w.worker_name `, [date]); // 2. 모든 작업자 가져오기 (배정 안 된 사람도 포함) const [allWorkers] = await db.query( "SELECT user_id, worker_name, job_type FROM workers WHERE status = 'active' AND department = '생산' ORDER BY worker_name" ); // 3. 작업자별 배정 현황 구성 const workerMap = {}; allWorkers.forEach(w => { workerMap[w.user_id] = { user_id: w.user_id, worker_name: w.worker_name, job_type: w.job_type, sessions: [], total_hours: 0, available: true }; }); assignments.forEach(a => { const hours = a.work_hours === null ? 8 : parseFloat(a.work_hours); if (workerMap[a.user_id]) { workerMap[a.user_id].sessions.push({ session_id: a.session_id, leader_name: a.leader_name, work_hours: hours, session_status: a.session_status }); workerMap[a.user_id].total_hours += hours; } }); Object.values(workerMap).forEach(w => { w.available = w.total_hours < 8; }); return Object.values(workerMap); } }; module.exports = TbmTransferModel;