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:
18
user-management/api/Dockerfile
Normal file
18
user-management/api/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN chown -R node:node /usr/src/app
|
||||
USER node
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1); })"
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
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
|
||||
};
|
||||
65
user-management/api/index.js
Normal file
65
user-management/api/index.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* tkuser-api - 사용자 관리 서비스
|
||||
*
|
||||
* 사용자 CRUD + 페이지 권한 관리 통합 API
|
||||
* MariaDB (sso_users + user_page_permissions) 직접 연결
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
const userRoutes = require('./routes/userRoutes');
|
||||
const permissionRoutes = require('./routes/permissionRoutes');
|
||||
const projectRoutes = require('./routes/projectRoutes');
|
||||
const workerRoutes = require('./routes/workerRoutes');
|
||||
const departmentRoutes = require('./routes/departmentRoutes');
|
||||
const workplaceRoutes = require('./routes/workplaceRoutes');
|
||||
const equipmentRoutes = require('./routes/equipmentRoutes');
|
||||
const taskRoutes = require('./routes/taskRoutes');
|
||||
const vacationRoutes = require('./routes/vacationRoutes');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true
|
||||
}));
|
||||
app.use(express.json());
|
||||
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||
|
||||
// Health check
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', service: 'tkuser-api', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/users', userRoutes);
|
||||
app.use('/api/permissions', permissionRoutes);
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/workers', workerRoutes);
|
||||
app.use('/api/departments', departmentRoutes);
|
||||
app.use('/api/workplaces', workplaceRoutes);
|
||||
app.use('/api/equipments', equipmentRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/vacations', vacationRoutes);
|
||||
|
||||
// 404
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ success: false, error: 'Not Found' });
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('tkuser-api Error:', err.message);
|
||||
res.status(err.status || 500).json({
|
||||
success: false,
|
||||
error: err.message || 'Internal Server Error'
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`tkuser-api running on port ${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
61
user-management/api/middleware/auth.js
Normal file
61
user-management/api/middleware/auth.js
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 인증 미들웨어
|
||||
* JWT 검증 + admin 체크
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const JWT_SECRET = process.env.SSO_JWT_SECRET;
|
||||
|
||||
/**
|
||||
* Bearer 토큰 또는 쿠키에서 토큰 추출
|
||||
*/
|
||||
function extractToken(req) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
return authHeader.split(' ')[1];
|
||||
}
|
||||
if (req.cookies && req.cookies.sso_token) {
|
||||
return req.cookies.sso_token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증 필수 미들웨어
|
||||
*/
|
||||
function requireAuth(req, res, next) {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리자 권한 미들웨어
|
||||
*/
|
||||
function requireAdmin(req, res, next) {
|
||||
const token = extractToken(req);
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, error: '인증이 필요합니다' });
|
||||
}
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
if (!['admin', 'system'].includes(decoded.role)) {
|
||||
return res.status(403).json({ success: false, error: '관리자 권한이 필요합니다' });
|
||||
}
|
||||
req.user = decoded;
|
||||
next();
|
||||
} catch {
|
||||
return res.status(401).json({ success: false, error: '유효하지 않은 토큰입니다' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { extractToken, requireAuth, requireAdmin };
|
||||
35
user-management/api/middleware/upload.js
Normal file
35
user-management/api/middleware/upload.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 파일 업로드 미들웨어 (multer)
|
||||
*/
|
||||
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, path.join(__dirname, '..', 'uploads'));
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const uniqueName = `workplace-layout-${Date.now()}-${crypto.randomInt(100000000, 999999999)}${ext}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
const fileFilter = (req, file, cb) => {
|
||||
const allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (allowed.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('허용되지 않는 파일 형식입니다. (JPEG, PNG, GIF, WebP만 가능)'), false);
|
||||
}
|
||||
};
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: { fileSize: 5 * 1024 * 1024 }
|
||||
});
|
||||
|
||||
module.exports = upload;
|
||||
71
user-management/api/models/departmentModel.js
Normal file
71
user-management/api/models/departmentModel.js
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Department Model
|
||||
*
|
||||
* departments 테이블 CRUD (MariaDB)
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
async function getAll() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT d.*, p.department_name AS parent_name
|
||||
FROM departments d
|
||||
LEFT JOIN departments p ON d.parent_id = p.department_id
|
||||
ORDER BY d.display_order ASC, d.department_id ASC`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT d.*, p.department_name AS parent_name
|
||||
FROM departments d
|
||||
LEFT JOIN departments p ON d.parent_id = p.department_id
|
||||
WHERE d.department_id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function create({ department_name, parent_id, description, display_order }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO departments (department_name, parent_id, description, display_order)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[department_name, parent_id || null, description || null, display_order || 0]
|
||||
);
|
||||
return getById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.department_name !== undefined) { fields.push('department_name = ?'); values.push(data.department_name); }
|
||||
if (data.parent_id !== undefined) { fields.push('parent_id = ?'); values.push(data.parent_id || null); }
|
||||
if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description || null); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); }
|
||||
if (data.display_order !== undefined) { fields.push('display_order = ?'); values.push(data.display_order); }
|
||||
|
||||
if (fields.length === 0) return getById(id);
|
||||
|
||||
values.push(id);
|
||||
await db.query(
|
||||
`UPDATE departments SET ${fields.join(', ')} WHERE department_id = ?`,
|
||||
values
|
||||
);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
async function deactivate(id) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE departments SET is_active = FALSE WHERE department_id = ?',
|
||||
[id]
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { getAll, getById, create, update, deactivate };
|
||||
192
user-management/api/models/equipmentModel.js
Normal file
192
user-management/api/models/equipmentModel.js
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* Equipment Model
|
||||
*
|
||||
* equipments + equipment_photos CRUD (MariaDB)
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
// ==================== 기본 CRUD ====================
|
||||
|
||||
async function getAll(filters = {}) {
|
||||
const db = getPool();
|
||||
let sql = `SELECT e.*, w.workplace_name, c.category_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
LEFT JOIN workplace_categories c ON w.category_id = c.category_id`;
|
||||
const conditions = [];
|
||||
const values = [];
|
||||
|
||||
if (filters.workplace_id) { conditions.push('e.workplace_id = ?'); values.push(filters.workplace_id); }
|
||||
if (filters.equipment_type) { conditions.push('e.equipment_type = ?'); values.push(filters.equipment_type); }
|
||||
if (filters.status) { conditions.push('e.status = ?'); values.push(filters.status); }
|
||||
if (filters.search) {
|
||||
conditions.push('(e.equipment_name LIKE ? OR e.equipment_code LIKE ?)');
|
||||
const term = `%${filters.search}%`;
|
||||
values.push(term, term);
|
||||
}
|
||||
|
||||
if (conditions.length) sql += ' WHERE ' + conditions.join(' AND ');
|
||||
sql += ' ORDER BY e.equipment_code ASC';
|
||||
|
||||
const [rows] = await db.query(sql, values);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT e.*, w.workplace_name, c.category_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
LEFT JOIN workplace_categories c ON w.category_id = c.category_id
|
||||
WHERE e.equipment_id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function getByWorkplace(workplaceId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT e.*, w.workplace_name
|
||||
FROM equipments e
|
||||
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
|
||||
WHERE e.workplace_id = ?
|
||||
ORDER BY e.equipment_code ASC`,
|
||||
[workplaceId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function create(data) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO equipments (equipment_code, equipment_name, equipment_type, model_name, manufacturer, supplier, purchase_price, installation_date, serial_number, specifications, status, notes, workplace_id, map_x_percent, map_y_percent, map_width_percent, map_height_percent)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
data.equipment_code, data.equipment_name,
|
||||
data.equipment_type || null, data.model_name || null,
|
||||
data.manufacturer || null, data.supplier || null,
|
||||
data.purchase_price || null, data.installation_date || null,
|
||||
data.serial_number || null, data.specifications || null,
|
||||
data.status || 'active', data.notes || null,
|
||||
data.workplace_id || null,
|
||||
data.map_x_percent || null, data.map_y_percent || null,
|
||||
data.map_width_percent || null, data.map_height_percent || null
|
||||
]
|
||||
);
|
||||
return getById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.equipment_code !== undefined) { fields.push('equipment_code = ?'); values.push(data.equipment_code); }
|
||||
if (data.equipment_name !== undefined) { fields.push('equipment_name = ?'); values.push(data.equipment_name); }
|
||||
if (data.equipment_type !== undefined) { fields.push('equipment_type = ?'); values.push(data.equipment_type || null); }
|
||||
if (data.model_name !== undefined) { fields.push('model_name = ?'); values.push(data.model_name || null); }
|
||||
if (data.manufacturer !== undefined) { fields.push('manufacturer = ?'); values.push(data.manufacturer || null); }
|
||||
if (data.supplier !== undefined) { fields.push('supplier = ?'); values.push(data.supplier || null); }
|
||||
if (data.purchase_price !== undefined) { fields.push('purchase_price = ?'); values.push(data.purchase_price || null); }
|
||||
if (data.installation_date !== undefined) { fields.push('installation_date = ?'); values.push(data.installation_date || null); }
|
||||
if (data.serial_number !== undefined) { fields.push('serial_number = ?'); values.push(data.serial_number || null); }
|
||||
if (data.specifications !== undefined) { fields.push('specifications = ?'); values.push(data.specifications || null); }
|
||||
if (data.status !== undefined) { fields.push('status = ?'); values.push(data.status); }
|
||||
if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); }
|
||||
if (data.workplace_id !== undefined) { fields.push('workplace_id = ?'); values.push(data.workplace_id || null); }
|
||||
if (data.map_x_percent !== undefined) { fields.push('map_x_percent = ?'); values.push(data.map_x_percent); }
|
||||
if (data.map_y_percent !== undefined) { fields.push('map_y_percent = ?'); values.push(data.map_y_percent); }
|
||||
if (data.map_width_percent !== undefined) { fields.push('map_width_percent = ?'); values.push(data.map_width_percent); }
|
||||
if (data.map_height_percent !== undefined) { fields.push('map_height_percent = ?'); values.push(data.map_height_percent); }
|
||||
|
||||
if (fields.length === 0) return getById(id);
|
||||
|
||||
values.push(id);
|
||||
await db.query(`UPDATE equipments SET ${fields.join(', ')} WHERE equipment_id = ?`, values);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
async function remove(id) {
|
||||
const db = getPool();
|
||||
await db.query('DELETE FROM equipments WHERE equipment_id = ?', [id]);
|
||||
}
|
||||
|
||||
async function getEquipmentTypes() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT DISTINCT equipment_type FROM equipments WHERE equipment_type IS NOT NULL AND equipment_type != "" ORDER BY equipment_type ASC'
|
||||
);
|
||||
return rows.map(r => r.equipment_type);
|
||||
}
|
||||
|
||||
async function getNextCode(prefix = 'TKP') {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT equipment_code FROM equipments WHERE equipment_code LIKE ? ORDER BY equipment_code DESC LIMIT 1',
|
||||
[`${prefix}-%`]
|
||||
);
|
||||
if (!rows.length) return `${prefix}-001`;
|
||||
const lastNum = parseInt(rows[0].equipment_code.replace(`${prefix}-`, ''), 10) || 0;
|
||||
return `${prefix}-${String(lastNum + 1).padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
async function checkDuplicateCode(code, excludeId) {
|
||||
const db = getPool();
|
||||
let sql = 'SELECT equipment_id FROM equipments WHERE equipment_code = ?';
|
||||
const values = [code];
|
||||
if (excludeId) { sql += ' AND equipment_id != ?'; values.push(excludeId); }
|
||||
const [rows] = await db.query(sql, values);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
// ==================== 지도 위치 ====================
|
||||
|
||||
async function updateMapPosition(id, positionData) {
|
||||
const db = getPool();
|
||||
const fields = ['map_x_percent = ?', 'map_y_percent = ?', 'map_width_percent = ?', 'map_height_percent = ?'];
|
||||
const values = [positionData.map_x_percent, positionData.map_y_percent, positionData.map_width_percent, positionData.map_height_percent];
|
||||
if (positionData.workplace_id !== undefined) {
|
||||
fields.push('workplace_id = ?');
|
||||
values.push(positionData.workplace_id);
|
||||
}
|
||||
values.push(id);
|
||||
await db.query(`UPDATE equipments SET ${fields.join(', ')} WHERE equipment_id = ?`, values);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
// ==================== 사진 ====================
|
||||
|
||||
async function addPhoto(equipmentId, photoData) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO equipment_photos (equipment_id, photo_path, description, display_order, uploaded_by) VALUES (?, ?, ?, ?, ?)`,
|
||||
[equipmentId, photoData.photo_path, photoData.description || null, photoData.display_order || 0, photoData.uploaded_by || null]
|
||||
);
|
||||
return { photo_id: result.insertId, equipment_id: equipmentId, ...photoData };
|
||||
}
|
||||
|
||||
async function getPhotos(equipmentId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM equipment_photos WHERE equipment_id = ? ORDER BY display_order ASC, created_at ASC',
|
||||
[equipmentId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function deletePhoto(photoId) {
|
||||
const db = getPool();
|
||||
const [photo] = await db.query('SELECT photo_path FROM equipment_photos WHERE photo_id = ?', [photoId]);
|
||||
await db.query('DELETE FROM equipment_photos WHERE photo_id = ?', [photoId]);
|
||||
return { photo_id: photoId, photo_path: photo[0]?.photo_path };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAll, getById, getByWorkplace, create, update, remove,
|
||||
getEquipmentTypes, getNextCode, checkDuplicateCode,
|
||||
updateMapPosition,
|
||||
addPhoto, getPhotos, deletePhoto
|
||||
};
|
||||
150
user-management/api/models/permissionModel.js
Normal file
150
user-management/api/models/permissionModel.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Permission Model
|
||||
*
|
||||
* MariaDB user_page_permissions 테이블 CRUD
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
// 기본 페이지 목록 (시스템별 구분)
|
||||
const DEFAULT_PAGES = {
|
||||
// ===== System 1 - 공장관리 =====
|
||||
// 작업 관리
|
||||
's1.dashboard': { title: '대시보드', system: 'system1', group: '작업 관리', default_access: true },
|
||||
's1.work.tbm': { title: 'TBM 관리', system: 'system1', group: '작업 관리', default_access: true },
|
||||
's1.work.report_create': { title: '작업보고서 작성', system: 'system1', group: '작업 관리', default_access: true },
|
||||
's1.work.analysis': { title: '작업 분석', system: 'system1', group: '작업 관리', default_access: false },
|
||||
's1.work.nonconformity': { title: '부적합 현황', system: 'system1', group: '작업 관리', default_access: true },
|
||||
// 공장 관리
|
||||
's1.factory.repair_management':{ title: '시설설비 관리', system: 'system1', group: '공장 관리', default_access: false },
|
||||
's1.inspection.daily_patrol': { title: '일일순회점검', system: 'system1', group: '공장 관리', default_access: false },
|
||||
's1.inspection.checkin': { title: '출근 체크', system: 'system1', group: '공장 관리', default_access: true },
|
||||
's1.inspection.work_status': { title: '근무 현황', system: 'system1', group: '공장 관리', default_access: false },
|
||||
// 안전 관리
|
||||
's1.safety.visit_request': { title: '출입 신청', system: 'system1', group: '안전 관리', default_access: true },
|
||||
's1.safety.management': { title: '안전 관리', system: 'system1', group: '안전 관리', default_access: false },
|
||||
's1.safety.checklist_manage': { title: '체크리스트 관리', system: 'system1', group: '안전 관리', default_access: false },
|
||||
// 근태 관리
|
||||
's1.attendance.my_vacation_info': { title: '내 연차 정보', system: 'system1', group: '근태 관리', default_access: true },
|
||||
's1.attendance.monthly': { title: '월간 근태', system: 'system1', group: '근태 관리', default_access: true },
|
||||
's1.attendance.vacation_request': { title: '휴가 신청', system: 'system1', group: '근태 관리', default_access: true },
|
||||
's1.attendance.vacation_management': { title: '휴가 관리', system: 'system1', group: '근태 관리', default_access: false },
|
||||
's1.attendance.vacation_allocation': { title: '휴가 발생 입력', system: 'system1', group: '근태 관리', default_access: false },
|
||||
's1.attendance.annual_overview': { title: '연간 휴가 현황', system: 'system1', group: '근태 관리', default_access: false },
|
||||
// 시스템 관리
|
||||
's1.admin.workers': { title: '작업자 관리', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
's1.admin.projects': { title: '프로젝트 관리', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
's1.admin.tasks': { title: '작업 관리', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
's1.admin.workplaces': { title: '작업장 관리', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
's1.admin.equipments': { title: '설비 관리', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
's1.admin.issue_categories': { title: '신고 카테고리 관리', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
's1.admin.attendance_report': { title: '출퇴근-보고서 대조', system: 'system1', group: '시스템 관리', default_access: false },
|
||||
|
||||
// ===== System 3 - 부적합관리 =====
|
||||
// 메인
|
||||
'issues_dashboard': { title: '현황판', system: 'system3', group: '메인', default_access: true },
|
||||
'issues_inbox': { title: '수신함', system: 'system3', group: '메인', default_access: true },
|
||||
'issues_management': { title: '관리함', system: 'system3', group: '메인', default_access: false },
|
||||
'issues_archive': { title: '폐기함', system: 'system3', group: '메인', default_access: false },
|
||||
// 업무
|
||||
'daily_work': { title: '일일 공수', system: 'system3', group: '업무', default_access: false },
|
||||
'projects_manage': { title: '프로젝트 관리', system: 'system3', group: '업무', default_access: false },
|
||||
// 보고서
|
||||
'reports': { title: '보고서', system: 'system3', group: '보고서', default_access: false },
|
||||
'reports_daily': { title: '일일보고서', system: 'system3', group: '보고서', default_access: false },
|
||||
'reports_weekly': { title: '주간보고서', system: 'system3', group: '보고서', default_access: false },
|
||||
'reports_monthly': { title: '월간보고서', system: 'system3', group: '보고서', default_access: false }
|
||||
};
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 권한 목록 조회
|
||||
*/
|
||||
async function getUserPermissions(userId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM user_page_permissions WHERE user_id = ? ORDER BY page_name',
|
||||
[userId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* 단건 권한 부여/업데이트 (UPSERT)
|
||||
*/
|
||||
async function grantPermission({ user_id, page_name, can_access, granted_by_id, notes }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO user_page_permissions (user_id, page_name, can_access, granted_by_id, notes)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE can_access = VALUES(can_access), granted_by_id = VALUES(granted_by_id), notes = VALUES(notes), granted_at = CURRENT_TIMESTAMP`,
|
||||
[user_id, page_name, can_access, granted_by_id, notes || null]
|
||||
);
|
||||
return { id: result.insertId, user_id, page_name, can_access };
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 권한 부여
|
||||
*/
|
||||
async function bulkGrant({ user_id, permissions, granted_by_id }) {
|
||||
const db = getPool();
|
||||
let count = 0;
|
||||
|
||||
for (const perm of permissions) {
|
||||
if (!DEFAULT_PAGES[perm.page_name]) continue;
|
||||
await db.query(
|
||||
`INSERT INTO user_page_permissions (user_id, page_name, can_access, granted_by_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE can_access = VALUES(can_access), granted_by_id = VALUES(granted_by_id), granted_at = CURRENT_TIMESTAMP`,
|
||||
[user_id, perm.page_name, perm.can_access, granted_by_id]
|
||||
);
|
||||
count++;
|
||||
}
|
||||
|
||||
return { updated_count: count };
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 권한 확인
|
||||
*/
|
||||
async function checkAccess(userId, pageName) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT can_access FROM user_page_permissions WHERE user_id = ? AND page_name = ?',
|
||||
[userId, pageName]
|
||||
);
|
||||
|
||||
if (rows.length > 0) {
|
||||
return { can_access: rows[0].can_access, reason: 'explicit_permission' };
|
||||
}
|
||||
|
||||
// 기본 권한
|
||||
const pageConfig = DEFAULT_PAGES[pageName];
|
||||
if (!pageConfig) {
|
||||
return { can_access: false, reason: 'invalid_page' };
|
||||
}
|
||||
return { can_access: pageConfig.default_access, reason: 'default_permission' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 권한 삭제 (기본값으로 되돌림)
|
||||
*/
|
||||
async function deletePermission(permissionId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM user_page_permissions WHERE id = ?',
|
||||
[permissionId]
|
||||
);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
await db.query('DELETE FROM user_page_permissions WHERE id = ?', [permissionId]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_PAGES,
|
||||
getUserPermissions,
|
||||
grantPermission,
|
||||
bulkGrant,
|
||||
checkAccess,
|
||||
deletePermission
|
||||
};
|
||||
79
user-management/api/models/projectModel.js
Normal file
79
user-management/api/models/projectModel.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Project Model
|
||||
*
|
||||
* projects 테이블 CRUD (MariaDB)
|
||||
* System 1과 같은 DB를 공유
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
async function getAll() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM projects ORDER BY project_id DESC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getActive() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM projects WHERE is_active = TRUE ORDER BY project_name ASC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM projects WHERE project_id = ?',
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function create({ job_no, project_name, contract_date, due_date, delivery_method, site, pm }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO projects (job_no, project_name, contract_date, due_date, delivery_method, site, pm)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[job_no, project_name, contract_date || null, due_date || null, delivery_method || null, site || null, pm || null]
|
||||
);
|
||||
return getById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.job_no !== undefined) { fields.push('job_no = ?'); values.push(data.job_no); }
|
||||
if (data.project_name !== undefined) { fields.push('project_name = ?'); values.push(data.project_name); }
|
||||
if (data.contract_date !== undefined) { fields.push('contract_date = ?'); values.push(data.contract_date || null); }
|
||||
if (data.due_date !== undefined) { fields.push('due_date = ?'); values.push(data.due_date || null); }
|
||||
if (data.delivery_method !== undefined) { fields.push('delivery_method = ?'); values.push(data.delivery_method); }
|
||||
if (data.site !== undefined) { fields.push('site = ?'); values.push(data.site); }
|
||||
if (data.pm !== undefined) { fields.push('pm = ?'); values.push(data.pm); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); }
|
||||
if (data.project_status !== undefined) { fields.push('project_status = ?'); values.push(data.project_status); }
|
||||
if (data.completed_date !== undefined) { fields.push('completed_date = ?'); values.push(data.completed_date || null); }
|
||||
|
||||
if (fields.length === 0) return getById(id);
|
||||
|
||||
values.push(id);
|
||||
await db.query(
|
||||
`UPDATE projects SET ${fields.join(', ')} WHERE project_id = ?`,
|
||||
values
|
||||
);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
async function deactivate(id) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE projects SET is_active = FALSE, project_status = ? WHERE project_id = ?',
|
||||
['completed', id]
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { getAll, getActive, getById, create, update, deactivate };
|
||||
127
user-management/api/models/taskModel.js
Normal file
127
user-management/api/models/taskModel.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Task Model
|
||||
*
|
||||
* work_types + tasks 테이블 CRUD (MariaDB)
|
||||
* System 1과 같은 DB를 공유
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
/* ===== Work Types (공정) ===== */
|
||||
|
||||
async function getWorkTypes() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM work_types ORDER BY category ASC, name ASC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getWorkTypeById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM work_types WHERE id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function createWorkType({ name, category, description }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
'INSERT INTO work_types (name, category, description) VALUES (?, ?, ?)',
|
||||
[name, category || null, description || null]
|
||||
);
|
||||
return getWorkTypeById(result.insertId);
|
||||
}
|
||||
|
||||
async function updateWorkType(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
|
||||
if (data.category !== undefined) { fields.push('category = ?'); values.push(data.category || null); }
|
||||
if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description || null); }
|
||||
if (fields.length === 0) return getWorkTypeById(id);
|
||||
values.push(id);
|
||||
await db.query(`UPDATE work_types SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
return getWorkTypeById(id);
|
||||
}
|
||||
|
||||
async function deleteWorkType(id) {
|
||||
const db = getPool();
|
||||
// 연결된 tasks의 work_type_id를 NULL로 설정 (FK cascade가 처리하지만 명시적으로)
|
||||
await db.query('UPDATE tasks SET work_type_id = NULL WHERE work_type_id = ?', [id]);
|
||||
await db.query('DELETE FROM work_types WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
/* ===== Tasks (작업) ===== */
|
||||
|
||||
async function getTasks(workTypeId) {
|
||||
const db = getPool();
|
||||
let sql = `SELECT t.*, wt.name AS work_type_name, wt.category AS work_type_category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id`;
|
||||
const params = [];
|
||||
if (workTypeId) {
|
||||
sql += ' WHERE t.work_type_id = ?';
|
||||
params.push(workTypeId);
|
||||
}
|
||||
sql += ' ORDER BY wt.category ASC, t.task_id DESC';
|
||||
const [rows] = await db.query(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getActiveTasks() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.*, wt.name AS work_type_name, wt.category AS work_type_category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.is_active = TRUE
|
||||
ORDER BY wt.category ASC, t.task_name ASC`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getTaskById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT t.*, wt.name AS work_type_name, wt.category AS work_type_category
|
||||
FROM tasks t
|
||||
LEFT JOIN work_types wt ON t.work_type_id = wt.id
|
||||
WHERE t.task_id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function createTask({ work_type_id, task_name, description, is_active }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
'INSERT INTO tasks (work_type_id, task_name, description, is_active) VALUES (?, ?, ?, ?)',
|
||||
[work_type_id || null, task_name, description || null, is_active !== undefined ? is_active : true]
|
||||
);
|
||||
return getTaskById(result.insertId);
|
||||
}
|
||||
|
||||
async function updateTask(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
if (data.work_type_id !== undefined) { fields.push('work_type_id = ?'); values.push(data.work_type_id || null); }
|
||||
if (data.task_name !== undefined) { fields.push('task_name = ?'); values.push(data.task_name); }
|
||||
if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description || null); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); }
|
||||
if (fields.length === 0) return getTaskById(id);
|
||||
values.push(id);
|
||||
await db.query(`UPDATE tasks SET ${fields.join(', ')} WHERE task_id = ?`, values);
|
||||
return getTaskById(id);
|
||||
}
|
||||
|
||||
async function deleteTask(id) {
|
||||
const db = getPool();
|
||||
await db.query('DELETE FROM tasks WHERE task_id = ?', [id]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getWorkTypes, getWorkTypeById, createWorkType, updateWorkType, deleteWorkType,
|
||||
getTasks, getActiveTasks, getTaskById, createTask, updateTask, deleteTask
|
||||
};
|
||||
158
user-management/api/models/userModel.js
Normal file
158
user-management/api/models/userModel.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* User Model
|
||||
*
|
||||
* sso_users 테이블 CRUD 및 비밀번호 관리
|
||||
* sso-auth-service/models/userModel.js 기반
|
||||
*/
|
||||
|
||||
const mysql = require('mysql2/promise');
|
||||
const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
|
||||
let pool;
|
||||
|
||||
function getPool() {
|
||||
if (!pool) {
|
||||
pool = mysql.createPool({
|
||||
host: process.env.DB_HOST || 'mariadb',
|
||||
port: parseInt(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER || 'hyungi_user',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'hyungi',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* pbkdf2_sha256 해시 검증 (passlib 호환)
|
||||
*/
|
||||
function verifyPbkdf2(password, storedHash) {
|
||||
try {
|
||||
const parts = storedHash.split('$');
|
||||
if (parts.length < 5) return false;
|
||||
|
||||
const rounds = parseInt(parts[2]);
|
||||
const salt = parts[3].replace(/\./g, '+');
|
||||
const hash = parts[4].replace(/\./g, '+');
|
||||
|
||||
const padded = (s) => s + '='.repeat((4 - s.length % 4) % 4);
|
||||
|
||||
const saltBuffer = Buffer.from(padded(salt), 'base64');
|
||||
const expectedHash = Buffer.from(padded(hash), 'base64');
|
||||
|
||||
const derivedKey = crypto.pbkdf2Sync(password, saltBuffer, rounds, expectedHash.length, 'sha256');
|
||||
return crypto.timingSafeEqual(derivedKey, expectedHash);
|
||||
} catch (err) {
|
||||
console.error('pbkdf2 verify error:', err.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 비밀번호 검증 (bcrypt 또는 pbkdf2_sha256 자동 감지)
|
||||
*/
|
||||
async function verifyPassword(password, storedHash) {
|
||||
if (!password || !storedHash) return false;
|
||||
|
||||
if (storedHash.startsWith('$pbkdf2-sha256$')) {
|
||||
return verifyPbkdf2(password, storedHash);
|
||||
}
|
||||
|
||||
if (storedHash.startsWith('$2b$') || storedHash.startsWith('$2a$')) {
|
||||
return bcrypt.compare(password, storedHash);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* bcrypt로 비밀번호 해시 생성
|
||||
*/
|
||||
async function hashPassword(password) {
|
||||
return bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
async function findByUsername(username) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM sso_users WHERE username = ? AND is_active = TRUE',
|
||||
[username]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function findById(userId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM sso_users WHERE user_id = ?',
|
||||
[userId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function findAll() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT user_id, username, name, department, role, system1_access, system2_access, system3_access, is_active, last_login, created_at FROM sso_users ORDER BY user_id'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function create({ username, password, name, department, role }) {
|
||||
const db = getPool();
|
||||
const password_hash = await hashPassword(password);
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO sso_users (username, password_hash, name, department, role)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[username, password_hash, name || null, department || null, role || 'user']
|
||||
);
|
||||
return findById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(userId, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.name !== undefined) { fields.push('name = ?'); values.push(data.name); }
|
||||
if (data.department !== undefined) { fields.push('department = ?'); values.push(data.department); }
|
||||
if (data.role !== undefined) { fields.push('role = ?'); values.push(data.role); }
|
||||
if (data.system1_access !== undefined) { fields.push('system1_access = ?'); values.push(data.system1_access); }
|
||||
if (data.system2_access !== undefined) { fields.push('system2_access = ?'); values.push(data.system2_access); }
|
||||
if (data.system3_access !== undefined) { fields.push('system3_access = ?'); values.push(data.system3_access); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); }
|
||||
if (data.password) {
|
||||
fields.push('password_hash = ?');
|
||||
values.push(await hashPassword(data.password));
|
||||
}
|
||||
|
||||
if (fields.length === 0) return findById(userId);
|
||||
|
||||
values.push(userId);
|
||||
await db.query(
|
||||
`UPDATE sso_users SET ${fields.join(', ')} WHERE user_id = ?`,
|
||||
values
|
||||
);
|
||||
return findById(userId);
|
||||
}
|
||||
|
||||
async function deleteUser(userId) {
|
||||
const db = getPool();
|
||||
await db.query('UPDATE sso_users SET is_active = FALSE WHERE user_id = ?', [userId]);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
verifyPassword,
|
||||
hashPassword,
|
||||
findByUsername,
|
||||
findById,
|
||||
findAll,
|
||||
create,
|
||||
update,
|
||||
deleteUser,
|
||||
getPool
|
||||
};
|
||||
219
user-management/api/models/vacationModel.js
Normal file
219
user-management/api/models/vacationModel.js
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Vacation Model
|
||||
*
|
||||
* vacation_types + vacation_balance_details CRUD (MariaDB)
|
||||
* System 1과 같은 DB를 공유
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
/* ===== Vacation Types (휴가 유형) ===== */
|
||||
|
||||
async function getVacationTypes() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM vacation_types WHERE is_active = TRUE ORDER BY priority ASC, id ASC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getAllVacationTypes() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM vacation_types ORDER BY priority ASC, id ASC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getVacationTypeById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query('SELECT * FROM vacation_types WHERE id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function createVacationType({ type_code, type_name, deduct_days, is_special, priority, description }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO vacation_types (type_code, type_name, deduct_days, is_special, priority, description, is_system)
|
||||
VALUES (?, ?, ?, ?, ?, ?, FALSE)`,
|
||||
[type_code, type_name, deduct_days ?? 1.0, is_special ? 1 : 0, priority ?? 99, description || null]
|
||||
);
|
||||
return getVacationTypeById(result.insertId);
|
||||
}
|
||||
|
||||
async function updateVacationType(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
if (data.type_code !== undefined) { fields.push('type_code = ?'); values.push(data.type_code); }
|
||||
if (data.type_name !== undefined) { fields.push('type_name = ?'); values.push(data.type_name); }
|
||||
if (data.deduct_days !== undefined) { fields.push('deduct_days = ?'); values.push(data.deduct_days); }
|
||||
if (data.is_special !== undefined) { fields.push('is_special = ?'); values.push(data.is_special ? 1 : 0); }
|
||||
if (data.priority !== undefined) { fields.push('priority = ?'); values.push(data.priority); }
|
||||
if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description || null); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active ? 1 : 0); }
|
||||
if (fields.length === 0) return getVacationTypeById(id);
|
||||
values.push(id);
|
||||
await db.query(`UPDATE vacation_types SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
return getVacationTypeById(id);
|
||||
}
|
||||
|
||||
async function deleteVacationType(id) {
|
||||
const db = getPool();
|
||||
// 시스템 유형은 비활성화만
|
||||
const type = await getVacationTypeById(id);
|
||||
if (type && type.is_system) {
|
||||
await db.query('UPDATE vacation_types SET is_active = FALSE WHERE id = ?', [id]);
|
||||
} else {
|
||||
await db.query('UPDATE vacation_types SET is_active = FALSE WHERE id = ?', [id]);
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePriorities(items) {
|
||||
const db = getPool();
|
||||
for (const { id, priority } of items) {
|
||||
await db.query('UPDATE vacation_types SET priority = ? WHERE id = ?', [priority, id]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Vacation Balances (연차 배정) ===== */
|
||||
|
||||
async function getBalancesByYear(year) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT vbd.*, vt.type_code, vt.type_name, vt.deduct_days, vt.priority,
|
||||
w.worker_name, w.hire_date
|
||||
FROM vacation_balance_details vbd
|
||||
JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
JOIN workers w ON vbd.worker_id = w.worker_id
|
||||
WHERE vbd.year = ?
|
||||
ORDER BY w.worker_name ASC, vt.priority ASC`,
|
||||
[year]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getBalancesByWorkerYear(workerId, year) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT vbd.*, vt.type_code, vt.type_name, vt.deduct_days, vt.priority
|
||||
FROM vacation_balance_details vbd
|
||||
JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ? AND vbd.year = ?
|
||||
ORDER BY vt.priority ASC`,
|
||||
[workerId, year]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getBalanceById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT vbd.*, vt.type_code, vt.type_name
|
||||
FROM vacation_balance_details vbd
|
||||
JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function createBalance({ worker_id, vacation_type_id, year, total_days, used_days, notes, created_by }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO vacation_balance_details (worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), used_days = VALUES(used_days), notes = VALUES(notes)`,
|
||||
[worker_id, vacation_type_id, year, total_days ?? 0, used_days ?? 0, notes || null, created_by]
|
||||
);
|
||||
return result.insertId ? getBalanceById(result.insertId) : null;
|
||||
}
|
||||
|
||||
async function updateBalance(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
if (data.total_days !== undefined) { fields.push('total_days = ?'); values.push(data.total_days); }
|
||||
if (data.used_days !== undefined) { fields.push('used_days = ?'); values.push(data.used_days); }
|
||||
if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); }
|
||||
if (fields.length === 0) return getBalanceById(id);
|
||||
values.push(id);
|
||||
await db.query(`UPDATE vacation_balance_details SET ${fields.join(', ')} WHERE id = ?`, values);
|
||||
return getBalanceById(id);
|
||||
}
|
||||
|
||||
async function deleteBalance(id) {
|
||||
const db = getPool();
|
||||
await db.query('DELETE FROM vacation_balance_details WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
async function bulkUpsertBalances(balances) {
|
||||
const db = getPool();
|
||||
let count = 0;
|
||||
for (const b of balances) {
|
||||
await db.query(
|
||||
`INSERT INTO vacation_balance_details (worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), notes = VALUES(notes)`,
|
||||
[b.worker_id, b.vacation_type_id, b.year, b.total_days ?? 0, b.used_days ?? 0, b.notes || null, b.created_by]
|
||||
);
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* ===== 연차 자동 계산 (근로기준법) ===== */
|
||||
|
||||
function calculateAnnualDays(hireDate, targetYear) {
|
||||
if (!hireDate) return 0;
|
||||
const hire = new Date(hireDate);
|
||||
const yearStart = new Date(targetYear, 0, 1);
|
||||
const monthsDiff = (yearStart.getFullYear() - hire.getFullYear()) * 12 + (yearStart.getMonth() - hire.getMonth());
|
||||
|
||||
if (monthsDiff < 0) return 0;
|
||||
if (monthsDiff < 12) {
|
||||
// 1년 미만: 근무 개월 수
|
||||
return Math.max(0, Math.floor(monthsDiff));
|
||||
}
|
||||
// 1년 이상: 15일 + 2년마다 1일 추가 (최대 25일)
|
||||
const yearsWorked = Math.floor(monthsDiff / 12);
|
||||
const additional = Math.floor((yearsWorked - 1) / 2);
|
||||
return Math.min(15 + additional, 25);
|
||||
}
|
||||
|
||||
async function autoCalculateForAllWorkers(year, createdBy) {
|
||||
const db = getPool();
|
||||
const [workers] = await db.query(
|
||||
'SELECT worker_id, worker_name, hire_date FROM workers WHERE status != ? ORDER BY worker_name',
|
||||
['inactive']
|
||||
);
|
||||
// 연차 유형 (ANNUAL_FULL) 찾기
|
||||
const [types] = await db.query(
|
||||
"SELECT id FROM vacation_types WHERE type_code = 'ANNUAL_FULL' AND is_active = TRUE LIMIT 1"
|
||||
);
|
||||
if (!types.length) return { count: 0, results: [] };
|
||||
const annualTypeId = types[0].id;
|
||||
|
||||
const results = [];
|
||||
for (const w of workers) {
|
||||
const days = calculateAnnualDays(w.hire_date, year);
|
||||
if (days > 0) {
|
||||
await db.query(
|
||||
`INSERT INTO vacation_balance_details (worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES (?, ?, ?, ?, 0, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), notes = VALUES(notes)`,
|
||||
[w.worker_id, annualTypeId, year, days, `자동계산 (입사: ${w.hire_date ? w.hire_date.toISOString().substring(0,10) : ''})`, createdBy]
|
||||
);
|
||||
results.push({ worker_id: w.worker_id, worker_name: w.worker_name, days, hire_date: w.hire_date });
|
||||
}
|
||||
}
|
||||
return { count: results.length, results };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getVacationTypes, getAllVacationTypes, getVacationTypeById,
|
||||
createVacationType, updateVacationType, deleteVacationType, updatePriorities,
|
||||
getBalancesByYear, getBalancesByWorkerYear, getBalanceById,
|
||||
createBalance, updateBalance, deleteBalance, bulkUpsertBalances,
|
||||
calculateAnnualDays, autoCalculateForAllWorkers
|
||||
};
|
||||
74
user-management/api/models/workerModel.js
Normal file
74
user-management/api/models/workerModel.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Worker Model
|
||||
*
|
||||
* workers 테이블 CRUD (MariaDB)
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
async function getAll() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.*, d.department_name
|
||||
FROM workers w
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
ORDER BY w.worker_id DESC`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.*, d.department_name
|
||||
FROM workers w
|
||||
LEFT JOIN departments d ON w.department_id = d.department_id
|
||||
WHERE w.worker_id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function create({ worker_name, job_type, department_id, phone_number, hire_date, notes }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workers (worker_name, job_type, department_id, phone_number, hire_date, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[worker_name, job_type || null, department_id || null, phone_number || null, hire_date || null, notes || null]
|
||||
);
|
||||
return getById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.worker_name !== undefined) { fields.push('worker_name = ?'); values.push(data.worker_name); }
|
||||
if (data.job_type !== undefined) { fields.push('job_type = ?'); values.push(data.job_type); }
|
||||
if (data.status !== undefined) { fields.push('status = ?'); values.push(data.status); }
|
||||
if (data.department_id !== undefined) { fields.push('department_id = ?'); values.push(data.department_id || null); }
|
||||
if (data.employment_status !== undefined) { fields.push('employment_status = ?'); values.push(data.employment_status); }
|
||||
if (data.phone_number !== undefined) { fields.push('phone_number = ?'); values.push(data.phone_number || null); }
|
||||
if (data.hire_date !== undefined) { fields.push('hire_date = ?'); values.push(data.hire_date || null); }
|
||||
if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); }
|
||||
|
||||
if (fields.length === 0) return getById(id);
|
||||
|
||||
values.push(id);
|
||||
await db.query(
|
||||
`UPDATE workers SET ${fields.join(', ')} WHERE worker_id = ?`,
|
||||
values
|
||||
);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
async function deactivate(id) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE workers SET status = ? WHERE worker_id = ?',
|
||||
['inactive', id]
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { getAll, getById, create, update, deactivate };
|
||||
158
user-management/api/models/workplaceModel.js
Normal file
158
user-management/api/models/workplaceModel.js
Normal file
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* Workplace Model
|
||||
*
|
||||
* workplaces + workplace_categories 테이블 CRUD (MariaDB)
|
||||
*/
|
||||
|
||||
const { getPool } = require('./userModel');
|
||||
|
||||
async function getAll() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.*, c.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories c ON w.category_id = c.category_id
|
||||
ORDER BY w.display_priority ASC, w.workplace_id DESC`
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getById(id) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.*, c.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories c ON w.category_id = c.category_id
|
||||
WHERE w.workplace_id = ?`,
|
||||
[id]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function getCategories() {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
'SELECT * FROM workplace_categories WHERE is_active = TRUE ORDER BY display_order ASC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function create({ workplace_name, category_id, workplace_purpose, description, display_priority }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplaces (workplace_name, category_id, workplace_purpose, description, display_priority)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[workplace_name, category_id || null, workplace_purpose || null, description || null, display_priority || 0]
|
||||
);
|
||||
return getById(result.insertId);
|
||||
}
|
||||
|
||||
async function update(id, data) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (data.workplace_name !== undefined) { fields.push('workplace_name = ?'); values.push(data.workplace_name); }
|
||||
if (data.category_id !== undefined) { fields.push('category_id = ?'); values.push(data.category_id || null); }
|
||||
if (data.workplace_purpose !== undefined) { fields.push('workplace_purpose = ?'); values.push(data.workplace_purpose); }
|
||||
if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description || null); }
|
||||
if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active); }
|
||||
if (data.display_priority !== undefined) { fields.push('display_priority = ?'); values.push(data.display_priority); }
|
||||
|
||||
if (fields.length === 0) return getById(id);
|
||||
|
||||
values.push(id);
|
||||
await db.query(
|
||||
`UPDATE workplaces SET ${fields.join(', ')} WHERE workplace_id = ?`,
|
||||
values
|
||||
);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
async function deactivate(id) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE workplaces SET is_active = FALSE WHERE workplace_id = ?',
|
||||
[id]
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== 구역지도 ====================
|
||||
|
||||
async function updateCategoryLayoutImage(id, imagePath) {
|
||||
const db = getPool();
|
||||
await db.query(
|
||||
'UPDATE workplace_categories SET layout_image = ? WHERE category_id = ?',
|
||||
[imagePath, id]
|
||||
);
|
||||
const [rows] = await db.query('SELECT * FROM workplace_categories WHERE category_id = ?', [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function createMapRegion({ workplace_id, category_id, x_start, y_start, x_end, y_end, shape, polygon_points }) {
|
||||
const db = getPool();
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplace_map_regions (workplace_id, category_id, x_start, y_start, x_end, y_end, shape, polygon_points)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[workplace_id, category_id, x_start, y_start, x_end, y_end, shape || 'rect', polygon_points || null]
|
||||
);
|
||||
const [rows] = await db.query('SELECT * FROM workplace_map_regions WHERE region_id = ?', [result.insertId]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async function getMapRegionsByCategory(categoryId) {
|
||||
const db = getPool();
|
||||
const [rows] = await db.query(
|
||||
`SELECT r.*, w.workplace_name
|
||||
FROM workplace_map_regions r
|
||||
LEFT JOIN workplaces w ON r.workplace_id = w.workplace_id
|
||||
WHERE r.category_id = ?
|
||||
ORDER BY r.region_id ASC`,
|
||||
[categoryId]
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function updateMapRegion(regionId, { x_start, y_start, x_end, y_end, workplace_id, shape, polygon_points }) {
|
||||
const db = getPool();
|
||||
const fields = [];
|
||||
const values = [];
|
||||
|
||||
if (x_start !== undefined) { fields.push('x_start = ?'); values.push(x_start); }
|
||||
if (y_start !== undefined) { fields.push('y_start = ?'); values.push(y_start); }
|
||||
if (x_end !== undefined) { fields.push('x_end = ?'); values.push(x_end); }
|
||||
if (y_end !== undefined) { fields.push('y_end = ?'); values.push(y_end); }
|
||||
if (workplace_id !== undefined) { fields.push('workplace_id = ?'); values.push(workplace_id); }
|
||||
if (shape !== undefined) { fields.push('shape = ?'); values.push(shape); }
|
||||
if (polygon_points !== undefined) { fields.push('polygon_points = ?'); values.push(polygon_points); }
|
||||
|
||||
if (fields.length === 0) return null;
|
||||
|
||||
values.push(regionId);
|
||||
await db.query(`UPDATE workplace_map_regions SET ${fields.join(', ')} WHERE region_id = ?`, values);
|
||||
const [rows] = await db.query(
|
||||
`SELECT r.*, w.workplace_name
|
||||
FROM workplace_map_regions r
|
||||
LEFT JOIN workplaces w ON r.workplace_id = w.workplace_id
|
||||
WHERE r.region_id = ?`,
|
||||
[regionId]
|
||||
);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function deleteMapRegion(regionId) {
|
||||
const db = getPool();
|
||||
await db.query('DELETE FROM workplace_map_regions WHERE region_id = ?', [regionId]);
|
||||
}
|
||||
|
||||
async function updateWorkplaceLayoutImage(id, imagePath) {
|
||||
const db = getPool();
|
||||
await db.query('UPDATE workplaces SET layout_image = ? WHERE workplace_id = ?', [imagePath, id]);
|
||||
return getById(id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAll, getById, getCategories, create, update, deactivate,
|
||||
updateCategoryLayoutImage, createMapRegion, getMapRegionsByCategory, updateMapRegion, deleteMapRegion,
|
||||
updateWorkplaceLayoutImage
|
||||
};
|
||||
18
user-management/api/package.json
Normal file
18
user-management/api/package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "tkuser-api",
|
||||
"version": "1.0.0",
|
||||
"description": "TK Factory Services - 사용자 관리 서비스",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node --watch index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"mysql2": "^3.14.1"
|
||||
}
|
||||
}
|
||||
16
user-management/api/routes/departmentRoutes.js
Normal file
16
user-management/api/routes/departmentRoutes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Department Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const departmentController = require('../controllers/departmentController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
router.get('/', requireAuth, departmentController.getAll);
|
||||
router.get('/:id', requireAuth, departmentController.getById);
|
||||
router.post('/', requireAdmin, departmentController.create);
|
||||
router.put('/:id', requireAdmin, departmentController.update);
|
||||
router.delete('/:id', requireAdmin, departmentController.remove);
|
||||
|
||||
module.exports = router;
|
||||
32
user-management/api/routes/equipmentRoutes.js
Normal file
32
user-management/api/routes/equipmentRoutes.js
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Equipment Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const equipmentController = require('../controllers/equipmentController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
const upload = require('../middleware/upload');
|
||||
|
||||
// 고정 경로를 /:id 보다 먼저 등록
|
||||
router.get('/types', requireAuth, equipmentController.getTypes);
|
||||
router.get('/next-code', requireAuth, equipmentController.getNextCode);
|
||||
router.get('/workplace/:workplaceId', requireAuth, equipmentController.getByWorkplace);
|
||||
// 사진 삭제 (photo_id만으로)
|
||||
router.delete('/photos/:photoId', requireAdmin, equipmentController.deletePhoto);
|
||||
|
||||
// 기본 CRUD
|
||||
router.get('/', requireAuth, equipmentController.getAll);
|
||||
router.get('/:id', requireAuth, equipmentController.getById);
|
||||
router.post('/', requireAdmin, equipmentController.create);
|
||||
router.put('/:id', requireAdmin, equipmentController.update);
|
||||
router.delete('/:id', requireAdmin, equipmentController.remove);
|
||||
|
||||
// 지도 위치
|
||||
router.patch('/:id/map-position', requireAdmin, equipmentController.updateMapPosition);
|
||||
|
||||
// 사진
|
||||
router.post('/:id/photos', requireAdmin, upload.single('photo'), equipmentController.addPhoto);
|
||||
router.get('/:id/photos', requireAuth, equipmentController.getPhotos);
|
||||
|
||||
module.exports = router;
|
||||
23
user-management/api/routes/permissionRoutes.js
Normal file
23
user-management/api/routes/permissionRoutes.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Permission Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const permissionController = require('../controllers/permissionController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// 권한 부여 (admin)
|
||||
router.post('/grant', requireAdmin, permissionController.grantPermission);
|
||||
router.post('/bulk-grant', requireAdmin, permissionController.bulkGrant);
|
||||
|
||||
// 접근 권한 확인 (auth)
|
||||
router.get('/check/:uid/:page', requireAuth, permissionController.checkAccess);
|
||||
|
||||
// 설정 가능 페이지 목록 (auth)
|
||||
router.get('/available-pages', requireAuth, permissionController.getAvailablePages);
|
||||
|
||||
// 권한 삭제 (admin)
|
||||
router.delete('/:id', requireAdmin, permissionController.deletePermission);
|
||||
|
||||
module.exports = router;
|
||||
17
user-management/api/routes/projectRoutes.js
Normal file
17
user-management/api/routes/projectRoutes.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Project Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const projectController = require('../controllers/projectController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
router.get('/', requireAuth, projectController.getAll);
|
||||
router.get('/active', requireAuth, projectController.getActive);
|
||||
router.get('/:id', requireAuth, projectController.getById);
|
||||
router.post('/', requireAdmin, projectController.create);
|
||||
router.put('/:id', requireAdmin, projectController.update);
|
||||
router.delete('/:id', requireAdmin, projectController.remove);
|
||||
|
||||
module.exports = router;
|
||||
26
user-management/api/routes/taskRoutes.js
Normal file
26
user-management/api/routes/taskRoutes.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Task Routes
|
||||
*
|
||||
* 공정(work-types) + 작업(tasks) 라우팅
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const taskController = require('../controllers/taskController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// Work Types (공정)
|
||||
router.get('/work-types', requireAuth, taskController.getWorkTypes);
|
||||
router.post('/work-types', requireAdmin, taskController.createWorkType);
|
||||
router.put('/work-types/:id', requireAdmin, taskController.updateWorkType);
|
||||
router.delete('/work-types/:id', requireAdmin, taskController.deleteWorkType);
|
||||
|
||||
// Tasks (작업)
|
||||
router.get('/', requireAuth, taskController.getTasks);
|
||||
router.get('/active', requireAuth, taskController.getActiveTasks);
|
||||
router.get('/:id', requireAuth, taskController.getTaskById);
|
||||
router.post('/', requireAdmin, taskController.createTask);
|
||||
router.put('/:id', requireAdmin, taskController.updateTask);
|
||||
router.delete('/:id', requireAdmin, taskController.deleteTask);
|
||||
|
||||
module.exports = router;
|
||||
24
user-management/api/routes/userRoutes.js
Normal file
24
user-management/api/routes/userRoutes.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* User Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const userController = require('../controllers/userController');
|
||||
const permissionController = require('../controllers/permissionController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// 사용자 CRUD (admin)
|
||||
router.get('/', requireAdmin, userController.getUsers);
|
||||
router.post('/', requireAdmin, userController.createUser);
|
||||
router.put('/:id', requireAdmin, userController.updateUser);
|
||||
router.delete('/:id', requireAdmin, userController.deleteUser);
|
||||
|
||||
// 비밀번호 관리
|
||||
router.post('/:id/reset-password', requireAdmin, userController.resetPassword);
|
||||
router.post('/change-password', requireAuth, userController.changePassword);
|
||||
|
||||
// 사용자별 페이지 권한 조회 (auth - /api/users/:id/page-permissions)
|
||||
router.get('/:id/page-permissions', requireAuth, permissionController.getUserPermissions);
|
||||
|
||||
module.exports = router;
|
||||
28
user-management/api/routes/vacationRoutes.js
Normal file
28
user-management/api/routes/vacationRoutes.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Vacation Routes
|
||||
*
|
||||
* 휴가 유형 + 연차 배정 라우팅
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const vc = require('../controllers/vacationController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
// Vacation Types (휴가 유형)
|
||||
router.get('/types', requireAuth, vc.getVacationTypes);
|
||||
router.post('/types', requireAdmin, vc.createVacationType);
|
||||
router.put('/types/priorities', requireAdmin, vc.updatePriorities);
|
||||
router.put('/types/:id', requireAdmin, vc.updateVacationType);
|
||||
router.delete('/types/:id', requireAdmin, vc.deleteVacationType);
|
||||
|
||||
// Vacation Balances (연차 배정)
|
||||
router.get('/balances/year/:year', requireAdmin, vc.getBalancesByYear);
|
||||
router.get('/balances/worker/:workerId/year/:year', requireAuth, vc.getBalancesByWorkerYear);
|
||||
router.post('/balances', requireAdmin, vc.createBalance);
|
||||
router.post('/balances/bulk-upsert', requireAdmin, vc.bulkUpsertBalances);
|
||||
router.post('/balances/auto-calculate', requireAdmin, vc.autoCalculate);
|
||||
router.put('/balances/:id', requireAdmin, vc.updateBalance);
|
||||
router.delete('/balances/:id', requireAdmin, vc.deleteBalance);
|
||||
|
||||
module.exports = router;
|
||||
16
user-management/api/routes/workerRoutes.js
Normal file
16
user-management/api/routes/workerRoutes.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Worker Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const workerController = require('../controllers/workerController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
|
||||
router.get('/', requireAuth, workerController.getAll);
|
||||
router.get('/:id', requireAuth, workerController.getById);
|
||||
router.post('/', requireAdmin, workerController.create);
|
||||
router.put('/:id', requireAdmin, workerController.update);
|
||||
router.delete('/:id', requireAdmin, workerController.remove);
|
||||
|
||||
module.exports = router;
|
||||
28
user-management/api/routes/workplaceRoutes.js
Normal file
28
user-management/api/routes/workplaceRoutes.js
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Workplace Routes
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const workplaceController = require('../controllers/workplaceController');
|
||||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||||
const upload = require('../middleware/upload');
|
||||
|
||||
router.get('/categories', requireAuth, workplaceController.getCategories);
|
||||
|
||||
// 구역지도 (/:id 보다 먼저 등록)
|
||||
router.post('/categories/:id/layout-image', requireAdmin, upload.single('image'), workplaceController.uploadCategoryLayoutImage);
|
||||
router.get('/categories/:categoryId/map-regions', requireAuth, workplaceController.getMapRegionsByCategory);
|
||||
router.post('/map-regions', requireAdmin, workplaceController.createMapRegion);
|
||||
router.put('/map-regions/:id', requireAdmin, workplaceController.updateMapRegion);
|
||||
router.delete('/map-regions/:id', requireAdmin, workplaceController.deleteMapRegion);
|
||||
|
||||
router.post('/:id/layout-image', requireAdmin, upload.single('image'), workplaceController.uploadWorkplaceLayoutImage);
|
||||
|
||||
router.get('/', requireAuth, workplaceController.getAll);
|
||||
router.get('/:id', requireAuth, workplaceController.getById);
|
||||
router.post('/', requireAdmin, workplaceController.create);
|
||||
router.put('/:id', requireAdmin, workplaceController.update);
|
||||
router.delete('/:id', requireAdmin, workplaceController.remove);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user