/** * 대리입력 + 일별 현황 컨트롤러 */ const ProxyInputModel = require('../models/proxyInputModel'); const { getDb } = require('../dbPool'); const logger = require('../../shared/utils/logger'); const ProxyInputController = { /** * POST /api/proxy-input — 대리입력 (단일 트랜잭션) */ proxyInput: async (req, res) => { const { session_date, leader_id, entries, safety_notes, work_location } = req.body; const userId = req.user.user_id || req.user.id; // 유효성 검사 if (!session_date) { return res.status(400).json({ success: false, message: '날짜는 필수입니다.' }); } if (!entries || !Array.isArray(entries) || entries.length === 0) { return res.status(400).json({ success: false, message: '작업자 정보는 최소 1명 필요합니다.' }); } if (entries.length > 30) { return res.status(400).json({ success: false, message: '한 번에 30명까지 입력 가능합니다.' }); } // 날짜 유효성 (과거 30일 ~ 오늘) const today = new Date(); today.setHours(0, 0, 0, 0); const inputDate = new Date(session_date); const diffDays = Math.floor((today - inputDate) / (1000 * 60 * 60 * 24)); if (diffDays < 0) { return res.status(400).json({ success: false, message: '미래 날짜는 입력할 수 없습니다.' }); } if (diffDays > 30) { return res.status(400).json({ success: false, message: '30일 이내 날짜만 입력 가능합니다.' }); } // entries 필수 필드 검사 for (const entry of entries) { if (!entry.user_id || !entry.project_id || !entry.work_type_id || !entry.work_hours) { return res.status(400).json({ success: false, message: '각 작업자의 user_id, project_id, work_type_id, work_hours는 필수입니다.' }); } if (entry.work_hours <= 0 || entry.work_hours > 24) { return res.status(400).json({ success: false, message: '근무 시간은 0 초과 24 이하여야 합니다.' }); } } const db = await getDb(); const conn = await db.getConnection(); try { await conn.beginTransaction(); const userIds = entries.map(e => e.user_id); // 1. 기존 보고서 확인 (UPSERT 분기) const [existingReports] = await conn.query( `SELECT id, user_id FROM daily_work_reports WHERE report_date = ? AND user_id IN (${userIds.map(() => '?').join(',')})`, [session_date, ...userIds] ); const existingMap = {}; existingReports.forEach(r => { existingMap[r.user_id] = r.id; }); // 2. 신규 작업자용 TBM 세션 생성 (기존 있으면 재사용) let sessionId = null; const newUserIds = userIds.filter(id => !existingMap[id]); if (newUserIds.length > 0) { const sessionResult = await ProxyInputModel.createProxySession(conn, { session_date, leader_id: leader_id || userId, proxy_input_by: userId, created_by: userId, safety_notes: safety_notes || '', work_location: work_location || '' }); sessionId = sessionResult.insertId; } // 3. 각 entry 처리 (UPSERT) const createdWorkers = []; for (const entry of entries) { const existingReportId = existingMap[entry.user_id]; if (existingReportId) { // UPDATE 기존 보고서 await conn.query(` UPDATE daily_work_reports SET project_id = ?, work_type_id = ?, work_hours = ?, work_status_id = ?, start_time = ?, end_time = ?, note = ?, updated_at = NOW() WHERE id = ? `, [entry.project_id, entry.work_type_id, entry.work_hours, entry.work_status_id || 1, entry.start_time || null, entry.end_time || null, entry.note || '', existingReportId]); createdWorkers.push({ user_id: entry.user_id, report_id: existingReportId, action: 'updated' }); } else { // INSERT 신규 — TBM 배정 + 작업보고서 const assignResult = await ProxyInputModel.createTeamAssignment(conn, { session_id: sessionId, user_id: entry.user_id, project_id: entry.project_id, work_type_id: entry.work_type_id, task_id: entry.task_id || null, workplace_id: entry.workplace_id || null, work_hours: entry.work_hours }); const assignmentId = assignResult.insertId; const reportResult = await ProxyInputModel.createWorkReport(conn, { report_date: session_date, user_id: entry.user_id, project_id: entry.project_id, work_type_id: entry.work_type_id, task_id: entry.task_id || null, work_status_id: entry.work_status_id || 1, work_hours: entry.work_hours, start_time: entry.start_time || null, end_time: entry.end_time || null, note: entry.note || '', tbm_session_id: sessionId, tbm_assignment_id: assignmentId, created_by: userId, created_by_name: req.user.name || req.user.username || '' }); createdWorkers.push({ user_id: entry.user_id, report_id: reportResult.insertId, action: 'created' }); } // 부적합 처리 (defect_hours > 0 && 기존 defect 없을 때만) const defectHours = parseFloat(entry.defect_hours) || 0; const reportId = existingReportId || createdWorkers[createdWorkers.length - 1].report_id; if (defectHours > 0) { const [existingDefects] = await conn.query( 'SELECT defect_id FROM work_report_defects WHERE report_id = ?', [reportId] ); if (existingDefects.length === 0) { await conn.query( `INSERT INTO work_report_defects (report_id, defect_hours, category_id, item_id, note) VALUES (?, ?, ?, ?, '대리입력')`, [reportId, defectHours, entry.defect_category_id || null, entry.defect_item_id || null] ); await conn.query( 'UPDATE daily_work_reports SET error_hours = ?, work_status_id = 2 WHERE id = ?', [defectHours, reportId] ); } } } await conn.commit(); res.status(201).json({ success: true, message: `${entries.length}명의 대리입력이 완료되었습니다.`, data: { session_id: sessionId, is_proxy_input: true, created_reports: entries.length, workers: createdWorkers } }); } catch (err) { try { await conn.rollback(); } catch (e) {} logger.error('대리입력 오류:', err); res.status(500).json({ success: false, message: '대리입력 처리 중 오류가 발생했습니다.' }); } finally { conn.release(); } }, /** * GET /api/proxy-input/daily-status — 일별 현황 */ getDailyStatus: async (req, res) => { try { const { date } = req.query; if (!date) { return res.status(400).json({ success: false, message: '날짜(date) 파라미터는 필수입니다.' }); } const data = await ProxyInputModel.getDailyStatus(date); res.json({ success: true, data }); } catch (err) { logger.error('일별 현황 조회 오류:', err); res.status(500).json({ success: false, message: '조회 중 오류가 발생했습니다.' }); } }, /** * GET /api/proxy-input/daily-status/detail — 작업자별 상세 */ getDailyStatusDetail: async (req, res) => { try { const { date, user_id } = req.query; if (!date || !user_id) { return res.status(400).json({ success: false, message: 'date와 user_id 파라미터는 필수입니다.' }); } const data = await ProxyInputModel.getDailyStatusDetail(date, parseInt(user_id)); if (!data.worker) { return res.status(404).json({ success: false, message: '작업자를 찾을 수 없습니다.' }); } res.json({ success: true, data }); } catch (err) { logger.error('일별 상세 조회 오류:', err); res.status(500).json({ success: false, message: '조회 중 오류가 발생했습니다.' }); } } }; module.exports = ProxyInputController;