diff --git a/api.hyungi.net/controllers/attendanceController.js b/api.hyungi.net/controllers/attendanceController.js index 2dece5b..4fa546b 100644 --- a/api.hyungi.net/controllers/attendanceController.js +++ b/api.hyungi.net/controllers/attendanceController.js @@ -1,306 +1,167 @@ -const AttendanceModel = require('../models/attendanceModel'); +/** + * 근태 관리 컨트롤러 + * + * 근태 기록 API 엔드포인트 핸들러 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ -class AttendanceController { - // 일일 근태 현황 조회 (대시보드용) - static async getDailyAttendanceStatus(req, res) { - try { - const { date } = req.query; - - if (!date) { - return res.status(400).json({ - success: false, - message: '날짜가 필요합니다.' - }); - } +const attendanceService = require('../services/attendanceService'); +const { asyncHandler } = require('../middlewares/errorHandler'); - const attendanceStatus = await AttendanceModel.getWorkerAttendanceStatus(date); - - res.json({ - success: true, - data: attendanceStatus, - message: '근태 현황을 성공적으로 조회했습니다.' - }); - } catch (error) { - console.error('근태 현황 조회 오류:', error); - res.status(500).json({ - success: false, - message: '근태 현황 조회 중 오류가 발생했습니다.', - error: error.message - }); - } - } +/** + * 일일 근태 현황 조회 (대시보드용) + */ +const getDailyAttendanceStatus = asyncHandler(async (req, res) => { + const { date } = req.query; + const data = await attendanceService.getDailyAttendanceStatusService(date); - // 일일 근태 기록 조회 - static async getDailyAttendanceRecords(req, res) { - try { - const { date, worker_id } = req.query; - - if (!date) { - return res.status(400).json({ - success: false, - message: '날짜가 필요합니다.' - }); - } + res.json({ + success: true, + data, + message: '근태 현황을 성공적으로 조회했습니다' + }); +}); - const records = await AttendanceModel.getDailyAttendanceRecords(date, worker_id); - - res.json({ - success: true, - data: records, - message: '근태 기록을 성공적으로 조회했습니다.' - }); - } catch (error) { - console.error('근태 기록 조회 오류:', error); - res.status(500).json({ - success: false, - message: '근태 기록 조회 중 오류가 발생했습니다.', - error: error.message - }); - } - } +/** + * 일일 근태 기록 조회 + */ +const getDailyAttendanceRecords = asyncHandler(async (req, res) => { + const { date, worker_id } = req.query; + const data = await attendanceService.getDailyAttendanceRecordsService(date, worker_id); - // 근태 기록 생성/업데이트 - static async upsertAttendanceRecord(req, res) { - try { - const { - record_date, - worker_id, - total_work_hours, - attendance_type_id, - vacation_type_id, - is_vacation_processed, - overtime_approved, - status, - notes - } = req.body; + res.json({ + success: true, + data, + message: '근태 기록을 성공적으로 조회했습니다' + }); +}); - // 필수 필드 검증 - if (!record_date || !worker_id) { - return res.status(400).json({ - success: false, - message: '날짜와 작업자 ID가 필요합니다.' - }); - } +/** + * 근태 기록 생성/업데이트 + */ +const upsertAttendanceRecord = asyncHandler(async (req, res) => { + const recordData = { + ...req.body, + created_by: req.user?.user_id || req.user?.id + }; - const recordData = { - record_date, - worker_id, - total_work_hours: total_work_hours || 0, - attendance_type_id, - vacation_type_id, - is_vacation_processed: is_vacation_processed || false, - overtime_approved: overtime_approved || false, - status: status || 'incomplete', - notes, - created_by: req.user.user_id - }; + const result = await attendanceService.upsertAttendanceRecordService(recordData); - const result = await AttendanceModel.upsertAttendanceRecord(recordData); - - res.json({ - success: true, - data: result, - message: '근태 기록이 성공적으로 저장되었습니다.' - }); - } catch (error) { - console.error('근태 기록 저장 오류:', error); - res.status(500).json({ - success: false, - message: '근태 기록 저장 중 오류가 발생했습니다.', - error: error.message - }); - } - } + res.json({ + success: true, + data: result, + message: '근태 기록이 성공적으로 저장되었습니다' + }); +}); - // 휴가 처리 - static async processVacation(req, res) { - try { - const { worker_id, date, vacation_type } = req.body; +/** + * 휴가 처리 + */ +const processVacation = asyncHandler(async (req, res) => { + const vacationData = { + record_date: req.body.date, + worker_id: req.body.worker_id, + vacation_type_id: req.body.vacation_type, + created_by: req.user?.user_id || req.user?.id + }; - // 필수 필드 검증 - if (!worker_id || !date || !vacation_type) { - return res.status(400).json({ - success: false, - message: '작업자 ID, 날짜, 휴가 유형이 필요합니다.' - }); - } + const result = await attendanceService.processVacationService(vacationData); - // 휴가 유형 검증 - const validVacationTypes = ['ANNUAL_FULL', 'ANNUAL_HALF', 'ANNUAL_QUARTER']; - if (!validVacationTypes.includes(vacation_type)) { - return res.status(400).json({ - success: false, - message: '유효하지 않은 휴가 유형입니다.' - }); - } + res.json({ + success: true, + data: result, + message: '휴가 처리가 성공적으로 완료되었습니다' + }); +}); - const result = await AttendanceModel.processVacation( - worker_id, - date, - vacation_type, - req.user.user_id - ); - - res.json({ - success: true, - data: result, - message: '휴가 처리가 성공적으로 완료되었습니다.' - }); - } catch (error) { - console.error('휴가 처리 오류:', error); - res.status(500).json({ - success: false, - message: '휴가 처리 중 오류가 발생했습니다.', - error: error.message - }); - } - } +/** + * 초과근무 승인 + */ +const approveOvertime = asyncHandler(async (req, res) => { + const overtimeData = { + record_date: req.body.date, + worker_id: req.body.worker_id, + overtime_approved: true, + approved_by: req.user?.user_id || req.user?.id + }; - // 초과근무 승인 - static async approveOvertime(req, res) { - try { - const { worker_id, date } = req.body; + const result = await attendanceService.approveOvertimeService(overtimeData); - // 필수 필드 검증 - if (!worker_id || !date) { - return res.status(400).json({ - success: false, - message: '작업자 ID와 날짜가 필요합니다.' - }); - } + res.json({ + success: true, + data: result, + message: '초과근무가 성공적으로 승인되었습니다' + }); +}); - const result = await AttendanceModel.approveOvertime( - worker_id, - date, - req.user.user_id - ); - - if (result) { - res.json({ - success: true, - message: '초과근무가 성공적으로 승인되었습니다.' - }); - } else { - res.status(404).json({ - success: false, - message: '해당 날짜의 근태 기록을 찾을 수 없습니다.' - }); - } - } catch (error) { - console.error('초과근무 승인 오류:', error); - res.status(500).json({ - success: false, - message: '초과근무 승인 중 오류가 발생했습니다.', - error: error.message - }); - } - } +/** + * 근로 유형 목록 조회 + */ +const getAttendanceTypes = asyncHandler(async (req, res) => { + const data = await attendanceService.getAttendanceTypesService(); - // 근로 유형 목록 조회 - static async getAttendanceTypes(req, res) { - try { - const attendanceTypes = await AttendanceModel.getAttendanceTypes(); - - res.json({ - success: true, - data: attendanceTypes, - message: '근로 유형 목록을 성공적으로 조회했습니다.' - }); - } catch (error) { - console.error('근로 유형 조회 오류:', error); - res.status(500).json({ - success: false, - message: '근로 유형 조회 중 오류가 발생했습니다.', - error: error.message - }); - } - } + res.json({ + success: true, + data, + message: '근로 유형 목록을 성공적으로 조회했습니다' + }); +}); - // 휴가 유형 목록 조회 - static async getVacationTypes(req, res) { - try { - const vacationTypes = await AttendanceModel.getVacationTypes(); - - res.json({ - success: true, - data: vacationTypes, - message: '휴가 유형 목록을 성공적으로 조회했습니다.' - }); - } catch (error) { - console.error('휴가 유형 조회 오류:', error); - res.status(500).json({ - success: false, - message: '휴가 유형 조회 중 오류가 발생했습니다.', - error: error.message - }); - } - } +/** + * 휴가 유형 목록 조회 + */ +const getVacationTypes = asyncHandler(async (req, res) => { + const data = await attendanceService.getVacationTypesService(); - // 작업자 휴가 잔여 조회 - static async getWorkerVacationBalance(req, res) { - try { - const { worker_id } = req.params; - const { year } = req.query; + res.json({ + success: true, + data, + message: '휴가 유형 목록을 성공적으로 조회했습니다' + }); +}); - if (!worker_id) { - return res.status(400).json({ - success: false, - message: '작업자 ID가 필요합니다.' - }); - } +/** + * 작업자 휴가 잔여 조회 + */ +const getWorkerVacationBalance = asyncHandler(async (req, res) => { + const { worker_id } = req.params; + const data = await attendanceService.getWorkerVacationBalanceService(parseInt(worker_id)); - const balance = await AttendanceModel.getWorkerVacationBalance( - parseInt(worker_id), - year ? parseInt(year) : null - ); - - res.json({ - success: true, - data: balance, - message: '휴가 잔여 정보를 성공적으로 조회했습니다.' - }); - } catch (error) { - console.error('휴가 잔여 조회 오류:', error); - res.status(500).json({ - success: false, - message: '휴가 잔여 조회 중 오류가 발생했습니다.', - error: error.message - }); - } - } + res.json({ + success: true, + data, + message: '휴가 잔여 정보를 성공적으로 조회했습니다' + }); +}); - // 월별 근태 통계 - static async getMonthlyAttendanceStats(req, res) { - try { - const { year, month, worker_id } = req.query; +/** + * 월별 근태 통계 + */ +const getMonthlyAttendanceStats = asyncHandler(async (req, res) => { + const { year, month, worker_id } = req.query; + const data = await attendanceService.getMonthlyAttendanceStatsService( + parseInt(year), + parseInt(month), + worker_id ? parseInt(worker_id) : null + ); - if (!year || !month) { - return res.status(400).json({ - success: false, - message: '연도와 월이 필요합니다.' - }); - } + res.json({ + success: true, + data, + message: '월별 근태 통계를 성공적으로 조회했습니다' + }); +}); - const stats = await AttendanceModel.getMonthlyAttendanceStats( - parseInt(year), - parseInt(month), - worker_id ? parseInt(worker_id) : null - ); - - res.json({ - success: true, - data: stats, - message: '월별 근태 통계를 성공적으로 조회했습니다.' - }); - } catch (error) { - console.error('월별 근태 통계 조회 오류:', error); - res.status(500).json({ - success: false, - message: '월별 근태 통계 조회 중 오류가 발생했습니다.', - error: error.message - }); - } - } -} - -module.exports = AttendanceController; +module.exports = { + getDailyAttendanceStatus, + getDailyAttendanceRecords, + upsertAttendanceRecord, + processVacation, + approveOvertime, + getAttendanceTypes, + getVacationTypes, + getWorkerVacationBalance, + getMonthlyAttendanceStats +}; diff --git a/api.hyungi.net/controllers/projectController.js b/api.hyungi.net/controllers/projectController.js index 22dfe5c..b1510a0 100644 --- a/api.hyungi.net/controllers/projectController.js +++ b/api.hyungi.net/controllers/projectController.js @@ -1,118 +1,162 @@ -const projectModel = require('../models/projectModel'); -const { ApiError, asyncHandler, handleDatabaseError, handleNotFoundError } = require('../utils/errorHandler'); -const { validateSchema, schemas } = require('../utils/validator'); +/** + * 프로젝트 관리 컨트롤러 + * + * 프로젝트 CRUD API 엔드포인트 핸들러 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ -// 1. 프로젝트 생성 +const projectModel = require('../models/projectModel'); +const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors'); +const { asyncHandler } = require('../middlewares/errorHandler'); +const logger = require('../utils/logger'); + +/** + * 프로젝트 생성 + */ exports.createProject = asyncHandler(async (req, res) => { const projectData = req.body; - - // 스키마 기반 유효성 검사 - validateSchema(projectData, schemas.createProject); - - try { - const id = await new Promise((resolve, reject) => { - projectModel.create(projectData, (err, lastID) => (err ? reject(err) : resolve(lastID))); + + logger.info('프로젝트 생성 요청', { name: projectData.name }); + + const id = await new Promise((resolve, reject) => { + projectModel.create(projectData, (err, lastID) => { + if (err) reject(new DatabaseError('프로젝트 생성 중 오류가 발생했습니다')); + else resolve(lastID); }); - - res.created({ project_id: id }, '프로젝트가 성공적으로 생성되었습니다.'); - } catch (err) { - handleDatabaseError(err, '프로젝트 생성'); - } + }); + + logger.info('프로젝트 생성 성공', { project_id: id }); + + res.status(201).json({ + success: true, + data: { project_id: id }, + message: '프로젝트가 성공적으로 생성되었습니다' + }); }); -// 2. 전체 조회 +/** + * 전체 프로젝트 조회 + */ exports.getAllProjects = asyncHandler(async (req, res) => { - try { - const rows = await new Promise((resolve, reject) => { - projectModel.getAll((err, data) => (err ? reject(err) : resolve(data))); + const rows = await new Promise((resolve, reject) => { + projectModel.getAll((err, data) => { + if (err) reject(new DatabaseError('프로젝트 목록 조회 중 오류가 발생했습니다')); + else resolve(data); }); - - res.list(rows, '프로젝트 목록 조회 성공'); - } catch (err) { - handleDatabaseError(err, '프로젝트 목록 조회'); - } + }); + + res.json({ + success: true, + data: rows, + message: '프로젝트 목록 조회 성공' + }); }); -// 2-1. 활성 프로젝트만 조회 (작업보고서용) +/** + * 활성 프로젝트만 조회 (작업보고서용) + */ exports.getActiveProjects = asyncHandler(async (req, res) => { - try { - const rows = await new Promise((resolve, reject) => { - projectModel.getActiveProjects((err, data) => (err ? reject(err) : resolve(data))); + const rows = await new Promise((resolve, reject) => { + projectModel.getActiveProjects((err, data) => { + if (err) reject(new DatabaseError('활성 프로젝트 목록 조회 중 오류가 발생했습니다')); + else resolve(data); }); - - res.list(rows, '활성 프로젝트 목록 조회 성공'); - } catch (err) { - handleDatabaseError(err, '활성 프로젝트 목록 조회'); - } + }); + + res.json({ + success: true, + data: rows, + message: '활성 프로젝트 목록 조회 성공' + }); }); -// 3. 단일 조회 +/** + * 단일 프로젝트 조회 + */ exports.getProjectById = asyncHandler(async (req, res) => { const id = parseInt(req.params.project_id, 10); - + if (isNaN(id)) { - throw new ApiError('유효하지 않은 프로젝트 ID입니다.', 400); + throw new ValidationError('유효하지 않은 프로젝트 ID입니다'); } - - try { - const row = await new Promise((resolve, reject) => { - projectModel.getById(id, (err, data) => (err ? reject(err) : resolve(data))); + + const row = await new Promise((resolve, reject) => { + projectModel.getById(id, (err, data) => { + if (err) reject(new DatabaseError('프로젝트 조회 중 오류가 발생했습니다')); + else resolve(data); }); - - if (!row) { - handleNotFoundError('프로젝트', id); - } - - res.success(row, '프로젝트 조회 성공'); - } catch (err) { - handleDatabaseError(err, '프로젝트 조회'); + }); + + if (!row) { + throw new NotFoundError('프로젝트를 찾을 수 없습니다'); } + + res.json({ + success: true, + data: row, + message: '프로젝트 조회 성공' + }); }); -// 4. 수정 +/** + * 프로젝트 수정 + */ exports.updateProject = asyncHandler(async (req, res) => { const id = parseInt(req.params.project_id, 10); - + if (isNaN(id)) { - throw new ApiError('유효하지 않은 프로젝트 ID입니다.', 400); + throw new ValidationError('유효하지 않은 프로젝트 ID입니다'); } - + const data = { ...req.body, project_id: id }; - - try { - const changes = await new Promise((resolve, reject) => { - projectModel.update(data, (err, ch) => (err ? reject(err) : resolve(ch))); + + const changes = await new Promise((resolve, reject) => { + projectModel.update(data, (err, ch) => { + if (err) reject(new DatabaseError('프로젝트 수정 중 오류가 발생했습니다')); + else resolve(ch); }); - - if (changes === 0) { - handleNotFoundError('프로젝트', id); - } - - res.updated({ changes }, '프로젝트 정보가 성공적으로 수정되었습니다.'); - } catch (err) { - handleDatabaseError(err, '프로젝트 수정'); + }); + + if (changes === 0) { + throw new NotFoundError('프로젝트를 찾을 수 없습니다'); } + + logger.info('프로젝트 수정 성공', { project_id: id }); + + res.json({ + success: true, + data: { changes }, + message: '프로젝트 정보가 성공적으로 수정되었습니다' + }); }); -// 5. 삭제 +/** + * 프로젝트 삭제 + */ exports.removeProject = asyncHandler(async (req, res) => { const id = parseInt(req.params.project_id, 10); - + if (isNaN(id)) { - throw new ApiError('유효하지 않은 프로젝트 ID입니다.', 400); + throw new ValidationError('유효하지 않은 프로젝트 ID입니다'); } - - try { - const changes = await new Promise((resolve, reject) => { - projectModel.remove(id, (err, ch) => (err ? reject(err) : resolve(ch))); + + const changes = await new Promise((resolve, reject) => { + projectModel.remove(id, (err, ch) => { + if (err) reject(new DatabaseError('프로젝트 삭제 중 오류가 발생했습니다')); + else resolve(ch); }); - - if (changes === 0) { - handleNotFoundError('프로젝트', id); - } - - res.deleted('프로젝트가 성공적으로 삭제되었습니다.'); - } catch (err) { - handleDatabaseError(err, '프로젝트 삭제'); + }); + + if (changes === 0) { + throw new NotFoundError('프로젝트를 찾을 수 없습니다'); } -}); \ No newline at end of file + + logger.info('프로젝트 삭제 성공', { project_id: id }); + + res.json({ + success: true, + message: '프로젝트가 성공적으로 삭제되었습니다' + }); +}); diff --git a/api.hyungi.net/controllers/workerController.js b/api.hyungi.net/controllers/workerController.js index d6c67bd..2c82f62 100644 --- a/api.hyungi.net/controllers/workerController.js +++ b/api.hyungi.net/controllers/workerController.js @@ -1,148 +1,171 @@ -// controllers/workerController.js +/** + * 작업자 관리 컨트롤러 + * + * 작업자 CRUD API 엔드포인트 핸들러 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ + const workerModel = require('../models/workerModel'); -const { ApiError, asyncHandler, handleDatabaseError, handleNotFoundError } = require('../utils/errorHandler'); -const { validateSchema, schemas } = require('../utils/validator'); +const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors'); +const { asyncHandler } = require('../middlewares/errorHandler'); +const logger = require('../utils/logger'); const cache = require('../utils/cache'); const { optimizedQueries } = require('../utils/queryOptimizer'); -// 1. 작업자 생성 +/** + * 작업자 생성 + */ exports.createWorker = asyncHandler(async (req, res) => { const workerData = req.body; - - // 스키마 기반 유효성 검사 - validateSchema(workerData, schemas.createWorker); - - try { - const lastID = await new Promise((resolve, reject) => { - workerModel.create(workerData, (err, id) => { - if (err) reject(err); - else resolve(id); - }); + + logger.info('작업자 생성 요청', { name: workerData.name }); + + const lastID = await new Promise((resolve, reject) => { + workerModel.create(workerData, (err, id) => { + if (err) reject(new DatabaseError('작업자 생성 중 오류가 발생했습니다')); + else resolve(id); }); - - // 작업자 관련 캐시 무효화 - await cache.invalidateCache.worker(); - - res.created({ worker_id: lastID }, '작업자가 성공적으로 생성되었습니다.'); - } catch (err) { - handleDatabaseError(err, '작업자 생성'); - } + }); + + // 작업자 관련 캐시 무효화 + await cache.invalidateCache.worker(); + + logger.info('작업자 생성 성공', { worker_id: lastID }); + + res.status(201).json({ + success: true, + data: { worker_id: lastID }, + message: '작업자가 성공적으로 생성되었습니다' + }); }); -// 2. 전체 작업자 조회 (캐싱 및 페이지네이션 적용) +/** + * 전체 작업자 조회 (캐싱 및 페이지네이션 적용) + */ exports.getAllWorkers = asyncHandler(async (req, res) => { const { page = 1, limit = 10, search = '' } = req.query; - - // 캐시 키 생성 + const cacheKey = cache.createKey('workers', 'list', page, limit, search); - - try { - // 캐시에서 조회 - const cachedData = await cache.get(cacheKey); - if (cachedData) { - console.log(`🎯 캐시 히트: ${cacheKey}`); - return res.paginated(cachedData.data, cachedData.pagination.totalCount, page, limit, '작업자 목록 조회 성공 (캐시)'); - } - - // 최적화된 쿼리 사용 - const result = await optimizedQueries.getWorkersPaged(page, limit, search); - - // 캐시에 저장 (5분) - await cache.set(cacheKey, result, cache.TTL.MEDIUM); - console.log(`💾 캐시 저장: ${cacheKey}`); - - res.paginated(result.data, result.pagination.totalCount, page, limit, '작업자 목록 조회 성공'); - } catch (err) { - handleDatabaseError(err, '작업자 목록 조회'); + + // 캐시에서 조회 + const cachedData = await cache.get(cacheKey); + if (cachedData) { + logger.debug('캐시 히트', { cacheKey }); + return res.json({ + success: true, + data: cachedData.data, + pagination: cachedData.pagination, + message: '작업자 목록 조회 성공 (캐시)' + }); } + + // 최적화된 쿼리 사용 + const result = await optimizedQueries.getWorkersPaged(page, limit, search); + + // 캐시에 저장 (5분) + await cache.set(cacheKey, result, cache.TTL.MEDIUM); + logger.debug('캐시 저장', { cacheKey }); + + res.json({ + success: true, + data: result.data, + pagination: result.pagination, + message: '작업자 목록 조회 성공' + }); }); -// 3. 단일 작업자 조회 +/** + * 단일 작업자 조회 + */ exports.getWorkerById = asyncHandler(async (req, res) => { const id = parseInt(req.params.worker_id, 10); - + if (isNaN(id)) { - throw new ApiError('유효하지 않은 작업자 ID입니다.', 400); + throw new ValidationError('유효하지 않은 작업자 ID입니다'); } - - try { - const row = await new Promise((resolve, reject) => { - workerModel.getById(id, (err, data) => { - if (err) reject(err); - else resolve(data); - }); + + const row = await new Promise((resolve, reject) => { + workerModel.getById(id, (err, data) => { + if (err) reject(new DatabaseError('작업자 조회 중 오류가 발생했습니다')); + else resolve(data); }); - - if (!row) { - handleNotFoundError('작업자', id); - } - - res.success(row, '작업자 조회 성공'); - } catch (err) { - handleDatabaseError(err, '작업자 조회'); + }); + + if (!row) { + throw new NotFoundError('작업자를 찾을 수 없습니다'); } + + res.json({ + success: true, + data: row, + message: '작업자 조회 성공' + }); }); -// 4. 작업자 수정 +/** + * 작업자 수정 + */ exports.updateWorker = asyncHandler(async (req, res) => { const id = parseInt(req.params.worker_id, 10); - + if (isNaN(id)) { - throw new ApiError('유효하지 않은 작업자 ID입니다.', 400); + throw new ValidationError('유효하지 않은 작업자 ID입니다'); } - + const workerData = { ...req.body, worker_id: id }; - - try { - const changes = await new Promise((resolve, reject) => { - workerModel.update(workerData, (err, affected) => { - if (err) reject(err); - else resolve(affected); - }); + + const changes = await new Promise((resolve, reject) => { + workerModel.update(workerData, (err, affected) => { + if (err) reject(new DatabaseError('작업자 수정 중 오류가 발생했습니다')); + else resolve(affected); }); - - if (changes === 0) { - handleNotFoundError('작업자', id); - } - - res.updated({ changes }, '작업자 정보가 성공적으로 수정되었습니다.'); - } catch (err) { - handleDatabaseError(err, '작업자 수정'); + }); + + if (changes === 0) { + throw new NotFoundError('작업자를 찾을 수 없습니다'); } + + logger.info('작업자 수정 성공', { worker_id: id }); + + res.json({ + success: true, + data: { changes }, + message: '작업자 정보가 성공적으로 수정되었습니다' + }); }); -// 5. 작업자 삭제 +/** + * 작업자 삭제 + */ exports.removeWorker = asyncHandler(async (req, res) => { const id = parseInt(req.params.worker_id, 10); - + if (isNaN(id)) { - throw new ApiError('유효하지 않은 작업자 ID입니다.', 400); + throw new ValidationError('유효하지 않은 작업자 ID입니다'); } - - try { - const changes = await new Promise((resolve, reject) => { - workerModel.remove(id, (err, affected) => { - if (err) reject(err); - else resolve(affected); - }); + + const changes = await new Promise((resolve, reject) => { + workerModel.remove(id, (err, affected) => { + if (err) reject(new DatabaseError('작업자 삭제 중 오류가 발생했습니다')); + else resolve(affected); }); - - if (changes === 0) { - handleNotFoundError('작업자', id); - } - - // 작업자 관련 캐시 무효화 - console.log('🗑️ 작업자 삭제 후 캐시 무효화 시작...'); - await cache.invalidateCache.worker(); - - // 추가로 전체 작업자 캐시도 강제 무효화 - await cache.delPattern('workers:*'); - await cache.flush(); // 전체 캐시 초기화 (임시) - - console.log('✅ 작업자 삭제 후 캐시 무효화 완료'); - - res.deleted('작업자가 성공적으로 삭제되었습니다.'); - } catch (err) { - handleDatabaseError(err, '작업자 삭제'); + }); + + if (changes === 0) { + throw new NotFoundError('작업자를 찾을 수 없습니다'); } + + // 작업자 관련 캐시 무효화 + logger.info('작업자 삭제 후 캐시 무효화 시작', { worker_id: id }); + await cache.invalidateCache.worker(); + await cache.delPattern('workers:*'); + await cache.flush(); + logger.info('작업자 삭제 후 캐시 무효화 완료', { worker_id: id }); + + res.json({ + success: true, + message: '작업자가 성공적으로 삭제되었습니다' + }); }); \ No newline at end of file diff --git a/api.hyungi.net/services/attendanceService.js b/api.hyungi.net/services/attendanceService.js new file mode 100644 index 0000000..fe91a2f --- /dev/null +++ b/api.hyungi.net/services/attendanceService.js @@ -0,0 +1,255 @@ +/** + * 근태 관리 서비스 + * + * 근태 기록 관련 비즈니스 로직 처리 + * + * @author TK-FB-Project + * @since 2025-12-11 + */ + +const AttendanceModel = require('../models/attendanceModel'); +const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors'); +const logger = require('../utils/logger'); + +/** + * 일일 근태 현황 조회 + */ +const getDailyAttendanceStatusService = async (date) => { + if (!date) { + throw new ValidationError('날짜가 필요합니다', { + required: ['date'], + received: { date } + }); + } + + logger.info('일일 근태 현황 조회 요청', { date }); + + try { + const attendanceStatus = await AttendanceModel.getWorkerAttendanceStatus(date); + logger.info('일일 근태 현황 조회 성공', { date, count: attendanceStatus.length }); + return attendanceStatus; + } catch (error) { + logger.error('일일 근태 현황 조회 실패', { date, error: error.message }); + throw new DatabaseError('근태 현황 조회 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 일일 근태 기록 조회 + */ +const getDailyAttendanceRecordsService = async (date, workerId = null) => { + if (!date) { + throw new ValidationError('날짜가 필요합니다', { + required: ['date'], + received: { date } + }); + } + + logger.info('일일 근태 기록 조회 요청', { date, workerId }); + + try { + const records = await AttendanceModel.getDailyAttendanceRecords(date, workerId); + logger.info('일일 근태 기록 조회 성공', { date, count: records.length }); + return records; + } catch (error) { + logger.error('일일 근태 기록 조회 실패', { date, error: error.message }); + throw new DatabaseError('근태 기록 조회 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 근태 기록 생성/업데이트 + */ +const upsertAttendanceRecordService = async (recordData) => { + const { + record_date, + worker_id, + total_work_hours, + attendance_type_id, + vacation_type_id, + is_vacation_processed, + overtime_approved, + status, + notes + } = recordData; + + // 필수 필드 검증 + if (!record_date || !worker_id) { + throw new ValidationError('필수 필드가 누락되었습니다', { + required: ['record_date', 'worker_id'], + received: { record_date, worker_id } + }); + } + + logger.info('근태 기록 저장 요청', { record_date, worker_id }); + + try { + const result = await AttendanceModel.upsertAttendanceRecord({ + record_date, + worker_id, + total_work_hours, + attendance_type_id, + vacation_type_id, + is_vacation_processed, + overtime_approved, + status, + notes + }); + + logger.info('근태 기록 저장 성공', { record_date, worker_id }); + return result; + } catch (error) { + logger.error('근태 기록 저장 실패', { record_date, worker_id, error: error.message }); + throw new DatabaseError('근태 기록 저장 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 휴가 처리 + */ +const processVacationService = async (vacationData) => { + const { record_date, worker_id, vacation_type_id } = vacationData; + + if (!record_date || !worker_id || !vacation_type_id) { + throw new ValidationError('필수 필드가 누락되었습니다', { + required: ['record_date', 'worker_id', 'vacation_type_id'], + received: { record_date, worker_id, vacation_type_id } + }); + } + + logger.info('휴가 처리 요청', { record_date, worker_id, vacation_type_id }); + + try { + const result = await AttendanceModel.processVacation({ + record_date, + worker_id, + vacation_type_id + }); + + logger.info('휴가 처리 성공', { record_date, worker_id }); + return result; + } catch (error) { + logger.error('휴가 처리 실패', { record_date, worker_id, error: error.message }); + throw new DatabaseError('휴가 처리 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 초과 근무 승인 + */ +const approveOvertimeService = async (overtimeData) => { + const { record_date, worker_id, overtime_approved } = overtimeData; + + if (!record_date || !worker_id || overtime_approved === undefined) { + throw new ValidationError('필수 필드가 누락되었습니다', { + required: ['record_date', 'worker_id', 'overtime_approved'], + received: { record_date, worker_id, overtime_approved } + }); + } + + logger.info('초과 근무 승인 요청', { record_date, worker_id, overtime_approved }); + + try { + const result = await AttendanceModel.approveOvertime({ + record_date, + worker_id, + overtime_approved + }); + + logger.info('초과 근무 승인 처리 성공', { record_date, worker_id }); + return result; + } catch (error) { + logger.error('초과 근무 승인 실패', { record_date, worker_id, error: error.message }); + throw new DatabaseError('초과 근무 승인 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 근태 유형 목록 조회 + */ +const getAttendanceTypesService = async () => { + logger.info('근태 유형 목록 조회 요청'); + + try { + const types = await AttendanceModel.getAttendanceTypes(); + logger.info('근태 유형 목록 조회 성공', { count: types.length }); + return types; + } catch (error) { + logger.error('근태 유형 목록 조회 실패', { error: error.message }); + throw new DatabaseError('근태 유형 목록 조회 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 휴가 유형 목록 조회 + */ +const getVacationTypesService = async () => { + logger.info('휴가 유형 목록 조회 요청'); + + try { + const types = await AttendanceModel.getVacationTypes(); + logger.info('휴가 유형 목록 조회 성공', { count: types.length }); + return types; + } catch (error) { + logger.error('휴가 유형 목록 조회 실패', { error: error.message }); + throw new DatabaseError('휴가 유형 목록 조회 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 작업자 휴가 잔여 일수 조회 + */ +const getWorkerVacationBalanceService = async (workerId) => { + if (!workerId) { + throw new ValidationError('작업자 ID가 필요합니다', { + required: ['worker_id'], + received: { workerId } + }); + } + + logger.info('휴가 잔여 일수 조회 요청', { workerId }); + + try { + const balance = await AttendanceModel.getWorkerVacationBalance(workerId); + logger.info('휴가 잔여 일수 조회 성공', { workerId }); + return balance; + } catch (error) { + logger.error('휴가 잔여 일수 조회 실패', { workerId, error: error.message }); + throw new DatabaseError('휴가 잔여 일수 조회 중 데이터베이스 오류가 발생했습니다'); + } +}; + +/** + * 월별 근태 통계 조회 + */ +const getMonthlyAttendanceStatsService = async (year, month, workerId = null) => { + if (!year || !month) { + throw new ValidationError('연도와 월이 필요합니다', { + required: ['year', 'month'], + received: { year, month } + }); + } + + logger.info('월별 근태 통계 조회 요청', { year, month, workerId }); + + try { + const stats = await AttendanceModel.getMonthlyAttendanceStats(year, month, workerId); + logger.info('월별 근태 통계 조회 성공', { year, month }); + return stats; + } catch (error) { + logger.error('월별 근태 통계 조회 실패', { year, month, error: error.message }); + throw new DatabaseError('월별 근태 통계 조회 중 데이터베이스 오류가 발생했습니다'); + } +}; + +module.exports = { + getDailyAttendanceStatusService, + getDailyAttendanceRecordsService, + upsertAttendanceRecordService, + processVacationService, + approveOvertimeService, + getAttendanceTypesService, + getVacationTypesService, + getWorkerVacationBalanceService, + getMonthlyAttendanceStatsService +};