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
|
// controllers/equipmentController.js
|
||||||
const EquipmentModel = require('../models/equipmentModel');
|
const EquipmentModel = require('../models/equipmentModel');
|
||||||
|
const imageUploadService = require('../services/imageUploadService');
|
||||||
|
|
||||||
const EquipmentController = {
|
const EquipmentController = {
|
||||||
// CREATE - 설비 생성
|
// CREATE - 설비 생성
|
||||||
@@ -379,6 +380,529 @@ const EquipmentController = {
|
|||||||
message: '서버 오류가 발생했습니다.'
|
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) {
|
} catch (error) {
|
||||||
callback(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 작업장별 설비
|
// READ 작업장별 설비
|
||||||
router.get('/workplace/:workplaceId', equipmentController.getEquipmentsByWorkplace);
|
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 설비
|
// READ ONE 설비
|
||||||
router.get('/:id', equipmentController.getEquipmentById);
|
router.get('/:id', equipmentController.getEquipmentById);
|
||||||
|
|
||||||
@@ -37,4 +62,39 @@ router.patch('/:id/map-position', equipmentController.updateMapPosition);
|
|||||||
// DELETE 설비
|
// DELETE 설비
|
||||||
router.delete('/:id', equipmentController.deleteEquipment);
|
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;
|
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 MAX_SIZE = { width: 1920, height: 1920 };
|
||||||
const QUALITY = 85;
|
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 {
|
try {
|
||||||
await fs.access(UPLOAD_DIR);
|
await fs.access(uploadDir);
|
||||||
} catch {
|
} catch {
|
||||||
await fs.mkdir(UPLOAD_DIR, { recursive: true });
|
await fs.mkdir(uploadDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
return uploadDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,10 +75,11 @@ function getImageExtension(base64String) {
|
|||||||
/**
|
/**
|
||||||
* Base64 이미지를 파일로 저장
|
* Base64 이미지를 파일로 저장
|
||||||
* @param {string} base64String - Base64 인코딩된 이미지 (data:image/...;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
|
* @returns {Promise<string|null>} 저장된 파일의 웹 경로 또는 null
|
||||||
*/
|
*/
|
||||||
async function saveBase64Image(base64String, prefix = 'issue') {
|
async function saveBase64Image(base64String, prefix = 'issue', category = 'issues') {
|
||||||
try {
|
try {
|
||||||
if (!base64String || typeof base64String !== 'string') {
|
if (!base64String || typeof base64String !== 'string') {
|
||||||
return null;
|
return null;
|
||||||
@@ -92,14 +100,14 @@ async function saveBase64Image(base64String, prefix = 'issue') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 디렉토리 확인
|
// 디렉토리 확인
|
||||||
await ensureUploadDir();
|
const uploadDir = await ensureUploadDir(category);
|
||||||
|
|
||||||
// 파일명 생성
|
// 파일명 생성
|
||||||
const timestamp = getTimestamp();
|
const timestamp = getTimestamp();
|
||||||
const uniqueId = generateId();
|
const uniqueId = generateId();
|
||||||
const extension = 'jpg'; // 모든 이미지를 JPEG로 저장
|
const extension = 'jpg'; // 모든 이미지를 JPEG로 저장
|
||||||
const filename = `${prefix}_${timestamp}_${uniqueId}.${extension}`;
|
const filename = `${prefix}_${timestamp}_${uniqueId}.${extension}`;
|
||||||
const filepath = path.join(UPLOAD_DIR, filename);
|
const filepath = path.join(uploadDir, filename);
|
||||||
|
|
||||||
// sharp가 설치되어 있으면 리사이징 및 최적화
|
// sharp가 설치되어 있으면 리사이징 및 최적화
|
||||||
if (sharp) {
|
if (sharp) {
|
||||||
@@ -122,7 +130,7 @@ async function saveBase64Image(base64String, prefix = 'issue') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 웹 접근 경로 반환
|
// 웹 접근 경로 반환
|
||||||
return `/uploads/issues/${filename}`;
|
return `/uploads/${category}/${filename}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('이미지 저장 실패:', error);
|
console.error('이미지 저장 실패:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
509
web-ui/css/equipment-detail.css
Normal file
509
web-ui/css/equipment-detail.css
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
/* equipment-detail.css - 설비 상세 페이지 스타일 */
|
||||||
|
|
||||||
|
/* 헤더 */
|
||||||
|
.eq-detail-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-detail-header .page-title-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #374151;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-back:hover {
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-arrow {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-header-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-header-meta {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-status-badge {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-status-badge.active { background: #d1fae5; color: #065f46; }
|
||||||
|
.eq-status-badge.maintenance { background: #fef3c7; color: #92400e; }
|
||||||
|
.eq-status-badge.repair_needed { background: #fee2e2; color: #991b1b; }
|
||||||
|
.eq-status-badge.inactive { background: #e5e7eb; color: #374151; }
|
||||||
|
.eq-status-badge.external { background: #dbeafe; color: #1e40af; }
|
||||||
|
.eq-status-badge.repair_external { background: #ede9fe; color: #5b21b6; }
|
||||||
|
|
||||||
|
/* 기본 정보 카드 */
|
||||||
|
.eq-info-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-info-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-info-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-info-value {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 섹션 */
|
||||||
|
.eq-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.25rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 사진 그리드 */
|
||||||
|
.eq-photo-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-photo-item {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-photo-item img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-photo-item:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-photo-delete {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: rgba(239, 68, 68, 0.9);
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-photo-item:hover .eq-photo-delete {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-photo-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 위치 정보 */
|
||||||
|
.eq-location-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-location-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-location-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-location-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-location-value {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-location-value.eq-moved {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-map-preview {
|
||||||
|
width: 200px;
|
||||||
|
height: 150px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-map-preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-map-marker {
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: #dc2626;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 액션 버튼 */
|
||||||
|
.eq-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 140px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action .btn-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-move {
|
||||||
|
background: #dbeafe;
|
||||||
|
color: #1e40af;
|
||||||
|
}
|
||||||
|
.btn-move:hover { background: #bfdbfe; }
|
||||||
|
|
||||||
|
.btn-repair {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
.btn-repair:hover { background: #fde68a; }
|
||||||
|
|
||||||
|
.btn-export {
|
||||||
|
background: #ede9fe;
|
||||||
|
color: #5b21b6;
|
||||||
|
}
|
||||||
|
.btn-export:hover { background: #ddd6fe; }
|
||||||
|
|
||||||
|
/* 이력 리스트 */
|
||||||
|
.eq-history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.875rem;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-date {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #6b7280;
|
||||||
|
white-space: nowrap;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-detail {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-status.pending { background: #fef3c7; color: #92400e; }
|
||||||
|
.eq-history-status.in_progress { background: #dbeafe; color: #1e40af; }
|
||||||
|
.eq-history-status.completed { background: #d1fae5; color: #065f46; }
|
||||||
|
.eq-history-status.exported { background: #ede9fe; color: #5b21b6; }
|
||||||
|
.eq-history-status.returned { background: #d1fae5; color: #065f46; }
|
||||||
|
|
||||||
|
.eq-history-empty {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-action {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
background: #10b981;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-history-action:hover {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 모달 스타일 추가 */
|
||||||
|
.photo-preview-container {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-step {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-instruction {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-map-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-map-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.move-marker {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #dc2626;
|
||||||
|
border: 3px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repair-photo-previews {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repair-photo-preview {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 사진 확대 보기 */
|
||||||
|
.photo-view-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-view-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-view-image {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 버튼 스타일 */
|
||||||
|
.btn-sm {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 반응형 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.eq-detail-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-location-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-map-preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-action {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eq-info-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
790
web-ui/js/equipment-detail.js
Normal file
790
web-ui/js/equipment-detail.js
Normal file
@@ -0,0 +1,790 @@
|
|||||||
|
/**
|
||||||
|
* equipment-detail.js - 설비 상세 페이지 스크립트
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 전역 변수
|
||||||
|
let currentEquipment = null;
|
||||||
|
let equipmentId = null;
|
||||||
|
let workplaces = [];
|
||||||
|
let factories = [];
|
||||||
|
let selectedMovePosition = null;
|
||||||
|
let repairPhotoBases = [];
|
||||||
|
|
||||||
|
// 상태 라벨
|
||||||
|
const STATUS_LABELS = {
|
||||||
|
active: '정상 가동',
|
||||||
|
maintenance: '점검 중',
|
||||||
|
repair_needed: '수리 필요',
|
||||||
|
inactive: '비활성',
|
||||||
|
external: '외부 반출',
|
||||||
|
repair_external: '수리 외주'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지 초기화
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// URL에서 equipment_id 추출
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
equipmentId = urlParams.get('id');
|
||||||
|
|
||||||
|
if (!equipmentId) {
|
||||||
|
alert('설비 ID가 필요합니다.');
|
||||||
|
goBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 설정 후 데이터 로드
|
||||||
|
waitForApiConfig().then(() => {
|
||||||
|
loadEquipmentData();
|
||||||
|
loadFactories();
|
||||||
|
loadRepairCategories();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// API 설정 대기
|
||||||
|
function waitForApiConfig() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const check = setInterval(() => {
|
||||||
|
if (window.API_BASE_URL) {
|
||||||
|
clearInterval(check);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 뒤로가기
|
||||||
|
function goBack() {
|
||||||
|
if (document.referrer && document.referrer.includes(window.location.host)) {
|
||||||
|
history.back();
|
||||||
|
} else {
|
||||||
|
window.location.href = '/pages/admin/equipments.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 설비 데이터 로드
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async function loadEquipmentData() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/equipments/${equipmentId}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
currentEquipment = response.data.data;
|
||||||
|
renderEquipmentInfo();
|
||||||
|
loadPhotos();
|
||||||
|
loadRepairHistory();
|
||||||
|
loadExternalLogs();
|
||||||
|
loadMoveLogs();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('설비 정보 로드 실패:', error);
|
||||||
|
alert('설비 정보를 불러오는데 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEquipmentInfo() {
|
||||||
|
const eq = currentEquipment;
|
||||||
|
|
||||||
|
// 헤더
|
||||||
|
document.getElementById('equipmentTitle').textContent = `[${eq.equipment_code}] ${eq.equipment_name}`;
|
||||||
|
document.getElementById('equipmentMeta').textContent = `${eq.model_name || '-'} | ${eq.manufacturer || '-'}`;
|
||||||
|
|
||||||
|
// 상태 배지
|
||||||
|
const statusBadge = document.getElementById('equipmentStatus');
|
||||||
|
statusBadge.textContent = STATUS_LABELS[eq.status] || eq.status;
|
||||||
|
statusBadge.className = `eq-status-badge ${eq.status}`;
|
||||||
|
|
||||||
|
// 기본 정보 카드
|
||||||
|
document.getElementById('equipmentInfoCard').innerHTML = `
|
||||||
|
<div class="eq-info-grid">
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">관리번호</span>
|
||||||
|
<span class="eq-info-value">${eq.equipment_code}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">설비명</span>
|
||||||
|
<span class="eq-info-value">${eq.equipment_name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">모델명</span>
|
||||||
|
<span class="eq-info-value">${eq.model_name || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">규격</span>
|
||||||
|
<span class="eq-info-value">${eq.specifications || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">제조사</span>
|
||||||
|
<span class="eq-info-value">${eq.manufacturer || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">구입처</span>
|
||||||
|
<span class="eq-info-value">${eq.supplier || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">구입일</span>
|
||||||
|
<span class="eq-info-value">${eq.installation_date ? formatDate(eq.installation_date) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">구입가격</span>
|
||||||
|
<span class="eq-info-value">${eq.purchase_price ? Number(eq.purchase_price).toLocaleString() + '원' : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">시리얼번호</span>
|
||||||
|
<span class="eq-info-value">${eq.serial_number || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-info-item">
|
||||||
|
<span class="eq-info-label">설비유형</span>
|
||||||
|
<span class="eq-info-value">${eq.equipment_type || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 위치 정보
|
||||||
|
const originalLocation = eq.workplace_name
|
||||||
|
? `${eq.category_name || ''} > ${eq.workplace_name}`
|
||||||
|
: '미배정';
|
||||||
|
document.getElementById('originalLocation').textContent = originalLocation;
|
||||||
|
|
||||||
|
if (eq.is_temporarily_moved && eq.current_workplace_id) {
|
||||||
|
document.getElementById('currentLocationRow').style.display = 'flex';
|
||||||
|
// 현재 위치 작업장 이름 로드 필요
|
||||||
|
loadCurrentWorkplaceName(eq.current_workplace_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 지도 미리보기 (작업장 지도 표시)
|
||||||
|
renderMapPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCurrentWorkplaceName(workplaceId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/workplaces/${workplaceId}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
const wp = response.data.data;
|
||||||
|
document.getElementById('currentLocation').textContent = `${wp.category_name || ''} > ${wp.workplace_name}`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('현재 위치 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMapPreview() {
|
||||||
|
const eq = currentEquipment;
|
||||||
|
const mapPreview = document.getElementById('mapPreview');
|
||||||
|
|
||||||
|
if (!eq.workplace_id) {
|
||||||
|
mapPreview.innerHTML = '<div style="padding: 1rem; text-align: center; color: #9ca3af;">위치 미배정</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 작업장 지도 정보 로드
|
||||||
|
axios.get(`/workplaces/${eq.workplace_id}`).then(response => {
|
||||||
|
if (response.data.success && response.data.data.map_image_url) {
|
||||||
|
const wp = response.data.data;
|
||||||
|
const xPercent = eq.is_temporarily_moved ? eq.current_map_x_percent : eq.map_x_percent;
|
||||||
|
const yPercent = eq.is_temporarily_moved ? eq.current_map_y_percent : eq.map_y_percent;
|
||||||
|
|
||||||
|
mapPreview.innerHTML = `
|
||||||
|
<img src="${window.API_BASE_URL}${wp.map_image_url}" alt="작업장 지도">
|
||||||
|
<div class="eq-map-marker" style="left: ${xPercent}%; top: ${yPercent}%;"></div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
mapPreview.innerHTML = '<div style="padding: 1rem; text-align: center; color: #9ca3af;">지도 없음</div>';
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
mapPreview.innerHTML = '<div style="padding: 1rem; text-align: center; color: #9ca3af;">지도 로드 실패</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 사진 관리
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async function loadPhotos() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/equipments/${equipmentId}/photos`);
|
||||||
|
if (response.data.success) {
|
||||||
|
renderPhotos(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사진 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPhotos(photos) {
|
||||||
|
const grid = document.getElementById('photoGrid');
|
||||||
|
|
||||||
|
if (!photos || photos.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="eq-photo-empty">등록된 사진이 없습니다</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = photos.map(photo => `
|
||||||
|
<div class="eq-photo-item" onclick="viewPhoto('${window.API_BASE_URL}${photo.photo_path}')">
|
||||||
|
<img src="${window.API_BASE_URL}${photo.photo_path}" alt="${photo.description || '설비 사진'}">
|
||||||
|
<button class="eq-photo-delete" onclick="event.stopPropagation(); deletePhoto(${photo.photo_id})">×</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPhotoModal() {
|
||||||
|
document.getElementById('photoInput').value = '';
|
||||||
|
document.getElementById('photoDescription').value = '';
|
||||||
|
document.getElementById('photoPreviewContainer').style.display = 'none';
|
||||||
|
document.getElementById('photoModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePhotoModal() {
|
||||||
|
document.getElementById('photoModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewPhoto(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
document.getElementById('photoPreview').src = e.target.result;
|
||||||
|
document.getElementById('photoPreviewContainer').style.display = 'block';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadPhoto() {
|
||||||
|
const fileInput = document.getElementById('photoInput');
|
||||||
|
const description = document.getElementById('photoDescription').value;
|
||||||
|
|
||||||
|
if (!fileInput.files[0]) {
|
||||||
|
alert('사진을 선택하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async e => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/equipments/${equipmentId}/photos`, {
|
||||||
|
photo_base64: e.target.result,
|
||||||
|
description: description
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
closePhotoModal();
|
||||||
|
loadPhotos();
|
||||||
|
alert('사진이 추가되었습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사진 업로드 실패:', error);
|
||||||
|
alert('사진 업로드에 실패했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(fileInput.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deletePhoto(photoId) {
|
||||||
|
if (!confirm('이 사진을 삭제하시겠습니까?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/equipments/photos/${photoId}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
loadPhotos();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사진 삭제 실패:', error);
|
||||||
|
alert('사진 삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewPhoto(url) {
|
||||||
|
document.getElementById('photoViewImage').src = url;
|
||||||
|
document.getElementById('photoViewModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePhotoView() {
|
||||||
|
document.getElementById('photoViewModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 임시 이동
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async function loadFactories() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/workplace-categories');
|
||||||
|
if (response.data.success) {
|
||||||
|
factories = response.data.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('공장 목록 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMoveModal() {
|
||||||
|
// 공장 선택 초기화
|
||||||
|
const factorySelect = document.getElementById('moveFactorySelect');
|
||||||
|
factorySelect.innerHTML = '<option value="">공장을 선택하세요</option>';
|
||||||
|
factories.forEach(f => {
|
||||||
|
factorySelect.innerHTML += `<option value="${f.category_id}">${f.category_name}</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('moveWorkplaceSelect').innerHTML = '<option value="">작업장을 선택하세요</option>';
|
||||||
|
document.getElementById('moveStep2').style.display = 'none';
|
||||||
|
document.getElementById('moveStep1').style.display = 'block';
|
||||||
|
document.getElementById('moveConfirmBtn').disabled = true;
|
||||||
|
document.getElementById('moveReason').value = '';
|
||||||
|
selectedMovePosition = null;
|
||||||
|
|
||||||
|
document.getElementById('moveModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMoveModal() {
|
||||||
|
document.getElementById('moveModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMoveWorkplaces() {
|
||||||
|
const categoryId = document.getElementById('moveFactorySelect').value;
|
||||||
|
const workplaceSelect = document.getElementById('moveWorkplaceSelect');
|
||||||
|
|
||||||
|
workplaceSelect.innerHTML = '<option value="">작업장을 선택하세요</option>';
|
||||||
|
|
||||||
|
if (!categoryId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/workplaces?category_id=${categoryId}`);
|
||||||
|
if (response.data.success) {
|
||||||
|
workplaces = response.data.data;
|
||||||
|
workplaces.forEach(wp => {
|
||||||
|
if (wp.map_image_url) {
|
||||||
|
workplaceSelect.innerHTML += `<option value="${wp.workplace_id}">${wp.workplace_name}</option>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('작업장 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMoveMap() {
|
||||||
|
const workplaceId = document.getElementById('moveWorkplaceSelect').value;
|
||||||
|
|
||||||
|
if (!workplaceId) {
|
||||||
|
document.getElementById('moveStep2').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workplace = workplaces.find(wp => wp.workplace_id == workplaceId);
|
||||||
|
if (!workplace || !workplace.map_image_url) {
|
||||||
|
alert('선택한 작업장에 지도가 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.getElementById('moveMapContainer');
|
||||||
|
container.innerHTML = `<img src="${window.API_BASE_URL}${workplace.map_image_url}" id="moveMapImage" onclick="onMoveMapClick(event)">`;
|
||||||
|
|
||||||
|
document.getElementById('moveStep2').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMoveMapClick(event) {
|
||||||
|
const img = event.target;
|
||||||
|
const rect = img.getBoundingClientRect();
|
||||||
|
const x = ((event.clientX - rect.left) / rect.width) * 100;
|
||||||
|
const y = ((event.clientY - rect.top) / rect.height) * 100;
|
||||||
|
|
||||||
|
selectedMovePosition = { x, y };
|
||||||
|
|
||||||
|
// 기존 마커 제거
|
||||||
|
const container = document.getElementById('moveMapContainer');
|
||||||
|
const existingMarker = container.querySelector('.move-marker');
|
||||||
|
if (existingMarker) existingMarker.remove();
|
||||||
|
|
||||||
|
// 새 마커 추가
|
||||||
|
const marker = document.createElement('div');
|
||||||
|
marker.className = 'move-marker';
|
||||||
|
marker.style.left = x + '%';
|
||||||
|
marker.style.top = y + '%';
|
||||||
|
container.appendChild(marker);
|
||||||
|
|
||||||
|
document.getElementById('moveConfirmBtn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmMove() {
|
||||||
|
const targetWorkplaceId = document.getElementById('moveWorkplaceSelect').value;
|
||||||
|
const reason = document.getElementById('moveReason').value;
|
||||||
|
|
||||||
|
if (!targetWorkplaceId || !selectedMovePosition) {
|
||||||
|
alert('이동할 위치를 선택하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/equipments/${equipmentId}/move`, {
|
||||||
|
target_workplace_id: targetWorkplaceId,
|
||||||
|
target_x_percent: selectedMovePosition.x.toFixed(2),
|
||||||
|
target_y_percent: selectedMovePosition.y.toFixed(2),
|
||||||
|
from_workplace_id: currentEquipment.workplace_id,
|
||||||
|
from_x_percent: currentEquipment.map_x_percent,
|
||||||
|
from_y_percent: currentEquipment.map_y_percent,
|
||||||
|
reason: reason
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
closeMoveModal();
|
||||||
|
loadEquipmentData();
|
||||||
|
loadMoveLogs();
|
||||||
|
alert('설비가 임시 이동되었습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('이동 실패:', error);
|
||||||
|
alert('설비 이동에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function returnToOriginal() {
|
||||||
|
if (!confirm('설비를 원래 위치로 복귀시키겠습니까?')) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/equipments/${equipmentId}/return`);
|
||||||
|
if (response.data.success) {
|
||||||
|
loadEquipmentData();
|
||||||
|
loadMoveLogs();
|
||||||
|
alert('설비가 원위치로 복귀되었습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('복귀 실패:', error);
|
||||||
|
alert('설비 복귀에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 수리 신청
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
let repairCategories = [];
|
||||||
|
|
||||||
|
async function loadRepairCategories() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/equipments/repair-categories');
|
||||||
|
if (response.data.success) {
|
||||||
|
repairCategories = response.data.data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('수리 항목 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRepairModal() {
|
||||||
|
const select = document.getElementById('repairItemSelect');
|
||||||
|
select.innerHTML = '<option value="">선택하세요</option>';
|
||||||
|
repairCategories.forEach(item => {
|
||||||
|
select.innerHTML += `<option value="${item.item_id}">${item.item_name}</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('repairDescription').value = '';
|
||||||
|
document.getElementById('repairPhotoInput').value = '';
|
||||||
|
document.getElementById('repairPhotoPreviews').innerHTML = '';
|
||||||
|
repairPhotoBases = [];
|
||||||
|
|
||||||
|
document.getElementById('repairModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRepairModal() {
|
||||||
|
document.getElementById('repairModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewRepairPhotos(event) {
|
||||||
|
const files = event.target.files;
|
||||||
|
const previewContainer = document.getElementById('repairPhotoPreviews');
|
||||||
|
previewContainer.innerHTML = '';
|
||||||
|
repairPhotoBases = [];
|
||||||
|
|
||||||
|
Array.from(files).forEach(file => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
repairPhotoBases.push(e.target.result);
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = e.target.result;
|
||||||
|
img.className = 'repair-photo-preview';
|
||||||
|
previewContainer.appendChild(img);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitRepairRequest() {
|
||||||
|
const itemId = document.getElementById('repairItemSelect').value;
|
||||||
|
const description = document.getElementById('repairDescription').value;
|
||||||
|
|
||||||
|
if (!description) {
|
||||||
|
alert('수리 내용을 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/equipments/${equipmentId}/repair-request`, {
|
||||||
|
item_id: itemId || null,
|
||||||
|
description: description,
|
||||||
|
photo_base64_list: repairPhotoBases,
|
||||||
|
workplace_id: currentEquipment.workplace_id
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
closeRepairModal();
|
||||||
|
loadEquipmentData();
|
||||||
|
loadRepairHistory();
|
||||||
|
alert('수리 신청이 접수되었습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('수리 신청 실패:', error);
|
||||||
|
alert('수리 신청에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadRepairHistory() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/equipments/${equipmentId}/repair-history`);
|
||||||
|
if (response.data.success) {
|
||||||
|
renderRepairHistory(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('수리 이력 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRepairHistory(history) {
|
||||||
|
const container = document.getElementById('repairHistory');
|
||||||
|
|
||||||
|
if (!history || history.length === 0) {
|
||||||
|
container.innerHTML = '<div class="eq-history-empty">수리 이력이 없습니다</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = history.map(h => `
|
||||||
|
<div class="eq-history-item">
|
||||||
|
<span class="eq-history-date">${formatDate(h.created_at)}</span>
|
||||||
|
<div class="eq-history-content">
|
||||||
|
<div class="eq-history-title">${h.item_name || '수리 요청'}</div>
|
||||||
|
<div class="eq-history-detail">${h.description || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<span class="eq-history-status ${h.status}">${getRepairStatusLabel(h.status)}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRepairStatusLabel(status) {
|
||||||
|
const labels = {
|
||||||
|
pending: '대기중',
|
||||||
|
in_progress: '처리중',
|
||||||
|
completed: '완료',
|
||||||
|
closed: '종료'
|
||||||
|
};
|
||||||
|
return labels[status] || status;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 외부 반출
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function openExportModal() {
|
||||||
|
document.getElementById('exportDate').value = new Date().toISOString().slice(0, 10);
|
||||||
|
document.getElementById('expectedReturnDate').value = '';
|
||||||
|
document.getElementById('exportDestination').value = '';
|
||||||
|
document.getElementById('exportReason').value = '';
|
||||||
|
document.getElementById('exportNotes').value = '';
|
||||||
|
document.getElementById('isRepairExport').checked = false;
|
||||||
|
|
||||||
|
document.getElementById('exportModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeExportModal() {
|
||||||
|
document.getElementById('exportModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleRepairFields() {
|
||||||
|
// 현재는 특별한 필드 차이 없음
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitExport() {
|
||||||
|
const exportDate = document.getElementById('exportDate').value;
|
||||||
|
const expectedReturnDate = document.getElementById('expectedReturnDate').value;
|
||||||
|
const destination = document.getElementById('exportDestination').value;
|
||||||
|
const reason = document.getElementById('exportReason').value;
|
||||||
|
const notes = document.getElementById('exportNotes').value;
|
||||||
|
const isRepair = document.getElementById('isRepairExport').checked;
|
||||||
|
|
||||||
|
if (!exportDate) {
|
||||||
|
alert('반출일을 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/equipments/${equipmentId}/export`, {
|
||||||
|
export_date: exportDate,
|
||||||
|
expected_return_date: expectedReturnDate || null,
|
||||||
|
destination: destination,
|
||||||
|
reason: reason,
|
||||||
|
notes: notes,
|
||||||
|
is_repair: isRepair
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
closeExportModal();
|
||||||
|
loadEquipmentData();
|
||||||
|
loadExternalLogs();
|
||||||
|
alert('외부 반출이 등록되었습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('반출 등록 실패:', error);
|
||||||
|
alert('반출 등록에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadExternalLogs() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/equipments/${equipmentId}/external-logs`);
|
||||||
|
if (response.data.success) {
|
||||||
|
renderExternalLogs(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('외부반출 이력 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExternalLogs(logs) {
|
||||||
|
const container = document.getElementById('externalHistory');
|
||||||
|
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
container.innerHTML = '<div class="eq-history-empty">외부반출 이력이 없습니다</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = logs.map(log => {
|
||||||
|
const dateRange = log.actual_return_date
|
||||||
|
? `${formatDate(log.export_date)} ~ ${formatDate(log.actual_return_date)}`
|
||||||
|
: `${formatDate(log.export_date)} ~ (미반입)`;
|
||||||
|
|
||||||
|
const isReturned = !!log.actual_return_date;
|
||||||
|
const statusClass = isReturned ? 'returned' : 'exported';
|
||||||
|
const statusLabel = isReturned ? '반입완료' : '반출중';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="eq-history-item">
|
||||||
|
<span class="eq-history-date">${dateRange}</span>
|
||||||
|
<div class="eq-history-content">
|
||||||
|
<div class="eq-history-title">${log.destination || '외부'}</div>
|
||||||
|
<div class="eq-history-detail">${log.reason || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<span class="eq-history-status ${statusClass}">${statusLabel}</span>
|
||||||
|
${!isReturned ? `<button class="eq-history-action" onclick="openReturnModal(${log.log_id})">반입처리</button>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReturnModal(logId) {
|
||||||
|
document.getElementById('returnLogId').value = logId;
|
||||||
|
document.getElementById('returnDate').value = new Date().toISOString().slice(0, 10);
|
||||||
|
document.getElementById('returnStatus').value = 'active';
|
||||||
|
document.getElementById('returnNotes').value = '';
|
||||||
|
|
||||||
|
document.getElementById('returnModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeReturnModal() {
|
||||||
|
document.getElementById('returnModal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitReturn() {
|
||||||
|
const logId = document.getElementById('returnLogId').value;
|
||||||
|
const returnDate = document.getElementById('returnDate').value;
|
||||||
|
const newStatus = document.getElementById('returnStatus').value;
|
||||||
|
const notes = document.getElementById('returnNotes').value;
|
||||||
|
|
||||||
|
if (!returnDate) {
|
||||||
|
alert('반입일을 입력하세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`/equipments/external-logs/${logId}/return`, {
|
||||||
|
return_date: returnDate,
|
||||||
|
new_status: newStatus,
|
||||||
|
notes: notes
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
closeReturnModal();
|
||||||
|
loadEquipmentData();
|
||||||
|
loadExternalLogs();
|
||||||
|
alert('반입 처리가 완료되었습니다.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('반입 처리 실패:', error);
|
||||||
|
alert('반입 처리에 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 이동 이력
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
async function loadMoveLogs() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/equipments/${equipmentId}/move-logs`);
|
||||||
|
if (response.data.success) {
|
||||||
|
renderMoveLogs(response.data.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('이동 이력 로드 실패:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMoveLogs(logs) {
|
||||||
|
const container = document.getElementById('moveHistory');
|
||||||
|
|
||||||
|
if (!logs || logs.length === 0) {
|
||||||
|
container.innerHTML = '<div class="eq-history-empty">이동 이력이 없습니다</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = logs.map(log => {
|
||||||
|
const typeLabel = log.move_type === 'temporary' ? '임시이동' : '복귀';
|
||||||
|
const location = log.move_type === 'temporary'
|
||||||
|
? `${log.to_workplace_name || '-'}`
|
||||||
|
: `원위치 복귀`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="eq-history-item">
|
||||||
|
<span class="eq-history-date">${formatDateTime(log.moved_at)}</span>
|
||||||
|
<div class="eq-history-content">
|
||||||
|
<div class="eq-history-title">${typeLabel}: ${location}</div>
|
||||||
|
<div class="eq-history-detail">${log.reason || '-'} (${log.moved_by_name || '시스템'})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 유틸리티
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
function formatDate(dateStr) {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('ko-KR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
}).replace(/\. /g, '-').replace('.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(dateStr) {
|
||||||
|
if (!dateStr) return '-';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('ko-KR', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
344
web-ui/pages/admin/equipment-detail.html
Normal file
344
web-ui/pages/admin/equipment-detail.html
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>설비 상세 | (주)테크니컬코리아</title>
|
||||||
|
<link rel="stylesheet" href="/css/design-system.css">
|
||||||
|
<link rel="stylesheet" href="/css/admin-pages.css?v=8">
|
||||||
|
<link rel="stylesheet" href="/css/equipment-detail.css?v=1">
|
||||||
|
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||||
|
<script src="/js/api-base.js"></script>
|
||||||
|
<script src="/js/app-init.js?v=2" defer></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- 네비게이션 바 -->
|
||||||
|
<div id="navbar-container"></div>
|
||||||
|
|
||||||
|
<!-- 메인 레이아웃 -->
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 메인 콘텐츠 -->
|
||||||
|
<main class="main-content">
|
||||||
|
<div class="dashboard-main">
|
||||||
|
<!-- 뒤로가기 & 제목 -->
|
||||||
|
<div class="page-header eq-detail-header">
|
||||||
|
<div class="page-title-section">
|
||||||
|
<button class="btn-back" onclick="goBack()">
|
||||||
|
<span class="back-arrow">←</span>
|
||||||
|
<span>뒤로</span>
|
||||||
|
</button>
|
||||||
|
<div class="eq-header-info">
|
||||||
|
<h1 class="page-title" id="equipmentTitle">설비 상세</h1>
|
||||||
|
<div class="eq-header-meta" id="equipmentMeta"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="eq-status-badge" id="equipmentStatus"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 기본 정보 카드 -->
|
||||||
|
<div class="eq-info-card" id="equipmentInfoCard">
|
||||||
|
<!-- JS에서 동적으로 렌더링 -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사진 섹션 -->
|
||||||
|
<div class="eq-section">
|
||||||
|
<div class="eq-section-header">
|
||||||
|
<h2 class="eq-section-title">설비 사진</h2>
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="openPhotoModal()">+ 사진 추가</button>
|
||||||
|
</div>
|
||||||
|
<div class="eq-photo-grid" id="photoGrid">
|
||||||
|
<div class="eq-photo-empty">등록된 사진이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 위치 정보 섹션 -->
|
||||||
|
<div class="eq-section">
|
||||||
|
<div class="eq-section-header">
|
||||||
|
<h2 class="eq-section-title">위치 정보</h2>
|
||||||
|
</div>
|
||||||
|
<div class="eq-location-card" id="locationCard">
|
||||||
|
<div class="eq-location-info">
|
||||||
|
<div class="eq-location-row">
|
||||||
|
<span class="eq-location-label">원래 위치:</span>
|
||||||
|
<span class="eq-location-value" id="originalLocation">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="eq-location-row" id="currentLocationRow" style="display: none;">
|
||||||
|
<span class="eq-location-label">현재 위치:</span>
|
||||||
|
<span class="eq-location-value eq-moved" id="currentLocation">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="eq-map-preview" id="mapPreview">
|
||||||
|
<!-- 지도 미리보기 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 액션 버튼들 -->
|
||||||
|
<div class="eq-action-buttons">
|
||||||
|
<button class="btn btn-action btn-move" onclick="openMoveModal()">
|
||||||
|
<span class="btn-icon">⇄</span>
|
||||||
|
<span>임시이동</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-action btn-repair" onclick="openRepairModal()">
|
||||||
|
<span class="btn-icon">🔧</span>
|
||||||
|
<span>수리신청</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-action btn-export" onclick="openExportModal()">
|
||||||
|
<span class="btn-icon">🚚</span>
|
||||||
|
<span>외부반출</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 수리 이력 섹션 -->
|
||||||
|
<div class="eq-section">
|
||||||
|
<div class="eq-section-header">
|
||||||
|
<h2 class="eq-section-title">수리 이력</h2>
|
||||||
|
</div>
|
||||||
|
<div class="eq-history-list" id="repairHistory">
|
||||||
|
<div class="eq-history-empty">수리 이력이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 외부반출 이력 섹션 -->
|
||||||
|
<div class="eq-section">
|
||||||
|
<div class="eq-section-header">
|
||||||
|
<h2 class="eq-section-title">외부반출 이력</h2>
|
||||||
|
</div>
|
||||||
|
<div class="eq-history-list" id="externalHistory">
|
||||||
|
<div class="eq-history-empty">외부반출 이력이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 이동 이력 섹션 -->
|
||||||
|
<div class="eq-section">
|
||||||
|
<div class="eq-section-header">
|
||||||
|
<h2 class="eq-section-title">이동 이력</h2>
|
||||||
|
</div>
|
||||||
|
<div class="eq-history-list" id="moveHistory">
|
||||||
|
<div class="eq-history-empty">이동 이력이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사진 추가 모달 -->
|
||||||
|
<div id="photoModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-container" style="max-width: 500px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>사진 추가</h2>
|
||||||
|
<button class="btn-close" onclick="closePhotoModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>사진 선택</label>
|
||||||
|
<input type="file" id="photoInput" accept="image/*" onchange="previewPhoto(event)">
|
||||||
|
</div>
|
||||||
|
<div class="photo-preview-container" id="photoPreviewContainer" style="display: none;">
|
||||||
|
<img id="photoPreview" class="photo-preview">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>설명 (선택)</label>
|
||||||
|
<input type="text" id="photoDescription" class="form-control" placeholder="사진 설명을 입력하세요">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closePhotoModal()">취소</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="uploadPhoto()">업로드</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 임시이동 모달 -->
|
||||||
|
<div id="moveModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-container" style="max-width: 600px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>설비 임시 이동</h2>
|
||||||
|
<button class="btn-close" onclick="closeMoveModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="move-step" id="moveStep1">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>이동할 공장 선택</label>
|
||||||
|
<select id="moveFactorySelect" class="form-control" onchange="loadMoveWorkplaces()">
|
||||||
|
<option value="">공장을 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>이동할 작업장 선택</label>
|
||||||
|
<select id="moveWorkplaceSelect" class="form-control" onchange="loadMoveMap()">
|
||||||
|
<option value="">작업장을 선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="move-step" id="moveStep2" style="display: none;">
|
||||||
|
<p class="move-instruction">지도에서 이동할 위치를 클릭하세요</p>
|
||||||
|
<div class="move-map-container" id="moveMapContainer">
|
||||||
|
<!-- 지도가 여기에 표시됨 -->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>이동 사유 (선택)</label>
|
||||||
|
<input type="text" id="moveReason" class="form-control" placeholder="이동 사유를 입력하세요">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeMoveModal()">취소</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="moveConfirmBtn" onclick="confirmMove()" disabled>이동 확인</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 수리신청 모달 -->
|
||||||
|
<div id="repairModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-container" style="max-width: 500px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>수리 신청</h2>
|
||||||
|
<button class="btn-close" onclick="closeRepairModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>수리 유형</label>
|
||||||
|
<select id="repairItemSelect" class="form-control">
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>상세 내용</label>
|
||||||
|
<textarea id="repairDescription" class="form-control" rows="3" placeholder="수리가 필요한 내용을 상세히 적어주세요"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>사진 첨부 (선택)</label>
|
||||||
|
<input type="file" id="repairPhotoInput" accept="image/*" multiple onchange="previewRepairPhotos(event)">
|
||||||
|
<div class="repair-photo-previews" id="repairPhotoPreviews"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeRepairModal()">취소</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitRepairRequest()">신청</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 외부반출 모달 -->
|
||||||
|
<div id="exportModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-container" style="max-width: 500px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>외부 반출</h2>
|
||||||
|
<button class="btn-close" onclick="closeExportModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input type="checkbox" id="isRepairExport" onchange="toggleRepairFields()">
|
||||||
|
<span>수리 외주 (외부 수리)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반출일</label>
|
||||||
|
<input type="date" id="exportDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반입 예정일</label>
|
||||||
|
<input type="date" id="expectedReturnDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반출처 (업체명)</label>
|
||||||
|
<input type="text" id="exportDestination" class="form-control" placeholder="예: 삼성정비">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반출 사유</label>
|
||||||
|
<textarea id="exportReason" class="form-control" rows="2" placeholder="반출 사유를 입력하세요"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>비고</label>
|
||||||
|
<textarea id="exportNotes" class="form-control" rows="2" placeholder="기타 메모사항"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeExportModal()">취소</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitExport()">반출 등록</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 반입 모달 -->
|
||||||
|
<div id="returnModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-container" style="max-width: 400px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>설비 반입</h2>
|
||||||
|
<button class="btn-close" onclick="closeReturnModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="returnLogId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반입일</label>
|
||||||
|
<input type="date" id="returnDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반입 후 상태</label>
|
||||||
|
<select id="returnStatus" class="form-control">
|
||||||
|
<option value="active">정상 가동</option>
|
||||||
|
<option value="maintenance">점검 필요</option>
|
||||||
|
<option value="repair_needed">추가 수리 필요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>비고</label>
|
||||||
|
<textarea id="returnNotes" class="form-control" rows="2" placeholder="반입 관련 메모"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeReturnModal()">취소</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="submitReturn()">반입 처리</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사진 확대 모달 -->
|
||||||
|
<div id="photoViewModal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="photo-view-container" onclick="closePhotoView()">
|
||||||
|
<button class="photo-view-close">×</button>
|
||||||
|
<img id="photoViewImage" class="photo-view-image">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import '/js/api-config.js?v=3';
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const checkApiConfig = setInterval(() => {
|
||||||
|
if (window.API_BASE_URL) {
|
||||||
|
clearInterval(checkApiConfig);
|
||||||
|
axios.defaults.baseURL = window.API_BASE_URL;
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
error => Promise.reject(error)
|
||||||
|
);
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
response => response,
|
||||||
|
error => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
alert('세션이 만료되었습니다. 다시 로그인해주세요.');
|
||||||
|
window.location.href = '/pages/login.html';
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script src="/js/equipment-detail.js?v=1"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -109,23 +109,346 @@
|
|||||||
<div class="toast-container" id="toastContainer"></div>
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
|
|
||||||
<!-- 작업장 상세 정보 모달 -->
|
<!-- 작업장 상세 정보 모달 -->
|
||||||
<div id="workplaceDetailModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; align-items: center; justify-content: center;">
|
<div id="workplaceDetailModal" class="workplace-modal-overlay">
|
||||||
<div style="background: white; border-radius: var(--radius-lg); padding: 32px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto; box-shadow: var(--shadow-2xl);">
|
<div class="workplace-modal-container">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
<!-- 모달 헤더 -->
|
||||||
<h2 id="modalWorkplaceName" style="margin: 0; font-size: var(--text-2xl); font-weight: 700;"></h2>
|
<div class="workplace-modal-header">
|
||||||
<button class="btn btn-secondary btn-sm" onclick="closeWorkplaceModal()">닫기</button>
|
<div class="workplace-modal-title-section">
|
||||||
|
<h2 id="modalWorkplaceName" class="workplace-modal-title"></h2>
|
||||||
|
<p id="modalWorkplaceDesc" class="workplace-modal-subtitle"></p>
|
||||||
|
</div>
|
||||||
|
<button class="workplace-modal-close" onclick="closeWorkplaceModal()">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 내부 작업자 -->
|
<!-- 모달 바디 -->
|
||||||
<div id="internalWorkersSection" style="margin-bottom: 24px;">
|
<div class="workplace-modal-body">
|
||||||
<h3 style="font-size: var(--text-lg); font-weight: 600; margin-bottom: 16px; color: var(--primary-600);">👷 내부 작업자</h3>
|
<!-- 탭 네비게이션 -->
|
||||||
<div id="internalWorkersList"></div>
|
<div class="workplace-modal-tabs">
|
||||||
|
<button class="workplace-tab active" data-tab="overview" onclick="switchWorkplaceTab('overview')">
|
||||||
|
<span class="tab-icon">📊</span>
|
||||||
|
<span class="tab-text">현황 개요</span>
|
||||||
|
</button>
|
||||||
|
<button class="workplace-tab" data-tab="workers" onclick="switchWorkplaceTab('workers')">
|
||||||
|
<span class="tab-icon">👷</span>
|
||||||
|
<span class="tab-text">작업자</span>
|
||||||
|
<span id="workerCountBadge" class="tab-badge">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="workplace-tab" data-tab="visitors" onclick="switchWorkplaceTab('visitors')">
|
||||||
|
<span class="tab-icon">🚪</span>
|
||||||
|
<span class="tab-text">방문자</span>
|
||||||
|
<span id="visitorCountBadge" class="tab-badge">0</span>
|
||||||
|
</button>
|
||||||
|
<button class="workplace-tab" data-tab="detail-map" onclick="switchWorkplaceTab('detail-map')">
|
||||||
|
<span class="tab-icon">🗺️</span>
|
||||||
|
<span class="tab-text">상세 지도</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 외부 방문자 -->
|
<!-- 탭 콘텐츠 -->
|
||||||
<div id="externalVisitorsSection">
|
<div class="workplace-tab-contents">
|
||||||
<h3 style="font-size: var(--text-lg); font-weight: 600; margin-bottom: 16px; color: var(--purple-600);">🚪 외부 방문자</h3>
|
<!-- 현황 개요 탭 -->
|
||||||
<div id="externalVisitorsList"></div>
|
<div id="tab-overview" class="workplace-tab-content active">
|
||||||
|
<!-- 요약 카드 -->
|
||||||
|
<div class="workplace-summary-cards">
|
||||||
|
<div class="summary-card workers">
|
||||||
|
<div class="summary-icon">👷</div>
|
||||||
|
<div class="summary-info">
|
||||||
|
<span class="summary-value" id="summaryWorkerCount">0</span>
|
||||||
|
<span class="summary-label">작업자</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card visitors">
|
||||||
|
<div class="summary-icon">🚪</div>
|
||||||
|
<div class="summary-info">
|
||||||
|
<span class="summary-value" id="summaryVisitorCount">0</span>
|
||||||
|
<span class="summary-label">방문자</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-card tasks">
|
||||||
|
<div class="summary-icon">📋</div>
|
||||||
|
<div class="summary-info">
|
||||||
|
<span class="summary-value" id="summaryTaskCount">0</span>
|
||||||
|
<span class="summary-label">작업 수</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 현재 작업 목록 -->
|
||||||
|
<div class="workplace-section">
|
||||||
|
<h4 class="section-title">
|
||||||
|
<span class="section-icon">🔧</span>
|
||||||
|
진행 중인 작업
|
||||||
|
</h4>
|
||||||
|
<div id="currentTasksList" class="current-tasks-list">
|
||||||
|
<p class="empty-message">진행 중인 작업이 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 현황 (간략) -->
|
||||||
|
<div class="workplace-section">
|
||||||
|
<h4 class="section-title">
|
||||||
|
<span class="section-icon">⚙️</span>
|
||||||
|
설비 현황
|
||||||
|
</h4>
|
||||||
|
<div id="equipmentSummary" class="equipment-summary">
|
||||||
|
<p class="empty-message">설비 정보를 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 작업자 탭 -->
|
||||||
|
<div id="tab-workers" class="workplace-tab-content">
|
||||||
|
<div id="internalWorkersList" class="workers-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 방문자 탭 -->
|
||||||
|
<div id="tab-visitors" class="workplace-tab-content">
|
||||||
|
<div id="externalVisitorsList" class="visitors-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 상세 지도 탭 -->
|
||||||
|
<div id="tab-detail-map" class="workplace-tab-content">
|
||||||
|
<div id="detailMapContainer" class="detail-map-container">
|
||||||
|
<div class="detail-map-placeholder">
|
||||||
|
<span class="placeholder-icon">🗺️</span>
|
||||||
|
<p>상세 지도를 불러오는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="detailMapLegend" class="detail-map-legend"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 모달 푸터 -->
|
||||||
|
<div class="workplace-modal-footer">
|
||||||
|
<button class="btn btn-outline" onclick="openPatrolPage()">
|
||||||
|
<span>🔍</span> 순회점검
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="closeWorkplaceModal()">닫기</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 상세 슬라이드 패널 -->
|
||||||
|
<div id="equipmentSlidePanel" class="equipment-slide-panel">
|
||||||
|
<div class="slide-panel-header">
|
||||||
|
<button class="slide-panel-back" onclick="closeEquipmentPanel()">←</button>
|
||||||
|
<div class="slide-panel-title-section">
|
||||||
|
<h3 id="panelEquipmentTitle" class="slide-panel-title"></h3>
|
||||||
|
<span id="panelEquipmentStatus" class="slide-panel-status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="slide-panel-body">
|
||||||
|
<!-- 기본 정보 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="panel-info-grid" id="panelEquipmentInfo"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 사진 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<div class="panel-section-header">
|
||||||
|
<h4>설비 사진</h4>
|
||||||
|
<button class="btn-icon-sm" onclick="openPanelPhotoUpload()">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-photo-grid" id="panelPhotoGrid">
|
||||||
|
<div class="panel-empty">등록된 사진이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 액션 버튼 -->
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button class="panel-action-btn move" onclick="openPanelMoveModal()">
|
||||||
|
<span>↔</span> 임시이동
|
||||||
|
</button>
|
||||||
|
<button class="panel-action-btn repair" onclick="openPanelRepairModal()">
|
||||||
|
<span>🔧</span> 수리신청
|
||||||
|
</button>
|
||||||
|
<button class="panel-action-btn export" onclick="openPanelExportModal()">
|
||||||
|
<span>🚚</span> 외부반출
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 수리 이력 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<h4 class="panel-section-title">수리 이력</h4>
|
||||||
|
<div id="panelRepairHistory" class="panel-history-list">
|
||||||
|
<div class="panel-empty">수리 이력이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 외부반출 이력 -->
|
||||||
|
<div class="panel-section">
|
||||||
|
<h4 class="panel-section-title">외부반출 이력</h4>
|
||||||
|
<div id="panelExternalHistory" class="panel-history-list">
|
||||||
|
<div class="panel-empty">외부반출 이력이 없습니다</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 사진 업로드 모달 -->
|
||||||
|
<div id="panelPhotoModal" class="mini-modal-overlay" style="display:none;">
|
||||||
|
<div class="mini-modal">
|
||||||
|
<div class="mini-modal-header">
|
||||||
|
<h4>사진 추가</h4>
|
||||||
|
<button onclick="closePanelPhotoModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-body">
|
||||||
|
<input type="file" id="panelPhotoInput" accept="image/*" onchange="previewPanelPhoto(event)">
|
||||||
|
<div id="panelPhotoPreview" class="mini-photo-preview"></div>
|
||||||
|
<input type="text" id="panelPhotoDesc" class="form-control" placeholder="설명 (선택)">
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-footer">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="closePanelPhotoModal()">취소</button>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="uploadPanelPhoto()">업로드</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 임시이동 모달 -->
|
||||||
|
<div id="panelMoveModal" class="mini-modal-overlay" style="display:none;">
|
||||||
|
<div class="mini-modal" style="max-width:700px;">
|
||||||
|
<div class="mini-modal-header">
|
||||||
|
<h4 id="panelMoveTitle">설비 임시 이동</h4>
|
||||||
|
<button onclick="closePanelMoveModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-body" style="padding:0;">
|
||||||
|
<!-- Step 1: 공장 선택 (대분류 지도) -->
|
||||||
|
<div id="moveStep1" class="move-step-content">
|
||||||
|
<div class="move-step-header">
|
||||||
|
<span class="step-badge">1</span>
|
||||||
|
<span>공장 선택</span>
|
||||||
|
</div>
|
||||||
|
<div class="move-factory-grid" id="moveFactoryGrid">
|
||||||
|
<!-- 공장 카드들 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: 작업장 선택 (공장 레이아웃 지도) -->
|
||||||
|
<div id="moveStep2" class="move-step-content" style="display:none;">
|
||||||
|
<div class="move-step-header">
|
||||||
|
<button class="btn-step-back" onclick="moveBackToStep1()">←</button>
|
||||||
|
<span class="step-badge">2</span>
|
||||||
|
<span id="moveStep2Title">작업장 선택</span>
|
||||||
|
</div>
|
||||||
|
<p class="move-help-text">지도에서 이동할 작업장을 클릭하세요</p>
|
||||||
|
<div class="move-layout-map" id="moveLayoutMapContainer"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: 위치 선택 (상세 지도) -->
|
||||||
|
<div id="moveStep3" class="move-step-content" style="display:none;">
|
||||||
|
<div class="move-step-header">
|
||||||
|
<button class="btn-step-back" onclick="moveBackToStep2()">←</button>
|
||||||
|
<span class="step-badge">3</span>
|
||||||
|
<span id="moveStep3Title">위치 선택</span>
|
||||||
|
</div>
|
||||||
|
<p class="move-help-text">지도에서 설비를 배치할 위치를 클릭하세요</p>
|
||||||
|
<div class="move-detail-map" id="moveDetailMapContainer"></div>
|
||||||
|
<div class="form-group" style="padding:12px;">
|
||||||
|
<input type="text" id="panelMoveReason" class="form-control" placeholder="이동 사유 (선택)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-footer">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="closePanelMoveModal()">취소</button>
|
||||||
|
<button class="btn btn-primary btn-sm" id="panelMoveConfirmBtn" onclick="confirmPanelMove()" disabled>이동 확인</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 수리신청 모달 -->
|
||||||
|
<div id="panelRepairModal" class="mini-modal-overlay" style="display:none;">
|
||||||
|
<div class="mini-modal">
|
||||||
|
<div class="mini-modal-header">
|
||||||
|
<h4>수리 신청</h4>
|
||||||
|
<button onclick="closePanelRepairModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>수리 유형</label>
|
||||||
|
<select id="panelRepairItem" class="form-control">
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>상세 내용</label>
|
||||||
|
<textarea id="panelRepairDesc" class="form-control" rows="3" placeholder="수리 필요 내용"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>사진 첨부</label>
|
||||||
|
<input type="file" id="panelRepairPhotoInput" accept="image/*" multiple>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-footer">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="closePanelRepairModal()">취소</button>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="submitPanelRepair()">신청</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 외부반출 모달 -->
|
||||||
|
<div id="panelExportModal" class="mini-modal-overlay" style="display:none;">
|
||||||
|
<div class="mini-modal">
|
||||||
|
<div class="mini-modal-header">
|
||||||
|
<h4>외부 반출</h4>
|
||||||
|
<button onclick="closePanelExportModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="checkbox-inline">
|
||||||
|
<input type="checkbox" id="panelIsRepairExport"> 수리 외주
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반출일</label>
|
||||||
|
<input type="date" id="panelExportDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반입 예정일</label>
|
||||||
|
<input type="date" id="panelExpectedReturn" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반출처</label>
|
||||||
|
<input type="text" id="panelExportDest" class="form-control" placeholder="업체명">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반출 사유</label>
|
||||||
|
<textarea id="panelExportReason" class="form-control" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-footer">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="closePanelExportModal()">취소</button>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="submitPanelExport()">반출</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설비 반입 모달 -->
|
||||||
|
<div id="panelReturnModal" class="mini-modal-overlay" style="display:none;">
|
||||||
|
<div class="mini-modal" style="max-width:350px;">
|
||||||
|
<div class="mini-modal-header">
|
||||||
|
<h4>설비 반입</h4>
|
||||||
|
<button onclick="closePanelReturnModal()">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-body">
|
||||||
|
<input type="hidden" id="panelReturnLogId">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반입일</label>
|
||||||
|
<input type="date" id="panelReturnDate" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>반입 후 상태</label>
|
||||||
|
<select id="panelReturnStatus" class="form-control">
|
||||||
|
<option value="active">정상 가동</option>
|
||||||
|
<option value="maintenance">점검 필요</option>
|
||||||
|
<option value="repair_needed">추가 수리 필요</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mini-modal-footer">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="closePanelReturnModal()">취소</button>
|
||||||
|
<button class="btn btn-primary btn-sm" onclick="submitPanelReturn()">반입</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user