// models/equipmentModel.js const { getDb } = require('../dbPool'); const notificationModel = require('./notificationModel'); const EquipmentModel = { // CREATE - 설비 생성 create: async (equipmentData, callback) => { try { const db = await getDb(); const query = ` INSERT INTO equipments ( equipment_code, equipment_name, equipment_type, model_name, manufacturer, supplier, purchase_price, installation_date, serial_number, specifications, status, notes, workplace_id, map_x_percent, map_y_percent, map_width_percent, map_height_percent ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; const values = [ equipmentData.equipment_code, equipmentData.equipment_name, equipmentData.equipment_type || null, equipmentData.model_name || null, equipmentData.manufacturer || null, equipmentData.supplier || null, equipmentData.purchase_price || null, equipmentData.installation_date || null, equipmentData.serial_number || null, equipmentData.specifications || null, equipmentData.status || 'active', equipmentData.notes || null, equipmentData.workplace_id || null, equipmentData.map_x_percent || null, equipmentData.map_y_percent || null, equipmentData.map_width_percent || null, equipmentData.map_height_percent || null ]; const [result] = await db.query(query, values); callback(null, { equipment_id: result.insertId, ...equipmentData }); } catch (error) { callback(error); } }, // READ ALL - 모든 설비 조회 (필터링 옵션 포함) getAll: async (filters, callback) => { try { const db = await getDb(); let query = ` SELECT e.*, w.workplace_name, wc.category_name FROM equipments e LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id WHERE 1=1 `; const values = []; // 필터링: 작업장 ID if (filters.workplace_id) { query += ' AND e.workplace_id = ?'; values.push(filters.workplace_id); } // 필터링: 설비 유형 if (filters.equipment_type) { query += ' AND e.equipment_type = ?'; values.push(filters.equipment_type); } // 필터링: 상태 if (filters.status) { query += ' AND e.status = ?'; values.push(filters.status); } // 필터링: 검색어 (설비명, 설비코드) if (filters.search) { query += ' AND (e.equipment_name LIKE ? OR e.equipment_code LIKE ?)'; const searchTerm = `%${filters.search}%`; values.push(searchTerm, searchTerm); } query += ' ORDER BY e.equipment_code ASC'; const [rows] = await db.query(query, values); callback(null, rows); } catch (error) { callback(error); } }, // READ ONE - 특정 설비 조회 getById: async (equipmentId, callback) => { try { const db = await getDb(); const query = ` SELECT e.*, w.workplace_name, wc.category_name FROM equipments e LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id WHERE e.equipment_id = ? `; const [rows] = await db.query(query, [equipmentId]); callback(null, rows[0]); } catch (error) { callback(error); } }, // READ BY WORKPLACE - 특정 작업장의 설비 조회 getByWorkplace: async (workplaceId, callback) => { try { const db = await getDb(); const query = ` SELECT e.* FROM equipments e WHERE e.workplace_id = ? ORDER BY e.equipment_code ASC `; const [rows] = await db.query(query, [workplaceId]); callback(null, rows); } catch (error) { callback(error); } }, // READ ACTIVE - 활성 설비만 조회 getActive: async (callback) => { try { const db = await getDb(); const query = ` SELECT e.*, w.workplace_name, wc.category_name FROM equipments e LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id WHERE e.status = 'active' ORDER BY e.equipment_code ASC `; const [rows] = await db.query(query); callback(null, rows); } catch (error) { callback(error); } }, // UPDATE - 설비 수정 update: async (equipmentId, equipmentData, callback) => { try { const db = await getDb(); const query = ` UPDATE equipments SET equipment_code = ?, equipment_name = ?, equipment_type = ?, model_name = ?, manufacturer = ?, supplier = ?, purchase_price = ?, installation_date = ?, serial_number = ?, specifications = ?, status = ?, notes = ?, workplace_id = ?, map_x_percent = ?, map_y_percent = ?, map_width_percent = ?, map_height_percent = ?, updated_at = NOW() WHERE equipment_id = ? `; const values = [ equipmentData.equipment_code, equipmentData.equipment_name, equipmentData.equipment_type || null, equipmentData.model_name || null, equipmentData.manufacturer || null, equipmentData.supplier || null, equipmentData.purchase_price || null, equipmentData.installation_date || null, equipmentData.serial_number || null, equipmentData.specifications || null, equipmentData.status || 'active', equipmentData.notes || null, equipmentData.workplace_id || null, equipmentData.map_x_percent || null, equipmentData.map_y_percent || null, equipmentData.map_width_percent || null, equipmentData.map_height_percent || null, equipmentId ]; const [result] = await db.query(query, values); if (result.affectedRows === 0) { return callback(new Error('Equipment not found')); } callback(null, { equipment_id: equipmentId, ...equipmentData }); } catch (error) { callback(error); } }, // UPDATE MAP POSITION - 지도상 위치 업데이트 (선택적으로 workplace_id도 업데이트) updateMapPosition: async (equipmentId, positionData, callback) => { try { const db = await getDb(); // workplace_id가 포함된 경우 함께 업데이트 const hasWorkplaceId = positionData.workplace_id !== undefined; const query = hasWorkplaceId ? ` UPDATE equipments SET workplace_id = ?, map_x_percent = ?, map_y_percent = ?, map_width_percent = ?, map_height_percent = ?, updated_at = NOW() WHERE equipment_id = ? ` : ` UPDATE equipments SET map_x_percent = ?, map_y_percent = ?, map_width_percent = ?, map_height_percent = ?, updated_at = NOW() WHERE equipment_id = ? `; const values = hasWorkplaceId ? [ positionData.workplace_id, positionData.map_x_percent, positionData.map_y_percent, positionData.map_width_percent, positionData.map_height_percent, equipmentId ] : [ positionData.map_x_percent, positionData.map_y_percent, positionData.map_width_percent, positionData.map_height_percent, equipmentId ]; const [result] = await db.query(query, values); if (result.affectedRows === 0) { return callback(new Error('Equipment not found')); } callback(null, { equipment_id: equipmentId, ...positionData }); } catch (error) { callback(error); } }, // DELETE - 설비 삭제 delete: async (equipmentId, callback) => { try { const db = await getDb(); const query = 'DELETE FROM equipments WHERE equipment_id = ?'; const [result] = await db.query(query, [equipmentId]); if (result.affectedRows === 0) { return callback(new Error('Equipment not found')); } callback(null, { equipment_id: equipmentId }); } catch (error) { callback(error); } }, // CHECK DUPLICATE CODE - 설비 코드 중복 확인 checkDuplicateCode: async (equipmentCode, excludeEquipmentId, callback) => { try { const db = await getDb(); let query = 'SELECT equipment_id FROM equipments WHERE equipment_code = ?'; const values = [equipmentCode]; if (excludeEquipmentId) { query += ' AND equipment_id != ?'; values.push(excludeEquipmentId); } const [rows] = await db.query(query, values); callback(null, rows.length > 0); } catch (error) { callback(error); } }, // GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회 getEquipmentTypes: async (callback) => { try { const db = await getDb(); const query = ` SELECT DISTINCT equipment_type FROM equipments WHERE equipment_type IS NOT NULL ORDER BY equipment_type ASC `; const [rows] = await db.query(query); callback(null, rows.map(row => row.equipment_type)); } catch (error) { callback(error); } }, // GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성 (TKP-001 형식) getNextEquipmentCode: async (prefix = 'TKP', callback) => { try { const db = await getDb(); // 해당 접두사로 시작하는 가장 큰 번호 찾기 const query = ` SELECT equipment_code FROM equipments WHERE equipment_code LIKE ? ORDER BY equipment_code DESC LIMIT 1 `; const [rows] = await db.query(query, [`${prefix}-%`]); let nextNumber = 1; if (rows.length > 0) { // TKP-001 형식에서 숫자 부분 추출 const lastCode = rows[0].equipment_code; const match = lastCode.match(new RegExp(`^${prefix}-(\\d+)$`)); if (match) { nextNumber = parseInt(match[1], 10) + 1; } } // 3자리로 패딩 (001, 002, ...) const nextCode = `${prefix}-${String(nextNumber).padStart(3, '0')}`; callback(null, nextCode); } catch (error) { callback(error); } }, // ========================================== // 설비 사진 관리 // ========================================== // ADD PHOTO - 설비 사진 추가 addPhoto: async (equipmentId, photoData, callback) => { try { const db = await getDb(); const query = ` INSERT INTO equipment_photos ( equipment_id, photo_path, description, display_order, uploaded_by ) VALUES (?, ?, ?, ?, ?) `; const values = [ equipmentId, photoData.photo_path, photoData.description || null, photoData.display_order || 0, photoData.uploaded_by || null ]; const [result] = await db.query(query, values); callback(null, { photo_id: result.insertId, equipment_id: equipmentId, ...photoData }); } catch (error) { callback(error); } }, // GET PHOTOS - 설비 사진 조회 getPhotos: async (equipmentId, callback) => { try { const db = await getDb(); const query = ` SELECT ep.*, u.name AS uploaded_by_name FROM equipment_photos ep LEFT JOIN users u ON ep.uploaded_by = u.user_id WHERE ep.equipment_id = ? ORDER BY ep.display_order ASC, ep.created_at ASC `; const [rows] = await db.query(query, [equipmentId]); callback(null, rows); } catch (error) { callback(error); } }, // DELETE PHOTO - 설비 사진 삭제 deletePhoto: async (photoId, callback) => { try { const db = await getDb(); // 먼저 사진 정보 조회 (파일 삭제용) const [photo] = await db.query( 'SELECT photo_path FROM equipment_photos WHERE photo_id = ?', [photoId] ); const query = 'DELETE FROM equipment_photos WHERE photo_id = ?'; const [result] = await db.query(query, [photoId]); if (result.affectedRows === 0) { return callback(new Error('Photo not found')); } callback(null, { photo_id: photoId, photo_path: photo[0]?.photo_path }); } catch (error) { callback(error); } }, // ========================================== // 설비 임시 이동 // ========================================== // MOVE TEMPORARILY - 설비 임시 이동 moveTemporarily: async (equipmentId, moveData, callback) => { try { const db = await getDb(); // 1. 설비 현재 위치 업데이트 const updateQuery = ` UPDATE equipments SET current_workplace_id = ?, current_map_x_percent = ?, current_map_y_percent = ?, current_map_width_percent = ?, current_map_height_percent = ?, is_temporarily_moved = TRUE, moved_at = NOW(), moved_by = ?, updated_at = NOW() WHERE equipment_id = ? `; const updateValues = [ moveData.target_workplace_id, moveData.target_x_percent, moveData.target_y_percent, moveData.target_width_percent || null, moveData.target_height_percent || null, moveData.moved_by || null, equipmentId ]; const [result] = await db.query(updateQuery, updateValues); if (result.affectedRows === 0) { return callback(new Error('Equipment not found')); } // 2. 이동 이력 기록 const logQuery = ` INSERT INTO equipment_move_logs ( equipment_id, move_type, from_workplace_id, to_workplace_id, from_x_percent, from_y_percent, to_x_percent, to_y_percent, reason, moved_by ) VALUES (?, 'temporary', ?, ?, ?, ?, ?, ?, ?, ?) `; await db.query(logQuery, [ equipmentId, moveData.from_workplace_id || null, moveData.target_workplace_id, moveData.from_x_percent || null, moveData.from_y_percent || null, moveData.target_x_percent, moveData.target_y_percent, moveData.reason || null, moveData.moved_by || null ]); callback(null, { equipment_id: equipmentId, moved: true }); } catch (error) { callback(error); } }, // RETURN TO ORIGINAL - 설비 원위치 복귀 returnToOriginal: async (equipmentId, userId, callback) => { try { const db = await getDb(); // 1. 현재 임시 위치 정보 조회 const [equipment] = await db.query( 'SELECT current_workplace_id, current_map_x_percent, current_map_y_percent FROM equipments WHERE equipment_id = ?', [equipmentId] ); if (!equipment[0]) { return callback(new Error('Equipment not found')); } // 2. 임시 위치 필드 초기화 const updateQuery = ` UPDATE equipments SET current_workplace_id = NULL, current_map_x_percent = NULL, current_map_y_percent = NULL, current_map_width_percent = NULL, current_map_height_percent = NULL, is_temporarily_moved = FALSE, moved_at = NULL, moved_by = NULL, updated_at = NOW() WHERE equipment_id = ? `; await db.query(updateQuery, [equipmentId]); // 3. 복귀 이력 기록 const logQuery = ` INSERT INTO equipment_move_logs ( equipment_id, move_type, from_workplace_id, from_x_percent, from_y_percent, reason, moved_by ) VALUES (?, 'return', ?, ?, ?, '원위치 복귀', ?) `; await db.query(logQuery, [ equipmentId, equipment[0].current_workplace_id, equipment[0].current_map_x_percent, equipment[0].current_map_y_percent, userId || null ]); callback(null, { equipment_id: equipmentId, returned: true }); } catch (error) { callback(error); } }, // GET TEMPORARILY MOVED - 임시 이동된 설비 목록 getTemporarilyMoved: async (callback) => { try { const db = await getDb(); const query = ` SELECT e.*, w_orig.workplace_name AS original_workplace_name, w_curr.workplace_name AS current_workplace_name, u.name AS moved_by_name FROM equipments e LEFT JOIN workplaces w_orig ON e.workplace_id = w_orig.workplace_id LEFT JOIN workplaces w_curr ON e.current_workplace_id = w_curr.workplace_id LEFT JOIN users u ON e.moved_by = u.user_id WHERE e.is_temporarily_moved = TRUE ORDER BY e.moved_at DESC `; const [rows] = await db.query(query); callback(null, rows); } catch (error) { callback(error); } }, // GET MOVE LOGS - 설비 이동 이력 조회 getMoveLogs: async (equipmentId, callback) => { try { const db = await getDb(); const query = ` SELECT eml.*, w_from.workplace_name AS from_workplace_name, w_to.workplace_name AS to_workplace_name, u.name AS moved_by_name FROM equipment_move_logs eml LEFT JOIN workplaces w_from ON eml.from_workplace_id = w_from.workplace_id LEFT JOIN workplaces w_to ON eml.to_workplace_id = w_to.workplace_id LEFT JOIN users u ON eml.moved_by = u.user_id WHERE eml.equipment_id = ? ORDER BY eml.moved_at DESC `; const [rows] = await db.query(query, [equipmentId]); callback(null, rows); } catch (error) { callback(error); } }, // ========================================== // 설비 외부 반출/반입 // ========================================== // EXPORT EQUIPMENT - 설비 외부 반출 exportEquipment: async (exportData, callback) => { try { const db = await getDb(); // 1. 반출 로그 생성 const logQuery = ` INSERT INTO equipment_external_logs ( equipment_id, log_type, export_date, expected_return_date, destination, reason, notes, exported_by ) VALUES (?, 'export', ?, ?, ?, ?, ?, ?) `; const logValues = [ exportData.equipment_id, exportData.export_date || new Date().toISOString().slice(0, 10), exportData.expected_return_date || null, exportData.destination || null, exportData.reason || null, exportData.notes || null, exportData.exported_by || null ]; const [logResult] = await db.query(logQuery, logValues); // 2. 설비 상태 업데이트 const status = exportData.is_repair ? 'repair_external' : 'external'; await db.query( 'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?', [status, exportData.equipment_id] ); callback(null, { log_id: logResult.insertId, equipment_id: exportData.equipment_id, exported: true }); } catch (error) { callback(error); } }, // RETURN EQUIPMENT - 설비 반입 (외부에서 복귀) returnEquipment: async (logId, returnData, callback) => { try { const db = await getDb(); // 1. 반출 로그 조회 const [logs] = await db.query( 'SELECT equipment_id FROM equipment_external_logs WHERE log_id = ?', [logId] ); if (!logs[0]) { return callback(new Error('Export log not found')); } const equipmentId = logs[0].equipment_id; // 2. 반출 로그 업데이트 await db.query( `UPDATE equipment_external_logs SET actual_return_date = ?, returned_by = ?, notes = CONCAT(IFNULL(notes, ''), '\n반입: ', IFNULL(?, '')), updated_at = NOW() WHERE log_id = ?`, [ returnData.return_date || new Date().toISOString().slice(0, 10), returnData.returned_by || null, returnData.notes || '', logId ] ); // 3. 설비 상태 복원 await db.query( 'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?', [returnData.new_status || 'active', equipmentId] ); callback(null, { log_id: logId, equipment_id: equipmentId, returned: true }); } catch (error) { callback(error); } }, // GET EXTERNAL LOGS - 설비 외부 반출 이력 조회 getExternalLogs: async (equipmentId, callback) => { try { const db = await getDb(); const query = ` SELECT eel.*, u_exp.name AS exported_by_name, u_ret.name AS returned_by_name FROM equipment_external_logs eel LEFT JOIN users u_exp ON eel.exported_by = u_exp.user_id LEFT JOIN users u_ret ON eel.returned_by = u_ret.user_id WHERE eel.equipment_id = ? ORDER BY eel.created_at DESC `; const [rows] = await db.query(query, [equipmentId]); callback(null, rows); } catch (error) { callback(error); } }, // GET EXPORTED EQUIPMENTS - 현재 외부 반출 중인 설비 목록 getExportedEquipments: async (callback) => { try { const db = await getDb(); const query = ` SELECT e.*, w.workplace_name, eel.export_date, eel.expected_return_date, eel.destination, eel.reason, u.name AS exported_by_name FROM equipments e INNER JOIN ( SELECT equipment_id, MAX(log_id) AS latest_log_id FROM equipment_external_logs WHERE actual_return_date IS NULL GROUP BY equipment_id ) latest ON e.equipment_id = latest.equipment_id INNER JOIN equipment_external_logs eel ON eel.log_id = latest.latest_log_id LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id LEFT JOIN users u ON eel.exported_by = u.user_id WHERE e.status IN ('external', 'repair_external') ORDER BY eel.export_date DESC `; const [rows] = await db.query(query); callback(null, rows); } catch (error) { callback(error); } }, // ========================================== // 설비 수리 신청 (work_issue_reports 연동) // ========================================== // CREATE REPAIR REQUEST - 수리 신청 (신고 시스템 활용) createRepairRequest: async (requestData, callback) => { try { const db = await getDb(); // 설비 수리 카테고리 ID 조회 const [categories] = await db.query( "SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리' LIMIT 1" ); if (!categories[0]) { return callback(new Error('설비 수리 카테고리가 없습니다')); } const categoryId = categories[0].category_id; // 항목 ID 조회 (지정된 항목이 없으면 첫번째 항목 사용) let itemId = requestData.item_id; if (!itemId) { const [items] = await db.query( 'SELECT item_id FROM issue_report_items WHERE category_id = ? LIMIT 1', [categoryId] ); itemId = items[0]?.item_id; } // 사진 경로 분리 (최대 5장) const photos = requestData.photo_paths || []; // work_issue_reports에 삽입 const query = ` INSERT INTO work_issue_reports ( reporter_id, issue_category_id, issue_item_id, workplace_id, equipment_id, additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reported') `; const values = [ requestData.reported_by || null, categoryId, itemId, requestData.workplace_id || null, requestData.equipment_id, requestData.description || null, photos[0] || null, photos[1] || null, photos[2] || null, photos[3] || null, photos[4] || null ]; const [result] = await db.query(query, values); // 설비 상태를 repair_needed로 업데이트 await db.query( 'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?', ['repair_needed', requestData.equipment_id] ); // 알림 생성 try { await notificationModel.createRepairNotification({ equipment_id: requestData.equipment_id, equipment_name: requestData.equipment_name || '설비', repair_type: requestData.repair_type || '일반 수리', request_id: result.insertId, created_by: requestData.reported_by }); } catch (notifError) { console.error('알림 생성 실패:', notifError); // 알림 생성 실패해도 수리 신청은 성공으로 처리 } callback(null, { report_id: result.insertId, equipment_id: requestData.equipment_id, created: true }); } catch (error) { callback(error); } }, // GET REPAIR HISTORY - 설비 수리 이력 조회 getRepairHistory: async (equipmentId, callback) => { try { const db = await getDb(); const query = ` SELECT wir.*, irc.category_name, iri.item_name, u_rep.name AS reported_by_name, u_res.name AS resolved_by_name, w.workplace_name FROM work_issue_reports wir LEFT 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 u_rep ON wir.reporter_id = u_rep.user_id LEFT JOIN users u_res ON wir.resolved_by = u_res.user_id LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id WHERE wir.equipment_id = ? ORDER BY wir.created_at DESC `; const [rows] = await db.query(query, [equipmentId]); callback(null, rows); } catch (error) { callback(error); } }, // GET REPAIR CATEGORIES - 설비 수리 항목 목록 조회 getRepairCategories: async (callback) => { try { const db = await getDb(); // 설비 수리 카테고리의 항목들 조회 const query = ` SELECT iri.item_id, iri.item_name, iri.description, iri.severity FROM issue_report_items iri INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id WHERE irc.category_name = '설비 수리' AND iri.is_active = 1 ORDER BY iri.display_order ASC `; const [rows] = await db.query(query); callback(null, rows); } catch (error) { callback(error); } }, // ADD REPAIR CATEGORY - 새 수리 항목 추가 addRepairCategory: async (itemName, callback) => { try { const db = await getDb(); // 설비 수리 카테고리 ID 조회 const [categories] = await db.query( "SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리'" ); if (categories.length === 0) { return callback(new Error('설비 수리 카테고리가 없습니다.')); } const categoryId = categories[0].category_id; // 중복 확인 const [existing] = await db.query( 'SELECT item_id FROM issue_report_items WHERE category_id = ? AND item_name = ?', [categoryId, itemName] ); if (existing.length > 0) { // 이미 존재하면 해당 ID 반환 return callback(null, { item_id: existing[0].item_id, item_name: itemName, isNew: false }); } // 다음 display_order 구하기 const [maxOrder] = await db.query( 'SELECT MAX(display_order) as max_order FROM issue_report_items WHERE category_id = ?', [categoryId] ); const nextOrder = (maxOrder[0].max_order || 0) + 1; // 새 항목 추가 const [result] = await db.query( `INSERT INTO issue_report_items (category_id, item_name, display_order, is_active) VALUES (?, ?, ?, 1)`, [categoryId, itemName, nextOrder] ); callback(null, { item_id: result.insertId, item_name: itemName, isNew: true }); } catch (error) { callback(error); } } }; module.exports = EquipmentModel;