/** * 작업 중 문제 신고 모델 * 부적합/안전 신고 관련 DB 쿼리 */ const { getDb } = require('../dbPool'); // ==================== 신고 카테고리 관리 ==================== /** * 모든 신고 카테고리 조회 */ const getAllCategories = async (callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT category_id, category_type, category_name, description, display_order, is_active, created_at FROM issue_report_categories ORDER BY category_type, display_order, category_id` ); callback(null, rows); } catch (err) { callback(err); } }; /** * 타입별 활성 카테고리 조회 (nonconformity/safety) */ const getCategoriesByType = async (categoryType, callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT category_id, category_type, category_name, description, display_order FROM issue_report_categories WHERE category_type = ? AND is_active = TRUE ORDER BY display_order, category_id`, [categoryType] ); callback(null, rows); } catch (err) { callback(err); } }; /** * 카테고리 생성 */ const createCategory = async (categoryData, callback) => { try { const db = await getDb(); const { category_type, category_name, description = null, display_order = 0 } = categoryData; const [result] = await db.query( `INSERT INTO issue_report_categories (category_type, category_name, description, display_order) VALUES (?, ?, ?, ?)`, [category_type, category_name, description, display_order] ); callback(null, result.insertId); } catch (err) { callback(err); } }; /** * 카테고리 수정 */ const updateCategory = async (categoryId, categoryData, callback) => { try { const db = await getDb(); const { category_name, description, display_order, is_active } = categoryData; const [result] = await db.query( `UPDATE issue_report_categories SET category_name = ?, description = ?, display_order = ?, is_active = ? WHERE category_id = ?`, [category_name, description, display_order, is_active, categoryId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 카테고리 삭제 */ const deleteCategory = async (categoryId, callback) => { try { const db = await getDb(); const [result] = await db.query( `DELETE FROM issue_report_categories WHERE category_id = ?`, [categoryId] ); callback(null, result); } catch (err) { callback(err); } }; // ==================== 사전 정의 신고 항목 관리 ==================== /** * 카테고리별 활성 항목 조회 */ const getItemsByCategory = async (categoryId, callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT item_id, category_id, item_name, description, severity, display_order FROM issue_report_items WHERE category_id = ? AND is_active = TRUE ORDER BY display_order, item_id`, [categoryId] ); callback(null, rows); } catch (err) { callback(err); } }; /** * 모든 항목 조회 (관리용) */ const getAllItems = async (callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT iri.item_id, iri.category_id, iri.item_name, iri.description, iri.severity, iri.display_order, iri.is_active, iri.created_at, irc.category_name, irc.category_type FROM issue_report_items iri INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id ORDER BY irc.category_type, irc.display_order, iri.display_order, iri.item_id` ); callback(null, rows); } catch (err) { callback(err); } }; /** * 항목 생성 */ const createItem = async (itemData, callback) => { try { const db = await getDb(); const { category_id, item_name, description = null, severity = 'medium', display_order = 0 } = itemData; const [result] = await db.query( `INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order) VALUES (?, ?, ?, ?, ?)`, [category_id, item_name, description, severity, display_order] ); callback(null, result.insertId); } catch (err) { callback(err); } }; /** * 항목 수정 */ const updateItem = async (itemId, itemData, callback) => { try { const db = await getDb(); const { item_name, description, severity, display_order, is_active } = itemData; const [result] = await db.query( `UPDATE issue_report_items SET item_name = ?, description = ?, severity = ?, display_order = ?, is_active = ? WHERE item_id = ?`, [item_name, description, severity, display_order, is_active, itemId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 항목 삭제 */ const deleteItem = async (itemId, callback) => { try { const db = await getDb(); const [result] = await db.query( `DELETE FROM issue_report_items WHERE item_id = ?`, [itemId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 카테고리 ID로 단건 조회 */ const getCategoryById = async (categoryId, callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT category_id, category_type, category_name, description FROM issue_report_categories WHERE category_id = ?`, [categoryId] ); callback(null, rows[0] || null); } catch (err) { callback(err); } }; // ==================== 문제 신고 관리 ==================== // 한국 시간 유틸리티 import const { getKoreaDatetime } = require('../utils/dateUtils'); /** * 신고 생성 */ const createReport = async (reportData, callback) => { try { const db = await getDb(); const { reporter_id, factory_category_id = null, workplace_id = null, project_id = null, custom_location = null, tbm_session_id = null, visit_request_id = null, issue_category_id, issue_item_id = null, additional_description = null, photo_path1 = null, photo_path2 = null, photo_path3 = null, photo_path4 = null, photo_path5 = null } = reportData; // 한국 시간 기준으로 신고 일시 설정 const reportDate = getKoreaDatetime(); const [result] = await db.query( `INSERT INTO work_issue_reports (reporter_id, report_date, factory_category_id, workplace_id, project_id, custom_location, tbm_session_id, visit_request_id, issue_category_id, issue_item_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [reporter_id, reportDate, factory_category_id, workplace_id, project_id, custom_location, tbm_session_id, visit_request_id, issue_category_id, issue_item_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5] ); // 상태 변경 로그 기록 await db.query( `INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by) VALUES (?, NULL, 'reported', ?)`, [result.insertId, reporter_id] ); callback(null, result.insertId); } catch (err) { callback(err); } }; /** * 신고 목록 조회 (필터 옵션 포함) */ const getAllReports = async (filters = {}, callback) => { try { const db = await getDb(); let query = ` SELECT wir.report_id, wir.reporter_id, wir.report_date, wir.factory_category_id, wir.workplace_id, wir.custom_location, wir.tbm_session_id, wir.visit_request_id, wir.issue_category_id, wir.issue_item_id, wir.additional_description, wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5, wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at, wir.resolution_notes, wir.resolved_at, wir.created_at, wir.updated_at, u.username as reporter_name, u.name as reporter_full_name, wc.category_name as factory_name, w.workplace_name, irc.category_type, irc.category_name as issue_category_name, iri.item_name as issue_item_name, iri.severity, assignee.username as assigned_user_name, assignee.name as assigned_full_name FROM work_issue_reports wir INNER JOIN users u ON wir.reporter_id = u.user_id LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id WHERE 1=1 `; const params = []; // 필터 적용 if (filters.status) { query += ` AND wir.status = ?`; params.push(filters.status); } if (filters.category_type) { query += ` AND irc.category_type = ?`; params.push(filters.category_type); } if (filters.issue_category_id) { query += ` AND wir.issue_category_id = ?`; params.push(filters.issue_category_id); } if (filters.factory_category_id) { query += ` AND wir.factory_category_id = ?`; params.push(filters.factory_category_id); } if (filters.workplace_id) { query += ` AND wir.workplace_id = ?`; params.push(filters.workplace_id); } if (filters.reporter_id) { query += ` AND wir.reporter_id = ?`; params.push(filters.reporter_id); } if (filters.assigned_user_id) { query += ` AND wir.assigned_user_id = ?`; params.push(filters.assigned_user_id); } if (filters.start_date && filters.end_date) { query += ` AND DATE(wir.report_date) BETWEEN ? AND ?`; params.push(filters.start_date, filters.end_date); } if (filters.search) { query += ` AND (wir.additional_description LIKE ? OR iri.item_name LIKE ? OR wir.custom_location LIKE ?)`; const searchTerm = `%${filters.search}%`; params.push(searchTerm, searchTerm, searchTerm); } query += ` ORDER BY wir.report_date DESC, wir.report_id DESC`; // 페이지네이션 if (filters.limit) { query += ` LIMIT ?`; params.push(parseInt(filters.limit)); if (filters.offset) { query += ` OFFSET ?`; params.push(parseInt(filters.offset)); } } const [rows] = await db.query(query, params); callback(null, rows); } catch (err) { callback(err); } }; /** * 신고 상세 조회 */ const getReportById = async (reportId, callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT wir.report_id, wir.reporter_id, wir.report_date, wir.factory_category_id, wir.workplace_id, wir.custom_location, wir.tbm_session_id, wir.visit_request_id, wir.issue_category_id, wir.issue_item_id, wir.additional_description, wir.photo_path1, wir.photo_path2, wir.photo_path3, wir.photo_path4, wir.photo_path5, wir.status, wir.assigned_department, wir.assigned_user_id, wir.assigned_at, wir.assigned_by, wir.resolution_notes, wir.resolution_photo_path1, wir.resolution_photo_path2, wir.resolved_at, wir.resolved_by, wir.modification_history, wir.created_at, wir.updated_at, u.username as reporter_name, u.name as reporter_full_name, wc.category_name as factory_name, w.workplace_name, irc.category_type, irc.category_name as issue_category_name, iri.item_name as issue_item_name, iri.severity, assignee.username as assigned_user_name, assignee.name as assigned_full_name, assigner.username as assigned_by_name, resolver.username as resolved_by_name FROM work_issue_reports wir INNER JOIN users u ON wir.reporter_id = u.user_id LEFT JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id LEFT JOIN issue_report_items iri ON wir.issue_item_id = iri.item_id LEFT JOIN users assignee ON wir.assigned_user_id = assignee.user_id LEFT JOIN users assigner ON wir.assigned_by = assigner.user_id LEFT JOIN users resolver ON wir.resolved_by = resolver.user_id WHERE wir.report_id = ?`, [reportId] ); callback(null, rows[0]); } catch (err) { callback(err); } }; /** * 신고 수정 */ const updateReport = async (reportId, reportData, userId, callback) => { try { const db = await getDb(); // 기존 데이터 조회 const [existing] = await db.query( `SELECT * FROM work_issue_reports WHERE report_id = ?`, [reportId] ); if (existing.length === 0) { return callback(new Error('신고를 찾을 수 없습니다.')); } const current = existing[0]; // 수정 이력 생성 const modifications = []; const now = new Date().toISOString(); for (const key of Object.keys(reportData)) { if (current[key] !== reportData[key] && reportData[key] !== undefined) { modifications.push({ field: key, old_value: current[key], new_value: reportData[key], modified_at: now, modified_by: userId }); } } // 기존 이력과 병합 const existingHistory = current.modification_history ? JSON.parse(current.modification_history) : []; const newHistory = [...existingHistory, ...modifications]; const { factory_category_id, workplace_id, custom_location, issue_category_id, issue_item_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5 } = reportData; const [result] = await db.query( `UPDATE work_issue_reports SET factory_category_id = COALESCE(?, factory_category_id), workplace_id = COALESCE(?, workplace_id), custom_location = COALESCE(?, custom_location), issue_category_id = COALESCE(?, issue_category_id), issue_item_id = COALESCE(?, issue_item_id), additional_description = COALESCE(?, additional_description), photo_path1 = COALESCE(?, photo_path1), photo_path2 = COALESCE(?, photo_path2), photo_path3 = COALESCE(?, photo_path3), photo_path4 = COALESCE(?, photo_path4), photo_path5 = COALESCE(?, photo_path5), modification_history = ?, updated_at = NOW() WHERE report_id = ?`, [factory_category_id, workplace_id, custom_location, issue_category_id, issue_item_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5, JSON.stringify(newHistory), reportId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 신고 삭제 */ const deleteReport = async (reportId, callback) => { try { const db = await getDb(); // 먼저 사진 경로 조회 (삭제용) const [photos] = await db.query( `SELECT photo_path1, photo_path2, photo_path3, photo_path4, photo_path5, resolution_photo_path1, resolution_photo_path2 FROM work_issue_reports WHERE report_id = ?`, [reportId] ); const [result] = await db.query( `DELETE FROM work_issue_reports WHERE report_id = ?`, [reportId] ); // 삭제할 사진 경로 반환 callback(null, { result, photos: photos[0] }); } catch (err) { callback(err); } }; // ==================== 상태 관리 ==================== /** * 신고 접수 (reported → received) */ const receiveReport = async (reportId, userId, callback) => { try { const db = await getDb(); // 현재 상태 확인 const [current] = await db.query( `SELECT status FROM work_issue_reports WHERE report_id = ?`, [reportId] ); if (current.length === 0) { return callback(new Error('신고를 찾을 수 없습니다.')); } if (current[0].status !== 'reported') { return callback(new Error('접수 대기 상태가 아닙니다.')); } const [result] = await db.query( `UPDATE work_issue_reports SET status = 'received', updated_at = NOW() WHERE report_id = ?`, [reportId] ); // 상태 변경 로그 await db.query( `INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by) VALUES (?, 'reported', 'received', ?)`, [reportId, userId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 담당자 배정 */ const assignReport = async (reportId, assignData, callback) => { try { const db = await getDb(); const { assigned_department, assigned_user_id, assigned_by } = assignData; // 현재 상태 확인 const [current] = await db.query( `SELECT status FROM work_issue_reports WHERE report_id = ?`, [reportId] ); if (current.length === 0) { return callback(new Error('신고를 찾을 수 없습니다.')); } // 접수 상태 이상이어야 배정 가능 const validStatuses = ['received', 'in_progress']; if (!validStatuses.includes(current[0].status)) { return callback(new Error('접수된 상태에서만 담당자 배정이 가능합니다.')); } const [result] = await db.query( `UPDATE work_issue_reports SET assigned_department = ?, assigned_user_id = ?, assigned_at = NOW(), assigned_by = ?, updated_at = NOW() WHERE report_id = ?`, [assigned_department, assigned_user_id, assigned_by, reportId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 처리 시작 (received → in_progress) */ const startProcessing = async (reportId, userId, callback) => { try { const db = await getDb(); // 현재 상태 확인 const [current] = await db.query( `SELECT status FROM work_issue_reports WHERE report_id = ?`, [reportId] ); if (current.length === 0) { return callback(new Error('신고를 찾을 수 없습니다.')); } if (current[0].status !== 'received') { return callback(new Error('접수된 상태에서만 처리를 시작할 수 있습니다.')); } const [result] = await db.query( `UPDATE work_issue_reports SET status = 'in_progress', updated_at = NOW() WHERE report_id = ?`, [reportId] ); // 상태 변경 로그 await db.query( `INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by) VALUES (?, 'received', 'in_progress', ?)`, [reportId, userId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 처리 완료 (in_progress → completed) */ const completeReport = async (reportId, completionData, callback) => { try { const db = await getDb(); const { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by } = completionData; // 현재 상태 확인 const [current] = await db.query( `SELECT status FROM work_issue_reports WHERE report_id = ?`, [reportId] ); if (current.length === 0) { return callback(new Error('신고를 찾을 수 없습니다.')); } if (current[0].status !== 'in_progress') { return callback(new Error('처리 중 상태에서만 완료할 수 있습니다.')); } const [result] = await db.query( `UPDATE work_issue_reports SET status = 'completed', resolution_notes = ?, resolution_photo_path1 = ?, resolution_photo_path2 = ?, resolved_at = NOW(), resolved_by = ?, updated_at = NOW() WHERE report_id = ?`, [resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by, reportId] ); // 상태 변경 로그 await db.query( `INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by, change_reason) VALUES (?, 'in_progress', 'completed', ?, ?)`, [reportId, resolved_by, resolution_notes] ); callback(null, result); } catch (err) { callback(err); } }; /** * 신고 종료 (completed → closed) */ const closeReport = async (reportId, userId, callback) => { try { const db = await getDb(); // 현재 상태 확인 const [current] = await db.query( `SELECT status FROM work_issue_reports WHERE report_id = ?`, [reportId] ); if (current.length === 0) { return callback(new Error('신고를 찾을 수 없습니다.')); } if (current[0].status !== 'completed') { return callback(new Error('완료된 상태에서만 종료할 수 있습니다.')); } const [result] = await db.query( `UPDATE work_issue_reports SET status = 'closed', updated_at = NOW() WHERE report_id = ?`, [reportId] ); // 상태 변경 로그 await db.query( `INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by) VALUES (?, 'completed', 'closed', ?)`, [reportId, userId] ); callback(null, result); } catch (err) { callback(err); } }; /** * 상태 변경 이력 조회 */ const getStatusLogs = async (reportId, callback) => { try { const db = await getDb(); const [rows] = await db.query( `SELECT wisl.log_id, wisl.report_id, wisl.previous_status, wisl.new_status, wisl.changed_by, wisl.change_reason, wisl.changed_at, u.username as changed_by_name, u.name as changed_by_full_name FROM work_issue_status_logs wisl INNER JOIN users u ON wisl.changed_by = u.user_id WHERE wisl.report_id = ? ORDER BY wisl.changed_at ASC`, [reportId] ); callback(null, rows); } catch (err) { callback(err); } }; /** * m_project_id 업데이트 (System 3 연동 후) */ const updateMProjectId = async (reportId, mProjectId, callback) => { try { const db = await getDb(); await db.query( `UPDATE work_issue_reports SET m_project_id = ? WHERE report_id = ?`, [mProjectId, reportId] ); callback(null); } catch (err) { callback(err); } }; // ==================== 통계 ==================== /** * 신고 통계 요약 */ const getStatsSummary = async (filters = {}, callback) => { try { const db = await getDb(); let whereClause = '1=1'; const params = []; let joinClause = ''; if (filters.category_type) { joinClause = ' INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id'; whereClause += ` AND irc.category_type = ?`; params.push(filters.category_type); } if (filters.start_date && filters.end_date) { whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`; params.push(filters.start_date, filters.end_date); } if (filters.factory_category_id) { whereClause += ` AND wir.factory_category_id = ?`; params.push(filters.factory_category_id); } const [rows] = await db.query( `SELECT COUNT(*) as total, SUM(CASE WHEN wir.status = 'reported' THEN 1 ELSE 0 END) as reported, SUM(CASE WHEN wir.status = 'received' THEN 1 ELSE 0 END) as received, SUM(CASE WHEN wir.status = 'in_progress' THEN 1 ELSE 0 END) as in_progress, SUM(CASE WHEN wir.status = 'completed' THEN 1 ELSE 0 END) as completed, SUM(CASE WHEN wir.status = 'closed' THEN 1 ELSE 0 END) as closed FROM work_issue_reports wir${joinClause} WHERE ${whereClause}`, params ); callback(null, rows[0]); } catch (err) { callback(err); } }; /** * 카테고리별 통계 */ const getStatsByCategory = async (filters = {}, callback) => { try { const db = await getDb(); let whereClause = '1=1'; const params = []; if (filters.start_date && filters.end_date) { whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`; params.push(filters.start_date, filters.end_date); } const [rows] = await db.query( `SELECT irc.category_type, irc.category_name, COUNT(*) as count FROM work_issue_reports wir INNER JOIN issue_report_categories irc ON wir.issue_category_id = irc.category_id WHERE ${whereClause} GROUP BY irc.category_id ORDER BY irc.category_type, count DESC`, params ); callback(null, rows); } catch (err) { callback(err); } }; /** * 작업장별 통계 */ const getStatsByWorkplace = async (filters = {}, callback) => { try { const db = await getDb(); let whereClause = 'wir.workplace_id IS NOT NULL'; const params = []; if (filters.start_date && filters.end_date) { whereClause += ` AND DATE(wir.report_date) BETWEEN ? AND ?`; params.push(filters.start_date, filters.end_date); } if (filters.factory_category_id) { whereClause += ` AND wir.factory_category_id = ?`; params.push(filters.factory_category_id); } const [rows] = await db.query( `SELECT wir.factory_category_id, wc.category_name as factory_name, wir.workplace_id, w.workplace_name, COUNT(*) as count FROM work_issue_reports wir INNER JOIN workplace_categories wc ON wir.factory_category_id = wc.category_id INNER JOIN workplaces w ON wir.workplace_id = w.workplace_id WHERE ${whereClause} GROUP BY wir.factory_category_id, wir.workplace_id ORDER BY count DESC`, params ); callback(null, rows); } catch (err) { callback(err); } }; module.exports = { // 카테고리 getAllCategories, getCategoriesByType, getCategoryById, createCategory, updateCategory, deleteCategory, // 항목 getItemsByCategory, getAllItems, createItem, updateItem, deleteItem, // 신고 createReport, getAllReports, getReportById, updateReport, deleteReport, // System 3 연동 updateMProjectId, // 상태 관리 receiveReport, assignReport, startProcessing, completeReport, closeReport, getStatusLogs, // 통계 getStatsSummary, getStatsByCategory, getStatsByWorkplace };