feat: 설비 상세 패널 및 임시 이동 기능 구현
- 설비 마커 클릭 시 슬라이드 패널로 상세 정보 표시 - 설비 사진 업로드/삭제 기능 - 설비 임시 이동 기능 (3단계 지도 기반 선택) - Step 1: 공장 선택 - Step 2: 레이아웃 지도에서 작업장 선택 - Step 3: 상세 지도에서 위치 선택 - 설비 외부 반출/반입 기능 - 설비 수리 신청 기능 (기존 신고 시스템 연동) - DB 마이그레이션 추가 (사진, 임시이동, 외부반출 테이블) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// controllers/equipmentController.js
|
||||
const EquipmentModel = require('../models/equipmentModel');
|
||||
const imageUploadService = require('../services/imageUploadService');
|
||||
|
||||
const EquipmentController = {
|
||||
// CREATE - 설비 생성
|
||||
@@ -379,6 +380,529 @@ const EquipmentController = {
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 사진 관리
|
||||
// ==========================================
|
||||
|
||||
// ADD PHOTO - 설비 사진 추가
|
||||
addPhoto: async (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const { photo_base64, description, display_order } = req.body;
|
||||
|
||||
if (!photo_base64) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '사진 데이터가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// Base64 이미지를 파일로 저장
|
||||
const photoPath = await imageUploadService.saveBase64Image(
|
||||
photo_base64,
|
||||
'equipment',
|
||||
'equipments'
|
||||
);
|
||||
|
||||
if (!photoPath) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 저장에 실패했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// DB에 사진 정보 저장
|
||||
const photoData = {
|
||||
photo_path: photoPath,
|
||||
description: description || null,
|
||||
display_order: display_order || 0,
|
||||
uploaded_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.addPhoto(equipmentId, photoData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('사진 정보 저장 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 정보 저장 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '사진이 성공적으로 추가되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 추가 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET PHOTOS - 설비 사진 조회
|
||||
getPhotos: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getPhotos(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('사진 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// DELETE PHOTO - 설비 사진 삭제
|
||||
deletePhoto: async (req, res) => {
|
||||
try {
|
||||
const photoId = req.params.photoId;
|
||||
|
||||
EquipmentModel.deletePhoto(photoId, async (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === 'Photo not found') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '사진을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
console.error('사진 삭제 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사진 삭제 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 파일 시스템에서 사진 삭제
|
||||
if (result.photo_path) {
|
||||
await imageUploadService.deleteFile(result.photo_path);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '사진이 성공적으로 삭제되었습니다.',
|
||||
data: { photo_id: photoId }
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('사진 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 임시 이동
|
||||
// ==========================================
|
||||
|
||||
// MOVE TEMPORARILY - 설비 임시 이동
|
||||
moveTemporarily: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const moveData = {
|
||||
target_workplace_id: req.body.target_workplace_id,
|
||||
target_x_percent: req.body.target_x_percent,
|
||||
target_y_percent: req.body.target_y_percent,
|
||||
target_width_percent: req.body.target_width_percent,
|
||||
target_height_percent: req.body.target_height_percent,
|
||||
from_workplace_id: req.body.from_workplace_id,
|
||||
from_x_percent: req.body.from_x_percent,
|
||||
from_y_percent: req.body.from_y_percent,
|
||||
reason: req.body.reason,
|
||||
moved_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
if (!moveData.target_workplace_id || moveData.target_x_percent === undefined || moveData.target_y_percent === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이동할 작업장과 위치가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
EquipmentModel.moveTemporarily(equipmentId, moveData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 이동 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 이동 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 임시 이동되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 이동 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// RETURN TO ORIGINAL - 설비 원위치 복귀
|
||||
returnToOriginal: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const userId = req.user?.user_id || null;
|
||||
|
||||
EquipmentModel.returnToOriginal(equipmentId, userId, (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === 'Equipment not found') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '설비를 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
console.error('설비 복귀 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 복귀 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 원위치로 복귀되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 복귀 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET TEMPORARILY MOVED - 임시 이동된 설비 목록
|
||||
getTemporarilyMoved: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getTemporarilyMoved((error, results) => {
|
||||
if (error) {
|
||||
console.error('임시 이동 설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '임시 이동 설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('임시 이동 설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET MOVE LOGS - 설비 이동 이력 조회
|
||||
getMoveLogs: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getMoveLogs(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('이동 이력 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '이동 이력 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('이동 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 외부 반출/반입
|
||||
// ==========================================
|
||||
|
||||
// EXPORT EQUIPMENT - 설비 외부 반출
|
||||
exportEquipment: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const exportData = {
|
||||
equipment_id: equipmentId,
|
||||
export_date: req.body.export_date,
|
||||
expected_return_date: req.body.expected_return_date,
|
||||
destination: req.body.destination,
|
||||
reason: req.body.reason,
|
||||
notes: req.body.notes,
|
||||
is_repair: req.body.is_repair || false,
|
||||
exported_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.exportEquipment(exportData, (error, result) => {
|
||||
if (error) {
|
||||
console.error('설비 반출 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 반출 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '설비가 외부로 반출되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 반출 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// RETURN EQUIPMENT - 설비 반입 (외부에서 복귀)
|
||||
returnEquipment: (req, res) => {
|
||||
try {
|
||||
const logId = req.params.logId;
|
||||
const returnData = {
|
||||
return_date: req.body.return_date,
|
||||
new_status: req.body.new_status || 'active',
|
||||
notes: req.body.notes,
|
||||
returned_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.returnEquipment(logId, returnData, (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === 'Export log not found') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '반출 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
console.error('설비 반입 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '설비 반입 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '설비가 반입되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('설비 반입 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET EXTERNAL LOGS - 설비 외부 반출 이력 조회
|
||||
getExternalLogs: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getExternalLogs(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('반출 이력 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '반출 이력 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('반출 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET EXPORTED EQUIPMENTS - 현재 외부 반출 중인 설비 목록
|
||||
getExportedEquipments: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getExportedEquipments((error, results) => {
|
||||
if (error) {
|
||||
console.error('반출 중 설비 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '반출 중 설비 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('반출 중 설비 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
// 설비 수리 신청
|
||||
// ==========================================
|
||||
|
||||
// CREATE REPAIR REQUEST - 수리 신청
|
||||
createRepairRequest: async (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
const { photo_base64_list, description, item_id, workplace_id } = req.body;
|
||||
|
||||
// 사진 저장 (있는 경우)
|
||||
let photoPaths = [];
|
||||
if (photo_base64_list && photo_base64_list.length > 0) {
|
||||
for (const base64 of photo_base64_list) {
|
||||
const path = await imageUploadService.saveBase64Image(base64, 'repair', 'issues');
|
||||
if (path) photoPaths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
const requestData = {
|
||||
equipment_id: equipmentId,
|
||||
item_id: item_id || null,
|
||||
workplace_id: workplace_id || null,
|
||||
description: description || null,
|
||||
photo_paths: photoPaths.length > 0 ? photoPaths : null,
|
||||
reported_by: req.user?.user_id || null
|
||||
};
|
||||
|
||||
EquipmentModel.createRepairRequest(requestData, (error, result) => {
|
||||
if (error) {
|
||||
if (error.message === '설비 수리 카테고리가 없습니다') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
console.error('수리 신청 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 신청 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '수리 신청이 접수되었습니다.',
|
||||
data: result
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 신청 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET REPAIR HISTORY - 설비 수리 이력 조회
|
||||
getRepairHistory: (req, res) => {
|
||||
try {
|
||||
const equipmentId = req.params.id;
|
||||
|
||||
EquipmentModel.getRepairHistory(equipmentId, (error, results) => {
|
||||
if (error) {
|
||||
console.error('수리 이력 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 이력 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 이력 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// GET REPAIR CATEGORIES - 설비 수리 항목 목록 조회
|
||||
getRepairCategories: (req, res) => {
|
||||
try {
|
||||
EquipmentModel.getRepairCategories((error, results) => {
|
||||
if (error) {
|
||||
console.error('수리 항목 조회 오류:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '수리 항목 조회 중 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('수리 항목 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다.'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 설비 사진 테이블 생성
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205001000_create_equipment_photos.sql
|
||||
|
||||
CREATE TABLE IF NOT EXISTS equipment_photos (
|
||||
photo_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL,
|
||||
photo_path VARCHAR(255) NOT NULL COMMENT '이미지 경로',
|
||||
description VARCHAR(200) COMMENT '사진 설명',
|
||||
display_order INT DEFAULT 0 COMMENT '표시 순서',
|
||||
uploaded_by INT COMMENT '업로드한 사용자 ID',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eq_photos_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eq_photos_user FOREIGN KEY (uploaded_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_eq_photos_equipment_id (equipment_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,174 @@
|
||||
-- 설비 임시이동 필드 추가 및 신고 시스템 연동
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205002000_add_equipment_move_fields.sql
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: equipments 테이블에 임시이동 필드 추가
|
||||
-- ============================================
|
||||
|
||||
-- current_workplace_id 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_workplace_id';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_workplace_id INT UNSIGNED NULL COMMENT ''현재 임시 위치 - 작업장 ID'' AFTER map_height_percent',
|
||||
'SELECT ''current_workplace_id already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_x_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_x_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_x_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 X%'' AFTER current_workplace_id',
|
||||
'SELECT ''current_map_x_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_y_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_y_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_y_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 Y%'' AFTER current_map_x_percent',
|
||||
'SELECT ''current_map_y_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_width_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_width_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_width_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 너비%'' AFTER current_map_y_percent',
|
||||
'SELECT ''current_map_width_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- current_map_height_percent 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'current_map_height_percent';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN current_map_height_percent DECIMAL(5,2) NULL COMMENT ''현재 위치 높이%'' AFTER current_map_width_percent',
|
||||
'SELECT ''current_map_height_percent already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- is_temporarily_moved 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'is_temporarily_moved';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN is_temporarily_moved BOOLEAN DEFAULT FALSE COMMENT ''임시 이동 상태'' AFTER current_map_height_percent',
|
||||
'SELECT ''is_temporarily_moved already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- moved_at 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'moved_at';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN moved_at DATETIME NULL COMMENT ''이동 일시'' AFTER is_temporarily_moved',
|
||||
'SELECT ''moved_at already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- moved_by 컬럼 추가
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND column_name = 'moved_by';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE equipments ADD COLUMN moved_by INT NULL COMMENT ''이동 처리자'' AFTER moved_at',
|
||||
'SELECT ''moved_by already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Foreign Key: current_workplace_id -> workplaces
|
||||
SELECT COUNT(*) INTO @fk_exists
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = @dbname AND table_name = 'equipments' AND constraint_name = 'fk_eq_current_workplace';
|
||||
|
||||
SET @sql = IF(@fk_exists = 0,
|
||||
'ALTER TABLE equipments ADD CONSTRAINT fk_eq_current_workplace FOREIGN KEY (current_workplace_id) REFERENCES workplaces(workplace_id) ON DELETE SET NULL',
|
||||
'SELECT ''fk_eq_current_workplace already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT 'equipments 임시이동 필드 추가 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: work_issue_reports에 equipment_id 필드 추가
|
||||
-- ============================================
|
||||
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @dbname AND table_name = 'work_issue_reports' AND column_name = 'equipment_id';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE work_issue_reports ADD COLUMN equipment_id INT UNSIGNED NULL COMMENT ''관련 설비 ID'' AFTER visit_request_id',
|
||||
'SELECT ''equipment_id already exists in work_issue_reports''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Foreign Key
|
||||
SELECT COUNT(*) INTO @fk_exists
|
||||
FROM information_schema.table_constraints
|
||||
WHERE table_schema = @dbname AND table_name = 'work_issue_reports' AND constraint_name = 'fk_wir_equipment';
|
||||
|
||||
SET @sql = IF(@fk_exists = 0 AND @col_exists = 0,
|
||||
'ALTER TABLE work_issue_reports ADD CONSTRAINT fk_wir_equipment FOREIGN KEY (equipment_id) REFERENCES equipments(equipment_id) ON DELETE SET NULL',
|
||||
'SELECT ''fk_wir_equipment already exists or column not added''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Index
|
||||
SELECT COUNT(*) INTO @idx_exists
|
||||
FROM information_schema.statistics
|
||||
WHERE table_schema = @dbname AND table_name = 'work_issue_reports' AND index_name = 'idx_wir_equipment_id';
|
||||
|
||||
SET @sql = IF(@idx_exists = 0 AND @col_exists = 0,
|
||||
'ALTER TABLE work_issue_reports ADD INDEX idx_wir_equipment_id (equipment_id)',
|
||||
'SELECT ''idx_wir_equipment_id already exists''');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT 'work_issue_reports equipment_id 추가 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: 설비 수리 카테고리 추가
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO issue_report_categories (category_type, category_name, description, display_order, is_active)
|
||||
SELECT 'nonconformity', '설비 수리', '설비 고장 및 수리 요청', 10, 1
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_categories WHERE category_name = '설비 수리'
|
||||
);
|
||||
|
||||
-- 설비 수리 카테고리에 기본 항목 추가
|
||||
SET @category_id = (SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리' LIMIT 1);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '기계 고장', '기계 작동 불가 또는 이상', 'high', 1, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '기계 고장'
|
||||
);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '부품 교체 필요', '소모품 또는 부품 교체 필요', 'medium', 2, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '부품 교체 필요'
|
||||
);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '정기 점검 필요', '예방 정비 또는 정기 점검', 'low', 3, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '정기 점검 필요'
|
||||
);
|
||||
|
||||
INSERT INTO issue_report_items (category_id, item_name, description, severity, display_order, is_active)
|
||||
SELECT @category_id, '외부 수리 필요', '전문 업체 수리가 필요한 경우', 'high', 4, 1
|
||||
WHERE @category_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM issue_report_items WHERE category_id = @category_id AND item_name = '외부 수리 필요'
|
||||
);
|
||||
|
||||
SELECT '설비 수리 카테고리 및 항목 추가 완료' AS status;
|
||||
@@ -0,0 +1,86 @@
|
||||
-- 설비 외부반출 테이블 생성 및 상태 ENUM 확장
|
||||
-- 실행: docker exec -i tkfb_db mysql -u hyungi -p'your_password' hyungi < db/migrations/20260205003000_create_equipment_external_logs.sql
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
|
||||
-- ============================================
|
||||
-- STEP 1: equipment_external_logs 테이블 생성
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS equipment_external_logs (
|
||||
log_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL COMMENT '설비 ID',
|
||||
log_type ENUM('export', 'return') NOT NULL COMMENT '반출/반입',
|
||||
export_date DATE COMMENT '반출일',
|
||||
expected_return_date DATE COMMENT '반입 예정일',
|
||||
actual_return_date DATE COMMENT '실제 반입일',
|
||||
destination VARCHAR(200) COMMENT '반출처 (수리업체명 등)',
|
||||
reason TEXT COMMENT '반출 사유',
|
||||
notes TEXT COMMENT '비고',
|
||||
exported_by INT COMMENT '반출 담당자',
|
||||
returned_by INT COMMENT '반입 담당자',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eel_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eel_exported_by FOREIGN KEY (exported_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_eel_returned_by FOREIGN KEY (returned_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_eel_equipment_id (equipment_id),
|
||||
INDEX idx_eel_log_type (log_type),
|
||||
INDEX idx_eel_export_date (export_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SELECT 'equipment_external_logs 테이블 생성 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 2: equipments 테이블 status ENUM 확장
|
||||
-- ============================================
|
||||
|
||||
-- 현재 status 컬럼의 ENUM 값 확인 후 확장
|
||||
-- 기존: active, maintenance, repair_needed, inactive
|
||||
-- 추가: external (외부 반출), repair_external (수리 외주)
|
||||
|
||||
ALTER TABLE equipments
|
||||
MODIFY COLUMN status ENUM(
|
||||
'active', -- 정상 가동
|
||||
'maintenance', -- 점검 중
|
||||
'repair_needed', -- 수리 필요
|
||||
'inactive', -- 비활성
|
||||
'external', -- 외부 반출
|
||||
'repair_external' -- 수리 외주 (외부 수리)
|
||||
) DEFAULT 'active' COMMENT '설비 상태';
|
||||
|
||||
SELECT 'equipments status ENUM 확장 완료' AS status;
|
||||
|
||||
-- ============================================
|
||||
-- STEP 3: 설비 이동 이력 테이블 생성 (선택)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS equipment_move_logs (
|
||||
log_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
equipment_id INT UNSIGNED NOT NULL COMMENT '설비 ID',
|
||||
move_type ENUM('temporary', 'return') NOT NULL COMMENT '임시이동/복귀',
|
||||
from_workplace_id INT UNSIGNED COMMENT '이전 작업장',
|
||||
to_workplace_id INT UNSIGNED COMMENT '이동 작업장',
|
||||
from_x_percent DECIMAL(5,2) COMMENT '이전 X좌표',
|
||||
from_y_percent DECIMAL(5,2) COMMENT '이전 Y좌표',
|
||||
to_x_percent DECIMAL(5,2) COMMENT '이동 X좌표',
|
||||
to_y_percent DECIMAL(5,2) COMMENT '이동 Y좌표',
|
||||
reason TEXT COMMENT '이동 사유',
|
||||
moved_by INT COMMENT '이동 처리자',
|
||||
moved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_eml_equipment FOREIGN KEY (equipment_id)
|
||||
REFERENCES equipments(equipment_id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_eml_from_workplace FOREIGN KEY (from_workplace_id)
|
||||
REFERENCES workplaces(workplace_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_eml_to_workplace FOREIGN KEY (to_workplace_id)
|
||||
REFERENCES workplaces(workplace_id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_eml_moved_by FOREIGN KEY (moved_by)
|
||||
REFERENCES users(user_id) ON DELETE SET NULL,
|
||||
INDEX idx_eml_equipment_id (equipment_id),
|
||||
INDEX idx_eml_move_type (move_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SELECT 'equipment_move_logs 테이블 생성 완료' AS status;
|
||||
@@ -353,6 +353,534 @@ const EquipmentModel = {
|
||||
} 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]
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,31 @@ router.get('/next-code', equipmentController.getNextEquipmentCode);
|
||||
// READ 작업장별 설비
|
||||
router.get('/workplace/:workplaceId', equipmentController.getEquipmentsByWorkplace);
|
||||
|
||||
// ==================== 임시 이동 (목록) ====================
|
||||
|
||||
// 임시 이동된 설비 목록
|
||||
router.get('/moved/list', equipmentController.getTemporarilyMoved);
|
||||
|
||||
// ==================== 외부 반출 (목록) ====================
|
||||
|
||||
// 현재 외부 반출 중인 설비 목록
|
||||
router.get('/exported/list', equipmentController.getExportedEquipments);
|
||||
|
||||
// 반출 로그 반입 처리
|
||||
router.post('/external-logs/:logId/return', equipmentController.returnEquipment);
|
||||
|
||||
// ==================== 수리 ====================
|
||||
|
||||
// 수리 항목 목록 조회
|
||||
router.get('/repair-categories', equipmentController.getRepairCategories);
|
||||
|
||||
// ==================== 사진 관리 ====================
|
||||
|
||||
// 사진 삭제 (설비 ID 없이 photo_id만으로)
|
||||
router.delete('/photos/:photoId', equipmentController.deletePhoto);
|
||||
|
||||
// ==================== 개별 설비 ====================
|
||||
|
||||
// READ ONE 설비
|
||||
router.get('/:id', equipmentController.getEquipmentById);
|
||||
|
||||
@@ -37,4 +62,39 @@ router.patch('/:id/map-position', equipmentController.updateMapPosition);
|
||||
// DELETE 설비
|
||||
router.delete('/:id', equipmentController.deleteEquipment);
|
||||
|
||||
// ==================== 설비 사진 ====================
|
||||
|
||||
// 설비 사진 추가
|
||||
router.post('/:id/photos', equipmentController.addPhoto);
|
||||
|
||||
// 설비 사진 목록
|
||||
router.get('/:id/photos', equipmentController.getPhotos);
|
||||
|
||||
// ==================== 설비 임시 이동 ====================
|
||||
|
||||
// 설비 임시 이동
|
||||
router.post('/:id/move', equipmentController.moveTemporarily);
|
||||
|
||||
// 설비 원위치 복귀
|
||||
router.post('/:id/return', equipmentController.returnToOriginal);
|
||||
|
||||
// 설비 이동 이력
|
||||
router.get('/:id/move-logs', equipmentController.getMoveLogs);
|
||||
|
||||
// ==================== 설비 외부 반출 ====================
|
||||
|
||||
// 설비 외부 반출
|
||||
router.post('/:id/export', equipmentController.exportEquipment);
|
||||
|
||||
// 설비 외부 반출 이력
|
||||
router.get('/:id/external-logs', equipmentController.getExternalLogs);
|
||||
|
||||
// ==================== 설비 수리 ====================
|
||||
|
||||
// 수리 신청
|
||||
router.post('/:id/repair-request', equipmentController.createRepairRequest);
|
||||
|
||||
// 수리 이력 조회
|
||||
router.get('/:id/repair-history', equipmentController.getRepairHistory);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -20,19 +20,26 @@ try {
|
||||
}
|
||||
|
||||
// 업로드 디렉토리 설정
|
||||
const UPLOAD_DIR = path.join(__dirname, '../public/uploads/issues');
|
||||
const UPLOAD_DIRS = {
|
||||
issues: path.join(__dirname, '../public/uploads/issues'),
|
||||
equipments: path.join(__dirname, '../public/uploads/equipments')
|
||||
};
|
||||
const UPLOAD_DIR = UPLOAD_DIRS.issues; // 기존 호환성 유지
|
||||
const MAX_SIZE = { width: 1920, height: 1920 };
|
||||
const QUALITY = 85;
|
||||
|
||||
/**
|
||||
* 업로드 디렉토리 확인 및 생성
|
||||
* @param {string} category - 카테고리 ('issues' 또는 'equipments')
|
||||
*/
|
||||
async function ensureUploadDir() {
|
||||
async function ensureUploadDir(category = 'issues') {
|
||||
const uploadDir = UPLOAD_DIRS[category] || UPLOAD_DIRS.issues;
|
||||
try {
|
||||
await fs.access(UPLOAD_DIR);
|
||||
await fs.access(uploadDir);
|
||||
} catch {
|
||||
await fs.mkdir(UPLOAD_DIR, { recursive: true });
|
||||
await fs.mkdir(uploadDir, { recursive: true });
|
||||
}
|
||||
return uploadDir;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,10 +75,11 @@ function getImageExtension(base64String) {
|
||||
/**
|
||||
* Base64 이미지를 파일로 저장
|
||||
* @param {string} base64String - Base64 인코딩된 이미지 (data:image/...;base64,... 형식)
|
||||
* @param {string} prefix - 파일명 접두사 (예: 'issue', 'resolution')
|
||||
* @param {string} prefix - 파일명 접두사 (예: 'issue', 'resolution', 'equipment')
|
||||
* @param {string} category - 저장 카테고리 ('issues' 또는 'equipments')
|
||||
* @returns {Promise<string|null>} 저장된 파일의 웹 경로 또는 null
|
||||
*/
|
||||
async function saveBase64Image(base64String, prefix = 'issue') {
|
||||
async function saveBase64Image(base64String, prefix = 'issue', category = 'issues') {
|
||||
try {
|
||||
if (!base64String || typeof base64String !== 'string') {
|
||||
return null;
|
||||
@@ -92,14 +100,14 @@ async function saveBase64Image(base64String, prefix = 'issue') {
|
||||
}
|
||||
|
||||
// 디렉토리 확인
|
||||
await ensureUploadDir();
|
||||
const uploadDir = await ensureUploadDir(category);
|
||||
|
||||
// 파일명 생성
|
||||
const timestamp = getTimestamp();
|
||||
const uniqueId = generateId();
|
||||
const extension = 'jpg'; // 모든 이미지를 JPEG로 저장
|
||||
const filename = `${prefix}_${timestamp}_${uniqueId}.${extension}`;
|
||||
const filepath = path.join(UPLOAD_DIR, filename);
|
||||
const filepath = path.join(uploadDir, filename);
|
||||
|
||||
// sharp가 설치되어 있으면 리사이징 및 최적화
|
||||
if (sharp) {
|
||||
@@ -122,7 +130,7 @@ async function saveBase64Image(base64String, prefix = 'issue') {
|
||||
}
|
||||
|
||||
// 웹 접근 경로 반환
|
||||
return `/uploads/issues/${filename}`;
|
||||
return `/uploads/${category}/${filename}`;
|
||||
} catch (error) {
|
||||
console.error('이미지 저장 실패:', error);
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user