feat: tkuser 통합 관리 서비스 + 전체 시스템 SSO 쿠키 인증 통합
- tkuser 서비스 신규 추가 (API + Web) - 사용자/권한/프로젝트/부서/작업자/작업장/설비/작업/휴가 통합 관리 - 작업장 탭: 공장→작업장 드릴다운 네비게이션 + 구역지도 클릭 연동 - 작업 탭: 공정(work_types)→작업(tasks) 계층 관리 - 휴가 탭: 유형 관리 + 연차 배정(근로기준법 자동계산) - 전 시스템 SSO 쿠키 인증으로 통합 (.technicalkorea.net 공유) - System 2: 작업 이슈 리포트 기능 강화 - System 3: tkuser API 연동, 페이지 권한 체계 적용 - docker-compose에 tkuser-api, tkuser-web 서비스 추가 - ARCHITECTURE.md, DEPLOYMENT.md 문서 작성 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
66
user-management/api/controllers/departmentController.js
Normal file
66
user-management/api/controllers/departmentController.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Department Controller
|
||||
*
|
||||
* 부서 CRUD
|
||||
*/
|
||||
|
||||
const departmentModel = require('../models/departmentModel');
|
||||
|
||||
async function getAll(req, res, next) {
|
||||
try {
|
||||
const departments = await departmentModel.getAll();
|
||||
res.json({ success: true, data: departments });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getById(req, res, next) {
|
||||
try {
|
||||
const dept = await departmentModel.getById(parseInt(req.params.id));
|
||||
if (!dept) {
|
||||
return res.status(404).json({ success: false, error: '부서를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: dept });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(req, res, next) {
|
||||
try {
|
||||
const { department_name } = req.body;
|
||||
if (!department_name) {
|
||||
return res.status(400).json({ success: false, error: '부서명은 필수입니다' });
|
||||
}
|
||||
const dept = await departmentModel.create(req.body);
|
||||
res.status(201).json({ success: true, data: dept });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function update(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const dept = await departmentModel.update(id, req.body);
|
||||
if (!dept) {
|
||||
return res.status(404).json({ success: false, error: '부서를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: dept });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await departmentModel.deactivate(id);
|
||||
res.json({ success: true, message: '부서가 비활성화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getAll, getById, create, update, remove };
|
||||
141
user-management/api/controllers/equipmentController.js
Normal file
141
user-management/api/controllers/equipmentController.js
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Equipment Controller
|
||||
*
|
||||
* 설비 CRUD + 지도위치 + 사진
|
||||
*/
|
||||
|
||||
const equipmentModel = require('../models/equipmentModel');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// ==================== 기본 CRUD ====================
|
||||
|
||||
async function getAll(req, res, next) {
|
||||
try {
|
||||
const filters = {};
|
||||
if (req.query.workplace_id) filters.workplace_id = parseInt(req.query.workplace_id);
|
||||
if (req.query.equipment_type) filters.equipment_type = req.query.equipment_type;
|
||||
if (req.query.status) filters.status = req.query.status;
|
||||
if (req.query.search) filters.search = req.query.search;
|
||||
const equipments = await equipmentModel.getAll(filters);
|
||||
res.json({ success: true, data: equipments });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getById(req, res, next) {
|
||||
try {
|
||||
const eq = await equipmentModel.getById(parseInt(req.params.id));
|
||||
if (!eq) return res.status(404).json({ success: false, error: '설비를 찾을 수 없습니다' });
|
||||
res.json({ success: true, data: eq });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getByWorkplace(req, res, next) {
|
||||
try {
|
||||
const equipments = await equipmentModel.getByWorkplace(parseInt(req.params.workplaceId));
|
||||
res.json({ success: true, data: equipments });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function create(req, res, next) {
|
||||
try {
|
||||
const { equipment_code, equipment_name } = req.body;
|
||||
if (!equipment_code || !equipment_name) return res.status(400).json({ success: false, error: '관리번호와 설비명은 필수입니다' });
|
||||
const dup = await equipmentModel.checkDuplicateCode(equipment_code);
|
||||
if (dup) return res.status(409).json({ success: false, error: '이미 존재하는 관리번호입니다' });
|
||||
const eq = await equipmentModel.create(req.body);
|
||||
res.status(201).json({ success: true, data: eq });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function update(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
if (req.body.equipment_code) {
|
||||
const dup = await equipmentModel.checkDuplicateCode(req.body.equipment_code, id);
|
||||
if (dup) return res.status(409).json({ success: false, error: '이미 존재하는 관리번호입니다' });
|
||||
}
|
||||
const eq = await equipmentModel.update(id, req.body);
|
||||
if (!eq) return res.status(404).json({ success: false, error: '설비를 찾을 수 없습니다' });
|
||||
res.json({ success: true, data: eq });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function remove(req, res, next) {
|
||||
try {
|
||||
await equipmentModel.remove(parseInt(req.params.id));
|
||||
res.json({ success: true, message: '설비가 삭제되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getTypes(req, res, next) {
|
||||
try {
|
||||
const types = await equipmentModel.getEquipmentTypes();
|
||||
res.json({ success: true, data: types });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getNextCode(req, res, next) {
|
||||
try {
|
||||
const code = await equipmentModel.getNextCode(req.query.prefix || 'TKP');
|
||||
res.json({ success: true, data: code });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
// ==================== 지도 위치 ====================
|
||||
|
||||
async function updateMapPosition(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(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
|
||||
};
|
||||
if (req.body.workplace_id !== undefined) positionData.workplace_id = req.body.workplace_id;
|
||||
const eq = await equipmentModel.updateMapPosition(id, positionData);
|
||||
res.json({ success: true, data: eq });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
// ==================== 사진 ====================
|
||||
|
||||
async function addPhoto(req, res, next) {
|
||||
try {
|
||||
const equipmentId = parseInt(req.params.id);
|
||||
if (!req.file) return res.status(400).json({ success: false, error: '사진 파일이 필요합니다' });
|
||||
const photoData = {
|
||||
photo_path: `/uploads/${req.file.filename}`,
|
||||
description: req.body.description || null,
|
||||
display_order: parseInt(req.body.display_order) || 0,
|
||||
uploaded_by: req.user?.user_id || null
|
||||
};
|
||||
const result = await equipmentModel.addPhoto(equipmentId, photoData);
|
||||
res.status(201).json({ success: true, data: result });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getPhotos(req, res, next) {
|
||||
try {
|
||||
const results = await equipmentModel.getPhotos(parseInt(req.params.id));
|
||||
res.json({ success: true, data: results });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function deletePhoto(req, res, next) {
|
||||
try {
|
||||
const result = await equipmentModel.deletePhoto(parseInt(req.params.photoId));
|
||||
if (result.photo_path) {
|
||||
const filePath = path.join(__dirname, '..', result.photo_path);
|
||||
fs.unlink(filePath, () => {});
|
||||
}
|
||||
res.json({ success: true, message: '사진이 삭제되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAll, getById, getByWorkplace, create, update, remove, getTypes, getNextCode,
|
||||
updateMapPosition,
|
||||
addPhoto, getPhotos, deletePhoto
|
||||
};
|
||||
159
user-management/api/controllers/permissionController.js
Normal file
159
user-management/api/controllers/permissionController.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Permission Controller
|
||||
*
|
||||
* 페이지 권한 관리 (system3 page_permissions.py 포팅)
|
||||
*/
|
||||
|
||||
const permissionModel = require('../models/permissionModel');
|
||||
const userModel = require('../models/userModel');
|
||||
|
||||
/**
|
||||
* GET /api/users/:id/page-permissions - 사용자 권한 조회
|
||||
*/
|
||||
async function getUserPermissions(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
const requesterId = req.user.user_id || req.user.id;
|
||||
|
||||
// 관리자이거나 본인만 조회 가능
|
||||
if (req.user.role !== 'admin' && requesterId !== userId) {
|
||||
return res.status(403).json({ success: false, error: '권한이 없습니다' });
|
||||
}
|
||||
|
||||
const user = await userModel.findById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
const permissions = await permissionModel.getUserPermissions(userId);
|
||||
res.json(permissions);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/permissions/grant - 단건 권한 부여
|
||||
*/
|
||||
async function grantPermission(req, res, next) {
|
||||
try {
|
||||
const { user_id, page_name, can_access, notes } = req.body;
|
||||
const grantedById = req.user.user_id || req.user.id;
|
||||
|
||||
// 대상 사용자 확인
|
||||
const targetUser = await userModel.findById(user_id);
|
||||
if (!targetUser) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
// 유효한 페이지명 확인
|
||||
if (!permissionModel.DEFAULT_PAGES[page_name]) {
|
||||
return res.status(400).json({ success: false, error: '유효하지 않은 페이지명입니다' });
|
||||
}
|
||||
|
||||
const result = await permissionModel.grantPermission({
|
||||
user_id,
|
||||
page_name,
|
||||
can_access,
|
||||
granted_by_id: grantedById,
|
||||
notes
|
||||
});
|
||||
|
||||
res.json({ success: true, message: '권한이 설정되었습니다', data: result });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/permissions/bulk-grant - 일괄 권한 부여
|
||||
*/
|
||||
async function bulkGrant(req, res, next) {
|
||||
try {
|
||||
const { user_id, permissions } = req.body;
|
||||
const grantedById = req.user.user_id || req.user.id;
|
||||
|
||||
// 대상 사용자 확인
|
||||
const targetUser = await userModel.findById(user_id);
|
||||
if (!targetUser) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
const result = await permissionModel.bulkGrant({
|
||||
user_id,
|
||||
permissions,
|
||||
granted_by_id: grantedById
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${result.updated_count}개의 권한이 설정되었습니다`,
|
||||
updated_count: result.updated_count
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/permissions/check/:uid/:page - 접근 권한 확인
|
||||
*/
|
||||
async function checkAccess(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.uid);
|
||||
const pageName = req.params.page;
|
||||
|
||||
// 사용자 확인
|
||||
const user = await userModel.findById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
// admin은 모든 페이지 접근 가능
|
||||
if (user.role === 'admin') {
|
||||
return res.json({ can_access: true, reason: 'admin_role' });
|
||||
}
|
||||
|
||||
const result = await permissionModel.checkAccess(userId, pageName);
|
||||
res.json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/permissions/available-pages - 설정 가능 페이지 목록
|
||||
*/
|
||||
async function getAvailablePages(req, res) {
|
||||
res.json({
|
||||
pages: permissionModel.DEFAULT_PAGES,
|
||||
total_count: Object.keys(permissionModel.DEFAULT_PAGES).length
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/permissions/:id - 권한 삭제
|
||||
*/
|
||||
async function deletePermission(req, res, next) {
|
||||
try {
|
||||
const permissionId = parseInt(req.params.id);
|
||||
const deleted = await permissionModel.deletePermission(permissionId);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ success: false, error: '권한을 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
res.json({ success: true, message: '권한이 삭제되었습니다. 기본값이 적용됩니다.' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUserPermissions,
|
||||
grantPermission,
|
||||
bulkGrant,
|
||||
checkAccess,
|
||||
getAvailablePages,
|
||||
deletePermission
|
||||
};
|
||||
83
user-management/api/controllers/projectController.js
Normal file
83
user-management/api/controllers/projectController.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Project Controller
|
||||
*
|
||||
* 프로젝트 CRUD
|
||||
*/
|
||||
|
||||
const projectModel = require('../models/projectModel');
|
||||
|
||||
async function getAll(req, res, next) {
|
||||
try {
|
||||
const projects = await projectModel.getAll();
|
||||
res.json({ success: true, data: projects });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getActive(req, res, next) {
|
||||
try {
|
||||
const projects = await projectModel.getActive();
|
||||
res.json({ success: true, data: projects });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getById(req, res, next) {
|
||||
try {
|
||||
const project = await projectModel.getById(parseInt(req.params.id));
|
||||
if (!project) {
|
||||
return res.status(404).json({ success: false, error: '프로젝트를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: project });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(req, res, next) {
|
||||
try {
|
||||
const { job_no, project_name } = req.body;
|
||||
|
||||
if (!job_no || !project_name) {
|
||||
return res.status(400).json({ success: false, error: 'Job No와 프로젝트명은 필수입니다' });
|
||||
}
|
||||
|
||||
const project = await projectModel.create(req.body);
|
||||
res.status(201).json({ success: true, data: project });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(409).json({ success: false, error: '이미 존재하는 Job No입니다' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function update(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const project = await projectModel.update(id, req.body);
|
||||
if (!project) {
|
||||
return res.status(404).json({ success: false, error: '프로젝트를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: project });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
return res.status(409).json({ success: false, error: '이미 존재하는 Job No입니다' });
|
||||
}
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await projectModel.deactivate(id);
|
||||
res.json({ success: true, message: '프로젝트가 비활성화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getAll, getActive, getById, create, update, remove };
|
||||
94
user-management/api/controllers/taskController.js
Normal file
94
user-management/api/controllers/taskController.js
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Task Controller
|
||||
*
|
||||
* 공정(work_types) + 작업(tasks) CRUD
|
||||
*/
|
||||
|
||||
const taskModel = require('../models/taskModel');
|
||||
|
||||
/* ===== Work Types ===== */
|
||||
|
||||
async function getWorkTypes(req, res, next) {
|
||||
try {
|
||||
const data = await taskModel.getWorkTypes();
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function createWorkType(req, res, next) {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
if (!name) return res.status(400).json({ success: false, error: '공정명은 필수입니다' });
|
||||
const data = await taskModel.createWorkType(req.body);
|
||||
res.status(201).json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function updateWorkType(req, res, next) {
|
||||
try {
|
||||
const data = await taskModel.updateWorkType(parseInt(req.params.id), req.body);
|
||||
if (!data) return res.status(404).json({ success: false, error: '공정을 찾을 수 없습니다' });
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function deleteWorkType(req, res, next) {
|
||||
try {
|
||||
await taskModel.deleteWorkType(parseInt(req.params.id));
|
||||
res.json({ success: true, message: '공정이 삭제되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
/* ===== Tasks ===== */
|
||||
|
||||
async function getTasks(req, res, next) {
|
||||
try {
|
||||
const workTypeId = req.query.work_type_id ? parseInt(req.query.work_type_id) : null;
|
||||
const data = await taskModel.getTasks(workTypeId);
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getActiveTasks(req, res, next) {
|
||||
try {
|
||||
const data = await taskModel.getActiveTasks();
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getTaskById(req, res, next) {
|
||||
try {
|
||||
const data = await taskModel.getTaskById(parseInt(req.params.id));
|
||||
if (!data) return res.status(404).json({ success: false, error: '작업을 찾을 수 없습니다' });
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function createTask(req, res, next) {
|
||||
try {
|
||||
const { task_name } = req.body;
|
||||
if (!task_name) return res.status(400).json({ success: false, error: '작업명은 필수입니다' });
|
||||
const data = await taskModel.createTask(req.body);
|
||||
res.status(201).json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function updateTask(req, res, next) {
|
||||
try {
|
||||
const data = await taskModel.updateTask(parseInt(req.params.id), req.body);
|
||||
if (!data) return res.status(404).json({ success: false, error: '작업을 찾을 수 없습니다' });
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function deleteTask(req, res, next) {
|
||||
try {
|
||||
await taskModel.deleteTask(parseInt(req.params.id));
|
||||
res.json({ success: true, message: '작업이 삭제되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWorkTypes, createWorkType, updateWorkType, deleteWorkType,
|
||||
getTasks, getActiveTasks, getTaskById, createTask, updateTask, deleteTask
|
||||
};
|
||||
146
user-management/api/controllers/userController.js
Normal file
146
user-management/api/controllers/userController.js
Normal file
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* User Controller
|
||||
*
|
||||
* 사용자 CRUD + 비밀번호 관리
|
||||
*/
|
||||
|
||||
const userModel = require('../models/userModel');
|
||||
|
||||
/**
|
||||
* GET /api/users - 전체 사용자 목록
|
||||
*/
|
||||
async function getUsers(req, res, next) {
|
||||
try {
|
||||
const users = await userModel.findAll();
|
||||
res.json({ success: true, data: users });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/users - 사용자 생성
|
||||
*/
|
||||
async function createUser(req, res, next) {
|
||||
try {
|
||||
const { username, password, name, full_name, department, role } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ success: false, error: '사용자명과 비밀번호는 필수입니다' });
|
||||
}
|
||||
|
||||
const existing = await userModel.findByUsername(username);
|
||||
if (existing) {
|
||||
return res.status(409).json({ success: false, error: '이미 존재하는 사용자명입니다' });
|
||||
}
|
||||
|
||||
const user = await userModel.create({
|
||||
username,
|
||||
password,
|
||||
name: name || full_name,
|
||||
department,
|
||||
role
|
||||
});
|
||||
res.status(201).json({ success: true, data: user });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/users/:id - 사용자 수정
|
||||
*/
|
||||
async function updateUser(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
const data = { ...req.body };
|
||||
|
||||
// full_name → name 매핑
|
||||
if (data.full_name !== undefined && data.name === undefined) {
|
||||
data.name = data.full_name;
|
||||
delete data.full_name;
|
||||
}
|
||||
|
||||
const user = await userModel.update(userId, data);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: user });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/users/:id - 사용자 비활성화
|
||||
*/
|
||||
async function deleteUser(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
await userModel.deleteUser(userId);
|
||||
res.json({ success: true, message: '사용자가 비활성화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/users/:id/reset-password - 비밀번호 초기화 (admin)
|
||||
*/
|
||||
async function resetPassword(req, res, next) {
|
||||
try {
|
||||
const userId = parseInt(req.params.id);
|
||||
const { new_password } = req.body;
|
||||
const password = new_password || '000000';
|
||||
|
||||
const user = await userModel.update(userId, { password });
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, message: '비밀번호가 초기화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/users/change-password - 본인 비밀번호 변경
|
||||
*/
|
||||
async function changePassword(req, res, next) {
|
||||
try {
|
||||
const { current_password, new_password } = req.body;
|
||||
const userId = req.user.user_id || req.user.id;
|
||||
|
||||
if (!current_password || !new_password) {
|
||||
return res.status(400).json({ success: false, error: '현재 비밀번호와 새 비밀번호를 입력하세요' });
|
||||
}
|
||||
|
||||
if (new_password.length < 6) {
|
||||
return res.status(400).json({ success: false, error: '새 비밀번호는 최소 6자 이상이어야 합니다' });
|
||||
}
|
||||
|
||||
const user = await userModel.findById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ success: false, error: '사용자를 찾을 수 없습니다' });
|
||||
}
|
||||
|
||||
const valid = await userModel.verifyPassword(current_password, user.password_hash);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ success: false, error: '현재 비밀번호가 올바르지 않습니다' });
|
||||
}
|
||||
|
||||
await userModel.update(userId, { password: new_password });
|
||||
res.json({ success: true, message: '비밀번호가 변경되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getUsers,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
resetPassword,
|
||||
changePassword
|
||||
};
|
||||
126
user-management/api/controllers/vacationController.js
Normal file
126
user-management/api/controllers/vacationController.js
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* Vacation Controller
|
||||
*
|
||||
* 휴가 유형 + 연차 배정 관리
|
||||
*/
|
||||
|
||||
const vacationModel = require('../models/vacationModel');
|
||||
|
||||
/* ===== Vacation Types ===== */
|
||||
|
||||
async function getVacationTypes(req, res, next) {
|
||||
try {
|
||||
const all = req.query.all === 'true';
|
||||
const data = all ? await vacationModel.getAllVacationTypes() : await vacationModel.getVacationTypes();
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function createVacationType(req, res, next) {
|
||||
try {
|
||||
const { type_code, type_name } = req.body;
|
||||
if (!type_code || !type_name) return res.status(400).json({ success: false, error: '유형 코드와 이름은 필수입니다' });
|
||||
const data = await vacationModel.createVacationType(req.body);
|
||||
res.status(201).json({ success: true, data });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ success: false, error: '이미 존재하는 유형 코드입니다' });
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateVacationType(req, res, next) {
|
||||
try {
|
||||
const data = await vacationModel.updateVacationType(parseInt(req.params.id), req.body);
|
||||
if (!data) return res.status(404).json({ success: false, error: '휴가 유형을 찾을 수 없습니다' });
|
||||
res.json({ success: true, data });
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') return res.status(409).json({ success: false, error: '이미 존재하는 유형 코드입니다' });
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteVacationType(req, res, next) {
|
||||
try {
|
||||
await vacationModel.deleteVacationType(parseInt(req.params.id));
|
||||
res.json({ success: true, message: '휴가 유형이 비활성화되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function updatePriorities(req, res, next) {
|
||||
try {
|
||||
const { items } = req.body;
|
||||
if (!items || !Array.isArray(items)) return res.status(400).json({ success: false, error: 'items 배열이 필요합니다' });
|
||||
await vacationModel.updatePriorities(items);
|
||||
res.json({ success: true, message: '우선순위가 업데이트되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
/* ===== Vacation Balances ===== */
|
||||
|
||||
async function getBalancesByYear(req, res, next) {
|
||||
try {
|
||||
const year = parseInt(req.params.year);
|
||||
const data = await vacationModel.getBalancesByYear(year);
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function getBalancesByWorkerYear(req, res, next) {
|
||||
try {
|
||||
const workerId = parseInt(req.params.workerId);
|
||||
const year = parseInt(req.params.year);
|
||||
const data = await vacationModel.getBalancesByWorkerYear(workerId, year);
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function createBalance(req, res, next) {
|
||||
try {
|
||||
const { worker_id, vacation_type_id, year } = req.body;
|
||||
if (!worker_id || !vacation_type_id || !year) {
|
||||
return res.status(400).json({ success: false, error: '작업자, 휴가유형, 연도는 필수입니다' });
|
||||
}
|
||||
const data = await vacationModel.createBalance({ ...req.body, created_by: req.user.user_id });
|
||||
res.status(201).json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function updateBalance(req, res, next) {
|
||||
try {
|
||||
const data = await vacationModel.updateBalance(parseInt(req.params.id), req.body);
|
||||
if (!data) return res.status(404).json({ success: false, error: '배정 정보를 찾을 수 없습니다' });
|
||||
res.json({ success: true, data });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function deleteBalance(req, res, next) {
|
||||
try {
|
||||
await vacationModel.deleteBalance(parseInt(req.params.id));
|
||||
res.json({ success: true, message: '삭제되었습니다' });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function bulkUpsertBalances(req, res, next) {
|
||||
try {
|
||||
const { balances } = req.body;
|
||||
if (!balances || !Array.isArray(balances)) return res.status(400).json({ success: false, error: 'balances 배열이 필요합니다' });
|
||||
const items = balances.map(b => ({ ...b, created_by: req.user.user_id }));
|
||||
const count = await vacationModel.bulkUpsertBalances(items);
|
||||
res.json({ success: true, data: { count }, message: `${count}건 처리되었습니다` });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
async function autoCalculate(req, res, next) {
|
||||
try {
|
||||
const { year } = req.body;
|
||||
if (!year) return res.status(400).json({ success: false, error: '연도는 필수입니다' });
|
||||
const result = await vacationModel.autoCalculateForAllWorkers(year, req.user.user_id);
|
||||
res.json({ success: true, data: result, message: `${result.count}명 자동 배정 완료` });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getVacationTypes, createVacationType, updateVacationType, deleteVacationType, updatePriorities,
|
||||
getBalancesByYear, getBalancesByWorkerYear, createBalance, updateBalance, deleteBalance,
|
||||
bulkUpsertBalances, autoCalculate
|
||||
};
|
||||
66
user-management/api/controllers/workerController.js
Normal file
66
user-management/api/controllers/workerController.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Worker Controller
|
||||
*
|
||||
* 작업자 CRUD
|
||||
*/
|
||||
|
||||
const workerModel = require('../models/workerModel');
|
||||
|
||||
async function getAll(req, res, next) {
|
||||
try {
|
||||
const workers = await workerModel.getAll();
|
||||
res.json({ success: true, data: workers });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getById(req, res, next) {
|
||||
try {
|
||||
const worker = await workerModel.getById(parseInt(req.params.id));
|
||||
if (!worker) {
|
||||
return res.status(404).json({ success: false, error: '작업자를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: worker });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(req, res, next) {
|
||||
try {
|
||||
const { worker_name } = req.body;
|
||||
if (!worker_name) {
|
||||
return res.status(400).json({ success: false, error: '작업자 이름은 필수입니다' });
|
||||
}
|
||||
const worker = await workerModel.create(req.body);
|
||||
res.status(201).json({ success: true, data: worker });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function update(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const worker = await workerModel.update(id, req.body);
|
||||
if (!worker) {
|
||||
return res.status(404).json({ success: false, error: '작업자를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: worker });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await workerModel.deactivate(id);
|
||||
res.json({ success: true, message: '작업자가 비활성화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getAll, getById, create, update, remove };
|
||||
155
user-management/api/controllers/workplaceController.js
Normal file
155
user-management/api/controllers/workplaceController.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Workplace Controller
|
||||
*
|
||||
* 작업장 CRUD + 카테고리 조회
|
||||
*/
|
||||
|
||||
const workplaceModel = require('../models/workplaceModel');
|
||||
|
||||
async function getAll(req, res, next) {
|
||||
try {
|
||||
const workplaces = await workplaceModel.getAll();
|
||||
res.json({ success: true, data: workplaces });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getById(req, res, next) {
|
||||
try {
|
||||
const wp = await workplaceModel.getById(parseInt(req.params.id));
|
||||
if (!wp) {
|
||||
return res.status(404).json({ success: false, error: '작업장을 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: wp });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCategories(req, res, next) {
|
||||
try {
|
||||
const categories = await workplaceModel.getCategories();
|
||||
res.json({ success: true, data: categories });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function create(req, res, next) {
|
||||
try {
|
||||
const { workplace_name } = req.body;
|
||||
if (!workplace_name) {
|
||||
return res.status(400).json({ success: false, error: '작업장명은 필수입니다' });
|
||||
}
|
||||
const wp = await workplaceModel.create(req.body);
|
||||
res.status(201).json({ success: true, data: wp });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function update(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const wp = await workplaceModel.update(id, req.body);
|
||||
if (!wp) {
|
||||
return res.status(404).json({ success: false, error: '작업장을 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: wp });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(req, res, next) {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await workplaceModel.deactivate(id);
|
||||
res.json({ success: true, message: '작업장이 비활성화되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 구역지도 ====================
|
||||
|
||||
async function uploadCategoryLayoutImage(req, res, next) {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ success: false, error: '이미지 파일이 필요합니다' });
|
||||
}
|
||||
const id = parseInt(req.params.id);
|
||||
const imagePath = `/uploads/${req.file.filename}`;
|
||||
const category = await workplaceModel.updateCategoryLayoutImage(id, imagePath);
|
||||
if (!category) {
|
||||
return res.status(404).json({ success: false, error: '카테고리를 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: { image_path: imagePath, category } });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function createMapRegion(req, res, next) {
|
||||
try {
|
||||
const { workplace_id, category_id } = req.body;
|
||||
if (!workplace_id || !category_id) {
|
||||
return res.status(400).json({ success: false, error: 'workplace_id와 category_id는 필수입니다' });
|
||||
}
|
||||
const region = await workplaceModel.createMapRegion(req.body);
|
||||
res.status(201).json({ success: true, data: region });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMapRegionsByCategory(req, res, next) {
|
||||
try {
|
||||
const categoryId = parseInt(req.params.categoryId);
|
||||
const regions = await workplaceModel.getMapRegionsByCategory(categoryId);
|
||||
res.json({ success: true, data: regions });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateMapRegion(req, res, next) {
|
||||
try {
|
||||
const regionId = parseInt(req.params.id);
|
||||
const region = await workplaceModel.updateMapRegion(regionId, req.body);
|
||||
if (!region) {
|
||||
return res.status(404).json({ success: false, error: '영역을 찾을 수 없습니다' });
|
||||
}
|
||||
res.json({ success: true, data: region });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMapRegion(req, res, next) {
|
||||
try {
|
||||
const regionId = parseInt(req.params.id);
|
||||
await workplaceModel.deleteMapRegion(regionId);
|
||||
res.json({ success: true, message: '영역이 삭제되었습니다' });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadWorkplaceLayoutImage(req, res, next) {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ success: false, error: '이미지 파일이 필요합니다' });
|
||||
const id = parseInt(req.params.id);
|
||||
const imagePath = `/uploads/${req.file.filename}`;
|
||||
const wp = await workplaceModel.updateWorkplaceLayoutImage(id, imagePath);
|
||||
if (!wp) return res.status(404).json({ success: false, error: '작업장을 찾을 수 없습니다' });
|
||||
res.json({ success: true, data: { image_path: imagePath, workplace: wp } });
|
||||
} catch (err) { next(err); }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAll, getById, getCategories, create, update, remove,
|
||||
uploadCategoryLayoutImage, uploadWorkplaceLayoutImage,
|
||||
createMapRegion, getMapRegionsByCategory, updateMapRegion, deleteMapRegion
|
||||
};
|
||||
Reference in New Issue
Block a user