feat: 대시보드 작업장 현황 지도 구현
- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,10 @@ function setupRoutes(app) {
|
||||
const equipmentRoutes = require('../routes/equipmentRoutes');
|
||||
const taskRoutes = require('../routes/taskRoutes');
|
||||
const tbmRoutes = require('../routes/tbmRoutes');
|
||||
const vacationRequestRoutes = require('../routes/vacationRequestRoutes');
|
||||
const vacationTypeRoutes = require('../routes/vacationTypeRoutes');
|
||||
const vacationBalanceRoutes = require('../routes/vacationBalanceRoutes');
|
||||
const visitRequestRoutes = require('../routes/visitRequestRoutes');
|
||||
|
||||
// Rate Limiters 설정
|
||||
const rateLimit = require('express-rate-limit');
|
||||
@@ -141,6 +145,10 @@ function setupRoutes(app) {
|
||||
app.use('/api/workplaces', workplaceRoutes);
|
||||
app.use('/api/equipments', equipmentRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/vacation-requests', vacationRequestRoutes); // 휴가 신청 관리
|
||||
app.use('/api/vacation-types', vacationTypeRoutes); // 휴가 유형 관리
|
||||
app.use('/api/vacation-balances', vacationBalanceRoutes); // 휴가 잔액 관리
|
||||
app.use('/api/workplace-visits', visitRequestRoutes); // 출입 신청 및 안전교육 관리
|
||||
app.use('/api', pageAccessRoutes); // 페이지 접근 권한 관리
|
||||
app.use('/api/tbm', tbmRoutes); // TBM 시스템
|
||||
app.use('/api', uploadBgRoutes);
|
||||
|
||||
@@ -86,6 +86,15 @@ const helmetOptions = {
|
||||
*/
|
||||
permittedCrossDomainPolicies: {
|
||||
permittedPolicies: 'none'
|
||||
},
|
||||
|
||||
/**
|
||||
* Cross-Origin-Resource-Policy
|
||||
* 크로스 오리진 리소스 공유 설정
|
||||
* 이미지 등 정적 파일을 다른 포트에서 로드할 수 있도록 허용
|
||||
*/
|
||||
crossOriginResourcePolicy: {
|
||||
policy: 'cross-origin'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -154,6 +154,34 @@ const getMonthlyAttendanceStats = asyncHandler(async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 출근 체크 목록 조회 (아침용, 휴가 정보 포함)
|
||||
*/
|
||||
const getCheckinList = asyncHandler(async (req, res) => {
|
||||
const { date } = req.query;
|
||||
const data = await attendanceService.getCheckinListService(date);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data,
|
||||
message: '출근 체크 목록을 성공적으로 조회했습니다'
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 출근 체크 저장 (일괄 처리)
|
||||
*/
|
||||
const saveCheckins = asyncHandler(async (req, res) => {
|
||||
const { date, checkins } = req.body; // checkins: [{worker_id, is_present}, ...]
|
||||
const result = await attendanceService.saveCheckinsService(date, checkins);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
message: '출근 체크가 성공적으로 저장되었습니다'
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getDailyAttendanceStatus,
|
||||
getDailyAttendanceRecords,
|
||||
@@ -163,5 +191,7 @@ module.exports = {
|
||||
getAttendanceTypes,
|
||||
getVacationTypes,
|
||||
getWorkerVacationBalance,
|
||||
getMonthlyAttendanceStats
|
||||
getMonthlyAttendanceStats,
|
||||
getCheckinList,
|
||||
saveCheckins
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ const TbmController = {
|
||||
createSession: (req, res) => {
|
||||
const sessionData = {
|
||||
session_date: req.body.session_date,
|
||||
leader_id: req.body.leader_id,
|
||||
leader_id: req.body.leader_id || null,
|
||||
project_id: req.body.project_id || null,
|
||||
work_location: req.body.work_location || null,
|
||||
work_description: req.body.work_description || null,
|
||||
@@ -19,11 +19,11 @@ const TbmController = {
|
||||
created_by: req.user.user_id
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!sessionData.session_date || !sessionData.leader_id) {
|
||||
// 필수 필드 검증 (날짜만 필수, leader_id는 관리자의 경우 null 허용)
|
||||
if (!sessionData.session_date) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'TBM 날짜와 팀장 정보는 필수입니다.'
|
||||
message: 'TBM 날짜는 필수입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ const TbmController = {
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
/**
|
||||
* 팀원 추가
|
||||
* 팀원 추가 (작업자별 상세 정보 포함)
|
||||
*/
|
||||
addTeamMember: (req, res) => {
|
||||
const assignmentData = {
|
||||
@@ -188,7 +188,12 @@ const TbmController = {
|
||||
assigned_role: req.body.assigned_role || null,
|
||||
work_detail: req.body.work_detail || null,
|
||||
is_present: req.body.is_present,
|
||||
absence_reason: req.body.absence_reason || null
|
||||
absence_reason: req.body.absence_reason || null,
|
||||
project_id: req.body.project_id || null,
|
||||
work_type_id: req.body.work_type_id || null,
|
||||
task_id: req.body.task_id || null,
|
||||
workplace_category_id: req.body.workplace_category_id || null,
|
||||
workplace_id: req.body.workplace_id || null
|
||||
};
|
||||
|
||||
if (!assignmentData.worker_id) {
|
||||
@@ -300,6 +305,30 @@ const TbmController = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 세션의 모든 팀원 삭제 (수정 시 사용)
|
||||
*/
|
||||
clearAllTeamMembers: (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
|
||||
TbmModel.clearAllTeamMembers(sessionId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('팀원 전체 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '팀원 전체 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '모든 팀원이 삭제되었습니다.',
|
||||
data: { deletedCount: result.affectedRows }
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
/**
|
||||
@@ -564,6 +593,33 @@ const TbmController = {
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업보고서가 작성되지 않은 TBM 팀 배정 조회
|
||||
*/
|
||||
getIncompleteWorkReports: (req, res) => {
|
||||
const userId = req.user.user_id;
|
||||
const accessLevel = req.user.access_level;
|
||||
|
||||
// 관리자는 모든 TBM 조회, 일반 사용자는 본인이 작성한 것만 조회
|
||||
const filterUserId = (accessLevel === 'system' || accessLevel === 'admin') ? null : userId;
|
||||
|
||||
TbmModel.getIncompleteWorkReports(filterUserId, (err, results) => {
|
||||
if (err) {
|
||||
console.error('미완료 작업보고서 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '미완료 작업보고서 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
|
||||
@@ -218,16 +218,16 @@ const updateUser = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
const { username, name, email, phone, role, password } = req.body;
|
||||
const { username, name, email, phone, role, role_id, password } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 수정 요청', { userId: id });
|
||||
logger.info('사용자 수정 요청', { userId: id, body: req.body });
|
||||
|
||||
// 최소 하나의 수정 필드가 필요
|
||||
if (!username && !name && email === undefined && phone === undefined && !role && !password) {
|
||||
if (!username && !name && email === undefined && phone === undefined && !role && !role_id && !password) {
|
||||
throw new ValidationError('수정할 필드가 없습니다');
|
||||
}
|
||||
|
||||
@@ -283,13 +283,35 @@ const updateUser = asyncHandler(async (req, res) => {
|
||||
values.push(phone || null);
|
||||
}
|
||||
|
||||
if (role) {
|
||||
const validRoles = ['admin', 'group_leader', 'worker'];
|
||||
if (!validRoles.includes(role)) {
|
||||
throw new ValidationError('유효하지 않은 권한입니다');
|
||||
// role_id 또는 role 문자열 처리
|
||||
if (role_id) {
|
||||
// role_id가 유효한지 확인
|
||||
const [roleCheck] = await db.execute('SELECT id, name FROM roles WHERE id = ?', [role_id]);
|
||||
if (roleCheck.length === 0) {
|
||||
throw new ValidationError('유효하지 않은 역할 ID입니다');
|
||||
}
|
||||
updates.push('role = ?, access_level = ?');
|
||||
values.push(role, role);
|
||||
updates.push('role_id = ?');
|
||||
values.push(role_id);
|
||||
logger.info('role_id로 역할 변경', { userId: id, role_id, role_name: roleCheck[0].name });
|
||||
} else if (role) {
|
||||
// role 문자열을 role_id로 변환 (하위 호환성)
|
||||
const roleNameMap = {
|
||||
'admin': 'Admin',
|
||||
'system': 'System Admin',
|
||||
'user': 'User',
|
||||
'guest': 'Guest',
|
||||
'group_leader': 'User', // 임시 매핑
|
||||
'worker': 'User' // 임시 매핑
|
||||
};
|
||||
const roleName = roleNameMap[role.toLowerCase()] || role;
|
||||
const [roleCheck] = await db.execute('SELECT id FROM roles WHERE name = ?', [roleName]);
|
||||
|
||||
if (roleCheck.length === 0) {
|
||||
throw new ValidationError(`유효하지 않은 권한입니다: ${role}`);
|
||||
}
|
||||
updates.push('role_id = ?');
|
||||
values.push(roleCheck[0].id);
|
||||
logger.info('role 문자열로 역할 변경', { userId: id, role, role_id: roleCheck[0].id });
|
||||
}
|
||||
|
||||
if (password) {
|
||||
@@ -297,7 +319,7 @@ const updateUser = asyncHandler(async (req, res) => {
|
||||
throw new ValidationError('비밀번호는 최소 6자 이상이어야 합니다');
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
updates.push('password_hash = ?');
|
||||
updates.push('password = ?');
|
||||
values.push(hashedPassword);
|
||||
}
|
||||
|
||||
@@ -306,6 +328,7 @@ const updateUser = asyncHandler(async (req, res) => {
|
||||
|
||||
const updateQuery = `UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`;
|
||||
|
||||
logger.info('실행할 UPDATE 쿼리', { query: updateQuery, values });
|
||||
await db.execute(updateQuery, values);
|
||||
|
||||
logger.info('사용자 수정 성공', {
|
||||
@@ -324,7 +347,7 @@ const updateUser = asyncHandler(async (req, res) => {
|
||||
if (error instanceof NotFoundError || error instanceof ValidationError || error instanceof ConflictError) {
|
||||
throw error;
|
||||
}
|
||||
logger.error('사용자 수정 실패', { userId: id, error: error.message });
|
||||
logger.error('사용자 수정 실패', { userId: id, error: error.message, stack: error.stack });
|
||||
throw new DatabaseError('사용자 정보를 수정하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
@@ -458,11 +481,127 @@ const deleteUser = asyncHandler(async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한 조회
|
||||
*/
|
||||
const getUserPageAccess = asyncHandler(async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 페이지 권한 조회 요청', { userId: id });
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT
|
||||
p.id as page_id,
|
||||
p.page_key,
|
||||
p.page_name,
|
||||
p.page_path,
|
||||
p.category,
|
||||
COALESCE(upa.can_access, 0) as can_access
|
||||
FROM pages p
|
||||
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
|
||||
WHERE p.is_active = 1
|
||||
ORDER BY p.category, p.display_order
|
||||
`;
|
||||
|
||||
const [pageAccess] = await db.execute(query, [id]);
|
||||
|
||||
logger.info('사용자 페이지 권한 조회 성공', { userId: id, pageCount: pageAccess.length });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
pageAccess
|
||||
},
|
||||
message: '페이지 권한 조회 성공'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('사용자 페이지 권한 조회 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('페이지 권한을 조회하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 사용자의 페이지 접근 권한 업데이트
|
||||
*/
|
||||
const updateUserPageAccess = asyncHandler(async (req, res) => {
|
||||
checkAdminPermission(req.user);
|
||||
|
||||
const { id } = req.params;
|
||||
const { pageAccess } = req.body;
|
||||
|
||||
if (!id || isNaN(id)) {
|
||||
throw new ValidationError('유효하지 않은 사용자 ID입니다');
|
||||
}
|
||||
|
||||
if (!Array.isArray(pageAccess)) {
|
||||
throw new ValidationError('pageAccess는 배열이어야 합니다');
|
||||
}
|
||||
|
||||
logger.info('사용자 페이지 권한 업데이트 요청', {
|
||||
userId: id,
|
||||
pageCount: pageAccess.length,
|
||||
updatedBy: req.user.username
|
||||
});
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
// 트랜잭션 시작
|
||||
await db.query('START TRANSACTION');
|
||||
|
||||
// 기존 권한 삭제
|
||||
await db.execute('DELETE FROM user_page_access WHERE user_id = ?', [id]);
|
||||
|
||||
// 새 권한 삽입
|
||||
if (pageAccess.length > 0) {
|
||||
const values = pageAccess.map(p => [id, p.page_id, p.can_access]);
|
||||
const placeholders = values.map(() => '(?, ?, ?)').join(', ');
|
||||
const flatValues = values.flat();
|
||||
|
||||
await db.execute(
|
||||
`INSERT INTO user_page_access (user_id, page_id, can_access) VALUES ${placeholders}`,
|
||||
flatValues
|
||||
);
|
||||
}
|
||||
|
||||
// 커밋
|
||||
await db.query('COMMIT');
|
||||
|
||||
logger.info('사용자 페이지 권한 업데이트 성공', {
|
||||
userId: id,
|
||||
pageCount: pageAccess.length,
|
||||
updatedBy: req.user.username
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { user_id: id },
|
||||
message: '페이지 권한이 성공적으로 업데이트되었습니다'
|
||||
});
|
||||
} catch (error) {
|
||||
// 롤백
|
||||
await db.query('ROLLBACK');
|
||||
logger.error('사용자 페이지 권한 업데이트 실패', { userId: id, error: error.message });
|
||||
throw new DatabaseError('페이지 권한을 업데이트하는데 실패했습니다');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
updateUserStatus,
|
||||
deleteUser
|
||||
deleteUser,
|
||||
getUserPageAccess,
|
||||
updateUserPageAccess
|
||||
};
|
||||
|
||||
357
api.hyungi.net/controllers/vacationBalanceController.js
Normal file
357
api.hyungi.net/controllers/vacationBalanceController.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* vacationBalanceController.js
|
||||
* 휴가 잔액 관련 컨트롤러
|
||||
*/
|
||||
|
||||
const vacationBalanceModel = require('../models/vacationBalanceModel');
|
||||
const vacationTypeModel = require('../models/vacationTypeModel');
|
||||
|
||||
const vacationBalanceController = {
|
||||
/**
|
||||
* 특정 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
* GET /api/vacation-balances/worker/:workerId/year/:year
|
||||
*/
|
||||
async getByWorkerAndYear(req, res) {
|
||||
try {
|
||||
const { workerId, year } = req.params;
|
||||
|
||||
vacationBalanceModel.getByWorkerAndYear(workerId, year, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getByWorkerAndYear 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 모든 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
* GET /api/vacation-balances/year/:year
|
||||
*/
|
||||
async getAllByYear(req, res) {
|
||||
try {
|
||||
const { year } = req.params;
|
||||
|
||||
vacationBalanceModel.getAllByYear(year, (err, results) => {
|
||||
if (err) {
|
||||
console.error('전체 휴가 잔액 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '전체 휴가 잔액을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getAllByYear 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 생성
|
||||
* POST /api/vacation-balances
|
||||
*/
|
||||
async createBalance(req, res) {
|
||||
try {
|
||||
const {
|
||||
worker_id,
|
||||
vacation_type_id,
|
||||
year,
|
||||
total_days,
|
||||
used_days,
|
||||
notes
|
||||
} = req.body;
|
||||
const created_by = req.user.user_id;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !vacation_type_id || !year || total_days === undefined) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다 (worker_id, vacation_type_id, year, total_days)'
|
||||
});
|
||||
}
|
||||
|
||||
// 중복 체크
|
||||
vacationBalanceModel.getByWorkerTypeYear(worker_id, vacation_type_id, year, (err, existing) => {
|
||||
if (err) {
|
||||
console.error('중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (existing && existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 해당 작업자의 해당 연도 휴가 잔액이 존재합니다'
|
||||
});
|
||||
}
|
||||
|
||||
const balanceData = {
|
||||
worker_id,
|
||||
vacation_type_id,
|
||||
year,
|
||||
total_days,
|
||||
used_days: used_days || 0,
|
||||
notes: notes || null,
|
||||
created_by
|
||||
};
|
||||
|
||||
vacationBalanceModel.create(balanceData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 생성하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '휴가 잔액이 생성되었습니다',
|
||||
data: { id: result.insertId }
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('createBalance 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 수정
|
||||
* PUT /api/vacation-balances/:id
|
||||
*/
|
||||
async updateBalance(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { total_days, used_days, notes } = req.body;
|
||||
|
||||
const updateData = {};
|
||||
if (total_days !== undefined) updateData.total_days = total_days;
|
||||
if (used_days !== undefined) updateData.used_days = used_days;
|
||||
if (notes !== undefined) updateData.notes = notes;
|
||||
updateData.updated_at = new Date();
|
||||
|
||||
if (Object.keys(updateData).length === 1) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '수정할 데이터가 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationBalanceModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 수정하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 잔액이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('updateBalance 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 삭제
|
||||
* DELETE /api/vacation-balances/:id
|
||||
*/
|
||||
async deleteBalance(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
vacationBalanceModel.delete(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 삭제하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 잔액이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('deleteBalance 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 근속년수 기반 연차 자동 계산 및 생성
|
||||
* POST /api/vacation-balances/auto-calculate
|
||||
*/
|
||||
async autoCalculateAndCreate(req, res) {
|
||||
try {
|
||||
const { worker_id, hire_date, year } = req.body;
|
||||
const created_by = req.user.user_id;
|
||||
|
||||
if (!worker_id || !hire_date || !year) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다 (worker_id, hire_date, year)'
|
||||
});
|
||||
}
|
||||
|
||||
// 연차 일수 계산
|
||||
const annualDays = vacationBalanceModel.calculateAnnualLeaveDays(hire_date, year);
|
||||
|
||||
// ANNUAL 휴가 유형 ID 조회
|
||||
vacationTypeModel.getByCode('ANNUAL', (err, types) => {
|
||||
if (err || !types || types.length === 0) {
|
||||
console.error('ANNUAL 휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'ANNUAL 휴가 유형을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const annualTypeId = types[0].id;
|
||||
|
||||
// 중복 체크
|
||||
vacationBalanceModel.getByWorkerTypeYear(worker_id, annualTypeId, year, (err, existing) => {
|
||||
if (err) {
|
||||
console.error('중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (existing && existing.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 해당 작업자의 해당 연도 연차가 존재합니다'
|
||||
});
|
||||
}
|
||||
|
||||
const balanceData = {
|
||||
worker_id,
|
||||
vacation_type_id: annualTypeId,
|
||||
year,
|
||||
total_days: annualDays,
|
||||
used_days: 0,
|
||||
notes: `근속년수 기반 자동 계산 (입사일: ${hire_date})`,
|
||||
created_by
|
||||
};
|
||||
|
||||
vacationBalanceModel.create(balanceData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 잔액 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 잔액을 생성하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: `${annualDays}일의 연차가 자동으로 생성되었습니다`,
|
||||
data: {
|
||||
id: result.insertId,
|
||||
calculated_days: annualDays
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('autoCalculateAndCreate 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자의 사용 가능한 휴가 일수 조회
|
||||
* GET /api/vacation-balances/worker/:workerId/year/:year/available
|
||||
*/
|
||||
async getAvailableDays(req, res) {
|
||||
try {
|
||||
const { workerId, year } = req.params;
|
||||
|
||||
vacationBalanceModel.getAvailableVacationDays(workerId, year, (err, results) => {
|
||||
if (err) {
|
||||
console.error('사용 가능 휴가 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '사용 가능 휴가를 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getAvailableDays 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationBalanceController;
|
||||
565
api.hyungi.net/controllers/vacationRequestController.js
Normal file
565
api.hyungi.net/controllers/vacationRequestController.js
Normal file
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* vacationRequestController.js
|
||||
* 휴가 신청 관련 컨트롤러
|
||||
*/
|
||||
|
||||
const vacationRequestModel = require('../models/vacationRequestModel');
|
||||
// TODO: workerVacationBalanceModel 구현 필요
|
||||
// const workerVacationBalanceModel = require('../models/workerVacationBalanceModel');
|
||||
|
||||
const vacationRequestController = {
|
||||
/**
|
||||
* 휴가 신청 생성
|
||||
*/
|
||||
async createRequest(req, res) {
|
||||
try {
|
||||
const { worker_id, vacation_type_id, start_date, end_date, days_used, reason } = req.body;
|
||||
const requested_by = req.user.user_id;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!worker_id || !vacation_type_id || !start_date || !end_date || !days_used) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 날짜 유효성 검증
|
||||
const startDate = new Date(start_date);
|
||||
const endDate = new Date(end_date);
|
||||
|
||||
if (endDate < startDate) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '종료일은 시작일보다 이후여야 합니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 기간 중복 체크
|
||||
vacationRequestModel.checkOverlap(worker_id, start_date, end_date, null, (err, results) => {
|
||||
if (err) {
|
||||
console.error('기간 중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '기간 중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results[0].count > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '해당 기간에 이미 신청된 휴가가 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 잔여 연차 확인 로직 구현 필요
|
||||
// 현재는 잔여 연차 확인 없이 신청 가능
|
||||
|
||||
// 휴가 신청 생성
|
||||
const requestData = {
|
||||
worker_id,
|
||||
vacation_type_id,
|
||||
start_date,
|
||||
end_date,
|
||||
days_used,
|
||||
reason: reason || null,
|
||||
status: 'pending',
|
||||
requested_by
|
||||
};
|
||||
|
||||
vacationRequestModel.create(requestData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 생성 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '휴가 신청이 완료되었습니다',
|
||||
data: {
|
||||
request_id: result.insertId
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 생성 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 목록 조회
|
||||
*/
|
||||
async getAllRequests(req, res) {
|
||||
try {
|
||||
const filters = {
|
||||
worker_id: req.query.worker_id,
|
||||
status: req.query.status,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
vacation_type_id: req.query.vacation_type_id
|
||||
};
|
||||
|
||||
// 일반 사용자는 자신의 신청만 조회 가능
|
||||
if (req.user.access_level !== 'system') {
|
||||
if (req.user.worker_id) {
|
||||
filters.worker_id = req.user.worker_id;
|
||||
} else {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vacationRequestModel.getAll(filters, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 목록 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 목록 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 휴가 신청 조회
|
||||
*/
|
||||
async getRequestById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const request = results[0];
|
||||
|
||||
// 권한 검증: 관리자 또는 본인만 조회 가능
|
||||
if (req.user.access_level !== 'system' && req.user.worker_id !== request.worker_id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: request
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 수정 (대기 중인 신청만)
|
||||
*/
|
||||
async updateRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { start_date, end_date, days_used, reason } = req.body;
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const existingRequest = results[0];
|
||||
|
||||
// 권한 검증
|
||||
if (req.user.access_level !== 'system' && req.user.worker_id !== existingRequest.worker_id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 대기 중인 신청만 수정 가능
|
||||
if (existingRequest.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '승인/거부된 신청은 수정할 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = {};
|
||||
if (start_date) updateData.start_date = start_date;
|
||||
if (end_date) updateData.end_date = end_date;
|
||||
if (days_used) updateData.days_used = days_used;
|
||||
if (reason !== undefined) updateData.reason = reason;
|
||||
|
||||
// 날짜가 변경된 경우 중복 체크
|
||||
if (start_date || end_date) {
|
||||
const newStartDate = start_date || existingRequest.start_date;
|
||||
const newEndDate = end_date || existingRequest.end_date;
|
||||
|
||||
vacationRequestModel.checkOverlap(
|
||||
existingRequest.worker_id,
|
||||
newStartDate,
|
||||
newEndDate,
|
||||
id,
|
||||
(err, overlapResults) => {
|
||||
if (err) {
|
||||
console.error('기간 중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '기간 중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (overlapResults[0].count > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '해당 기간에 이미 신청된 휴가가 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 수정 실행
|
||||
vacationRequestModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 수정 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// 날짜 변경 없이 바로 수정
|
||||
vacationRequestModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 수정 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 수정 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 삭제 (대기 중인 신청만)
|
||||
*/
|
||||
async deleteRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const existingRequest = results[0];
|
||||
|
||||
// 권한 검증
|
||||
if (req.user.access_level !== 'system' && req.user.worker_id !== existingRequest.worker_id) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '권한이 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 대기 중인 신청만 삭제 가능
|
||||
if (existingRequest.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '승인/거부된 신청은 삭제할 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationRequestModel.delete(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 삭제 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 신청 삭제 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 승인 (관리자만)
|
||||
*/
|
||||
async approveRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { review_note } = req.body;
|
||||
const reviewed_by = req.user.user_id;
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.access_level !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자만 승인할 수 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const request = results[0];
|
||||
|
||||
if (request.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 처리된 신청입니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
const statusData = {
|
||||
status: 'approved',
|
||||
reviewed_by,
|
||||
review_note
|
||||
};
|
||||
|
||||
vacationRequestModel.updateStatus(id, statusData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 승인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 승인 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: 잔여 연차에서 차감 로직 구현 필요
|
||||
// 현재는 연차 차감 없이 승인만 처리
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 승인되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 승인 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 거부 (관리자만)
|
||||
*/
|
||||
async rejectRequest(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { review_note } = req.body;
|
||||
const reviewed_by = req.user.user_id;
|
||||
|
||||
// 관리자 권한 확인
|
||||
if (req.user.access_level !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자만 거부할 수 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 기존 신청 조회
|
||||
vacationRequestModel.getById(id, (err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '해당 휴가 신청을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const request = results[0];
|
||||
|
||||
if (request.status !== 'pending') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 처리된 신청입니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
const statusData = {
|
||||
status: 'rejected',
|
||||
reviewed_by,
|
||||
review_note
|
||||
};
|
||||
|
||||
vacationRequestModel.updateStatus(id, statusData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 거부 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 거부 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 신청이 거부되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('휴가 거부 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 대기 중인 휴가 신청 목록 (관리자용)
|
||||
*/
|
||||
async getPendingRequests(req, res) {
|
||||
try {
|
||||
// 관리자 권한 확인
|
||||
if (req.user.access_level !== 'system') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '관리자만 조회할 수 있습니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationRequestModel.getAllPending((err, results) => {
|
||||
if (err) {
|
||||
console.error('대기 중인 휴가 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '대기 중인 휴가 신청 조회 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('대기 중인 휴가 신청 조회 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationRequestController;
|
||||
333
api.hyungi.net/controllers/vacationTypeController.js
Normal file
333
api.hyungi.net/controllers/vacationTypeController.js
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* vacationTypeController.js
|
||||
* 휴가 유형 관련 컨트롤러
|
||||
*/
|
||||
|
||||
const vacationTypeModel = require('../models/vacationTypeModel');
|
||||
|
||||
const vacationTypeController = {
|
||||
/**
|
||||
* 모든 활성 휴가 유형 조회
|
||||
* GET /api/vacation-types
|
||||
*/
|
||||
async getAllTypes(req, res) {
|
||||
try {
|
||||
vacationTypeModel.getAll((err, results) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getAllTypes 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 시스템 기본 휴가 유형 조회
|
||||
* GET /api/vacation-types/system
|
||||
*/
|
||||
async getSystemTypes(req, res) {
|
||||
try {
|
||||
vacationTypeModel.getSystemTypes((err, results) => {
|
||||
if (err) {
|
||||
console.error('시스템 휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '시스템 휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getSystemTypes 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특별 휴가 유형 조회
|
||||
* GET /api/vacation-types/special
|
||||
*/
|
||||
async getSpecialTypes(req, res) {
|
||||
try {
|
||||
vacationTypeModel.getSpecialTypes((err, results) => {
|
||||
if (err) {
|
||||
console.error('특별 휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '특별 휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('getSpecialTypes 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특별 휴가 유형 생성 (관리자만)
|
||||
* POST /api/vacation-types
|
||||
*/
|
||||
async createType(req, res) {
|
||||
try {
|
||||
const {
|
||||
type_code,
|
||||
type_name,
|
||||
deduct_days,
|
||||
priority,
|
||||
description
|
||||
} = req.body;
|
||||
|
||||
// 필수 필드 검증
|
||||
if (!type_code || !type_name || !deduct_days) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '필수 필드가 누락되었습니다 (type_code, type_name, deduct_days)'
|
||||
});
|
||||
}
|
||||
|
||||
// type_code 중복 체크
|
||||
vacationTypeModel.getByCode(type_code, (err, existingTypes) => {
|
||||
if (err) {
|
||||
console.error('type_code 중복 체크 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'type_code 중복 체크 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (existingTypes && existingTypes.length > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '이미 존재하는 type_code입니다'
|
||||
});
|
||||
}
|
||||
|
||||
// 특별 휴가 유형으로 생성
|
||||
const typeData = {
|
||||
type_code,
|
||||
type_name,
|
||||
deduct_days,
|
||||
priority: priority || 50,
|
||||
description: description || null,
|
||||
is_special: true,
|
||||
is_system: false,
|
||||
is_active: true
|
||||
};
|
||||
|
||||
vacationTypeModel.create(typeData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 생성하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '특별 휴가 유형이 생성되었습니다',
|
||||
data: { id: result.insertId }
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('createType 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 수정 (관리자만)
|
||||
* PUT /api/vacation-types/:id
|
||||
*/
|
||||
async updateType(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const {
|
||||
type_name,
|
||||
deduct_days,
|
||||
priority,
|
||||
description,
|
||||
is_active
|
||||
} = req.body;
|
||||
|
||||
// 먼저 해당 유형 조회
|
||||
vacationTypeModel.getById(id, (err, types) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 조회하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (!types || types.length === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 찾을 수 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
const type = types[0];
|
||||
|
||||
// 시스템 기본 휴가의 경우 제한적으로만 수정 가능
|
||||
const updateData = {};
|
||||
if (type.is_system) {
|
||||
// 시스템 휴가는 priority와 description만 수정 가능
|
||||
if (priority !== undefined) updateData.priority = priority;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
} else {
|
||||
// 특별 휴가는 모든 필드 수정 가능
|
||||
if (type_name) updateData.type_name = type_name;
|
||||
if (deduct_days !== undefined) updateData.deduct_days = deduct_days;
|
||||
if (priority !== undefined) updateData.priority = priority;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (is_active !== undefined) updateData.is_active = is_active;
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '수정할 데이터가 없습니다'
|
||||
});
|
||||
}
|
||||
|
||||
updateData.updated_at = new Date();
|
||||
|
||||
vacationTypeModel.update(id, updateData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 수정하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 유형이 수정되었습니다'
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('updateType 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특별 휴가 유형 삭제 (관리자만, 시스템 기본 휴가는 삭제 불가)
|
||||
* DELETE /api/vacation-types/:id
|
||||
*/
|
||||
async deleteType(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
vacationTypeModel.delete(id, (err, result) => {
|
||||
if (err) {
|
||||
console.error('휴가 유형 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '휴가 유형을 삭제하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '삭제할 수 없습니다. 시스템 기본 휴가이거나 존재하지 않는 휴가 유형입니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '휴가 유형이 삭제되었습니다'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('deleteType 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 우선순위 일괄 업데이트 (관리자만)
|
||||
* PUT /api/vacation-types/priorities
|
||||
*/
|
||||
async updatePriorities(req, res) {
|
||||
try {
|
||||
const { priorities } = req.body;
|
||||
|
||||
// priorities = [{ id: 1, priority: 10 }, { id: 2, priority: 20 }, ...]
|
||||
if (!priorities || !Array.isArray(priorities)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'priorities 배열이 필요합니다'
|
||||
});
|
||||
}
|
||||
|
||||
vacationTypeModel.updatePriorities(priorities, (err, result) => {
|
||||
if (err) {
|
||||
console.error('우선순위 업데이트 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '우선순위를 업데이트하는 중 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '우선순위가 업데이트되었습니다',
|
||||
data: { updated: result.affectedRows }
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('updatePriorities 오류:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '서버 오류가 발생했습니다'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationTypeController;
|
||||
555
api.hyungi.net/controllers/visitRequestController.js
Normal file
555
api.hyungi.net/controllers/visitRequestController.js
Normal file
@@ -0,0 +1,555 @@
|
||||
const visitRequestModel = require('../models/visitRequestModel');
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 생성
|
||||
*/
|
||||
exports.createVisitRequest = (req, res) => {
|
||||
const requester_id = req.user.user_id;
|
||||
const requestData = {
|
||||
requester_id,
|
||||
...req.body
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = ['visitor_company', 'category_id', 'workplace_id', 'visit_date', 'visit_time', 'purpose_id'];
|
||||
for (const field of requiredFields) {
|
||||
if (!requestData[field]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `${field}는 필수 입력 항목입니다.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
visitRequestModel.createVisitRequest(requestData, (err, requestId) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '출입 신청이 성공적으로 생성되었습니다.',
|
||||
data: { request_id: requestId }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 목록 조회
|
||||
*/
|
||||
exports.getAllVisitRequests = (req, res) => {
|
||||
const filters = {
|
||||
status: req.query.status,
|
||||
visit_date: req.query.visit_date,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
requester_id: req.query.requester_id,
|
||||
category_id: req.query.category_id
|
||||
};
|
||||
|
||||
visitRequestModel.getAllVisitRequests(filters, (err, requests) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: requests
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 상세 조회
|
||||
*/
|
||||
exports.getVisitRequestById = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
|
||||
visitRequestModel.getVisitRequestById(requestId, (err, request) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (!request) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: request
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 수정
|
||||
*/
|
||||
exports.updateVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
const requestData = req.body;
|
||||
|
||||
visitRequestModel.updateVisitRequest(requestId, requestData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 삭제
|
||||
*/
|
||||
exports.deleteVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
|
||||
visitRequestModel.deleteVisitRequest(requestId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 삭제되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 승인
|
||||
*/
|
||||
exports.approveVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
const approvedBy = req.user.user_id;
|
||||
|
||||
visitRequestModel.approveVisitRequest(requestId, approvedBy, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 승인 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 승인 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 승인되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 반려
|
||||
*/
|
||||
exports.rejectVisitRequest = (req, res) => {
|
||||
const requestId = req.params.id;
|
||||
const approvedBy = req.user.user_id;
|
||||
const rejectionReason = req.body.rejection_reason || '사유 없음';
|
||||
|
||||
const rejectionData = {
|
||||
approved_by: approvedBy,
|
||||
rejection_reason: rejectionReason
|
||||
};
|
||||
|
||||
visitRequestModel.rejectVisitRequest(requestId, rejectionData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 반려 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '출입 신청 반려 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '출입 신청을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '출입 신청이 반려되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 방문 목적 조회
|
||||
*/
|
||||
exports.getAllVisitPurposes = (req, res) => {
|
||||
visitRequestModel.getAllVisitPurposes((err, purposes) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: purposes
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 방문 목적만 조회
|
||||
*/
|
||||
exports.getActiveVisitPurposes = (req, res) => {
|
||||
visitRequestModel.getActiveVisitPurposes((err, purposes) => {
|
||||
if (err) {
|
||||
console.error('활성 방문 목적 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '활성 방문 목적 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: purposes
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 추가
|
||||
*/
|
||||
exports.createVisitPurpose = (req, res) => {
|
||||
const purposeData = req.body;
|
||||
|
||||
if (!purposeData.purpose_name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'purpose_name은 필수 입력 항목입니다.'
|
||||
});
|
||||
}
|
||||
|
||||
visitRequestModel.createVisitPurpose(purposeData, (err, purposeId) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 추가 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 추가 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '방문 목적이 추가되었습니다.',
|
||||
data: { purpose_id: purposeId }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 수정
|
||||
*/
|
||||
exports.updateVisitPurpose = (req, res) => {
|
||||
const purposeId = req.params.id;
|
||||
const purposeData = req.body;
|
||||
|
||||
visitRequestModel.updateVisitPurpose(purposeId, purposeData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '방문 목적을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '방문 목적이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 삭제
|
||||
*/
|
||||
exports.deleteVisitPurpose = (req, res) => {
|
||||
const purposeId = req.params.id;
|
||||
|
||||
visitRequestModel.deleteVisitPurpose(purposeId, (err, result) => {
|
||||
if (err) {
|
||||
console.error('방문 목적 삭제 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '방문 목적 삭제 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '방문 목적을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '방문 목적이 삭제되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
/**
|
||||
* 안전교육 기록 생성
|
||||
*/
|
||||
exports.createTrainingRecord = (req, res) => {
|
||||
const trainerId = req.user.user_id;
|
||||
const trainingData = {
|
||||
trainer_id: trainerId,
|
||||
...req.body
|
||||
};
|
||||
|
||||
// 필수 필드 검증
|
||||
const requiredFields = ['request_id', 'training_date', 'training_start_time'];
|
||||
for (const field of requiredFields) {
|
||||
if (!trainingData[field]) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `${field}는 필수 입력 항목입니다.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
visitRequestModel.createTrainingRecord(trainingData, (err, trainingId) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 생성 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 생성 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
// 안전교육 기록이 생성되면 출입 신청 상태를 training_completed로 변경
|
||||
console.log(`[교육 완료] request_id=${trainingData.request_id} 상태를 training_completed로 변경 중...`);
|
||||
visitRequestModel.updateVisitRequestStatus(trainingData.request_id, 'training_completed', (statusErr) => {
|
||||
if (statusErr) {
|
||||
console.error('출입 신청 상태 업데이트 오류:', statusErr);
|
||||
// 에러가 발생해도 교육 기록은 생성되었으므로 성공 응답
|
||||
} else {
|
||||
console.log(`[교육 완료] request_id=${trainingData.request_id} 상태 변경 성공`);
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: '안전교육 기록이 생성되었습니다.',
|
||||
data: { training_id: trainingId }
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 출입 신청의 안전교육 기록 조회
|
||||
*/
|
||||
exports.getTrainingRecordByRequestId = (req, res) => {
|
||||
const requestId = req.params.requestId;
|
||||
|
||||
visitRequestModel.getTrainingRecordByRequestId(requestId, (err, record) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record || null
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 기록 수정
|
||||
*/
|
||||
exports.updateTrainingRecord = (req, res) => {
|
||||
const trainingId = req.params.id;
|
||||
const trainingData = req.body;
|
||||
|
||||
visitRequestModel.updateTrainingRecord(trainingId, trainingData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 수정 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 수정 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '안전교육 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전교육 기록이 수정되었습니다.'
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 완료 (서명 포함)
|
||||
*/
|
||||
exports.completeTraining = (req, res) => {
|
||||
const trainingId = req.params.id;
|
||||
const signatureData = req.body.signature_data;
|
||||
|
||||
if (!signatureData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '서명 데이터가 필요합니다.'
|
||||
});
|
||||
}
|
||||
|
||||
visitRequestModel.completeTraining(trainingId, signatureData, (err, result) => {
|
||||
if (err) {
|
||||
console.error('안전교육 완료 처리 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 완료 처리 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
if (result.affectedRows === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '안전교육 기록을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
// 교육 완료 후 출입 신청 상태를 'training_completed'로 변경
|
||||
visitRequestModel.getTrainingRecordByRequestId(trainingId, (err, record) => {
|
||||
if (err || !record) {
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '안전교육이 완료되었습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed', (err) => {
|
||||
if (err) {
|
||||
console.error('출입 신청 상태 업데이트 오류:', err);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '안전교육이 완료되었습니다.'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 기록 목록 조회
|
||||
*/
|
||||
exports.getTrainingRecords = (req, res) => {
|
||||
const filters = {
|
||||
training_date: req.query.training_date,
|
||||
start_date: req.query.start_date,
|
||||
end_date: req.query.end_date,
|
||||
trainer_id: req.query.trainer_id
|
||||
};
|
||||
|
||||
visitRequestModel.getTrainingRecords(filters, (err, records) => {
|
||||
if (err) {
|
||||
console.error('안전교육 기록 목록 조회 오류:', err);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '안전교육 기록 목록 조회 중 오류가 발생했습니다.',
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: records
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -178,36 +178,68 @@ exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
// 계정 생성/해제 처리
|
||||
const db = await getDb();
|
||||
const hasAccount = currentWorker.user_id !== null && currentWorker.user_id !== undefined;
|
||||
let accountAction = null;
|
||||
let accountUsername = null;
|
||||
|
||||
console.log('🔍 계정 생성 체크:', {
|
||||
createAccount,
|
||||
hasAccount,
|
||||
currentWorker_user_id: currentWorker.user_id,
|
||||
worker_name: workerData.worker_name
|
||||
});
|
||||
|
||||
if (createAccount && !hasAccount && workerData.worker_name) {
|
||||
// 계정 생성
|
||||
console.log('✅ 계정 생성 로직 시작');
|
||||
try {
|
||||
console.log('🔑 사용자명 생성 중...');
|
||||
const username = await generateUniqueUsername(workerData.worker_name, db);
|
||||
console.log('🔑 생성된 사용자명:', username);
|
||||
|
||||
const hashedPassword = await bcrypt.hash('1234', 10);
|
||||
console.log('🔒 비밀번호 해싱 완료');
|
||||
|
||||
// User 역할 조회
|
||||
console.log('👤 User 역할 조회 중...');
|
||||
const [userRole] = await db.query('SELECT id FROM roles WHERE name = ?', ['User']);
|
||||
console.log('👤 User 역할 조회 결과:', userRole);
|
||||
|
||||
if (userRole && userRole.length > 0) {
|
||||
console.log('💾 계정 DB 삽입 시작...');
|
||||
await db.query(
|
||||
`INSERT INTO users (username, password, name, worker_id, role_id, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, NOW(), NOW())`,
|
||||
[username, hashedPassword, workerData.worker_name, id, userRole[0].id]
|
||||
);
|
||||
console.log('✅ 계정 DB 삽입 완료');
|
||||
|
||||
accountAction = 'created';
|
||||
accountUsername = username;
|
||||
logger.info('작업자 계정 생성 성공', { worker_id: id, username });
|
||||
} else {
|
||||
console.log('❌ User 역할을 찾을 수 없음');
|
||||
}
|
||||
} catch (accountError) {
|
||||
console.error('❌ 계정 생성 오류:', accountError);
|
||||
logger.error('계정 생성 실패', { worker_id: id, error: accountError.message });
|
||||
accountAction = 'failed';
|
||||
}
|
||||
} else if (!createAccount && hasAccount) {
|
||||
} else {
|
||||
console.log('⏭️ 계정 생성 조건 불만족:', { createAccount, hasAccount, hasWorkerName: !!workerData.worker_name });
|
||||
}
|
||||
|
||||
if (!createAccount && hasAccount) {
|
||||
// 계정 연동 해제 (users.worker_id = NULL)
|
||||
try {
|
||||
await db.query('UPDATE users SET worker_id = NULL WHERE worker_id = ?', [id]);
|
||||
accountAction = 'unlinked';
|
||||
logger.info('작업자 계정 연동 해제 성공', { worker_id: id });
|
||||
} catch (unlinkError) {
|
||||
logger.error('계정 연동 해제 실패', { worker_id: id, error: unlinkError.message });
|
||||
accountAction = 'unlink_failed';
|
||||
}
|
||||
} else if (createAccount && hasAccount) {
|
||||
accountAction = 'already_exists';
|
||||
}
|
||||
|
||||
// 작업자 관련 캐시 무효화
|
||||
@@ -216,10 +248,26 @@ exports.updateWorker = asyncHandler(async (req, res) => {
|
||||
|
||||
logger.info('작업자 수정 성공', { worker_id: id });
|
||||
|
||||
// 응답 메시지 구성
|
||||
let message = '작업자 정보가 성공적으로 수정되었습니다';
|
||||
if (accountAction === 'created') {
|
||||
message += ` (계정 생성 완료: ${accountUsername}, 초기 비밀번호: 1234)`;
|
||||
} else if (accountAction === 'unlinked') {
|
||||
message += ' (계정 연동 해제 완료)';
|
||||
} else if (accountAction === 'already_exists') {
|
||||
message += ' (이미 계정이 존재합니다)';
|
||||
} else if (accountAction === 'failed') {
|
||||
message += ' (계정 생성 실패)';
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: { changes },
|
||||
message: '작업자 정보가 성공적으로 수정되었습니다'
|
||||
data: {
|
||||
changes,
|
||||
account_action: accountAction,
|
||||
account_username: accountUsername
|
||||
},
|
||||
message
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 마이그레이션: tbm_team_assignments 테이블 확장
|
||||
* 작업자별 프로젝트/공정/작업/작업장 정보 저장 가능하도록 컬럼 추가 및 외래키 설정
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. workplace_category_id와 workplace_id를 UNSIGNED로 변경
|
||||
await knex.raw(`
|
||||
ALTER TABLE tbm_team_assignments
|
||||
MODIFY COLUMN workplace_category_id INT UNSIGNED NULL COMMENT '작업자별 작업장 대분류 (공장)',
|
||||
MODIFY COLUMN workplace_id INT UNSIGNED NULL COMMENT '작업자별 작업장 ID'
|
||||
`);
|
||||
|
||||
// 2. 외래키 제약조건 추가
|
||||
return knex.schema.alterTable('tbm_team_assignments', function(table) {
|
||||
// 외래키 제약조건 추가
|
||||
table.foreign('workplace_category_id')
|
||||
.references('category_id')
|
||||
.inTable('workplace_categories')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('workplace_id')
|
||||
.references('workplace_id')
|
||||
.inTable('workplaces')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('tbm_team_assignments', function(table) {
|
||||
// 외래키 제약조건 제거
|
||||
table.dropForeign('workplace_category_id');
|
||||
table.dropForeign('workplace_id');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* 마이그레이션: tbm_sessions 테이블에서 불필요한 컬럼 제거
|
||||
* work_description, safety_notes, start_time 컬럼 제거
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('tbm_sessions', function(table) {
|
||||
table.dropColumn('work_description');
|
||||
table.dropColumn('safety_notes');
|
||||
table.dropColumn('start_time');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('tbm_sessions', function(table) {
|
||||
table.text('work_description').nullable().comment('작업 내용');
|
||||
table.text('safety_notes').nullable().comment('안전 관련 특이사항');
|
||||
table.time('start_time').nullable().comment('시작 시간');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 마이그레이션: 작업장 지도 이미지 기능 추가
|
||||
* - workplace_categories에 layout_image 필드 추가
|
||||
* - workplace_map_regions 테이블 생성 (클릭 가능한 영역 정의)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. workplace_categories 테이블에 layout_image 필드 추가
|
||||
await knex.schema.alterTable('workplace_categories', function(table) {
|
||||
table.string('layout_image', 500).nullable().comment('공장 배치도 이미지 경로');
|
||||
});
|
||||
|
||||
// 2. 작업장 지도 클릭 영역 정의 테이블 생성
|
||||
await knex.schema.createTable('workplace_map_regions', function(table) {
|
||||
table.increments('region_id').primary().comment('영역 ID');
|
||||
table.integer('workplace_id').unsigned().notNullable().comment('작업장 ID');
|
||||
table.integer('category_id').unsigned().notNullable().comment('공장 카테고리 ID');
|
||||
|
||||
// 좌표 정보 (비율 기반: 0~100%)
|
||||
table.decimal('x_start', 5, 2).notNullable().comment('시작 X 좌표 (%)');
|
||||
table.decimal('y_start', 5, 2).notNullable().comment('시작 Y 좌표 (%)');
|
||||
table.decimal('x_end', 5, 2).notNullable().comment('끝 X 좌표 (%)');
|
||||
table.decimal('y_end', 5, 2).notNullable().comment('끝 Y 좌표 (%)');
|
||||
|
||||
table.string('shape', 20).defaultTo('rect').comment('영역 모양 (rect, circle, polygon)');
|
||||
table.text('polygon_points').nullable().comment('다각형인 경우 좌표 배열 (JSON)');
|
||||
|
||||
table.timestamps(true, true);
|
||||
|
||||
// 외래키
|
||||
table.foreign('workplace_id')
|
||||
.references('workplace_id')
|
||||
.inTable('workplaces')
|
||||
.onDelete('CASCADE')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('category_id')
|
||||
.references('category_id')
|
||||
.inTable('workplace_categories')
|
||||
.onDelete('CASCADE')
|
||||
.onUpdate('CASCADE');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('workplace_map_regions');
|
||||
|
||||
// 필드 제거
|
||||
await knex.schema.alterTable('workplace_categories', function(table) {
|
||||
table.dropColumn('layout_image');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 마이그레이션: 작업장 용도 및 표시 순서 필드 추가
|
||||
*/
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.alterTable('workplaces', function(table) {
|
||||
table.string('workplace_purpose', 50).nullable().comment('작업장 용도 (작업구역, 설비, 휴게시설, 회의실 등)');
|
||||
table.integer('display_priority').defaultTo(0).comment('표시 우선순위 (숫자가 작을수록 먼저 표시)');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.alterTable('workplaces', function(table) {
|
||||
table.dropColumn('workplace_purpose');
|
||||
table.dropColumn('display_priority');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* leader_id를 nullable로 변경
|
||||
* 관리자가 TBM을 입력할 때 leader_id를 NULL로 설정하고 created_by를 사용
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. 외래 키 제약조건 삭제 (존재하는 경우에만)
|
||||
try {
|
||||
await knex.raw('ALTER TABLE tbm_sessions DROP FOREIGN KEY tbm_sessions_leader_id_foreign');
|
||||
} catch (err) {
|
||||
console.log('외래 키가 이미 존재하지 않음 (정상)');
|
||||
}
|
||||
|
||||
// 2. leader_id를 nullable로 변경 (UNSIGNED 제거하여 workers.worker_id와 타입 일치)
|
||||
await knex.raw('ALTER TABLE tbm_sessions MODIFY leader_id INT(11) NULL');
|
||||
|
||||
// 3. 외래 키 제약조건 다시 추가 (nullable 허용)
|
||||
await knex.raw('ALTER TABLE tbm_sessions ADD CONSTRAINT tbm_sessions_leader_id_foreign FOREIGN KEY (leader_id) REFERENCES workers(worker_id) ON DELETE SET NULL');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 1. 외래 키 제약조건 삭제
|
||||
await knex.raw('ALTER TABLE tbm_sessions DROP FOREIGN KEY tbm_sessions_leader_id_foreign');
|
||||
|
||||
// 2. leader_id를 NOT NULL로 되돌림
|
||||
await knex.raw('ALTER TABLE tbm_sessions MODIFY leader_id INT(11) NOT NULL');
|
||||
|
||||
// 3. 외래 키 제약조건 다시 추가
|
||||
await knex.raw('ALTER TABLE tbm_sessions ADD CONSTRAINT tbm_sessions_leader_id_foreign FOREIGN KEY (leader_id) REFERENCES workers(worker_id) ON DELETE CASCADE');
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* daily_work_reports 테이블에 TBM 연동 필드 추가
|
||||
* - TBM 세션 및 팀 배정과 연결
|
||||
* - 작업 시간 및 오류 시간 추적
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex.schema.table('daily_work_reports', (table) => {
|
||||
// TBM 연동 필드
|
||||
table.integer('tbm_session_id').unsigned().nullable()
|
||||
.comment('연결된 TBM 세션 ID');
|
||||
table.integer('tbm_assignment_id').unsigned().nullable()
|
||||
.comment('연결된 TBM 팀 배정 ID');
|
||||
|
||||
// 작업 시간 추적
|
||||
table.time('start_time').nullable()
|
||||
.comment('작업 시작 시간');
|
||||
table.time('end_time').nullable()
|
||||
.comment('작업 종료 시간');
|
||||
table.decimal('total_hours', 5, 2).nullable()
|
||||
.comment('총 작업 시간');
|
||||
table.decimal('regular_hours', 5, 2).nullable()
|
||||
.comment('정규 작업 시간 (총 시간 - 오류 시간)');
|
||||
table.decimal('error_hours', 5, 2).nullable()
|
||||
.comment('부적합 사항 처리 시간');
|
||||
|
||||
// 외래 키 제약조건
|
||||
table.foreign('tbm_session_id')
|
||||
.references('session_id')
|
||||
.inTable('tbm_sessions')
|
||||
.onDelete('SET NULL');
|
||||
|
||||
table.foreign('tbm_assignment_id')
|
||||
.references('assignment_id')
|
||||
.inTable('tbm_team_assignments')
|
||||
.onDelete('SET NULL');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.table('daily_work_reports', (table) => {
|
||||
// 외래 키 제약조건 삭제
|
||||
table.dropForeign('tbm_session_id');
|
||||
table.dropForeign('tbm_assignment_id');
|
||||
|
||||
// 컬럼 삭제
|
||||
table.dropColumn('tbm_session_id');
|
||||
table.dropColumn('tbm_assignment_id');
|
||||
table.dropColumn('start_time');
|
||||
table.dropColumn('end_time');
|
||||
table.dropColumn('total_hours');
|
||||
table.dropColumn('regular_hours');
|
||||
table.dropColumn('error_hours');
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* 현재 사용 중인 페이지를 pages 테이블에 업데이트
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 기존 페이지 모두 삭제
|
||||
await knex('pages').del();
|
||||
|
||||
// 현재 사용 중인 페이지들을 등록
|
||||
await knex('pages').insert([
|
||||
// 공통 페이지
|
||||
{
|
||||
page_key: 'dashboard',
|
||||
page_name: '대시보드',
|
||||
page_path: '/pages/dashboard.html',
|
||||
category: 'common',
|
||||
description: '전체 현황 대시보드',
|
||||
is_admin_only: 0,
|
||||
display_order: 1
|
||||
},
|
||||
|
||||
// 작업 관련 페이지
|
||||
{
|
||||
page_key: 'work.tbm',
|
||||
page_name: 'TBM',
|
||||
page_path: '/pages/work/tbm.html',
|
||||
category: 'work',
|
||||
description: 'TBM (Tool Box Meeting) 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 10
|
||||
},
|
||||
{
|
||||
page_key: 'work.report_create',
|
||||
page_name: '작업보고서 작성',
|
||||
page_path: '/pages/work/report-create.html',
|
||||
category: 'work',
|
||||
description: '일일 작업보고서 작성',
|
||||
is_admin_only: 0,
|
||||
display_order: 11
|
||||
},
|
||||
{
|
||||
page_key: 'work.report_view',
|
||||
page_name: '작업보고서 조회',
|
||||
page_path: '/pages/work/report-view.html',
|
||||
category: 'work',
|
||||
description: '작업보고서 조회 및 검색',
|
||||
is_admin_only: 0,
|
||||
display_order: 12
|
||||
},
|
||||
{
|
||||
page_key: 'work.analysis',
|
||||
page_name: '작업 분석',
|
||||
page_path: '/pages/work/analysis.html',
|
||||
category: 'work',
|
||||
description: '작업 통계 및 분석',
|
||||
is_admin_only: 0,
|
||||
display_order: 13
|
||||
},
|
||||
|
||||
// Admin 페이지
|
||||
{
|
||||
page_key: 'admin.accounts',
|
||||
page_name: '계정 관리',
|
||||
page_path: '/pages/admin/accounts.html',
|
||||
category: 'admin',
|
||||
description: '사용자 계정 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 20
|
||||
},
|
||||
{
|
||||
page_key: 'admin.page_access',
|
||||
page_name: '페이지 권한 관리',
|
||||
page_path: '/pages/admin/page-access.html',
|
||||
category: 'admin',
|
||||
description: '사용자별 페이지 접근 권한 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 21
|
||||
},
|
||||
{
|
||||
page_key: 'admin.workers',
|
||||
page_name: '작업자 관리',
|
||||
page_path: '/pages/admin/workers.html',
|
||||
category: 'admin',
|
||||
description: '작업자 정보 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 22
|
||||
},
|
||||
{
|
||||
page_key: 'admin.projects',
|
||||
page_name: '프로젝트 관리',
|
||||
page_path: '/pages/admin/projects.html',
|
||||
category: 'admin',
|
||||
description: '프로젝트 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 23
|
||||
},
|
||||
{
|
||||
page_key: 'admin.workplaces',
|
||||
page_name: '작업장 관리',
|
||||
page_path: '/pages/admin/workplaces.html',
|
||||
category: 'admin',
|
||||
description: '작업장소 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 24
|
||||
},
|
||||
{
|
||||
page_key: 'admin.codes',
|
||||
page_name: '코드 관리',
|
||||
page_path: '/pages/admin/codes.html',
|
||||
category: 'admin',
|
||||
description: '시스템 코드 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 25
|
||||
},
|
||||
{
|
||||
page_key: 'admin.tasks',
|
||||
page_name: '작업 관리',
|
||||
page_path: '/pages/admin/tasks.html',
|
||||
category: 'admin',
|
||||
description: '작업 유형 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 26
|
||||
},
|
||||
|
||||
// 프로필 페이지
|
||||
{
|
||||
page_key: 'profile.info',
|
||||
page_name: '내 정보',
|
||||
page_path: '/pages/profile/info.html',
|
||||
category: 'profile',
|
||||
description: '내 프로필 정보',
|
||||
is_admin_only: 0,
|
||||
display_order: 30
|
||||
},
|
||||
{
|
||||
page_key: 'profile.password',
|
||||
page_name: '비밀번호 변경',
|
||||
page_path: '/pages/profile/password.html',
|
||||
category: 'profile',
|
||||
description: '비밀번호 변경',
|
||||
is_admin_only: 0,
|
||||
display_order: 31
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 현재 사용 중인 페이지 목록 업데이트 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').del();
|
||||
console.log('✅ 페이지 목록 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Migration: Create vacation_requests table
|
||||
* Purpose: Track vacation request workflow (request, approval/rejection)
|
||||
* Date: 2026-01-29
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// Create vacation_requests table
|
||||
await knex.schema.createTable('vacation_requests', (table) => {
|
||||
table.increments('request_id').primary().comment('휴가 신청 ID');
|
||||
|
||||
// 작업자 정보
|
||||
table.integer('worker_id').notNullable().comment('작업자 ID');
|
||||
table.foreign('worker_id').references('worker_id').inTable('workers').onDelete('CASCADE');
|
||||
|
||||
// 휴가 정보
|
||||
table.integer('vacation_type_id').unsigned().notNullable().comment('휴가 유형 ID');
|
||||
table.foreign('vacation_type_id').references('id').inTable('vacation_types').onDelete('RESTRICT');
|
||||
|
||||
table.date('start_date').notNullable().comment('휴가 시작일');
|
||||
table.date('end_date').notNullable().comment('휴가 종료일');
|
||||
table.decimal('days_used', 4, 1).notNullable().comment('사용 일수 (0.5일 단위)');
|
||||
|
||||
table.text('reason').nullable().comment('휴가 사유');
|
||||
|
||||
// 신청 및 승인 정보
|
||||
table.enum('status', ['pending', 'approved', 'rejected'])
|
||||
.notNullable()
|
||||
.defaultTo('pending')
|
||||
.comment('승인 상태: pending(대기), approved(승인), rejected(거부)');
|
||||
|
||||
table.integer('requested_by').notNullable().comment('신청자 user_id');
|
||||
table.foreign('requested_by').references('user_id').inTable('users').onDelete('RESTRICT');
|
||||
|
||||
table.integer('reviewed_by').nullable().comment('승인/거부자 user_id');
|
||||
table.foreign('reviewed_by').references('user_id').inTable('users').onDelete('SET NULL');
|
||||
|
||||
table.timestamp('reviewed_at').nullable().comment('승인/거부 일시');
|
||||
table.text('review_note').nullable().comment('승인/거부 메모');
|
||||
|
||||
// 타임스탬프
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now()).comment('신청 일시');
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now()).comment('수정 일시');
|
||||
|
||||
// 인덱스
|
||||
table.index('worker_id', 'idx_vacation_requests_worker');
|
||||
table.index('status', 'idx_vacation_requests_status');
|
||||
table.index(['start_date', 'end_date'], 'idx_vacation_requests_dates');
|
||||
});
|
||||
|
||||
console.log('✅ vacation_requests 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.dropTableIfExists('vacation_requests');
|
||||
console.log('✅ vacation_requests 테이블 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Migration: Register attendance management pages
|
||||
* Purpose: Add 4 new pages to pages table for attendance management system
|
||||
* Date: 2026-01-29
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 페이지 등록 (실제 pages 테이블 컬럼에 맞춤)
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'daily-attendance',
|
||||
page_name: '일일 출퇴근 입력',
|
||||
page_path: '/pages/common/daily-attendance.html',
|
||||
description: '일일 출퇴근 기록 입력 페이지 (관리자/조장)',
|
||||
category: 'common',
|
||||
is_admin_only: false,
|
||||
display_order: 50
|
||||
},
|
||||
{
|
||||
page_key: 'monthly-attendance',
|
||||
page_name: '월별 출퇴근 현황',
|
||||
page_path: '/pages/common/monthly-attendance.html',
|
||||
description: '월별 출퇴근 현황 조회 페이지',
|
||||
category: 'common',
|
||||
is_admin_only: false,
|
||||
display_order: 51
|
||||
},
|
||||
{
|
||||
page_key: 'vacation-management',
|
||||
page_name: '휴가 관리',
|
||||
page_path: '/pages/common/vacation-management.html',
|
||||
description: '휴가 신청 및 승인 관리 페이지',
|
||||
category: 'common',
|
||||
is_admin_only: false,
|
||||
display_order: 52
|
||||
},
|
||||
{
|
||||
page_key: 'attendance-report-comparison',
|
||||
page_name: '출퇴근-작업보고서 대조',
|
||||
page_path: '/pages/admin/attendance-report-comparison.html',
|
||||
description: '출퇴근 기록과 작업보고서 대조 페이지 (관리자)',
|
||||
category: 'admin',
|
||||
is_admin_only: true,
|
||||
display_order: 120
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 출퇴근 관리 페이지 4개 등록 완료');
|
||||
|
||||
// Admin 사용자(user_id=1)에게 페이지 접근 권한 부여
|
||||
const adminUserId = 1;
|
||||
const pages = await knex('pages')
|
||||
.whereIn('page_key', [
|
||||
'daily-attendance',
|
||||
'monthly-attendance',
|
||||
'vacation-management',
|
||||
'attendance-report-comparison'
|
||||
])
|
||||
.select('id');
|
||||
|
||||
const accessRecords = pages.map(page => ({
|
||||
user_id: adminUserId,
|
||||
page_id: page.id,
|
||||
can_access: true,
|
||||
granted_by: adminUserId
|
||||
}));
|
||||
|
||||
await knex('user_page_access').insert(accessRecords);
|
||||
console.log('✅ Admin 사용자에게 출퇴근 관리 페이지 접근 권한 부여 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 페이지 삭제 (user_page_access는 FK CASCADE로 자동 삭제됨)
|
||||
await knex('pages')
|
||||
.whereIn('page_key', [
|
||||
'daily-attendance',
|
||||
'monthly-attendance',
|
||||
'vacation-management',
|
||||
'attendance-report-comparison'
|
||||
])
|
||||
.delete();
|
||||
|
||||
console.log('✅ 출퇴근 관리 페이지 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 출퇴근 출근 여부 필드 추가
|
||||
* 아침 출근 확인용 간단한 필드
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 컬럼 존재 여부 확인
|
||||
const hasColumn = await knex.schema.hasColumn('daily_attendance_records', 'is_present');
|
||||
|
||||
if (!hasColumn) {
|
||||
await knex.schema.table('daily_attendance_records', (table) => {
|
||||
// 출근 여부 (아침에 체크)
|
||||
table.boolean('is_present').defaultTo(true).comment('출근 여부');
|
||||
});
|
||||
|
||||
// 기존 데이터는 모두 출근으로 처리
|
||||
await knex('daily_attendance_records')
|
||||
.whereNotNull('id')
|
||||
.update({ is_present: true });
|
||||
|
||||
console.log('✅ is_present 컬럼 추가 완료');
|
||||
} else {
|
||||
console.log('⏭️ is_present 컬럼이 이미 존재합니다');
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
const hasColumn = await knex.schema.hasColumn('daily_attendance_records', 'is_present');
|
||||
|
||||
if (hasColumn) {
|
||||
await knex.schema.table('daily_attendance_records', (table) => {
|
||||
table.dropColumn('is_present');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 휴가 관리 페이지 분리 및 등록
|
||||
* - 기존 vacation-management.html을 2개 페이지로 분리
|
||||
* - vacation-request.html: 작업자 휴가 신청 및 본인 내역 확인
|
||||
* - vacation-management.html: 관리자 휴가 승인/직접입력/전체내역 (3개 탭)
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 기존 vacation-management 페이지 삭제
|
||||
await knex('pages')
|
||||
.where('page_key', 'vacation-management')
|
||||
.del();
|
||||
|
||||
// 새로운 휴가 관리 페이지 2개 등록
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'vacation-request',
|
||||
page_name: '휴가 신청',
|
||||
page_path: '/pages/common/vacation-request.html',
|
||||
category: 'common',
|
||||
description: '작업자가 휴가를 신청하고 본인의 신청 내역을 확인하는 페이지',
|
||||
is_admin_only: 0,
|
||||
display_order: 51
|
||||
},
|
||||
{
|
||||
page_key: 'vacation-management',
|
||||
page_name: '휴가 관리',
|
||||
page_path: '/pages/common/vacation-management.html',
|
||||
category: 'common',
|
||||
description: '관리자가 휴가 승인, 직접 입력, 전체 내역을 관리하는 페이지',
|
||||
is_admin_only: 1,
|
||||
display_order: 52
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 분리 완료 (기존 1개 → 신규 2개)');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 새로운 페이지 삭제
|
||||
await knex('pages')
|
||||
.whereIn('page_key', ['vacation-request', 'vacation-management'])
|
||||
.del();
|
||||
|
||||
// 기존 vacation-management 페이지 복원
|
||||
await knex('pages').insert({
|
||||
page_key: 'vacation-management',
|
||||
page_name: '휴가 관리',
|
||||
page_path: '/pages/common/vacation-management.html.old',
|
||||
category: 'common',
|
||||
description: '휴가 신청 및 승인 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 50
|
||||
});
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* vacation_types 테이블 확장
|
||||
* - 특별 휴가 유형 추가 기능
|
||||
* - 차감 우선순위 관리
|
||||
* - 시스템 기본 휴가 보호
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// vacation_types 테이블 확장
|
||||
await knex.schema.table('vacation_types', (table) => {
|
||||
table.boolean('is_special').defaultTo(false).comment('특별 휴가 여부 (장기근속, 출산 등)');
|
||||
table.integer('priority').defaultTo(99).comment('차감 우선순위 (낮을수록 먼저 차감)');
|
||||
table.text('description').nullable().comment('휴가 설명');
|
||||
table.boolean('is_system').defaultTo(true).comment('시스템 기본 휴가 (삭제 불가)');
|
||||
});
|
||||
|
||||
// 기존 휴가 유형에 우선순위 설정
|
||||
await knex('vacation_types').where('type_code', 'ANNUAL').update({
|
||||
priority: 10,
|
||||
is_system: true,
|
||||
description: '근로기준법에 따른 연차 유급휴가'
|
||||
});
|
||||
|
||||
await knex('vacation_types').where('type_code', 'HALF_ANNUAL').update({
|
||||
priority: 10,
|
||||
is_system: true,
|
||||
description: '반일 연차 (0.5일)'
|
||||
});
|
||||
|
||||
await knex('vacation_types').where('type_code', 'SICK').update({
|
||||
priority: 20,
|
||||
is_system: true,
|
||||
description: '병가'
|
||||
});
|
||||
|
||||
await knex('vacation_types').where('type_code', 'SPECIAL').update({
|
||||
priority: 0,
|
||||
is_system: true,
|
||||
description: '경조사 휴가 (무급)'
|
||||
});
|
||||
|
||||
console.log('✅ vacation_types 테이블 확장 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 컬럼 삭제
|
||||
await knex.schema.table('vacation_types', (table) => {
|
||||
table.dropColumn('is_special');
|
||||
table.dropColumn('priority');
|
||||
table.dropColumn('description');
|
||||
table.dropColumn('is_system');
|
||||
});
|
||||
|
||||
console.log('✅ vacation_types 테이블 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* vacation_balance_details 테이블 생성 및 데이터 마이그레이션
|
||||
* - 작업자별, 휴가 유형별, 연도별 휴가 잔액 관리
|
||||
* - 기존 worker_vacation_balance 데이터 이관
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// vacation_balance_details 테이블 생성
|
||||
await knex.schema.createTable('vacation_balance_details', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.integer('worker_id').notNullable().comment('작업자 ID');
|
||||
table.integer('vacation_type_id').unsigned().notNullable().comment('휴가 유형 ID');
|
||||
table.integer('year').notNullable().comment('연도');
|
||||
table.decimal('total_days', 4, 1).defaultTo(0).comment('총 발생 일수');
|
||||
table.decimal('used_days', 4, 1).defaultTo(0).comment('사용 일수');
|
||||
table.text('notes').nullable().comment('비고');
|
||||
table.integer('created_by').notNullable().comment('생성자 ID');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 인덱스
|
||||
table.unique(['worker_id', 'vacation_type_id', 'year'], 'unique_worker_vacation_year');
|
||||
table.index(['worker_id', 'year'], 'idx_worker_year');
|
||||
table.index('vacation_type_id', 'idx_vacation_type');
|
||||
|
||||
// 외래키
|
||||
table.foreign('worker_id').references('worker_id').inTable('workers').onDelete('CASCADE');
|
||||
table.foreign('vacation_type_id').references('id').inTable('vacation_types').onDelete('RESTRICT');
|
||||
table.foreign('created_by').references('user_id').inTable('users');
|
||||
});
|
||||
|
||||
// remaining_days를 generated column으로 추가 (Raw SQL)
|
||||
await knex.raw(`
|
||||
ALTER TABLE vacation_balance_details
|
||||
ADD COLUMN remaining_days DECIMAL(4,1)
|
||||
GENERATED ALWAYS AS (total_days - used_days) STORED
|
||||
COMMENT '잔여 일수'
|
||||
`);
|
||||
|
||||
console.log('✅ vacation_balance_details 테이블 생성 완료');
|
||||
|
||||
// 기존 worker_vacation_balance 데이터를 vacation_balance_details로 마이그레이션
|
||||
const existingBalances = await knex('worker_vacation_balance').select('*');
|
||||
|
||||
if (existingBalances.length > 0) {
|
||||
// ANNUAL 휴가 유형 ID 조회
|
||||
const annualType = await knex('vacation_types')
|
||||
.where('type_code', 'ANNUAL')
|
||||
.first();
|
||||
|
||||
if (!annualType) {
|
||||
throw new Error('ANNUAL 휴가 유형을 찾을 수 없습니다');
|
||||
}
|
||||
|
||||
// 관리자 사용자 ID 조회 (created_by 용)
|
||||
// role_id 1 = System Admin, 2 = Admin
|
||||
const adminUser = await knex('users')
|
||||
.whereIn('role_id', [1, 2])
|
||||
.first();
|
||||
|
||||
const createdById = adminUser ? adminUser.user_id : 1;
|
||||
|
||||
// 데이터 변환 및 삽입
|
||||
const balanceDetails = existingBalances.map(balance => ({
|
||||
worker_id: balance.worker_id,
|
||||
vacation_type_id: annualType.id,
|
||||
year: balance.year,
|
||||
total_days: balance.total_annual_leave || 0,
|
||||
used_days: balance.used_annual_leave || 0,
|
||||
notes: 'Migrated from worker_vacation_balance',
|
||||
created_by: createdById,
|
||||
created_at: balance.created_at,
|
||||
updated_at: balance.updated_at
|
||||
}));
|
||||
|
||||
await knex('vacation_balance_details').insert(balanceDetails);
|
||||
|
||||
console.log(`✅ ${balanceDetails.length}건의 기존 휴가 데이터 마이그레이션 완료`);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// vacation_balance_details 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('vacation_balance_details');
|
||||
|
||||
console.log('✅ vacation_balance_details 테이블 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 새로운 휴가 관리 페이지 등록
|
||||
* - annual-vacation-overview: 연간 연차 현황 (차트)
|
||||
* - vacation-allocation: 휴가 발생 입력 및 관리
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
await knex('pages').insert([
|
||||
{
|
||||
page_key: 'annual-vacation-overview',
|
||||
page_name: '연간 연차 현황',
|
||||
page_path: '/pages/common/annual-vacation-overview.html',
|
||||
category: 'common',
|
||||
description: '모든 작업자의 연간 연차 현황을 차트로 시각화',
|
||||
is_admin_only: 1,
|
||||
display_order: 54
|
||||
},
|
||||
{
|
||||
page_key: 'vacation-allocation',
|
||||
page_name: '휴가 발생 입력',
|
||||
page_path: '/pages/common/vacation-allocation.html',
|
||||
category: 'common',
|
||||
description: '작업자별 휴가 발생 입력 및 특별 휴가 관리',
|
||||
is_admin_only: 1,
|
||||
display_order: 55
|
||||
}
|
||||
]);
|
||||
|
||||
console.log('✅ 휴가 관리 신규 페이지 2개 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages')
|
||||
.whereIn('page_key', ['annual-vacation-overview', 'vacation-allocation'])
|
||||
.del();
|
||||
|
||||
console.log('✅ 휴가 관리 페이지 롤백 완료');
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 마이그레이션: 출입 신청 및 안전교육 시스템
|
||||
* - 방문 목적 타입 테이블
|
||||
* - 출입 신청 테이블
|
||||
* - 안전교육 기록 테이블
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. 방문 목적 타입 테이블 생성
|
||||
await knex.schema.createTable('visit_purpose_types', function(table) {
|
||||
table.increments('purpose_id').primary().comment('방문 목적 ID');
|
||||
table.string('purpose_name', 100).notNullable().comment('방문 목적명');
|
||||
table.integer('display_order').defaultTo(0).comment('표시 순서');
|
||||
table.boolean('is_active').defaultTo(true).comment('활성 여부');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
// 초기 데이터 삽입
|
||||
await knex('visit_purpose_types').insert([
|
||||
{ purpose_name: '외주작업', display_order: 1, is_active: true },
|
||||
{ purpose_name: '검사', display_order: 2, is_active: true },
|
||||
{ purpose_name: '견학', display_order: 3, is_active: true },
|
||||
{ purpose_name: '기타', display_order: 99, is_active: true }
|
||||
]);
|
||||
|
||||
// 2. 출입 신청 테이블 생성
|
||||
await knex.schema.createTable('workplace_visit_requests', function(table) {
|
||||
table.increments('request_id').primary().comment('신청 ID');
|
||||
|
||||
// 신청자 정보
|
||||
table.integer('requester_id').notNullable().comment('신청자 user_id');
|
||||
|
||||
// 방문자 정보
|
||||
table.string('visitor_company', 200).notNullable().comment('방문자 소속 (회사명 또는 "일용직")');
|
||||
table.integer('visitor_count').defaultTo(1).comment('방문 인원');
|
||||
|
||||
// 방문 장소
|
||||
table.integer('category_id').unsigned().notNullable().comment('방문 구역 (공장)');
|
||||
table.integer('workplace_id').unsigned().notNullable().comment('방문 작업장');
|
||||
|
||||
// 방문 일시
|
||||
table.date('visit_date').notNullable().comment('방문 날짜');
|
||||
table.time('visit_time').notNullable().comment('방문 시간');
|
||||
|
||||
// 방문 목적
|
||||
table.integer('purpose_id').unsigned().notNullable().comment('방문 목적 ID');
|
||||
table.text('notes').nullable().comment('비고');
|
||||
|
||||
// 상태 관리
|
||||
table.enum('status', ['pending', 'approved', 'rejected', 'training_completed'])
|
||||
.defaultTo('pending')
|
||||
.comment('신청 상태');
|
||||
|
||||
// 승인 정보
|
||||
table.integer('approved_by').nullable().comment('승인자 user_id');
|
||||
table.timestamp('approved_at').nullable().comment('승인 시간');
|
||||
table.text('rejection_reason').nullable().comment('반려 사유');
|
||||
|
||||
// 타임스탬프
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 외래키
|
||||
table.foreign('requester_id')
|
||||
.references('user_id')
|
||||
.inTable('users')
|
||||
.onDelete('RESTRICT')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('category_id')
|
||||
.references('category_id')
|
||||
.inTable('workplace_categories')
|
||||
.onDelete('RESTRICT')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('workplace_id')
|
||||
.references('workplace_id')
|
||||
.inTable('workplaces')
|
||||
.onDelete('RESTRICT')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('purpose_id')
|
||||
.references('purpose_id')
|
||||
.inTable('visit_purpose_types')
|
||||
.onDelete('RESTRICT')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('approved_by')
|
||||
.references('user_id')
|
||||
.inTable('users')
|
||||
.onDelete('SET NULL')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스
|
||||
table.index('visit_date', 'idx_visit_date');
|
||||
table.index('status', 'idx_status');
|
||||
table.index(['visit_date', 'status'], 'idx_visit_date_status');
|
||||
});
|
||||
|
||||
// 3. 안전교육 기록 테이블 생성
|
||||
await knex.schema.createTable('safety_training_records', function(table) {
|
||||
table.increments('training_id').primary().comment('교육 기록 ID');
|
||||
|
||||
table.integer('request_id').unsigned().notNullable().comment('출입 신청 ID');
|
||||
|
||||
// 교육 진행 정보
|
||||
table.integer('trainer_id').notNullable().comment('교육 진행자 user_id');
|
||||
table.date('training_date').notNullable().comment('교육 날짜');
|
||||
table.time('training_start_time').notNullable().comment('교육 시작 시간');
|
||||
table.time('training_end_time').nullable().comment('교육 종료 시간');
|
||||
|
||||
// 교육 내용
|
||||
table.text('training_topics').nullable().comment('교육 내용 (JSON 배열)');
|
||||
|
||||
// 서명 데이터 (Base64 이미지)
|
||||
table.text('signature_data', 'longtext').nullable().comment('교육 이수자 서명 (Base64 PNG)');
|
||||
|
||||
// 완료 정보
|
||||
table.timestamp('completed_at').nullable().comment('교육 완료 시간');
|
||||
|
||||
// 타임스탬프
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
// 외래키
|
||||
table.foreign('request_id')
|
||||
.references('request_id')
|
||||
.inTable('workplace_visit_requests')
|
||||
.onDelete('CASCADE')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
table.foreign('trainer_id')
|
||||
.references('user_id')
|
||||
.inTable('users')
|
||||
.onDelete('RESTRICT')
|
||||
.onUpdate('CASCADE');
|
||||
|
||||
// 인덱스
|
||||
table.index('training_date', 'idx_training_date');
|
||||
table.index('request_id', 'idx_request_id');
|
||||
});
|
||||
|
||||
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 생성 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
// 역순으로 테이블 삭제
|
||||
await knex.schema.dropTableIfExists('safety_training_records');
|
||||
await knex.schema.dropTableIfExists('workplace_visit_requests');
|
||||
await knex.schema.dropTableIfExists('visit_purpose_types');
|
||||
|
||||
console.log('✅ 출입 신청 및 안전교육 시스템 테이블 삭제 완료');
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 마이그레이션: 출입 신청 및 안전관리 페이지 등록
|
||||
*/
|
||||
|
||||
exports.up = async function(knex) {
|
||||
// 1. 출입 신청 페이지 등록
|
||||
await knex('pages').insert({
|
||||
page_key: 'visit-request',
|
||||
page_name: '출입 신청',
|
||||
page_path: '/pages/work/visit-request.html',
|
||||
category: 'work',
|
||||
description: '작업장 출입 신청 및 안전교육 신청',
|
||||
is_admin_only: 0,
|
||||
display_order: 15
|
||||
});
|
||||
|
||||
// 2. 안전관리 대시보드 페이지 등록
|
||||
await knex('pages').insert({
|
||||
page_key: 'safety-management',
|
||||
page_name: '안전관리',
|
||||
page_path: '/pages/admin/safety-management.html',
|
||||
category: 'admin',
|
||||
description: '출입 신청 승인 및 안전교육 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 60
|
||||
});
|
||||
|
||||
// 3. 안전교육 진행 페이지 등록
|
||||
await knex('pages').insert({
|
||||
page_key: 'safety-training-conduct',
|
||||
page_name: '안전교육 진행',
|
||||
page_path: '/pages/admin/safety-training-conduct.html',
|
||||
category: 'admin',
|
||||
description: '안전교육 실시 및 서명 관리',
|
||||
is_admin_only: 0,
|
||||
display_order: 61
|
||||
});
|
||||
|
||||
console.log('✅ 출입 신청 및 안전관리 페이지 등록 완료');
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex('pages').whereIn('page_key', [
|
||||
'visit-request',
|
||||
'safety-management',
|
||||
'safety-training-conduct'
|
||||
]).delete();
|
||||
|
||||
console.log('✅ 출입 신청 및 안전관리 페이지 삭제 완료');
|
||||
};
|
||||
@@ -297,7 +297,7 @@ class AttendanceModel {
|
||||
|
||||
// 휴가 유형 정보 조회
|
||||
const [vacationTypes] = await db.execute(
|
||||
'SELECT id, type_code, type_name, hours_deduction, description, is_active, created_at, updated_at FROM vacation_types WHERE type_code = ?',
|
||||
'SELECT id, type_code, type_name, deduct_days, is_active, created_at, updated_at FROM vacation_types WHERE type_code = ?',
|
||||
[vacationType]
|
||||
);
|
||||
|
||||
@@ -391,7 +391,7 @@ class AttendanceModel {
|
||||
static async getVacationTypes() {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(
|
||||
'SELECT id, type_code, type_name, hours_deduction, description, is_active, created_at, updated_at FROM vacation_types WHERE is_active = TRUE ORDER BY hours_deduction DESC'
|
||||
'SELECT id, type_code, type_name, deduct_days, is_active, created_at, updated_at FROM vacation_types WHERE is_active = TRUE ORDER BY deduct_days DESC'
|
||||
);
|
||||
return rows;
|
||||
}
|
||||
@@ -458,6 +458,68 @@ class AttendanceModel {
|
||||
const [rows] = await db.execute(query, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// 출근 체크 기록 생성 또는 업데이트
|
||||
static async upsertCheckin(checkinData) {
|
||||
const db = await getDb();
|
||||
const { worker_id, record_date, is_present } = checkinData;
|
||||
|
||||
// 해당 날짜에 기록이 있는지 확인
|
||||
const [existing] = await db.execute(
|
||||
'SELECT id FROM daily_attendance_records WHERE worker_id = ? AND record_date = ?',
|
||||
[worker_id, record_date]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// 업데이트
|
||||
await db.execute(
|
||||
'UPDATE daily_attendance_records SET is_present = ? WHERE id = ?',
|
||||
[is_present, existing[0].id]
|
||||
);
|
||||
return existing[0].id;
|
||||
} else {
|
||||
// 새로 생성 (기본값으로)
|
||||
const [result] = await db.execute(
|
||||
`INSERT INTO daily_attendance_records
|
||||
(worker_id, record_date, is_present, attendance_type_id, created_by)
|
||||
VALUES (?, ?, ?, 1, 1)`,
|
||||
[worker_id, record_date, is_present]
|
||||
);
|
||||
return result.insertId;
|
||||
}
|
||||
}
|
||||
|
||||
// 특정 날짜의 출근 체크 목록 조회 (휴가 정보 포함)
|
||||
static async getCheckinList(date) {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
w.worker_id,
|
||||
w.worker_name,
|
||||
w.job_type,
|
||||
w.employment_status,
|
||||
COALESCE(dar.is_present, TRUE) as is_present,
|
||||
dar.id as record_id,
|
||||
vr.request_id as vacation_request_id,
|
||||
vr.status as vacation_status,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
vr.days_used as vacation_days
|
||||
FROM workers w
|
||||
LEFT JOIN daily_attendance_records dar
|
||||
ON w.worker_id = dar.worker_id AND dar.record_date = ?
|
||||
LEFT JOIN vacation_requests vr
|
||||
ON w.worker_id = vr.worker_id
|
||||
AND ? BETWEEN vr.start_date AND vr.end_date
|
||||
AND vr.status = 'approved'
|
||||
LEFT JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
WHERE w.employment_status = 'employed'
|
||||
ORDER BY w.worker_name
|
||||
`;
|
||||
|
||||
const [rows] = await db.execute(query, [date, date]);
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttendanceModel;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
288
api.hyungi.net/models/vacationBalanceModel.js
Normal file
288
api.hyungi.net/models/vacationBalanceModel.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* vacationBalanceModel.js
|
||||
* 휴가 잔액 관련 데이터베이스 쿼리 모델
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const vacationBalanceModel = {
|
||||
/**
|
||||
* 특정 작업자의 모든 휴가 잔액 조회 (특정 연도)
|
||||
*/
|
||||
async getByWorkerAndYear(workerId, year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority,
|
||||
vt.is_special
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ? AND vbd.year = ?
|
||||
ORDER BY vt.priority ASC, vt.type_name ASC
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 특정 휴가 유형 잔액 조회
|
||||
*/
|
||||
async getByWorkerTypeYear(workerId, vacationTypeId, year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
vt.type_name,
|
||||
vt.type_code
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ?
|
||||
AND vbd.vacation_type_id = ?
|
||||
AND vbd.year = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, vacationTypeId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 모든 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
* - 연간 연차 현황 차트용
|
||||
*/
|
||||
async getAllByYear(year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
w.worker_name,
|
||||
w.employment_status,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN workers w ON vbd.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.year = ?
|
||||
AND w.employment_status = 'employed'
|
||||
ORDER BY w.worker_name ASC, vt.priority ASC
|
||||
`;
|
||||
const [rows] = await db.query(query, [year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 생성
|
||||
*/
|
||||
async create(balanceData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `INSERT INTO vacation_balance_details SET ?`;
|
||||
const [rows] = await db.query(query, balanceData);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 수정
|
||||
*/
|
||||
async update(id, updateData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_balance_details SET ? WHERE id = ?`;
|
||||
const [rows] = await db.query(query, [updateData, id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 잔액 삭제
|
||||
*/
|
||||
async delete(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `DELETE FROM vacation_balance_details WHERE id = ?`;
|
||||
const [rows] = await db.query(query, [id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자의 휴가 사용 일수 업데이트 (차감)
|
||||
* - 휴가 신청 승인 시 호출
|
||||
*/
|
||||
async deductDays(workerId, vacationTypeId, year, daysToDeduct, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE vacation_balance_details
|
||||
SET used_days = used_days + ?,
|
||||
updated_at = NOW()
|
||||
WHERE worker_id = ?
|
||||
AND vacation_type_id = ?
|
||||
AND year = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [daysToDeduct, workerId, vacationTypeId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자의 휴가 사용 일수 복구 (취소)
|
||||
* - 휴가 신청 취소/거부 시 호출
|
||||
*/
|
||||
async restoreDays(workerId, vacationTypeId, year, daysToRestore, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE vacation_balance_details
|
||||
SET used_days = GREATEST(0, used_days - ?),
|
||||
updated_at = NOW()
|
||||
WHERE worker_id = ?
|
||||
AND vacation_type_id = ?
|
||||
AND year = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [daysToRestore, workerId, vacationTypeId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 사용 가능한 휴가 일수 확인
|
||||
* - 우선순위가 높은 순서대로 차감 가능 여부 확인
|
||||
*/
|
||||
async getAvailableVacationDays(workerId, year, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.id,
|
||||
vbd.vacation_type_id,
|
||||
vt.type_name,
|
||||
vt.type_code,
|
||||
vt.priority,
|
||||
vbd.total_days,
|
||||
vbd.used_days,
|
||||
vbd.remaining_days
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.worker_id = ?
|
||||
AND vbd.year = ?
|
||||
AND vbd.remaining_days > 0
|
||||
ORDER BY vt.priority ASC
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, year]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 작업자별 휴가 잔액 일괄 생성 (연도별)
|
||||
* - 매년 초 또는 입사 시 사용
|
||||
*/
|
||||
async bulkCreate(balances, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
if (!balances || balances.length === 0) {
|
||||
return callback(new Error('생성할 휴가 잔액 데이터가 없습니다'));
|
||||
}
|
||||
|
||||
const query = `INSERT INTO vacation_balance_details
|
||||
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
|
||||
VALUES ?`;
|
||||
|
||||
const values = balances.map(b => [
|
||||
b.worker_id,
|
||||
b.vacation_type_id,
|
||||
b.year,
|
||||
b.total_days || 0,
|
||||
b.used_days || 0,
|
||||
b.notes || null,
|
||||
b.created_by
|
||||
]);
|
||||
|
||||
const [rows] = await db.query(query, [values]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 근속년수 기반 연차 일수 계산 (한국 근로기준법)
|
||||
* @param {Date} hireDate - 입사일
|
||||
* @param {number} targetYear - 대상 연도
|
||||
* @returns {number} - 부여받을 연차 일수
|
||||
*/
|
||||
calculateAnnualLeaveDays(hireDate, targetYear) {
|
||||
const hire = new Date(hireDate);
|
||||
const targetDate = new Date(targetYear, 0, 1);
|
||||
|
||||
// 근속 월수 계산
|
||||
const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12
|
||||
+ (targetDate.getMonth() - hire.getMonth());
|
||||
|
||||
// 1년 미만: 월 1일
|
||||
if (monthsDiff < 12) {
|
||||
return Math.floor(monthsDiff);
|
||||
}
|
||||
|
||||
// 1년 이상: 15일 기본 + 2년마다 1일 추가 (최대 25일)
|
||||
const yearsWorked = Math.floor(monthsDiff / 12);
|
||||
const additionalDays = Math.floor((yearsWorked - 1) / 2);
|
||||
|
||||
return Math.min(15 + additionalDays, 25);
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 ID로 휴가 잔액 조회
|
||||
*/
|
||||
async getById(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vbd.*,
|
||||
w.worker_name,
|
||||
vt.type_name,
|
||||
vt.type_code
|
||||
FROM vacation_balance_details vbd
|
||||
INNER JOIN workers w ON vbd.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vbd.vacation_type_id = vt.id
|
||||
WHERE vbd.id = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationBalanceModel;
|
||||
271
api.hyungi.net/models/vacationRequestModel.js
Normal file
271
api.hyungi.net/models/vacationRequestModel.js
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* vacationRequestModel.js
|
||||
* 휴가 신청 관련 데이터베이스 쿼리 모델
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const vacationRequestModel = {
|
||||
/**
|
||||
* 휴가 신청 생성
|
||||
*/
|
||||
async create(requestData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `INSERT INTO vacation_requests SET ?`;
|
||||
const [result] = await db.query(query, requestData);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 목록 조회 (필터링 지원)
|
||||
*/
|
||||
async getAll(filters = {}, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
vr.*,
|
||||
w.worker_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.deduct_days as vacation_deduct_days,
|
||||
requester.name as requester_name,
|
||||
reviewer.name as reviewer_name
|
||||
FROM vacation_requests vr
|
||||
INNER JOIN workers w ON vr.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN users requester ON vr.requested_by = requester.user_id
|
||||
LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// 작업자 필터
|
||||
if (filters.worker_id) {
|
||||
query += ` AND vr.worker_id = ?`;
|
||||
params.push(filters.worker_id);
|
||||
}
|
||||
|
||||
// 상태 필터
|
||||
if (filters.status) {
|
||||
query += ` AND vr.status = ?`;
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
// 기간 필터
|
||||
if (filters.start_date) {
|
||||
query += ` AND vr.start_date >= ?`;
|
||||
params.push(filters.start_date);
|
||||
}
|
||||
|
||||
if (filters.end_date) {
|
||||
query += ` AND vr.end_date <= ?`;
|
||||
params.push(filters.end_date);
|
||||
}
|
||||
|
||||
// 휴가 유형 필터
|
||||
if (filters.vacation_type_id) {
|
||||
query += ` AND vr.vacation_type_id = ?`;
|
||||
params.push(filters.vacation_type_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY vr.created_at DESC`;
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 휴가 신청 조회
|
||||
*/
|
||||
async getById(requestId, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vr.*,
|
||||
w.worker_name,
|
||||
w.phone_number as worker_phone,
|
||||
w.email as worker_email,
|
||||
vt.type_name as vacation_type_name,
|
||||
vt.type_code as vacation_type_code,
|
||||
vt.deduct_days as vacation_deduct_days,
|
||||
requester.name as requester_name,
|
||||
requester.username as requester_username,
|
||||
reviewer.name as reviewer_name,
|
||||
reviewer.username as reviewer_username
|
||||
FROM vacation_requests vr
|
||||
INNER JOIN workers w ON vr.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN users requester ON vr.requested_by = requester.user_id
|
||||
LEFT JOIN users reviewer ON vr.reviewed_by = reviewer.user_id
|
||||
WHERE vr.request_id = ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [requestId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 수정
|
||||
*/
|
||||
async update(requestId, updateData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_requests SET ? WHERE request_id = ?`;
|
||||
const [result] = await db.query(query, [updateData, requestId]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 삭제
|
||||
*/
|
||||
async delete(requestId, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `DELETE FROM vacation_requests WHERE request_id = ?`;
|
||||
const [result] = await db.query(query, [requestId]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 신청 승인/거부
|
||||
*/
|
||||
async updateStatus(requestId, statusData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
UPDATE vacation_requests
|
||||
SET
|
||||
status = ?,
|
||||
reviewed_by = ?,
|
||||
reviewed_at = NOW(),
|
||||
review_note = ?
|
||||
WHERE request_id = ?
|
||||
`;
|
||||
const [result] = await db.query(query, [
|
||||
statusData.status,
|
||||
statusData.reviewed_by,
|
||||
statusData.review_note || null,
|
||||
requestId
|
||||
]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 대기 중인 휴가 신청 수
|
||||
*/
|
||||
async getPendingCount(workerId, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM vacation_requests
|
||||
WHERE worker_id = ? AND status = 'pending'
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 작업자의 승인된 휴가 일수 합계 (특정 기간)
|
||||
*/
|
||||
async getApprovedDaysInPeriod(workerId, startDate, endDate, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT COALESCE(SUM(days_used), 0) as total_days
|
||||
FROM vacation_requests
|
||||
WHERE worker_id = ?
|
||||
AND status = 'approved'
|
||||
AND start_date >= ?
|
||||
AND end_date <= ?
|
||||
`;
|
||||
const [rows] = await db.query(query, [workerId, startDate, endDate]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 기간 중복 체크
|
||||
*/
|
||||
async checkOverlap(workerId, startDate, endDate, excludeRequestId = null, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT COUNT(*) as count
|
||||
FROM vacation_requests
|
||||
WHERE worker_id = ?
|
||||
AND status IN ('pending', 'approved')
|
||||
AND (
|
||||
(start_date <= ? AND end_date >= ?) OR
|
||||
(start_date <= ? AND end_date >= ?) OR
|
||||
(start_date >= ? AND end_date <= ?)
|
||||
)
|
||||
`;
|
||||
const params = [workerId, startDate, startDate, endDate, endDate, startDate, endDate];
|
||||
|
||||
if (excludeRequestId) {
|
||||
query += ` AND request_id != ?`;
|
||||
params.push(excludeRequestId);
|
||||
}
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 모든 대기 중인 휴가 신청 (관리자용)
|
||||
*/
|
||||
async getAllPending(callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT
|
||||
vr.*,
|
||||
w.worker_name,
|
||||
vt.type_name as vacation_type_name,
|
||||
requester.name as requester_name
|
||||
FROM vacation_requests vr
|
||||
INNER JOIN workers w ON vr.worker_id = w.worker_id
|
||||
INNER JOIN vacation_types vt ON vr.vacation_type_id = vt.id
|
||||
LEFT JOIN users requester ON vr.requested_by = requester.user_id
|
||||
WHERE vr.status = 'pending'
|
||||
ORDER BY vr.created_at ASC
|
||||
`;
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationRequestModel;
|
||||
132
api.hyungi.net/models/vacationTypeModel.js
Normal file
132
api.hyungi.net/models/vacationTypeModel.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* vacationTypeModel.js
|
||||
* 휴가 유형 관련 데이터베이스 쿼리 모델
|
||||
*/
|
||||
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
const vacationTypeModel = {
|
||||
/**
|
||||
* 모든 활성 휴가 유형 조회 (우선순위 순서대로)
|
||||
*/
|
||||
async getAll(callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM vacation_types
|
||||
WHERE is_active = 1
|
||||
ORDER BY priority ASC, id ASC
|
||||
`;
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 시스템 기본 휴가 유형만 조회
|
||||
*/
|
||||
async getSystemTypes(callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM vacation_types
|
||||
WHERE is_system = 1 AND is_active = 1
|
||||
ORDER BY priority ASC
|
||||
`;
|
||||
const [rows] = await db.query(query);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 ID로 휴가 유형 조회
|
||||
*/
|
||||
async getById(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `SELECT * FROM vacation_types WHERE id = ?`;
|
||||
const [rows] = await db.query(query, [id]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 코드로 조회
|
||||
*/
|
||||
async getByCode(code, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `SELECT * FROM vacation_types WHERE type_code = ?`;
|
||||
const [rows] = await db.query(query, [code]);
|
||||
callback(null, rows);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 생성
|
||||
*/
|
||||
async create(typeData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `INSERT INTO vacation_types SET ?`;
|
||||
const [result] = await db.query(query, typeData);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 수정
|
||||
*/
|
||||
async update(id, updateData, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_types SET ? WHERE id = ?`;
|
||||
const [result] = await db.query(query, [updateData, id]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 휴가 유형 삭제 (논리적 삭제 - is_active = 0)
|
||||
*/
|
||||
async delete(id, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_types SET is_active = 0, updated_at = NOW() WHERE id = ?`;
|
||||
const [result] = await db.query(query, [id]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 우선순위 업데이트
|
||||
*/
|
||||
async updatePriority(id, priority, callback) {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const query = `UPDATE vacation_types SET priority = ?, updated_at = NOW() WHERE id = ?`;
|
||||
const [result] = await db.query(query, [priority, id]);
|
||||
callback(null, result);
|
||||
} catch (error) {
|
||||
callback(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = vacationTypeModel;
|
||||
505
api.hyungi.net/models/visitRequestModel.js
Normal file
505
api.hyungi.net/models/visitRequestModel.js
Normal file
@@ -0,0 +1,505 @@
|
||||
const { getDb } = require('../dbPool');
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
/**
|
||||
* 출입 신청 생성
|
||||
*/
|
||||
const createVisitRequest = async (requestData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
requester_id,
|
||||
visitor_company,
|
||||
visitor_count = 1,
|
||||
category_id,
|
||||
workplace_id,
|
||||
visit_date,
|
||||
visit_time,
|
||||
purpose_id,
|
||||
notes = null
|
||||
} = requestData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplace_visit_requests
|
||||
(requester_id, visitor_company, visitor_count, category_id, workplace_id,
|
||||
visit_date, visit_time, purpose_id, notes)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[requester_id, visitor_company, visitor_count, category_id, workplace_id,
|
||||
visit_date, visit_time, purpose_id, notes]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 목록 조회 (필터 옵션 포함)
|
||||
*/
|
||||
const getAllVisitRequests = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
|
||||
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
|
||||
vr.purpose_id, vr.notes, vr.status,
|
||||
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
||||
vr.created_at, vr.updated_at,
|
||||
u.username as requester_name, u.name as requester_full_name,
|
||||
wc.category_name, w.workplace_name,
|
||||
vpt.purpose_name,
|
||||
approver.username as approver_name
|
||||
FROM workplace_visit_requests vr
|
||||
INNER JOIN users u ON vr.requester_id = u.user_id
|
||||
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
||||
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
||||
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
||||
LEFT JOIN users approver ON vr.approved_by = approver.user_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
// 필터 적용
|
||||
if (filters.status) {
|
||||
query += ` AND vr.status = ?`;
|
||||
params.push(filters.status);
|
||||
}
|
||||
|
||||
if (filters.visit_date) {
|
||||
query += ` AND vr.visit_date = ?`;
|
||||
params.push(filters.visit_date);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
query += ` AND vr.visit_date BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.requester_id) {
|
||||
query += ` AND vr.requester_id = ?`;
|
||||
params.push(filters.requester_id);
|
||||
}
|
||||
|
||||
if (filters.category_id) {
|
||||
query += ` AND vr.category_id = ?`;
|
||||
params.push(filters.category_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY vr.visit_date DESC, vr.visit_time DESC, vr.created_at DESC`;
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 상세 조회
|
||||
*/
|
||||
const getVisitRequestById = async (requestId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
vr.request_id, vr.requester_id, vr.visitor_company, vr.visitor_count,
|
||||
vr.category_id, vr.workplace_id, vr.visit_date, vr.visit_time,
|
||||
vr.purpose_id, vr.notes, vr.status,
|
||||
vr.approved_by, vr.approved_at, vr.rejection_reason,
|
||||
vr.created_at, vr.updated_at,
|
||||
u.username as requester_name, u.name as requester_full_name,
|
||||
wc.category_name, w.workplace_name,
|
||||
vpt.purpose_name,
|
||||
approver.username as approver_name
|
||||
FROM workplace_visit_requests vr
|
||||
INNER JOIN users u ON vr.requester_id = u.user_id
|
||||
INNER JOIN workplace_categories wc ON vr.category_id = wc.category_id
|
||||
INNER JOIN workplaces w ON vr.workplace_id = w.workplace_id
|
||||
INNER JOIN visit_purpose_types vpt ON vr.purpose_id = vpt.purpose_id
|
||||
LEFT JOIN users approver ON vr.approved_by = approver.user_id
|
||||
WHERE vr.request_id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 수정
|
||||
*/
|
||||
const updateVisitRequest = async (requestId, requestData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
visitor_company,
|
||||
visitor_count,
|
||||
category_id,
|
||||
workplace_id,
|
||||
visit_date,
|
||||
visit_time,
|
||||
purpose_id,
|
||||
notes
|
||||
} = requestData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET visitor_company = ?, visitor_count = ?, category_id = ?, workplace_id = ?,
|
||||
visit_date = ?, visit_time = ?, purpose_id = ?, notes = ?, updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[visitor_company, visitor_count, category_id, workplace_id,
|
||||
visit_date, visit_time, purpose_id, notes, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 삭제
|
||||
*/
|
||||
const deleteVisitRequest = async (requestId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_visit_requests WHERE request_id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 승인
|
||||
*/
|
||||
const approveVisitRequest = async (requestId, approvedBy, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET status = 'approved', approved_by = ?, approved_at = NOW(), updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[approvedBy, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 반려
|
||||
*/
|
||||
const rejectVisitRequest = async (requestId, rejectionData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { approved_by, rejection_reason } = rejectionData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET status = 'rejected', approved_by = ?, approved_at = NOW(),
|
||||
rejection_reason = ?, updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[approved_by, rejection_reason, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출입 신청 상태 변경
|
||||
*/
|
||||
const updateVisitRequestStatus = async (requestId, status, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_visit_requests
|
||||
SET status = ?, updated_at = NOW()
|
||||
WHERE request_id = ?`,
|
||||
[status, requestId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
/**
|
||||
* 모든 방문 목적 조회
|
||||
*/
|
||||
const getAllVisitPurposes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
||||
FROM visit_purpose_types
|
||||
ORDER BY display_order ASC, purpose_id ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 활성 방문 목적만 조회
|
||||
*/
|
||||
const getActiveVisitPurposes = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
|
||||
FROM visit_purpose_types
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC, purpose_id ASC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 추가
|
||||
*/
|
||||
const createVisitPurpose = async (purposeData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { purpose_name, display_order = 0, is_active = true } = purposeData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO visit_purpose_types (purpose_name, display_order, is_active)
|
||||
VALUES (?, ?, ?)`,
|
||||
[purpose_name, display_order, is_active]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 수정
|
||||
*/
|
||||
const updateVisitPurpose = async (purposeId, purposeData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const { purpose_name, display_order, is_active } = purposeData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE visit_purpose_types
|
||||
SET purpose_name = ?, display_order = ?, is_active = ?
|
||||
WHERE purpose_id = ?`,
|
||||
[purpose_name, display_order, is_active, purposeId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 방문 목적 삭제
|
||||
*/
|
||||
const deleteVisitPurpose = async (purposeId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM visit_purpose_types WHERE purpose_id = ?`,
|
||||
[purposeId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
/**
|
||||
* 안전교육 기록 생성
|
||||
*/
|
||||
const createTrainingRecord = async (trainingData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
request_id,
|
||||
trainer_id,
|
||||
training_date,
|
||||
training_start_time,
|
||||
training_end_time = null,
|
||||
training_topics = null
|
||||
} = trainingData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO safety_training_records
|
||||
(request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 특정 출입 신청의 안전교육 기록 조회
|
||||
*/
|
||||
const getTrainingRecordByRequestId = async (requestId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT
|
||||
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
||||
str.training_start_time, str.training_end_time, str.training_topics,
|
||||
str.signature_data, str.completed_at, str.created_at, str.updated_at,
|
||||
u.username as trainer_name, u.name as trainer_full_name
|
||||
FROM safety_training_records str
|
||||
INNER JOIN users u ON str.trainer_id = u.user_id
|
||||
WHERE str.request_id = ?`,
|
||||
[requestId]
|
||||
);
|
||||
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 기록 수정
|
||||
*/
|
||||
const updateTrainingRecord = async (trainingId, trainingData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
training_date,
|
||||
training_start_time,
|
||||
training_end_time,
|
||||
training_topics
|
||||
} = trainingData;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE safety_training_records
|
||||
SET training_date = ?, training_start_time = ?, training_end_time = ?,
|
||||
training_topics = ?, updated_at = NOW()
|
||||
WHERE training_id = ?`,
|
||||
[training_date, training_start_time, training_end_time, training_topics, trainingId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 완료 (서명 포함)
|
||||
*/
|
||||
const completeTraining = async (trainingId, signatureData, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`UPDATE safety_training_records
|
||||
SET signature_data = ?, completed_at = NOW(), updated_at = NOW()
|
||||
WHERE training_id = ?`,
|
||||
[signatureData, trainingId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 안전교육 목록 조회 (날짜별 필터)
|
||||
*/
|
||||
const getTrainingRecords = async (filters = {}, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
let query = `
|
||||
SELECT
|
||||
str.training_id, str.request_id, str.trainer_id, str.training_date,
|
||||
str.training_start_time, str.training_end_time, str.training_topics,
|
||||
str.completed_at, str.created_at, str.updated_at,
|
||||
u.username as trainer_name, u.name as trainer_full_name,
|
||||
vr.visitor_company, vr.visitor_count, vr.visit_date
|
||||
FROM safety_training_records str
|
||||
INNER JOIN users u ON str.trainer_id = u.user_id
|
||||
INNER JOIN workplace_visit_requests vr ON str.request_id = vr.request_id
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
|
||||
if (filters.training_date) {
|
||||
query += ` AND str.training_date = ?`;
|
||||
params.push(filters.training_date);
|
||||
}
|
||||
|
||||
if (filters.start_date && filters.end_date) {
|
||||
query += ` AND str.training_date BETWEEN ? AND ?`;
|
||||
params.push(filters.start_date, filters.end_date);
|
||||
}
|
||||
|
||||
if (filters.trainer_id) {
|
||||
query += ` AND str.trainer_id = ?`;
|
||||
params.push(filters.trainer_id);
|
||||
}
|
||||
|
||||
query += ` ORDER BY str.training_date DESC, str.training_start_time DESC`;
|
||||
|
||||
const [rows] = await db.query(query, params);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 출입 신청
|
||||
createVisitRequest,
|
||||
getAllVisitRequests,
|
||||
getVisitRequestById,
|
||||
updateVisitRequest,
|
||||
deleteVisitRequest,
|
||||
approveVisitRequest,
|
||||
rejectVisitRequest,
|
||||
updateVisitRequestStatus,
|
||||
|
||||
// 방문 목적
|
||||
getAllVisitPurposes,
|
||||
getActiveVisitPurposes,
|
||||
createVisitPurpose,
|
||||
updateVisitPurpose,
|
||||
deleteVisitPurpose,
|
||||
|
||||
// 안전교육
|
||||
createTrainingRecord,
|
||||
getTrainingRecordByRequestId,
|
||||
updateTrainingRecord,
|
||||
completeTraining,
|
||||
getTrainingRecords
|
||||
};
|
||||
@@ -35,7 +35,7 @@ const getAllCategories = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_name, description, display_order, is_active, created_at, updated_at
|
||||
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
|
||||
FROM workplace_categories
|
||||
ORDER BY display_order ASC, category_id ASC`
|
||||
);
|
||||
@@ -52,7 +52,7 @@ const getActiveCategories = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_name, description, display_order, is_active, created_at, updated_at
|
||||
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
|
||||
FROM workplace_categories
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY display_order ASC, category_id ASC`
|
||||
@@ -70,7 +70,7 @@ const getCategoryById = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT category_id, category_name, description, display_order, is_active, created_at, updated_at
|
||||
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
|
||||
FROM workplace_categories
|
||||
WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
@@ -91,14 +91,15 @@ const updateCategory = async (categoryId, category, callback) => {
|
||||
category_name,
|
||||
description,
|
||||
display_order,
|
||||
is_active
|
||||
is_active,
|
||||
layout_image
|
||||
} = category;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_categories
|
||||
SET category_name = ?, description = ?, display_order = ?, is_active = ?, updated_at = NOW()
|
||||
SET category_name = ?, description = ?, display_order = ?, is_active = ?, layout_image = ?, updated_at = NOW()
|
||||
WHERE category_id = ?`,
|
||||
[category_name, description, display_order, is_active, categoryId]
|
||||
[category_name, description, display_order, is_active, layout_image, categoryId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
@@ -135,14 +136,16 @@ const createWorkplace = async (workplace, callback) => {
|
||||
category_id = null,
|
||||
workplace_name,
|
||||
description = null,
|
||||
is_active = true
|
||||
is_active = true,
|
||||
workplace_purpose = null,
|
||||
display_priority = 0
|
||||
} = workplace;
|
||||
|
||||
const [result] = await db.query(
|
||||
`INSERT INTO workplaces
|
||||
(category_id, workplace_name, description, is_active)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
[category_id, workplace_name, description, is_active]
|
||||
(category_id, workplace_name, description, is_active, workplace_purpose, display_priority)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[category_id, workplace_name, description, is_active, workplace_purpose, display_priority]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
@@ -158,12 +161,12 @@ const getAllWorkplaces = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active,
|
||||
w.created_at, w.updated_at,
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.workplace_purpose, w.display_priority, w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
|
||||
ORDER BY wc.display_order ASC, w.workplace_id DESC`
|
||||
ORDER BY wc.display_order ASC, w.display_priority ASC, w.workplace_id DESC`
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
@@ -178,7 +181,7 @@ const getActiveWorkplaces = async (callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active,
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
@@ -199,7 +202,7 @@ const getWorkplacesByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active,
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
@@ -221,7 +224,7 @@ const getWorkplaceById = async (workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active,
|
||||
`SELECT w.workplace_id, w.category_id, w.workplace_name, w.description, w.is_active, w.workplace_purpose, w.display_priority,
|
||||
w.created_at, w.updated_at,
|
||||
wc.category_name
|
||||
FROM workplaces w
|
||||
@@ -245,14 +248,17 @@ const updateWorkplace = async (workplaceId, workplace, callback) => {
|
||||
category_id,
|
||||
workplace_name,
|
||||
description,
|
||||
is_active
|
||||
is_active,
|
||||
workplace_purpose,
|
||||
display_priority
|
||||
} = workplace;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplaces
|
||||
SET category_id = ?, workplace_name = ?, description = ?, is_active = ?, updated_at = NOW()
|
||||
SET category_id = ?, workplace_name = ?, description = ?, is_active = ?,
|
||||
workplace_purpose = ?, display_priority = ?, updated_at = NOW()
|
||||
WHERE workplace_id = ?`,
|
||||
[category_id, workplace_name, description, is_active, workplaceId]
|
||||
[category_id, workplace_name, description, is_active, workplace_purpose, display_priority, workplaceId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
@@ -277,6 +283,134 @@ const deleteWorkplace = async (workplaceId, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== 작업장 지도 영역 관련 ====================
|
||||
|
||||
/**
|
||||
* 작업장 지도 영역 생성
|
||||
*/
|
||||
const createMapRegion = async (region, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
workplace_id,
|
||||
category_id,
|
||||
x_start,
|
||||
y_start,
|
||||
x_end,
|
||||
y_end,
|
||||
shape = 'rect',
|
||||
polygon_points = null
|
||||
} = region;
|
||||
|
||||
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, polygon_points]
|
||||
);
|
||||
|
||||
callback(null, result.insertId);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 카테고리(공장)별 지도 영역 조회
|
||||
*/
|
||||
const getMapRegionsByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT mr.*, w.workplace_name, w.description
|
||||
FROM workplace_map_regions mr
|
||||
INNER JOIN workplaces w ON mr.workplace_id = w.workplace_id
|
||||
WHERE mr.category_id = ? AND w.is_active = TRUE
|
||||
ORDER BY mr.region_id ASC`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, rows);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장별 지도 영역 조회
|
||||
*/
|
||||
const getMapRegionByWorkplace = async (workplaceId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [rows] = await db.query(
|
||||
`SELECT * FROM workplace_map_regions WHERE workplace_id = ?`,
|
||||
[workplaceId]
|
||||
);
|
||||
callback(null, rows[0]);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 지도 영역 수정
|
||||
*/
|
||||
const updateMapRegion = async (regionId, region, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const {
|
||||
x_start,
|
||||
y_start,
|
||||
x_end,
|
||||
y_end,
|
||||
shape,
|
||||
polygon_points
|
||||
} = region;
|
||||
|
||||
const [result] = await db.query(
|
||||
`UPDATE workplace_map_regions
|
||||
SET x_start = ?, y_start = ?, x_end = ?, y_end = ?, shape = ?, polygon_points = ?, updated_at = NOW()
|
||||
WHERE region_id = ?`,
|
||||
[x_start, y_start, x_end, y_end, shape, polygon_points, regionId]
|
||||
);
|
||||
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 지도 영역 삭제
|
||||
*/
|
||||
const deleteMapRegion = async (regionId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_map_regions WHERE region_id = ?`,
|
||||
[regionId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 작업장 영역 일괄 삭제 (카테고리별)
|
||||
*/
|
||||
const deleteMapRegionsByCategory = async (categoryId, callback) => {
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [result] = await db.query(
|
||||
`DELETE FROM workplace_map_regions WHERE category_id = ?`,
|
||||
[categoryId]
|
||||
);
|
||||
callback(null, result);
|
||||
} catch (err) {
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// 카테고리
|
||||
createCategory,
|
||||
@@ -293,5 +427,13 @@ module.exports = {
|
||||
getWorkplacesByCategory,
|
||||
getWorkplaceById,
|
||||
updateWorkplace,
|
||||
deleteWorkplace
|
||||
deleteWorkplace,
|
||||
|
||||
// 지도 영역
|
||||
createMapRegion,
|
||||
getMapRegionsByCategory,
|
||||
getMapRegionByWorkplace,
|
||||
updateMapRegion,
|
||||
deleteMapRegion,
|
||||
deleteMapRegionsByCategory
|
||||
};
|
||||
|
||||
@@ -34,4 +34,10 @@ router.get('/vacation-balance/:worker_id', AttendanceController.getWorkerVacatio
|
||||
// 월별 근태 통계
|
||||
router.get('/monthly-stats', AttendanceController.getMonthlyAttendanceStats);
|
||||
|
||||
// 출근 체크 목록 조회 (아침용, 휴가 정보 포함)
|
||||
router.get('/checkin-list', AttendanceController.getCheckinList);
|
||||
|
||||
// 출근 체크 일괄 저장
|
||||
router.post('/checkins', AttendanceController.saveCheckins);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -70,6 +70,9 @@ router.get('/stats', dailyWorkReportController.getWorkReportStats);
|
||||
// 📝 일일 작업보고서 생성 (누적 방식 - 덮어쓰기 없음!)
|
||||
router.post('/', dailyWorkReportController.createDailyWorkReport);
|
||||
|
||||
// 📝 TBM 기반 작업보고서 생성
|
||||
router.post('/from-tbm', dailyWorkReportController.createFromTbm);
|
||||
|
||||
// 📊 일일 작업보고서 조회 (날짜별 - 경로 파라미터)
|
||||
router.get('/date/:date', dailyWorkReportController.getDailyWorkReportsByDate);
|
||||
|
||||
|
||||
@@ -2,70 +2,76 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const TbmController = require('../controllers/tbmController');
|
||||
const { authenticateToken } = require('../middlewares/auth');
|
||||
const { requireAuth } = require('../middlewares/auth');
|
||||
|
||||
// ==================== TBM 세션 관련 ====================
|
||||
|
||||
// TBM 세션 생성
|
||||
router.post('/sessions', authenticateToken, TbmController.createSession);
|
||||
router.post('/sessions', requireAuth, TbmController.createSession);
|
||||
|
||||
// 작업보고서가 작성되지 않은 TBM 팀 배정 조회 (구체적인 경로이므로 먼저 정의)
|
||||
router.get('/sessions/incomplete-reports', requireAuth, TbmController.getIncompleteWorkReports);
|
||||
|
||||
// 특정 날짜의 TBM 세션 목록 조회
|
||||
router.get('/sessions/date/:date', authenticateToken, TbmController.getSessionsByDate);
|
||||
router.get('/sessions/date/:date', requireAuth, TbmController.getSessionsByDate);
|
||||
|
||||
// TBM 세션 상세 조회
|
||||
router.get('/sessions/:sessionId', authenticateToken, TbmController.getSessionById);
|
||||
router.get('/sessions/:sessionId', requireAuth, TbmController.getSessionById);
|
||||
|
||||
// TBM 세션 수정
|
||||
router.put('/sessions/:sessionId', authenticateToken, TbmController.updateSession);
|
||||
router.put('/sessions/:sessionId', requireAuth, TbmController.updateSession);
|
||||
|
||||
// TBM 세션 완료 처리
|
||||
router.post('/sessions/:sessionId/complete', authenticateToken, TbmController.completeSession);
|
||||
router.post('/sessions/:sessionId/complete', requireAuth, TbmController.completeSession);
|
||||
|
||||
// ==================== 팀 구성 관련 ====================
|
||||
|
||||
// 팀원 추가 (단일)
|
||||
router.post('/sessions/:sessionId/team', authenticateToken, TbmController.addTeamMember);
|
||||
router.post('/sessions/:sessionId/team', requireAuth, TbmController.addTeamMember);
|
||||
|
||||
// 팀 구성 일괄 추가
|
||||
router.post('/sessions/:sessionId/team/batch', authenticateToken, TbmController.addTeamMembers);
|
||||
router.post('/sessions/:sessionId/team/batch', requireAuth, TbmController.addTeamMembers);
|
||||
|
||||
// TBM 세션의 팀 구성 조회
|
||||
router.get('/sessions/:sessionId/team', authenticateToken, TbmController.getTeamMembers);
|
||||
router.get('/sessions/:sessionId/team', requireAuth, TbmController.getTeamMembers);
|
||||
|
||||
// 팀원 전체 삭제 (수정 시 사용) - 더 구체적인 경로이므로 먼저 정의
|
||||
router.delete('/sessions/:sessionId/team/clear', requireAuth, TbmController.clearAllTeamMembers);
|
||||
|
||||
// 팀원 제거
|
||||
router.delete('/sessions/:sessionId/team/:workerId', authenticateToken, TbmController.removeTeamMember);
|
||||
router.delete('/sessions/:sessionId/team/:workerId', requireAuth, TbmController.removeTeamMember);
|
||||
|
||||
// ==================== 안전 체크리스트 관련 ====================
|
||||
|
||||
// 모든 안전 체크 항목 조회
|
||||
router.get('/safety-checks', authenticateToken, TbmController.getAllSafetyChecks);
|
||||
router.get('/safety-checks', requireAuth, TbmController.getAllSafetyChecks);
|
||||
|
||||
// TBM 세션의 안전 체크 기록 조회
|
||||
router.get('/sessions/:sessionId/safety', authenticateToken, TbmController.getSafetyRecords);
|
||||
router.get('/sessions/:sessionId/safety', requireAuth, TbmController.getSafetyRecords);
|
||||
|
||||
// 안전 체크 일괄 저장
|
||||
router.post('/sessions/:sessionId/safety', authenticateToken, TbmController.saveSafetyRecords);
|
||||
router.post('/sessions/:sessionId/safety', requireAuth, TbmController.saveSafetyRecords);
|
||||
|
||||
// ==================== 작업 인계 관련 ====================
|
||||
|
||||
// 작업 인계 생성
|
||||
router.post('/handovers', authenticateToken, TbmController.createHandover);
|
||||
router.post('/handovers', requireAuth, TbmController.createHandover);
|
||||
|
||||
// 작업 인계 확인
|
||||
router.post('/handovers/:handoverId/confirm', authenticateToken, TbmController.confirmHandover);
|
||||
router.post('/handovers/:handoverId/confirm', requireAuth, TbmController.confirmHandover);
|
||||
|
||||
// 특정 날짜의 작업 인계 목록 조회
|
||||
router.get('/handovers/date/:date', authenticateToken, TbmController.getHandoversByDate);
|
||||
router.get('/handovers/date/:date', requireAuth, TbmController.getHandoversByDate);
|
||||
|
||||
// 나에게 온 미확인 인계 건 조회
|
||||
router.get('/handovers/pending', authenticateToken, TbmController.getMyPendingHandovers);
|
||||
router.get('/handovers/pending', requireAuth, TbmController.getMyPendingHandovers);
|
||||
|
||||
// ==================== 통계 및 리포트 ====================
|
||||
|
||||
// TBM 통계 조회
|
||||
router.get('/statistics/tbm', authenticateToken, TbmController.getTbmStatistics);
|
||||
router.get('/statistics/tbm', requireAuth, TbmController.getTbmStatistics);
|
||||
|
||||
// 리더별 TBM 진행 현황 조회
|
||||
router.get('/statistics/leaders', authenticateToken, TbmController.getLeaderStatistics);
|
||||
router.get('/statistics/leaders', requireAuth, TbmController.getLeaderStatistics);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -128,4 +128,10 @@ router.put('/:id/status', userController.updateUserStatus);
|
||||
// 🗑️ 사용자 삭제
|
||||
router.delete('/:id', userController.deleteUser);
|
||||
|
||||
// 📄 사용자 페이지 접근 권한 조회
|
||||
router.get('/:id/page-access', userController.getUserPageAccess);
|
||||
|
||||
// 🔐 사용자 페이지 접근 권한 업데이트
|
||||
router.put('/:id/page-access', userController.updateUserPageAccess);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
31
api.hyungi.net/routes/vacationBalanceRoutes.js
Normal file
31
api.hyungi.net/routes/vacationBalanceRoutes.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* vacationBalanceRoutes.js
|
||||
* 휴가 잔액 관련 라우트
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const vacationBalanceController = require('../controllers/vacationBalanceController');
|
||||
|
||||
// 모든 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
router.get('/year/:year', vacationBalanceController.getAllByYear);
|
||||
|
||||
// 특정 작업자의 휴가 잔액 조회 (특정 연도)
|
||||
router.get('/worker/:workerId/year/:year', vacationBalanceController.getByWorkerAndYear);
|
||||
|
||||
// 작업자의 사용 가능한 휴가 일수 조회
|
||||
router.get('/worker/:workerId/year/:year/available', vacationBalanceController.getAvailableDays);
|
||||
|
||||
// 근속년수 기반 연차 자동 계산 및 생성 (관리자만)
|
||||
router.post('/auto-calculate', vacationBalanceController.autoCalculateAndCreate);
|
||||
|
||||
// 휴가 잔액 생성 (관리자만)
|
||||
router.post('/', vacationBalanceController.createBalance);
|
||||
|
||||
// 휴가 잔액 수정 (관리자만)
|
||||
router.put('/:id', vacationBalanceController.updateBalance);
|
||||
|
||||
// 휴가 잔액 삭제 (관리자만)
|
||||
router.delete('/:id', vacationBalanceController.deleteBalance);
|
||||
|
||||
module.exports = router;
|
||||
34
api.hyungi.net/routes/vacationRequestRoutes.js
Normal file
34
api.hyungi.net/routes/vacationRequestRoutes.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* vacationRequestRoutes.js
|
||||
* 휴가 신청 관련 라우트
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const vacationRequestController = require('../controllers/vacationRequestController');
|
||||
|
||||
// 휴가 신청 생성
|
||||
router.post('/', vacationRequestController.createRequest);
|
||||
|
||||
// 휴가 신청 목록 조회
|
||||
router.get('/', vacationRequestController.getAllRequests);
|
||||
|
||||
// 대기 중인 휴가 신청 목록 (관리자용)
|
||||
router.get('/pending', vacationRequestController.getPendingRequests);
|
||||
|
||||
// 특정 휴가 신청 조회
|
||||
router.get('/:id', vacationRequestController.getRequestById);
|
||||
|
||||
// 휴가 신청 수정
|
||||
router.put('/:id', vacationRequestController.updateRequest);
|
||||
|
||||
// 휴가 신청 삭제
|
||||
router.delete('/:id', vacationRequestController.deleteRequest);
|
||||
|
||||
// 휴가 신청 승인
|
||||
router.patch('/:id/approve', vacationRequestController.approveRequest);
|
||||
|
||||
// 휴가 신청 거부
|
||||
router.patch('/:id/reject', vacationRequestController.rejectRequest);
|
||||
|
||||
module.exports = router;
|
||||
31
api.hyungi.net/routes/vacationTypeRoutes.js
Normal file
31
api.hyungi.net/routes/vacationTypeRoutes.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* vacationTypeRoutes.js
|
||||
* 휴가 유형 관련 라우트
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const vacationTypeController = require('../controllers/vacationTypeController');
|
||||
|
||||
// 모든 활성 휴가 유형 조회
|
||||
router.get('/', vacationTypeController.getAllTypes);
|
||||
|
||||
// 시스템 기본 휴가 유형 조회
|
||||
router.get('/system', vacationTypeController.getSystemTypes);
|
||||
|
||||
// 특별 휴가 유형 조회
|
||||
router.get('/special', vacationTypeController.getSpecialTypes);
|
||||
|
||||
// 휴가 유형 우선순위 일괄 업데이트 (관리자만)
|
||||
router.put('/priorities', vacationTypeController.updatePriorities);
|
||||
|
||||
// 특별 휴가 유형 생성 (관리자만)
|
||||
router.post('/', vacationTypeController.createType);
|
||||
|
||||
// 휴가 유형 수정 (관리자만)
|
||||
router.put('/:id', vacationTypeController.updateType);
|
||||
|
||||
// 특별 휴가 유형 삭제 (관리자만, 시스템 기본 휴가는 삭제 불가)
|
||||
router.delete('/:id', vacationTypeController.deleteType);
|
||||
|
||||
module.exports = router;
|
||||
66
api.hyungi.net/routes/visitRequestRoutes.js
Normal file
66
api.hyungi.net/routes/visitRequestRoutes.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const visitRequestController = require('../controllers/visitRequestController');
|
||||
const { verifyToken } = require('../middlewares/authMiddleware');
|
||||
|
||||
// 모든 라우트에 인증 미들웨어 적용
|
||||
router.use(verifyToken);
|
||||
|
||||
// ==================== 출입 신청 관리 ====================
|
||||
|
||||
// 출입 신청 생성
|
||||
router.post('/requests', visitRequestController.createVisitRequest);
|
||||
|
||||
// 출입 신청 목록 조회 (필터: status, visit_date, start_date, end_date, requester_id, category_id)
|
||||
router.get('/requests', visitRequestController.getAllVisitRequests);
|
||||
|
||||
// 출입 신청 상세 조회
|
||||
router.get('/requests/:id', visitRequestController.getVisitRequestById);
|
||||
|
||||
// 출입 신청 수정
|
||||
router.put('/requests/:id', visitRequestController.updateVisitRequest);
|
||||
|
||||
// 출입 신청 삭제
|
||||
router.delete('/requests/:id', visitRequestController.deleteVisitRequest);
|
||||
|
||||
// 출입 신청 승인
|
||||
router.put('/requests/:id/approve', visitRequestController.approveVisitRequest);
|
||||
|
||||
// 출입 신청 반려
|
||||
router.put('/requests/:id/reject', visitRequestController.rejectVisitRequest);
|
||||
|
||||
// ==================== 방문 목적 관리 ====================
|
||||
|
||||
// 모든 방문 목적 조회
|
||||
router.get('/purposes', visitRequestController.getAllVisitPurposes);
|
||||
|
||||
// 활성 방문 목적만 조회
|
||||
router.get('/purposes/active', visitRequestController.getActiveVisitPurposes);
|
||||
|
||||
// 방문 목적 추가
|
||||
router.post('/purposes', visitRequestController.createVisitPurpose);
|
||||
|
||||
// 방문 목적 수정
|
||||
router.put('/purposes/:id', visitRequestController.updateVisitPurpose);
|
||||
|
||||
// 방문 목적 삭제
|
||||
router.delete('/purposes/:id', visitRequestController.deleteVisitPurpose);
|
||||
|
||||
// ==================== 안전교육 기록 관리 ====================
|
||||
|
||||
// 안전교육 기록 생성
|
||||
router.post('/training', visitRequestController.createTrainingRecord);
|
||||
|
||||
// 안전교육 기록 목록 조회 (필터: training_date, start_date, end_date, trainer_id)
|
||||
router.get('/training', visitRequestController.getTrainingRecords);
|
||||
|
||||
// 특정 출입 신청의 안전교육 기록 조회
|
||||
router.get('/training/request/:requestId', visitRequestController.getTrainingRecordByRequestId);
|
||||
|
||||
// 안전교육 기록 수정
|
||||
router.put('/training/:id', visitRequestController.updateTrainingRecord);
|
||||
|
||||
// 안전교육 완료 (서명 포함)
|
||||
router.post('/training/:id/complete', visitRequestController.completeTraining);
|
||||
|
||||
module.exports = router;
|
||||
@@ -248,6 +248,77 @@ const getMonthlyAttendanceStatsService = async (year, month, workerId = null) =>
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출근 체크 목록 조회 (휴가 정보 포함)
|
||||
*/
|
||||
const getCheckinListService = async (date) => {
|
||||
if (!date) {
|
||||
throw new ValidationError('날짜가 필요합니다', {
|
||||
required: ['date'],
|
||||
received: { date }
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('출근 체크 목록 조회 요청', { date });
|
||||
|
||||
try {
|
||||
const checkinList = await AttendanceModel.getCheckinList(date);
|
||||
logger.info('출근 체크 목록 조회 성공', { date, count: checkinList.length });
|
||||
return checkinList;
|
||||
} catch (error) {
|
||||
logger.error('출근 체크 목록 조회 실패', { date, error: error.message });
|
||||
throw new DatabaseError('출근 체크 목록 조회 중 데이터베이스 오류가 발생했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 출근 체크 일괄 저장
|
||||
*/
|
||||
const saveCheckinsService = async (date, checkins) => {
|
||||
if (!date || !checkins || !Array.isArray(checkins)) {
|
||||
throw new ValidationError('날짜와 출근 체크 목록이 필요합니다', {
|
||||
required: ['date', 'checkins'],
|
||||
received: { date, checkins: checkins ? `Array[${checkins.length}]` : null }
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('출근 체크 일괄 저장 요청', { date, count: checkins.length });
|
||||
|
||||
try {
|
||||
const results = [];
|
||||
|
||||
for (const checkin of checkins) {
|
||||
const { worker_id, is_present } = checkin;
|
||||
|
||||
if (!worker_id || is_present === undefined) {
|
||||
logger.warn('출근 체크 데이터 누락', { checkin });
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await AttendanceModel.upsertCheckin({
|
||||
worker_id,
|
||||
record_date: date,
|
||||
is_present
|
||||
});
|
||||
|
||||
results.push({
|
||||
worker_id,
|
||||
record_id: result,
|
||||
is_present
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('출근 체크 일괄 저장 성공', { date, saved: results.length });
|
||||
return {
|
||||
saved_count: results.length,
|
||||
results
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('출근 체크 일괄 저장 실패', { date, error: error.message });
|
||||
throw new DatabaseError('출근 체크 저장 중 데이터베이스 오류가 발생했습니다');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getDailyAttendanceStatusService,
|
||||
getDailyAttendanceRecordsService,
|
||||
@@ -257,5 +328,7 @@ module.exports = {
|
||||
getAttendanceTypesService,
|
||||
getVacationTypesService,
|
||||
getWorkerVacationBalanceService,
|
||||
getMonthlyAttendanceStatsService
|
||||
getMonthlyAttendanceStatsService,
|
||||
getCheckinListService,
|
||||
saveCheckinsService
|
||||
};
|
||||
|
||||
@@ -122,19 +122,29 @@ function convertToRoman(text) {
|
||||
/**
|
||||
* 사용자명 생성 (중복 확인 및 처리)
|
||||
* @param {string} koreanName - 한글 이름
|
||||
* @param {object} knex - Knex 인스턴스
|
||||
* @param {object} db - Database connection (mysql2 pool or knex)
|
||||
* @returns {Promise<string>} 고유한 username
|
||||
*/
|
||||
async function generateUniqueUsername(koreanName, knex) {
|
||||
async function generateUniqueUsername(koreanName, db) {
|
||||
const baseUsername = hangulToRoman(koreanName);
|
||||
let username = baseUsername;
|
||||
let counter = 1;
|
||||
|
||||
// 중복 확인
|
||||
while (true) {
|
||||
const existing = await knex('users')
|
||||
.where('username', username)
|
||||
.first();
|
||||
let existing;
|
||||
|
||||
// mysql2 pool 또는 knex 모두 지원
|
||||
if (typeof db === 'function') {
|
||||
// Knex
|
||||
existing = await db('users')
|
||||
.where('username', username)
|
||||
.first();
|
||||
} else {
|
||||
// mysql2 pool
|
||||
const [rows] = await db.query('SELECT username FROM users WHERE username = ?', [username]);
|
||||
existing = rows[0];
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
break; // 중복 없음
|
||||
|
||||
Reference in New Issue
Block a user