+ + 설비 관리 +
+작업장별 설비 정보를 등록하고 관리합니다
+diff --git a/api.hyungi.net/config/routes.js b/api.hyungi.net/config/routes.js index 890953b..51c5dfc 100644 --- a/api.hyungi.net/config/routes.js +++ b/api.hyungi.net/config/routes.js @@ -41,8 +41,9 @@ function setupRoutes(app) { const monthlyStatusRoutes = require('../routes/monthlyStatusRoutes'); const pageAccessRoutes = require('../routes/pageAccessRoutes'); const workplaceRoutes = require('../routes/workplaceRoutes'); + const equipmentRoutes = require('../routes/equipmentRoutes'); const taskRoutes = require('../routes/taskRoutes'); - // const tbmRoutes = require('../routes/tbmRoutes'); // 임시 비활성화 - db/connection 문제 + const tbmRoutes = require('../routes/tbmRoutes'); // Rate Limiters 설정 const rateLimit = require('express-rate-limit'); @@ -57,10 +58,18 @@ function setupRoutes(app) { const apiLimiter = rateLimit({ windowMs: 1 * 60 * 1000, // 1분 - max: 100, // 최대 100회 + max: 1000, // 최대 1000회 (기존 100회에서 대폭 증가) message: 'API 요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.', standardHeaders: true, - legacyHeaders: false + legacyHeaders: false, + // 관리자 및 시스템 계정은 rate limit 제외 + skip: (req) => { + // 인증된 사용자 정보 확인 + if (req.user && (req.user.access_level === 'system' || req.user.access_level === 'admin')) { + return true; // rate limit 건너뛰기 + } + return false; + } }); // 모든 API 요청에 활동 로거 적용 @@ -75,9 +84,6 @@ function setupRoutes(app) { // Health check app.use('/api/health', healthRoutes); - // 일반 API에 속도 제한 적용 - app.use('/api/', apiLimiter); - // 인증이 필요 없는 공개 경로 목록 const publicPaths = [ '/api/auth/login', @@ -95,7 +101,7 @@ function setupRoutes(app) { '/api/monthly-status/daily-details' ]; - // 인증 미들웨어 - 공개 경로를 제외한 모든 API + // 인증 미들웨어 - 공개 경로를 제외한 모든 API (rate limiter보다 먼저 실행) app.use('/api/*', (req, res, next) => { const isPublicPath = publicPaths.some(path => { return req.originalUrl === path || @@ -112,6 +118,9 @@ function setupRoutes(app) { verifyToken(req, res, next); }); + // 인증 후 일반 API에 속도 제한 적용 (인증된 사용자 정보로 skip 판단) + app.use('/api/', apiLimiter); + // 인증된 사용자만 접근 가능한 라우트들 app.use('/api/issue-reports', dailyIssueReportRoutes); app.use('/api/issue-types', issueTypeRoutes); @@ -130,9 +139,10 @@ function setupRoutes(app) { app.use('/api/tools', toolsRoute); app.use('/api/users', userRoutes); app.use('/api/workplaces', workplaceRoutes); + app.use('/api/equipments', equipmentRoutes); app.use('/api/tasks', taskRoutes); app.use('/api', pageAccessRoutes); // 페이지 접근 권한 관리 - // app.use('/api/tbm', tbmRoutes); // TBM 시스템 - 임시 비활성화 + app.use('/api/tbm', tbmRoutes); // TBM 시스템 app.use('/api', uploadBgRoutes); // Swagger API 문서 diff --git a/api.hyungi.net/controllers/equipmentController.js b/api.hyungi.net/controllers/equipmentController.js new file mode 100644 index 0000000..54538f1 --- /dev/null +++ b/api.hyungi.net/controllers/equipmentController.js @@ -0,0 +1,349 @@ +// controllers/equipmentController.js +const EquipmentModel = require('../models/equipmentModel'); + +const EquipmentController = { + // CREATE - 설비 생성 + createEquipment: async (req, res) => { + try { + const equipmentData = req.body; + + // 필수 필드 검증 + if (!equipmentData.equipment_code || !equipmentData.equipment_name) { + return res.status(400).json({ + success: false, + message: '설비 코드와 설비명은 필수입니다.' + }); + } + + // 설비 코드 중복 확인 + EquipmentModel.checkDuplicateCode(equipmentData.equipment_code, null, (error, isDuplicate) => { + if (error) { + console.error('설비 코드 중복 확인 오류:', error); + return res.status(500).json({ + success: false, + message: '설비 코드 중복 확인 중 오류가 발생했습니다.' + }); + } + + if (isDuplicate) { + return res.status(409).json({ + success: false, + message: '이미 사용 중인 설비 코드입니다.' + }); + } + + // 설비 생성 + EquipmentModel.create(equipmentData, (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: '서버 오류가 발생했습니다.' + }); + } + }, + + // READ ALL - 모든 설비 조회 (필터링 가능) + getAllEquipments: (req, res) => { + try { + const filters = { + workplace_id: req.query.workplace_id, + equipment_type: req.query.equipment_type, + status: req.query.status, + search: req.query.search + }; + + EquipmentModel.getAll(filters, (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: '서버 오류가 발생했습니다.' + }); + } + }, + + // READ ONE - 특정 설비 조회 + getEquipmentById: (req, res) => { + try { + const equipmentId = req.params.id; + + EquipmentModel.getById(equipmentId, (error, result) => { + if (error) { + console.error('설비 조회 오류:', error); + return res.status(500).json({ + success: false, + message: '설비 조회 중 오류가 발생했습니다.' + }); + } + + if (!result) { + return res.status(404).json({ + success: false, + message: '설비를 찾을 수 없습니다.' + }); + } + + res.json({ + success: true, + data: result + }); + }); + } catch (error) { + console.error('설비 조회 오류:', error); + res.status(500).json({ + success: false, + message: '서버 오류가 발생했습니다.' + }); + } + }, + + // READ BY WORKPLACE - 특정 작업장의 설비 조회 + getEquipmentsByWorkplace: (req, res) => { + try { + const workplaceId = req.params.workplaceId; + + EquipmentModel.getByWorkplace(workplaceId, (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: '서버 오류가 발생했습니다.' + }); + } + }, + + // READ ACTIVE - 활성 설비만 조회 + getActiveEquipments: (req, res) => { + try { + EquipmentModel.getActive((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: '서버 오류가 발생했습니다.' + }); + } + }, + + // UPDATE - 설비 수정 + updateEquipment: async (req, res) => { + try { + const equipmentId = req.params.id; + const equipmentData = req.body; + + // 필수 필드 검증 + if (!equipmentData.equipment_code || !equipmentData.equipment_name) { + return res.status(400).json({ + success: false, + message: '설비 코드와 설비명은 필수입니다.' + }); + } + + // 설비 존재 확인 + EquipmentModel.getById(equipmentId, (error, existingEquipment) => { + if (error) { + console.error('설비 조회 오류:', error); + return res.status(500).json({ + success: false, + message: '설비 조회 중 오류가 발생했습니다.' + }); + } + + if (!existingEquipment) { + return res.status(404).json({ + success: false, + message: '설비를 찾을 수 없습니다.' + }); + } + + // 설비 코드 중복 확인 (자신 제외) + EquipmentModel.checkDuplicateCode(equipmentData.equipment_code, equipmentId, (error, isDuplicate) => { + if (error) { + console.error('설비 코드 중복 확인 오류:', error); + return res.status(500).json({ + success: false, + message: '설비 코드 중복 확인 중 오류가 발생했습니다.' + }); + } + + if (isDuplicate) { + return res.status(409).json({ + success: false, + message: '이미 사용 중인 설비 코드입니다.' + }); + } + + // 설비 수정 + EquipmentModel.update(equipmentId, equipmentData, (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: '서버 오류가 발생했습니다.' + }); + } + }, + + // UPDATE MAP POSITION - 지도상 위치 업데이트 + updateMapPosition: (req, res) => { + try { + const equipmentId = req.params.id; + const positionData = { + map_x_percent: req.body.map_x_percent, + map_y_percent: req.body.map_y_percent, + map_width_percent: req.body.map_width_percent, + map_height_percent: req.body.map_height_percent + }; + + EquipmentModel.updateMapPosition(equipmentId, positionData, (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: '서버 오류가 발생했습니다.' + }); + } + }, + + // DELETE - 설비 삭제 + deleteEquipment: (req, res) => { + try { + const equipmentId = req.params.id; + + EquipmentModel.delete(equipmentId, (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: '서버 오류가 발생했습니다.' + }); + } + }, + + // GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회 + getEquipmentTypes: (req, res) => { + try { + EquipmentModel.getEquipmentTypes((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: '서버 오류가 발생했습니다.' + }); + } + } +}; + +module.exports = EquipmentController; diff --git a/api.hyungi.net/controllers/workplaceController.js b/api.hyungi.net/controllers/workplaceController.js index 3543b86..b384529 100644 --- a/api.hyungi.net/controllers/workplaceController.js +++ b/api.hyungi.net/controllers/workplaceController.js @@ -115,8 +115,28 @@ exports.updateCategory = asyncHandler(async (req, res) => { 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, categoryData, (err, result) => { + workplaceModel.updateCategory(categoryId, updateData, (err, result) => { if (err) reject(new DatabaseError('카테고리 수정 중 오류가 발생했습니다')); else resolve(result); }); @@ -275,8 +295,28 @@ exports.updateWorkplace = asyncHandler(async (req, res) => { 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, workplaceData, (err, result) => { + workplaceModel.updateWorkplace(workplaceId, updateData, (err, result) => { if (err) reject(new DatabaseError('작업장 수정 중 오류가 발생했습니다')); else resolve(result); }); @@ -312,3 +352,224 @@ exports.deleteWorkplace = asyncHandler(async (req, res) => { 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: '지도 영역이 성공적으로 삭제되었습니다' + }); +}); diff --git a/api.hyungi.net/db/migrations/20260128000000_add_layout_image_to_workplaces.js b/api.hyungi.net/db/migrations/20260128000000_add_layout_image_to_workplaces.js new file mode 100644 index 0000000..c4a5db4 --- /dev/null +++ b/api.hyungi.net/db/migrations/20260128000000_add_layout_image_to_workplaces.js @@ -0,0 +1,22 @@ +/** + * 작업장 테이블에 레이아웃 이미지 컬럼 추가 + * + * @author TK-FB-Project + * @since 2026-01-28 + */ + +exports.up = async function(knex) { + await knex.schema.table('workplaces', (table) => { + table.string('layout_image', 500).nullable().comment('작업장 레이아웃 이미지 경로'); + }); + + console.log('✅ workplaces 테이블에 layout_image 컬럼 추가 완료'); +}; + +exports.down = async function(knex) { + await knex.schema.table('workplaces', (table) => { + table.dropColumn('layout_image'); + }); + + console.log('✅ workplaces 테이블에서 layout_image 컬럼 제거 완료'); +}; diff --git a/api.hyungi.net/db/migrations/20260128010000_create_equipments_table.js b/api.hyungi.net/db/migrations/20260128010000_create_equipments_table.js new file mode 100644 index 0000000..c1fb50b --- /dev/null +++ b/api.hyungi.net/db/migrations/20260128010000_create_equipments_table.js @@ -0,0 +1,48 @@ +/** + * 설비 관리 테이블 생성 + * + * @author TK-FB-Project + * @since 2026-01-28 + */ + +exports.up = async function(knex) { + await knex.schema.createTable('equipments', (table) => { + table.increments('equipment_id').primary().comment('설비 ID'); + table.string('equipment_code', 50).notNullable().unique().comment('설비 코드 (예: CNC-01, LATHE-A)'); + table.string('equipment_name', 100).notNullable().comment('설비명'); + table.string('equipment_type', 50).nullable().comment('설비 유형 (예: CNC, 선반, 밀링 등)'); + table.string('model_name', 100).nullable().comment('모델명'); + table.string('manufacturer', 100).nullable().comment('제조사'); + table.date('installation_date').nullable().comment('설치일'); + table.string('serial_number', 100).nullable().comment('시리얼 번호'); + table.text('specifications').nullable().comment('사양 정보 (JSON 형태로 저장 가능)'); + table.enum('status', ['active', 'maintenance', 'inactive']).defaultTo('active').comment('설비 상태'); + table.text('notes').nullable().comment('비고'); + + // 작업장 연결 + table.integer('workplace_id').unsigned().nullable().comment('연결된 작업장 ID'); + table.foreign('workplace_id').references('workplace_id').inTable('workplaces').onDelete('SET NULL'); + + // 지도상 위치 정보 (백분율 기반) + table.decimal('map_x_percent', 5, 2).nullable().comment('지도상 X 위치 (%)'); + table.decimal('map_y_percent', 5, 2).nullable().comment('지도상 Y 위치 (%)'); + table.decimal('map_width_percent', 5, 2).nullable().comment('지도상 영역 너비 (%)'); + table.decimal('map_height_percent', 5, 2).nullable().comment('지도상 영역 높이 (%)'); + + // 타임스탬프 + table.timestamp('created_at').defaultTo(knex.fn.now()).comment('생성일시'); + table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정일시'); + + // 인덱스 + table.index('workplace_id'); + table.index('equipment_type'); + table.index('status'); + }); + + console.log('✅ equipments 테이블 생성 완료'); +}; + +exports.down = async function(knex) { + await knex.schema.dropTableIfExists('equipments'); + console.log('✅ equipments 테이블 삭제 완료'); +}; diff --git a/api.hyungi.net/models/equipmentModel.js b/api.hyungi.net/models/equipmentModel.js new file mode 100644 index 0000000..d5a3350 --- /dev/null +++ b/api.hyungi.net/models/equipmentModel.js @@ -0,0 +1,300 @@ +// models/equipmentModel.js +const { getDb } = require('../dbPool'); + +const EquipmentModel = { + // CREATE - 설비 생성 + create: async (equipmentData, callback) => { + try { + const db = await getDb(); + const query = ` + INSERT INTO equipments ( + equipment_code, equipment_name, equipment_type, model_name, + manufacturer, installation_date, serial_number, specifications, + status, notes, workplace_id, map_x_percent, map_y_percent, + map_width_percent, map_height_percent + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + const values = [ + equipmentData.equipment_code, + equipmentData.equipment_name, + equipmentData.equipment_type || null, + equipmentData.model_name || null, + equipmentData.manufacturer || null, + equipmentData.installation_date || null, + equipmentData.serial_number || null, + equipmentData.specifications || null, + equipmentData.status || 'active', + equipmentData.notes || null, + equipmentData.workplace_id || null, + equipmentData.map_x_percent || null, + equipmentData.map_y_percent || null, + equipmentData.map_width_percent || null, + equipmentData.map_height_percent || null + ]; + + const [result] = await db.query(query, values); + callback(null, { + equipment_id: result.insertId, + ...equipmentData + }); + } catch (error) { + callback(error); + } + }, + + // READ ALL - 모든 설비 조회 (필터링 옵션 포함) + getAll: async (filters, callback) => { + try { + const db = await getDb(); + let query = ` + SELECT + e.*, + w.workplace_name, + wc.category_name + FROM equipments e + LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id + LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id + WHERE 1=1 + `; + + const values = []; + + // 필터링: 작업장 ID + if (filters.workplace_id) { + query += ' AND e.workplace_id = ?'; + values.push(filters.workplace_id); + } + + // 필터링: 설비 유형 + if (filters.equipment_type) { + query += ' AND e.equipment_type = ?'; + values.push(filters.equipment_type); + } + + // 필터링: 상태 + if (filters.status) { + query += ' AND e.status = ?'; + values.push(filters.status); + } + + // 필터링: 검색어 (설비명, 설비코드) + if (filters.search) { + query += ' AND (e.equipment_name LIKE ? OR e.equipment_code LIKE ?)'; + const searchTerm = `%${filters.search}%`; + values.push(searchTerm, searchTerm); + } + + query += ' ORDER BY e.equipment_code ASC'; + + const [rows] = await db.query(query, values); + callback(null, rows); + } catch (error) { + callback(error); + } + }, + + // READ ONE - 특정 설비 조회 + getById: async (equipmentId, callback) => { + try { + const db = await getDb(); + const query = ` + SELECT + e.*, + w.workplace_name, + wc.category_name + FROM equipments e + LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id + LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id + WHERE e.equipment_id = ? + `; + + const [rows] = await db.query(query, [equipmentId]); + callback(null, rows[0]); + } catch (error) { + callback(error); + } + }, + + // READ BY WORKPLACE - 특정 작업장의 설비 조회 + getByWorkplace: async (workplaceId, callback) => { + try { + const db = await getDb(); + const query = ` + SELECT e.* + FROM equipments e + WHERE e.workplace_id = ? + ORDER BY e.equipment_code ASC + `; + + const [rows] = await db.query(query, [workplaceId]); + callback(null, rows); + } catch (error) { + callback(error); + } + }, + + // READ ACTIVE - 활성 설비만 조회 + getActive: async (callback) => { + try { + const db = await getDb(); + const query = ` + SELECT + e.*, + w.workplace_name, + wc.category_name + FROM equipments e + LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id + LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id + WHERE e.status = 'active' + ORDER BY e.equipment_code ASC + `; + + const [rows] = await db.query(query); + callback(null, rows); + } catch (error) { + callback(error); + } + }, + + // UPDATE - 설비 수정 + update: async (equipmentId, equipmentData, callback) => { + try { + const db = await getDb(); + const query = ` + UPDATE equipments SET + equipment_code = ?, + equipment_name = ?, + equipment_type = ?, + model_name = ?, + manufacturer = ?, + installation_date = ?, + serial_number = ?, + specifications = ?, + status = ?, + notes = ?, + workplace_id = ?, + map_x_percent = ?, + map_y_percent = ?, + map_width_percent = ?, + map_height_percent = ?, + updated_at = NOW() + WHERE equipment_id = ? + `; + + const values = [ + equipmentData.equipment_code, + equipmentData.equipment_name, + equipmentData.equipment_type || null, + equipmentData.model_name || null, + equipmentData.manufacturer || null, + equipmentData.installation_date || null, + equipmentData.serial_number || null, + equipmentData.specifications || null, + equipmentData.status || 'active', + equipmentData.notes || null, + equipmentData.workplace_id || null, + equipmentData.map_x_percent || null, + equipmentData.map_y_percent || null, + equipmentData.map_width_percent || null, + equipmentData.map_height_percent || null, + equipmentId + ]; + + const [result] = await db.query(query, values); + if (result.affectedRows === 0) { + return callback(new Error('Equipment not found')); + } + callback(null, { equipment_id: equipmentId, ...equipmentData }); + } catch (error) { + callback(error); + } + }, + + // UPDATE MAP POSITION - 지도상 위치 업데이트 + updateMapPosition: async (equipmentId, positionData, callback) => { + try { + const db = await getDb(); + const query = ` + UPDATE equipments SET + map_x_percent = ?, + map_y_percent = ?, + map_width_percent = ?, + map_height_percent = ?, + updated_at = NOW() + WHERE equipment_id = ? + `; + + const values = [ + positionData.map_x_percent, + positionData.map_y_percent, + positionData.map_width_percent, + positionData.map_height_percent, + equipmentId + ]; + + const [result] = await db.query(query, values); + if (result.affectedRows === 0) { + return callback(new Error('Equipment not found')); + } + callback(null, { equipment_id: equipmentId, ...positionData }); + } catch (error) { + callback(error); + } + }, + + // DELETE - 설비 삭제 + delete: async (equipmentId, callback) => { + try { + const db = await getDb(); + const query = 'DELETE FROM equipments WHERE equipment_id = ?'; + + const [result] = await db.query(query, [equipmentId]); + if (result.affectedRows === 0) { + return callback(new Error('Equipment not found')); + } + callback(null, { equipment_id: equipmentId }); + } catch (error) { + callback(error); + } + }, + + // CHECK DUPLICATE CODE - 설비 코드 중복 확인 + checkDuplicateCode: async (equipmentCode, excludeEquipmentId, callback) => { + try { + const db = await getDb(); + let query = 'SELECT equipment_id FROM equipments WHERE equipment_code = ?'; + const values = [equipmentCode]; + + if (excludeEquipmentId) { + query += ' AND equipment_id != ?'; + values.push(excludeEquipmentId); + } + + const [rows] = await db.query(query, values); + callback(null, rows.length > 0); + } catch (error) { + callback(error); + } + }, + + // GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회 + getEquipmentTypes: async (callback) => { + try { + const db = await getDb(); + const query = ` + SELECT DISTINCT equipment_type + FROM equipments + WHERE equipment_type IS NOT NULL + ORDER BY equipment_type ASC + `; + + const [rows] = await db.query(query); + callback(null, rows.map(row => row.equipment_type)); + } catch (error) { + callback(error); + } + } +}; + +module.exports = EquipmentModel; diff --git a/api.hyungi.net/routes/equipmentRoutes.js b/api.hyungi.net/routes/equipmentRoutes.js new file mode 100644 index 0000000..50e862a --- /dev/null +++ b/api.hyungi.net/routes/equipmentRoutes.js @@ -0,0 +1,36 @@ +// routes/equipmentRoutes.js +const express = require('express'); +const router = express.Router(); +const equipmentController = require('../controllers/equipmentController'); + +// ==================== 설비 관리 ==================== + +// CREATE 설비 +router.post('/', equipmentController.createEquipment); + +// READ ALL 설비 (쿼리 파라미터로 필터링 가능) +// ?workplace_id=1&equipment_type=CNC&status=active&search=설비명 +router.get('/', equipmentController.getAllEquipments); + +// READ ACTIVE 설비 +router.get('/active/list', equipmentController.getActiveEquipments); + +// READ 설비 유형 목록 +router.get('/types', equipmentController.getEquipmentTypes); + +// READ 작업장별 설비 +router.get('/workplace/:workplaceId', equipmentController.getEquipmentsByWorkplace); + +// READ ONE 설비 +router.get('/:id', equipmentController.getEquipmentById); + +// UPDATE 설비 +router.put('/:id', equipmentController.updateEquipment); + +// UPDATE 설비 지도 위치 +router.patch('/:id/map-position', equipmentController.updateMapPosition); + +// DELETE 설비 +router.delete('/:id', equipmentController.deleteEquipment); + +module.exports = router; diff --git a/api.hyungi.net/routes/workplaceRoutes.js b/api.hyungi.net/routes/workplaceRoutes.js index 57cd1e9..7c1f42b 100644 --- a/api.hyungi.net/routes/workplaceRoutes.js +++ b/api.hyungi.net/routes/workplaceRoutes.js @@ -1,8 +1,36 @@ // routes/workplaceRoutes.js const express = require('express'); const router = express.Router(); +const multer = require('multer'); +const path = require('path'); const workplaceController = require('../controllers/workplaceController'); +// Multer 설정 - 작업장 레이아웃 이미지 업로드 +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, '../uploads')); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, 'workplace-layout-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +const upload = multer({ + storage, + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|gif/; + const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); + const mimetype = allowedTypes.test(file.mimetype); + if (mimetype && extname) { + return cb(null, true); + } else { + cb(new Error('이미지 파일만 업로드 가능합니다 (jpeg, jpg, png, gif)')); + } + }, + limits: { fileSize: 5 * 1024 * 1024 } // 5MB 제한 +}); + // ==================== 카테고리(공장) 관리 ==================== // CREATE 카테고리 @@ -43,4 +71,27 @@ router.put('/:id', workplaceController.updateWorkplace); // DELETE 작업장 router.delete('/:id', workplaceController.deleteWorkplace); +// ==================== 작업장 지도 영역 관리 ==================== + +// 카테고리 레이아웃 이미지 업로드 +router.post('/categories/:id/layout-image', upload.single('image'), workplaceController.uploadCategoryLayoutImage); + +// 작업장 레이아웃 이미지 업로드 +router.post('/:id/layout-image', upload.single('image'), workplaceController.uploadWorkplaceLayoutImage); + +// CREATE 지도 영역 +router.post('/map-regions', workplaceController.createMapRegion); + +// READ 카테고리별 지도 영역 +router.get('/categories/:categoryId/map-regions', workplaceController.getMapRegionsByCategory); + +// READ 작업장별 지도 영역 +router.get('/map-regions/workplace/:workplaceId', workplaceController.getMapRegionByWorkplace); + +// UPDATE 지도 영역 +router.put('/map-regions/:id', workplaceController.updateMapRegion); + +// DELETE 지도 영역 +router.delete('/map-regions/:id', workplaceController.deleteMapRegion); + module.exports = router; diff --git a/web-ui/js/equipment-management.js b/web-ui/js/equipment-management.js new file mode 100644 index 0000000..644b5ef --- /dev/null +++ b/web-ui/js/equipment-management.js @@ -0,0 +1,344 @@ +// equipment-management.js +// 설비 관리 페이지 JavaScript + +let equipments = []; +let workplaces = []; +let equipmentTypes = []; +let currentEquipment = null; + +// 페이지 로드 시 초기화 +document.addEventListener('DOMContentLoaded', async () => { + await loadInitialData(); +}); + +// 초기 데이터 로드 +async function loadInitialData() { + try { + await Promise.all([ + loadEquipments(), + loadWorkplaces(), + loadEquipmentTypes() + ]); + } catch (error) { + console.error('초기 데이터 로드 실패:', error); + alert('데이터를 불러오는데 실패했습니다.'); + } +} + +// 설비 목록 로드 +async function loadEquipments() { + try { + const response = await axios.get('/api/equipments'); + if (response.data.success) { + equipments = response.data.data; + renderEquipmentList(); + } + } catch (error) { + console.error('설비 목록 로드 실패:', error); + throw error; + } +} + +// 작업장 목록 로드 +async function loadWorkplaces() { + try { + const response = await axios.get('/api/workplaces'); + if (response.data.success) { + workplaces = response.data.data; + populateWorkplaceFilters(); + } + } catch (error) { + console.error('작업장 목록 로드 실패:', error); + throw error; + } +} + +// 설비 유형 목록 로드 +async function loadEquipmentTypes() { + try { + const response = await axios.get('/api/equipments/types'); + if (response.data.success) { + equipmentTypes = response.data.data; + populateTypeFilter(); + } + } catch (error) { + console.error('설비 유형 로드 실패:', error); + // 실패해도 계속 진행 (유형이 없을 수 있음) + } +} + +// 작업장 필터 채우기 +function populateWorkplaceFilters() { + const filterWorkplace = document.getElementById('filterWorkplace'); + const modalWorkplace = document.getElementById('workplaceId'); + + const workplaceOptions = workplaces.map(w => + `` + ).join(''); + + filterWorkplace.innerHTML = '' + workplaceOptions; + modalWorkplace.innerHTML = '' + workplaceOptions; +} + +// 설비 유형 필터 채우기 +function populateTypeFilter() { + const filterType = document.getElementById('filterType'); + const typeOptions = equipmentTypes.map(type => + `` + ).join(''); + filterType.innerHTML = '' + typeOptions; +} + +// 설비 목록 렌더링 +function renderEquipmentList() { + const container = document.getElementById('equipmentList'); + + if (equipments.length === 0) { + container.innerHTML = ` +
등록된 설비가 없습니다.
+ +| 설비 코드 | +설비명 | +유형 | +작업장 | +제조사 | +모델명 | +상태 | +관리 | +
|---|---|---|---|---|---|---|---|
| ${equipment.equipment_code} | +${equipment.equipment_name} | +${equipment.equipment_type || '-'} | +${equipment.workplace_name || '-'} | +${equipment.manufacturer || '-'} | +${equipment.model_name || '-'} | ++ + ${getStatusText(equipment.status)} + + | ++ + | +
+ 클릭하여 작업장 영역을 수정하려면 "지도 설정" 버튼을 누르세요 +
++ 이 공장의 레이아웃 이미지가 아직 등록되지 않았습니다 +
++ "지도 설정" 버튼을 눌러 레이아웃 이미지를 업로드하고 작업장 위치를 지정하세요 +
+${workplace.description}
` : ''} + + + +