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:
Hyungi Ahn
2026-01-29 15:46:47 +09:00
parent e1227a69fe
commit b6485e3140
87 changed files with 17509 additions and 698 deletions

View File

@@ -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
};

View File

@@ -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

View File

@@ -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
};

View 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;

View 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;

View 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;

View 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
});
});
};

View File

@@ -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
});
});