feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
949
deploy/tkfb-package/api.hyungi.net/models/equipmentModel.js
Normal file
949
deploy/tkfb-package/api.hyungi.net/models/equipmentModel.js
Normal file
@@ -0,0 +1,949 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user