## 주요 기능 - 설비 등록/수정/삭제 기능 - 작업장별 설비 연결 - 작업장 지도에서 설비 위치 정의 - 필터링 및 검색 기능 ## 백엔드 - equipments 테이블 생성 (마이그레이션) - 설비 API (모델, 컨트롤러, 라우트) 구현 - workplaces 테이블에 layout_image 컬럼 추가 ## 프론트엔드 - 설비 관리 페이지 (equipments.html) - 설비 관리 JavaScript (equipment-management.js) - 작업장 지도 모달 개선 ## 버그 수정 - 카테고리/작업장 이미지 보존 로직 개선 (null 처리) - 작업장 레이아웃 이미지 업로드 경로 수정 (public/uploads → uploads) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
576 lines
16 KiB
JavaScript
576 lines
16 KiB
JavaScript
/**
|
|
* 작업장 관리 컨트롤러
|
|
*
|
|
* 작업장 카테고리(공장) 및 작업장 CRUD API 엔드포인트 핸들러
|
|
*
|
|
* @author TK-FB-Project
|
|
* @since 2026-01-26
|
|
*/
|
|
|
|
const workplaceModel = require('../models/workplaceModel');
|
|
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
|
const { asyncHandler } = require('../middlewares/errorHandler');
|
|
const logger = require('../utils/logger');
|
|
|
|
// ==================== 카테고리(공장) 관련 ====================
|
|
|
|
/**
|
|
* 카테고리 생성
|
|
*/
|
|
exports.createCategory = asyncHandler(async (req, res) => {
|
|
const categoryData = req.body;
|
|
|
|
if (!categoryData.category_name) {
|
|
throw new ValidationError('카테고리명은 필수 입력 항목입니다');
|
|
}
|
|
|
|
logger.info('카테고리 생성 요청', { name: categoryData.category_name });
|
|
|
|
const id = await new Promise((resolve, reject) => {
|
|
workplaceModel.createCategory(categoryData, (err, lastID) => {
|
|
if (err) reject(new DatabaseError('카테고리 생성 중 오류가 발생했습니다'));
|
|
else resolve(lastID);
|
|
});
|
|
});
|
|
|
|
logger.info('카테고리 생성 성공', { category_id: id });
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: { category_id: id },
|
|
message: '카테고리가 성공적으로 생성되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 전체 카테고리 조회
|
|
*/
|
|
exports.getAllCategories = asyncHandler(async (req, res) => {
|
|
const rows = await new Promise((resolve, reject) => {
|
|
workplaceModel.getAllCategories((err, data) => {
|
|
if (err) reject(new DatabaseError('카테고리 목록 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: rows,
|
|
message: '카테고리 목록 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 활성 카테고리만 조회
|
|
*/
|
|
exports.getActiveCategories = asyncHandler(async (req, res) => {
|
|
const rows = await new Promise((resolve, reject) => {
|
|
workplaceModel.getActiveCategories((err, data) => {
|
|
if (err) reject(new DatabaseError('활성 카테고리 목록 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: rows,
|
|
message: '활성 카테고리 목록 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 단일 카테고리 조회
|
|
*/
|
|
exports.getCategoryById = asyncHandler(async (req, res) => {
|
|
const categoryId = req.params.id;
|
|
|
|
const category = await new Promise((resolve, reject) => {
|
|
workplaceModel.getCategoryById(categoryId, (err, data) => {
|
|
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
if (!category) {
|
|
throw new NotFoundError('카테고리를 찾을 수 없습니다');
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: category,
|
|
message: '카테고리 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 카테고리 수정
|
|
*/
|
|
exports.updateCategory = asyncHandler(async (req, res) => {
|
|
const categoryId = req.params.id;
|
|
const categoryData = req.body;
|
|
|
|
if (!categoryData.category_name) {
|
|
throw new ValidationError('카테고리명은 필수 입력 항목입니다');
|
|
}
|
|
|
|
logger.info('카테고리 수정 요청', { category_id: categoryId });
|
|
|
|
// 기존 카테고리 정보 가져오기
|
|
const existingCategory = await new Promise((resolve, reject) => {
|
|
workplaceModel.getCategoryById(categoryId, (err, data) => {
|
|
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
if (!existingCategory) {
|
|
throw new NotFoundError('카테고리를 찾을 수 없습니다');
|
|
}
|
|
|
|
// layout_image가 요청에 없거나 null이면 기존 값 보존
|
|
const updateData = {
|
|
...categoryData,
|
|
layout_image: (categoryData.layout_image !== undefined && categoryData.layout_image !== null)
|
|
? categoryData.layout_image
|
|
: existingCategory.layout_image
|
|
};
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.updateCategory(categoryId, updateData, (err, result) => {
|
|
if (err) reject(new DatabaseError('카테고리 수정 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('카테고리 수정 성공', { category_id: categoryId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '카테고리가 성공적으로 수정되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 카테고리 삭제
|
|
*/
|
|
exports.deleteCategory = asyncHandler(async (req, res) => {
|
|
const categoryId = req.params.id;
|
|
|
|
logger.info('카테고리 삭제 요청', { category_id: categoryId });
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.deleteCategory(categoryId, (err, result) => {
|
|
if (err) reject(new DatabaseError('카테고리 삭제 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('카테고리 삭제 성공', { category_id: categoryId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '카테고리가 성공적으로 삭제되었습니다'
|
|
});
|
|
});
|
|
|
|
// ==================== 작업장 관련 ====================
|
|
|
|
/**
|
|
* 작업장 생성
|
|
*/
|
|
exports.createWorkplace = asyncHandler(async (req, res) => {
|
|
const workplaceData = req.body;
|
|
|
|
if (!workplaceData.workplace_name) {
|
|
throw new ValidationError('작업장명은 필수 입력 항목입니다');
|
|
}
|
|
|
|
logger.info('작업장 생성 요청', { name: workplaceData.workplace_name });
|
|
|
|
const id = await new Promise((resolve, reject) => {
|
|
workplaceModel.createWorkplace(workplaceData, (err, lastID) => {
|
|
if (err) reject(new DatabaseError('작업장 생성 중 오류가 발생했습니다'));
|
|
else resolve(lastID);
|
|
});
|
|
});
|
|
|
|
logger.info('작업장 생성 성공', { workplace_id: id });
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: { workplace_id: id },
|
|
message: '작업장이 성공적으로 생성되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 전체 작업장 조회
|
|
*/
|
|
exports.getAllWorkplaces = asyncHandler(async (req, res) => {
|
|
const categoryId = req.query.category_id;
|
|
|
|
// 카테고리별 필터링
|
|
if (categoryId) {
|
|
const rows = await new Promise((resolve, reject) => {
|
|
workplaceModel.getWorkplacesByCategory(categoryId, (err, data) => {
|
|
if (err) reject(new DatabaseError('작업장 목록 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: rows,
|
|
message: '작업장 목록 조회 성공'
|
|
});
|
|
}
|
|
|
|
// 전체 조회
|
|
const rows = await new Promise((resolve, reject) => {
|
|
workplaceModel.getAllWorkplaces((err, data) => {
|
|
if (err) reject(new DatabaseError('작업장 목록 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: rows,
|
|
message: '작업장 목록 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 활성 작업장만 조회
|
|
*/
|
|
exports.getActiveWorkplaces = asyncHandler(async (req, res) => {
|
|
const rows = await new Promise((resolve, reject) => {
|
|
workplaceModel.getActiveWorkplaces((err, data) => {
|
|
if (err) reject(new DatabaseError('활성 작업장 목록 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: rows,
|
|
message: '활성 작업장 목록 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 단일 작업장 조회
|
|
*/
|
|
exports.getWorkplaceById = asyncHandler(async (req, res) => {
|
|
const workplaceId = req.params.id;
|
|
|
|
const workplace = await new Promise((resolve, reject) => {
|
|
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
|
|
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
if (!workplace) {
|
|
throw new NotFoundError('작업장을 찾을 수 없습니다');
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: workplace,
|
|
message: '작업장 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 작업장 수정
|
|
*/
|
|
exports.updateWorkplace = asyncHandler(async (req, res) => {
|
|
const workplaceId = req.params.id;
|
|
const workplaceData = req.body;
|
|
|
|
if (!workplaceData.workplace_name) {
|
|
throw new ValidationError('작업장명은 필수 입력 항목입니다');
|
|
}
|
|
|
|
logger.info('작업장 수정 요청', { workplace_id: workplaceId });
|
|
|
|
// 기존 작업장 정보 가져오기
|
|
const existingWorkplace = await new Promise((resolve, reject) => {
|
|
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
|
|
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
if (!existingWorkplace) {
|
|
throw new NotFoundError('작업장을 찾을 수 없습니다');
|
|
}
|
|
|
|
// layout_image가 요청에 없거나 null이면 기존 값 보존
|
|
const updateData = {
|
|
...workplaceData,
|
|
layout_image: (workplaceData.layout_image !== undefined && workplaceData.layout_image !== null)
|
|
? workplaceData.layout_image
|
|
: existingWorkplace.layout_image
|
|
};
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.updateWorkplace(workplaceId, updateData, (err, result) => {
|
|
if (err) reject(new DatabaseError('작업장 수정 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('작업장 수정 성공', { workplace_id: workplaceId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '작업장이 성공적으로 수정되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 작업장 삭제
|
|
*/
|
|
exports.deleteWorkplace = asyncHandler(async (req, res) => {
|
|
const workplaceId = req.params.id;
|
|
|
|
logger.info('작업장 삭제 요청', { workplace_id: workplaceId });
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.deleteWorkplace(workplaceId, (err, result) => {
|
|
if (err) reject(new DatabaseError('작업장 삭제 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('작업장 삭제 성공', { workplace_id: workplaceId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '작업장이 성공적으로 삭제되었습니다'
|
|
});
|
|
});
|
|
|
|
// ==================== 작업장 지도 영역 관련 ====================
|
|
|
|
/**
|
|
* 카테고리 레이아웃 이미지 업로드
|
|
*/
|
|
exports.uploadCategoryLayoutImage = asyncHandler(async (req, res) => {
|
|
const categoryId = req.params.id;
|
|
|
|
if (!req.file) {
|
|
throw new ValidationError('이미지 파일이 필요합니다');
|
|
}
|
|
|
|
const imagePath = `/uploads/${req.file.filename}`;
|
|
|
|
logger.info('카테고리 레이아웃 이미지 업로드 요청', { category_id: categoryId, path: imagePath });
|
|
|
|
// 현재 카테고리 정보 가져오기
|
|
const category = await new Promise((resolve, reject) => {
|
|
workplaceModel.getCategoryById(categoryId, (err, data) => {
|
|
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
if (!category) {
|
|
throw new NotFoundError('카테고리를 찾을 수 없습니다');
|
|
}
|
|
|
|
// 카테고리 정보 업데이트 (이미지 경로만 변경)
|
|
const updatedData = {
|
|
category_name: category.category_name,
|
|
description: category.description,
|
|
display_order: category.display_order,
|
|
is_active: category.is_active,
|
|
layout_image: imagePath
|
|
};
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.updateCategory(categoryId, updatedData, (err, result) => {
|
|
if (err) reject(new DatabaseError('이미지 경로 저장 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('레이아웃 이미지 업로드 성공', { category_id: categoryId });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { image_path: imagePath },
|
|
message: '레이아웃 이미지가 성공적으로 업로드되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 작업장 레이아웃 이미지 업로드
|
|
*/
|
|
exports.uploadWorkplaceLayoutImage = asyncHandler(async (req, res) => {
|
|
const workplaceId = req.params.id;
|
|
|
|
if (!req.file) {
|
|
throw new ValidationError('이미지 파일이 필요합니다');
|
|
}
|
|
|
|
const imagePath = `/uploads/${req.file.filename}`;
|
|
|
|
logger.info('작업장 레이아웃 이미지 업로드 요청', { workplace_id: workplaceId, path: imagePath });
|
|
|
|
// 현재 작업장 정보 가져오기
|
|
const workplace = await new Promise((resolve, reject) => {
|
|
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
|
|
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
if (!workplace) {
|
|
throw new NotFoundError('작업장을 찾을 수 없습니다');
|
|
}
|
|
|
|
// 작업장 정보 업데이트 (이미지 경로만 변경)
|
|
const updatedData = {
|
|
workplace_name: workplace.workplace_name,
|
|
category_id: workplace.category_id,
|
|
description: workplace.description,
|
|
workplace_purpose: workplace.workplace_purpose,
|
|
display_priority: workplace.display_priority,
|
|
is_active: workplace.is_active,
|
|
layout_image: imagePath
|
|
};
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.updateWorkplace(workplaceId, updatedData, (err, result) => {
|
|
if (err) reject(new DatabaseError('이미지 경로 저장 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('작업장 레이아웃 이미지 업로드 성공', { workplace_id: workplaceId });
|
|
|
|
res.json({
|
|
success: true,
|
|
data: { image_path: imagePath },
|
|
message: '작업장 레이아웃 이미지가 성공적으로 업로드되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 지도 영역 생성
|
|
*/
|
|
exports.createMapRegion = asyncHandler(async (req, res) => {
|
|
const regionData = req.body;
|
|
|
|
if (!regionData.workplace_id || !regionData.category_id) {
|
|
throw new ValidationError('작업장 ID와 카테고리 ID는 필수 입력 항목입니다');
|
|
}
|
|
|
|
logger.info('지도 영역 생성 요청', { workplace_id: regionData.workplace_id });
|
|
|
|
const id = await new Promise((resolve, reject) => {
|
|
workplaceModel.createMapRegion(regionData, (err, lastID) => {
|
|
if (err) reject(new DatabaseError('지도 영역 생성 중 오류가 발생했습니다'));
|
|
else resolve(lastID);
|
|
});
|
|
});
|
|
|
|
logger.info('지도 영역 생성 성공', { region_id: id });
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: { region_id: id },
|
|
message: '지도 영역이 성공적으로 생성되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 카테고리별 지도 영역 조회 (작업장 정보 포함)
|
|
*/
|
|
exports.getMapRegionsByCategory = asyncHandler(async (req, res) => {
|
|
const categoryId = req.params.categoryId;
|
|
|
|
const rows = await new Promise((resolve, reject) => {
|
|
workplaceModel.getMapRegionsByCategory(categoryId, (err, data) => {
|
|
if (err) reject(new DatabaseError('지도 영역 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: rows,
|
|
message: '지도 영역 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 작업장별 지도 영역 조회
|
|
*/
|
|
exports.getMapRegionByWorkplace = asyncHandler(async (req, res) => {
|
|
const workplaceId = req.params.workplaceId;
|
|
|
|
const region = await new Promise((resolve, reject) => {
|
|
workplaceModel.getMapRegionByWorkplace(workplaceId, (err, data) => {
|
|
if (err) reject(new DatabaseError('지도 영역 조회 중 오류가 발생했습니다'));
|
|
else resolve(data);
|
|
});
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
data: region,
|
|
message: '지도 영역 조회 성공'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 지도 영역 수정
|
|
*/
|
|
exports.updateMapRegion = asyncHandler(async (req, res) => {
|
|
const regionId = req.params.id;
|
|
const regionData = req.body;
|
|
|
|
logger.info('지도 영역 수정 요청', { region_id: regionId });
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.updateMapRegion(regionId, regionData, (err, result) => {
|
|
if (err) reject(new DatabaseError('지도 영역 수정 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('지도 영역 수정 성공', { region_id: regionId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '지도 영역이 성공적으로 수정되었습니다'
|
|
});
|
|
});
|
|
|
|
/**
|
|
* 지도 영역 삭제
|
|
*/
|
|
exports.deleteMapRegion = asyncHandler(async (req, res) => {
|
|
const regionId = req.params.id;
|
|
|
|
logger.info('지도 영역 삭제 요청', { region_id: regionId });
|
|
|
|
await new Promise((resolve, reject) => {
|
|
workplaceModel.deleteMapRegion(regionId, (err, result) => {
|
|
if (err) reject(new DatabaseError('지도 영역 삭제 중 오류가 발생했습니다'));
|
|
else resolve(result);
|
|
});
|
|
});
|
|
|
|
logger.info('지도 영역 삭제 성공', { region_id: regionId });
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '지도 영역이 성공적으로 삭제되었습니다'
|
|
});
|
|
});
|