refactor: System 1 모델/컨트롤러 콜백→async/await 전환

11개 모델 파일의 171개 콜백 메서드를 직접 return/throw 패턴으로 변환.
8개 컨트롤러에서 new Promise 래퍼와 중첩 콜백 제거, console.error→logger.error 교체.
미사용 pageAccessModel.js 삭제. 전체 -3,600줄 감소.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-02-25 09:40:33 +09:00
parent c29a1506bb
commit 5183e9ff85
22 changed files with 4791 additions and 8406 deletions

View File

@@ -14,7 +14,7 @@ const { asyncHandler } = require('../middlewares/errorHandler');
const logger = require('../utils/logger');
/**
* 📝 작업보고서 생성 (V2 - Service Layer 사용)
* 작업보고서 생성 (V2 - Service Layer 사용)
*/
const createDailyWorkReport = asyncHandler(async (req, res) => {
const reportData = {
@@ -33,29 +33,19 @@ const createDailyWorkReport = asyncHandler(async (req, res) => {
});
/**
* 📊 기여자별 요약 조회 (새로운 기능)
* 기여자별 요약 조회
*/
const getContributorsSummary = asyncHandler(async (req, res) => {
const { date, worker_id } = req.query;
if (!date || !worker_id) {
throw new ApiError('date와 worker_id가 필요합니다.', 400);
return res.status(400).json({ error: 'date와 worker_id가 필요합니다.' });
}
console.log(`📊 기여자별 요약 조회: date=${date}, worker_id=${worker_id}`);
try {
const data = await new Promise((resolve, reject) => {
dailyWorkReportModel.getContributorsByDate(date, worker_id, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const data = await dailyWorkReportModel.getContributorsByDate(date, worker_id);
const totalHours = data.reduce((sum, contributor) => sum + parseFloat(contributor.total_hours || 0), 0);
console.log(`📊 기여자별 요약: ${data.length}명, 총 ${totalHours}시간`);
const result = {
date,
worker_id,
@@ -65,15 +55,12 @@ const getContributorsSummary = asyncHandler(async (req, res) => {
};
res.success(result, '기여자별 요약 조회 성공');
} catch (err) {
handleDatabaseError(err, '기여자별 요약 조회');
}
});
/**
* 📊 개인 누적 현황 조회 (새로운 기능)
* 개인 누적 현황 조회
*/
const getMyAccumulatedData = (req, res) => {
const getMyAccumulatedData = async (req, res) => {
const { date, worker_id } = req.query;
const created_by = req.user?.user_id || req.user?.id;
@@ -90,18 +77,9 @@ const getMyAccumulatedData = (req, res) => {
});
}
console.log(`📊 개인 누적 현황 조회: date=${date}, worker_id=${worker_id}, created_by=${created_by}`);
try {
const data = await dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by);
dailyWorkReportModel.getMyAccumulatedHours(date, worker_id, created_by, (err, data) => {
if (err) {
console.error('개인 누적 현황 조회 오류:', err);
return res.status(500).json({
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
details: err.message
});
}
console.log(`📊 개인 누적: ${data.my_entry_count}개 항목, ${data.my_total_hours}시간`);
res.json({
date,
worker_id,
@@ -109,13 +87,19 @@ const getMyAccumulatedData = (req, res) => {
my_data: data,
timestamp: new Date().toISOString()
});
} catch (err) {
logger.error('개인 누적 현황 조회 오류:', err);
res.status(500).json({
error: '개인 누적 현황 조회 중 오류가 발생했습니다.',
details: err.message
});
}
};
/**
* 🗑️ 개별 항목 삭제 (본인 작성분만 - 새로운 기능)
* 개별 항목 삭제 (본인 작성분만)
*/
const removeMyEntry = (req, res) => {
const removeMyEntry = async (req, res) => {
const { id } = req.params;
const deleted_by = req.user?.user_id || req.user?.id;
@@ -125,18 +109,9 @@ const removeMyEntry = (req, res) => {
});
}
console.log(`🗑️ 개별 항목 삭제 요청: id=${id}, 삭제자=${deleted_by}`);
try {
const result = await dailyWorkReportModel.removeSpecificEntry(id, deleted_by);
dailyWorkReportModel.removeSpecificEntry(id, deleted_by, (err, result) => {
if (err) {
console.error('개별 항목 삭제 오류:', err);
return res.status(500).json({
error: '항목 삭제 중 오류가 발생했습니다.',
details: err.message
});
}
console.log(`✅ 개별 항목 삭제 완료: id=${id}`);
res.json({
message: '항목이 성공적으로 삭제되었습니다.',
id: id,
@@ -144,17 +119,23 @@ const removeMyEntry = (req, res) => {
timestamp: new Date().toISOString(),
...result
});
} catch (err) {
logger.error('개별 항목 삭제 오류:', err);
res.status(500).json({
error: '항목 삭제 중 오류가 발생했습니다.',
details: err.message
});
}
};
/**
* 📊 작업보고서 조회 (V2 - Service Layer 사용)
* 작업보고서 조회 (V2 - Service Layer 사용)
*/
const getDailyWorkReports = async (req, res) => {
try {
const userInfo = {
user_id: req.user?.user_id || req.user?.id,
role: req.user?.role || 'user' // 기본값을 'user'로 설정하여 안전하게 처리
role: req.user?.role || 'user'
};
if (!userInfo.user_id) {
@@ -166,7 +147,7 @@ const getDailyWorkReports = async (req, res) => {
res.json(reports);
} catch (error) {
console.error('💥 작업보고서 조회 컨트롤러 오류:', error.message);
logger.error('작업보고서 조회 컨트롤러 오류:', error.message);
res.status(400).json({
success: false,
error: '작업보고서 조회에 실패했습니다.',
@@ -176,13 +157,11 @@ const getDailyWorkReports = async (req, res) => {
};
/**
* 📊 날짜별 작업보고서 조회 (경로 파라미터 - 권한별 전체 조회 지원)
* 날짜별 작업보고서 조회 (경로 파라미터 - 권한별 전체 조회 지원)
*/
const getDailyWorkReportsByDate = (req, res) => {
const getDailyWorkReportsByDate = async (req, res) => {
const { date } = req.params;
const current_user_id = req.user?.user_id || req.user?.id;
const user_access_level = req.user?.access_level;
const user_job_type = req.user?.job_type;
if (!current_user_id) {
return res.status(401).json({
@@ -190,40 +169,24 @@ const getDailyWorkReportsByDate = (req, res) => {
});
}
const isAdmin = user_access_level === 'system' || user_access_level === 'admin' || user_access_level === 'leader' || user_job_type === 'leader';
try {
const data = await dailyWorkReportModel.getByDate(date);
console.log(`📊 날짜별 조회 (경로): date=${date}, user=${current_user_id}, 권한=${user_access_level}, 직책=${user_job_type}, 관리자=${isAdmin}`);
console.log(`🔍 사용자 정보 상세:`, req.user);
dailyWorkReportModel.getByDate(date, (err, data) => {
if (err) {
console.error('날짜별 작업보고서 조회 오류:', err);
return res.status(500).json({
// 임시로 모든 사용자에게 전체 조회 허용
res.json(data);
} catch (err) {
logger.error('날짜별 작업보고서 조회 오류:', err);
res.status(500).json({
error: '작업보고서 조회 중 오류가 발생했습니다.',
details: err.message
});
}
// 🎯 권한별 필터링 (임시로 비활성화)
let finalData = data;
console.log(`📊 임시로 모든 사용자에게 전체 조회 허용: ${data.length}`);
console.log(`📊 권한 정보: access_level=${user_access_level}, job_type=${user_job_type}, isAdmin=${isAdmin}`);
// if (!isAdmin) {
// finalData = data.filter(report => report.created_by === current_user_id);
// console.log(`📊 권한 필터링: 전체 ${data.length}개 → ${finalData.length}개`);
// } else {
// console.log(`📊 관리자 권한으로 전체 조회: ${data.length}개`);
// }
res.json(finalData);
});
};
/**
* 🔍 작업보고서 검색 (페이지네이션 포함)
* 작업보고서 검색 (페이지네이션 포함)
*/
const searchWorkReports = (req, res) => {
const searchWorkReports = async (req, res) => {
const { start_date, end_date, worker_id, project_id, work_status_id, page = 1, limit = 20 } = req.query;
const created_by = req.user?.user_id || req.user?.id;
@@ -247,36 +210,32 @@ const searchWorkReports = (req, res) => {
worker_id: worker_id ? parseInt(worker_id) : null,
project_id: project_id ? parseInt(project_id) : null,
work_status_id: work_status_id ? parseInt(work_status_id) : null,
created_by, // 작성자 필터링 추가
created_by,
page: parseInt(page),
limit: parseInt(limit)
};
console.log('🔍 작업보고서 검색 요청:', searchParams);
dailyWorkReportModel.searchWithDetails(searchParams, (err, data) => {
if (err) {
console.error('작업보고서 검색 오류:', err);
return res.status(500).json({
try {
const data = await dailyWorkReportModel.searchWithDetails(searchParams);
res.json(data);
} catch (err) {
logger.error('작업보고서 검색 오류:', err);
res.status(500).json({
error: '작업보고서 검색 중 오류가 발생했습니다.',
details: err.message
});
}
console.log(`🔍 검색 결과: ${data.reports?.length || 0}개 (전체: ${data.total || 0}개)`);
res.json(data);
});
};
/**
* 📈 통계 조회 (V2 - Service Layer 사용)
* 통계 조회 (V2 - Service Layer 사용)
*/
const getWorkReportStats = async (req, res) => {
try {
const statsData = await dailyWorkReportService.getStatisticsService(req.query);
res.json(statsData);
} catch (error) {
console.error('💥 통계 조회 컨트롤러 오류:', error.message);
logger.error('통계 조회 컨트롤러 오류:', error.message);
res.status(400).json({
success: false,
error: '통계 조회에 실패했습니다.',
@@ -286,14 +245,14 @@ const getWorkReportStats = async (req, res) => {
};
/**
* 📊 일일 근무 요약 조회 (V2 - Service Layer 사용)
* 일일 근무 요약 조회 (V2 - Service Layer 사용)
*/
const getDailySummary = async (req, res) => {
try {
const summaryData = await dailyWorkReportService.getSummaryService(req.query);
res.json(summaryData);
} catch (error) {
console.error('💥 일일 요약 조회 컨트롤러 오류:', error.message);
logger.error('일일 요약 조회 컨트롤러 오류:', error.message);
res.status(400).json({
success: false,
error: '일일 요약 조회에 실패했습니다.',
@@ -303,9 +262,9 @@ const getDailySummary = async (req, res) => {
};
/**
* 📅 월간 요약 조회
* 월간 요약 조회
*/
const getMonthlySummary = (req, res) => {
const getMonthlySummary = async (req, res) => {
const { year, month } = req.query;
if (!year || !month) {
@@ -316,16 +275,8 @@ const getMonthlySummary = (req, res) => {
});
}
console.log(`📅 월간 요약 조회: ${year}-${month}`);
dailyWorkReportModel.getMonthlySummary(year, month, (err, data) => {
if (err) {
console.error('월간 요약 조회 오류:', err);
return res.status(500).json({
error: '월간 요약 조회 중 오류가 발생했습니다.',
details: err.message
});
}
try {
const data = await dailyWorkReportModel.getMonthlySummary(year, month);
res.json({
year: parseInt(year),
@@ -334,11 +285,17 @@ const getMonthlySummary = (req, res) => {
total_entries: data.length,
timestamp: new Date().toISOString()
});
} catch (err) {
logger.error('월간 요약 조회 오류:', err);
res.status(500).json({
error: '월간 요약 조회 중 오류가 발생했습니다.',
details: err.message
});
}
};
/**
* ✏️ 작업보고서 수정 (V2 - Service Layer 사용)
* 작업보고서 수정 (V2 - Service Layer 사용)
*/
const updateWorkReport = async (req, res) => {
try {
@@ -362,7 +319,7 @@ const updateWorkReport = async (req, res) => {
});
} catch (error) {
console.error(`💥 작업보고서 수정 컨트롤러 오류 (id: ${req.params.id}):`, error.message);
logger.error(`작업보고서 수정 컨트롤러 오류 (id: ${req.params.id}):`, error.message);
const statusCode = error.statusCode || 400;
res.status(statusCode).json({
success: false,
@@ -373,7 +330,7 @@ const updateWorkReport = async (req, res) => {
};
/**
* 🗑️ 특정 작업보고서 삭제 (V2 - Service Layer 사용)
* 특정 작업보고서 삭제 (V2 - Service Layer 사용)
* 권한: 그룹장(group_leader), 시스템(system), 관리자(admin)만 가능
*/
const removeDailyWorkReport = async (req, res) => {
@@ -406,7 +363,7 @@ const removeDailyWorkReport = async (req, res) => {
});
} catch (error) {
console.error(`💥 작업보고서 삭제 컨트롤러 오류 (id: ${req.params.id}):`, error.message);
logger.error(`작업보고서 삭제 컨트롤러 오류 (id: ${req.params.id}):`, error.message);
const statusCode = error.statusCode || 400;
res.status(statusCode).json({
success: false,
@@ -417,9 +374,9 @@ const removeDailyWorkReport = async (req, res) => {
};
/**
* <20><> 작업자의 특정 날짜 전체 삭제
* 작업자의 특정 날짜 전체 삭제
*/
const removeDailyWorkReportByDateAndWorker = (req, res) => {
const removeDailyWorkReportByDateAndWorker = async (req, res) => {
const { date, worker_id } = req.params;
const deleted_by = req.user?.user_id || req.user?.id;
const access_level = req.user?.access_level || req.user?.role;
@@ -439,16 +396,8 @@ const removeDailyWorkReportByDateAndWorker = (req, res) => {
});
}
console.log(`🗑️ 날짜+작업자별 전체 삭제 요청: date=${date}, worker_id=${worker_id}, 삭제자=${deleted_by}`);
dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by, (err, affectedRows) => {
if (err) {
console.error('작업보고서 전체 삭제 오류:', err);
return res.status(500).json({
error: '작업보고서 삭제 중 오류가 발생했습니다.',
details: err.message
});
}
try {
const affectedRows = await dailyWorkReportModel.removeByDateAndWorker(date, worker_id, deleted_by);
if (affectedRows === 0) {
return res.status(404).json({
@@ -458,7 +407,6 @@ const removeDailyWorkReportByDateAndWorker = (req, res) => {
});
}
console.log(`✅ 날짜+작업자별 전체 삭제 완료: ${affectedRows}`);
res.json({
message: `${date} 날짜의 작업자 ${worker_id} 작업보고서 ${affectedRows}개가 삭제되었습니다.`,
date,
@@ -467,18 +415,29 @@ const removeDailyWorkReportByDateAndWorker = (req, res) => {
deleted_by,
timestamp: new Date().toISOString()
});
} catch (err) {
logger.error('작업보고서 전체 삭제 오류:', err);
res.status(500).json({
error: '작업보고서 삭제 중 오류가 발생했습니다.',
details: err.message
});
}
};
/**
* 📋 마스터 데이터 조회 함수들
* 마스터 데이터 조회 함수들
*/
const getWorkTypes = (req, res) => {
console.log('📋 작업 유형 조회 요청');
dailyWorkReportModel.getAllWorkTypes((err, data) => {
if (err) {
console.error('작업 유형 조회 오류:', err);
return res.status(500).json({
const getWorkTypes = async (req, res) => {
try {
const data = await dailyWorkReportModel.getAllWorkTypes();
res.json({
success: true,
data: data,
message: '작업 유형 조회 성공'
});
} catch (err) {
logger.error('작업 유형 조회 오류:', err);
res.status(500).json({
success: false,
error: {
message: '작업 유형 조회 중 오류가 발생했습니다.',
@@ -486,316 +445,203 @@ const getWorkTypes = (req, res) => {
}
});
}
console.log(`📋 작업 유형 조회 결과: ${data.length}`);
res.json({
success: true,
data: data,
message: '작업 유형 조회 성공'
});
});
};
const getWorkStatusTypes = (req, res) => {
console.log('📋 업무 상태 유형 조회 요청');
dailyWorkReportModel.getAllWorkStatusTypes((err, data) => {
if (err) {
console.error('업무 상태 유형 조회 오류:', err);
return res.status(500).json({
const getWorkStatusTypes = async (req, res) => {
try {
const data = await dailyWorkReportModel.getAllWorkStatusTypes();
res.json(data);
} catch (err) {
logger.error('업무 상태 유형 조회 오류:', err);
res.status(500).json({
error: '업무 상태 유형 조회 중 오류가 발생했습니다.',
details: err.message
});
}
console.log(`📋 업무 상태 유형 조회 결과: ${data.length}`);
res.json(data);
});
};
const getErrorTypes = (req, res) => {
console.log('📋 에러 유형 조회 요청');
dailyWorkReportModel.getAllErrorTypes((err, data) => {
if (err) {
console.error('에러 유형 조회 오류:', err);
return res.status(500).json({
const getErrorTypes = async (req, res) => {
try {
const data = await dailyWorkReportModel.getAllErrorTypes();
res.json(data);
} catch (err) {
logger.error('에러 유형 조회 오류:', err);
res.status(500).json({
error: '에러 유형 조회 중 오류가 발생했습니다.',
details: err.message
});
}
console.log(`📋 에러 유형 조회 결과: ${data.length}`);
res.json(data);
});
};
// ========== 작업 유형 CRUD ==========
/**
* 📝 작업 유형 생성
* 작업 유형 생성
*/
const createWorkType = asyncHandler(async (req, res) => {
const { name, description, category } = req.body;
if (!name) {
throw new ApiError('작업 유형 이름이 필요합니다.', 400);
return res.status(400).json({ error: '작업 유형 이름이 필요합니다.' });
}
console.log('📝 작업 유형 생성:', { name, description, category });
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.createWorkType({ name, description, category }, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.createWorkType({ name, description, category });
res.created(result, '작업 유형이 성공적으로 생성되었습니다.');
} catch (err) {
handleDatabaseError(err, '작업 유형 생성');
}
});
/**
* ✏️ 작업 유형 수정
* 작업 유형 수정
*/
const updateWorkType = asyncHandler(async (req, res) => {
const { id } = req.params;
const { name, description, category } = req.body;
if (!id) {
throw new ApiError('작업 유형 ID가 필요합니다.', 400);
return res.status(400).json({ error: '작업 유형 ID가 필요합니다.' });
}
console.log('✏️ 작업 유형 수정:', { id, name, description, category });
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.updateWorkType(id, { name, description, category }, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.updateWorkType(id, { name, description, category });
if (result.affectedRows === 0) {
throw new ApiError('수정할 작업 유형을 찾을 수 없습니다.', 404);
return res.status(404).json({ error: '수정할 작업 유형을 찾을 수 없습니다.' });
}
res.success(result, '작업 유형이 성공적으로 수정되었습니다.');
} catch (err) {
handleDatabaseError(err, '작업 유형 수정');
}
});
/**
* 🗑️ 작업 유형 삭제
* 작업 유형 삭제
*/
const deleteWorkType = asyncHandler(async (req, res) => {
const { id } = req.params;
if (!id) {
throw new ApiError('작업 유형 ID가 필요합니다.', 400);
return res.status(400).json({ error: '작업 유형 ID가 필요합니다.' });
}
console.log('🗑️ 작업 유형 삭제:', id);
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.deleteWorkType(id, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.deleteWorkType(id);
if (result.affectedRows === 0) {
throw new ApiError('삭제할 작업 유형을 찾을 수 없습니다.', 404);
return res.status(404).json({ error: '삭제할 작업 유형을 찾을 수 없습니다.' });
}
res.success(result, '작업 유형이 성공적으로 삭제되었습니다.');
} catch (err) {
handleDatabaseError(err, '작업 유형 삭제');
}
});
// ========== 작업 상태 CRUD ==========
/**
* 📝 작업 상태 생성
* 작업 상태 생성
*/
const createWorkStatus = asyncHandler(async (req, res) => {
const { name, description, is_error } = req.body;
if (!name) {
throw new ApiError('작업 상태 이름이 필요합니다.', 400);
return res.status(400).json({ error: '작업 상태 이름이 필요합니다.' });
}
console.log('📝 작업 상태 생성:', { name, description, is_error });
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.createWorkStatus({ name, description, is_error }, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.createWorkStatus({ name, description, is_error });
res.created(result, '작업 상태가 성공적으로 생성되었습니다.');
} catch (err) {
handleDatabaseError(err, '작업 상태 생성');
}
});
/**
* ✏️ 작업 상태 수정
* 작업 상태 수정
*/
const updateWorkStatus = asyncHandler(async (req, res) => {
const { id } = req.params;
const { name, description, is_error } = req.body;
if (!id) {
throw new ApiError('작업 상태 ID가 필요합니다.', 400);
return res.status(400).json({ error: '작업 상태 ID가 필요합니다.' });
}
console.log('✏️ 작업 상태 수정:', { id, name, description, is_error });
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.updateWorkStatus(id, { name, description, is_error }, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.updateWorkStatus(id, { name, description, is_error });
if (result.affectedRows === 0) {
throw new ApiError('수정할 작업 상태를 찾을 수 없습니다.', 404);
return res.status(404).json({ error: '수정할 작업 상태를 찾을 수 없습니다.' });
}
res.success(result, '작업 상태가 성공적으로 수정되었습니다.');
} catch (err) {
handleDatabaseError(err, '작업 상태 수정');
}
});
/**
* 🗑️ 작업 상태 삭제
* 작업 상태 삭제
*/
const deleteWorkStatus = asyncHandler(async (req, res) => {
const { id } = req.params;
if (!id) {
throw new ApiError('작업 상태 ID가 필요합니다.', 400);
return res.status(400).json({ error: '작업 상태 ID가 필요합니다.' });
}
console.log('🗑️ 작업 상태 삭제:', id);
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.deleteWorkStatus(id, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.deleteWorkStatus(id);
if (result.affectedRows === 0) {
throw new ApiError('삭제할 작업 상태를 찾을 수 없습니다.', 404);
return res.status(404).json({ error: '삭제할 작업 상태를 찾을 수 없습니다.' });
}
res.success(result, '작업 상태가 성공적으로 삭제되었습니다.');
} catch (err) {
handleDatabaseError(err, '작업 상태 삭제');
}
});
// ========== 오류 유형 CRUD ==========
/**
* 📝 오류 유형 생성
* 오류 유형 생성
*/
const createErrorType = asyncHandler(async (req, res) => {
const { name, description, severity } = req.body;
if (!name) {
throw new ApiError('오류 유형 이름이 필요합니다.', 400);
return res.status(400).json({ error: '오류 유형 이름이 필요합니다.' });
}
console.log('📝 오류 유형 생성:', { name, description, severity });
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.createErrorType({ name, description, severity }, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.createErrorType({ name, description, severity });
res.created(result, '오류 유형이 성공적으로 생성되었습니다.');
} catch (err) {
handleDatabaseError(err, '오류 유형 생성');
}
});
/**
* ✏️ 오류 유형 수정
* 오류 유형 수정
*/
const updateErrorType = asyncHandler(async (req, res) => {
const { id } = req.params;
const { name, description, severity } = req.body;
if (!id) {
throw new ApiError('오류 유형 ID가 필요합니다.', 400);
return res.status(400).json({ error: '오류 유형 ID가 필요합니다.' });
}
console.log('✏️ 오류 유형 수정:', { id, name, description, severity });
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.updateErrorType(id, { name, description, severity }, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.updateErrorType(id, { name, description, severity });
if (result.affectedRows === 0) {
throw new ApiError('수정할 오류 유형을 찾을 수 없습니다.', 404);
return res.status(404).json({ error: '수정할 오류 유형을 찾을 수 없습니다.' });
}
res.success(result, '오류 유형이 성공적으로 수정되었습니다.');
} catch (err) {
handleDatabaseError(err, '오류 유형 수정');
}
});
/**
* 🗑️ 오류 유형 삭제
* 오류 유형 삭제
*/
const deleteErrorType = asyncHandler(async (req, res) => {
const { id } = req.params;
if (!id) {
throw new ApiError('오류 유형 ID가 필요합니다.', 400);
return res.status(400).json({ error: '오류 유형 ID가 필요합니다.' });
}
console.log('🗑️ 오류 유형 삭제:', id);
try {
const result = await new Promise((resolve, reject) => {
dailyWorkReportModel.deleteErrorType(id, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const result = await dailyWorkReportModel.deleteErrorType(id);
if (result.affectedRows === 0) {
throw new ApiError('삭제할 오류 유형을 찾을 수 없습니다.', 404);
return res.status(404).json({ error: '삭제할 오류 유형을 찾을 수 없습니다.' });
}
res.success(result, '오류 유형이 성공적으로 삭제되었습니다.');
} catch (err) {
handleDatabaseError(err, '오류 유형 삭제');
}
});
/**
* 📊 누적 현황 조회
* 누적 현황 조회
*/
const getAccumulatedReports = (req, res) => {
const getAccumulatedReports = async (req, res) => {
const { date, worker_id } = req.query;
if (!date || !worker_id) {
@@ -805,18 +651,9 @@ const getAccumulatedReports = (req, res) => {
});
}
console.log(`📊 누적 현황 조회: date=${date}, worker_id=${worker_id}`);
try {
const data = await dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id);
dailyWorkReportModel.getAccumulatedReportsByDate(date, worker_id, (err, data) => {
if (err) {
console.error('누적 현황 조회 오류:', err);
return res.status(500).json({
error: '누적 현황 조회 중 오류가 발생했습니다.',
details: err.message
});
}
console.log(`📊 누적 현황 조회 결과: ${data.length}`);
res.json({
date,
worker_id,
@@ -824,7 +661,13 @@ const getAccumulatedReports = (req, res) => {
accumulated_data: data,
timestamp: new Date().toISOString()
});
} catch (err) {
logger.error('누적 현황 조회 오류:', err);
res.status(500).json({
error: '누적 현황 조회 중 오류가 발생했습니다.',
details: err.message
});
}
};
/**
@@ -870,7 +713,7 @@ const createFromTbm = async (req, res) => {
total_hours,
error_hours: error_hours || 0,
regular_hours,
work_status_id: work_status_id || (error_hours > 0 ? 2 : 1), // error_hours가 있으면 상태 2 (부적합)
work_status_id: work_status_id || (error_hours > 0 ? 2 : 1),
error_type_id,
created_by: req.user.user_id
};
@@ -884,31 +727,29 @@ const createFromTbm = async (req, res) => {
});
} catch (err) {
console.error('TBM 작업보고서 생성 오류:', err);
console.error('Error stack:', err.stack);
logger.error('TBM 작업보고서 생성 오류:', err);
res.status(500).json({
success: false,
message: 'TBM 작업보고서 생성 중 오류가 발생했습니다.',
error: err.message,
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
error: err.message
});
}
};
// 모든 컨트롤러 함수 내보내기 (리팩토링된 함수 위주로 재구성)
// 모든 컨트롤러 함수 내보내기
module.exports = {
// 📝 V2 핵심 CRUD 함수
// V2 핵심 CRUD 함수
createDailyWorkReport,
getDailyWorkReports,
updateWorkReport,
removeDailyWorkReport,
createFromTbm,
// 📊 V2 통계 및 요약 함수
// V2 통계 및 요약 함수
getWorkReportStats,
getDailySummary,
// 🔽 아직 리팩토링되지 않은 레거시 함수들
// 레거시 함수 (콜백 제거 완료)
getAccumulatedReports,
getContributorsSummary,
getMyAccumulatedData,
@@ -921,7 +762,7 @@ module.exports = {
getWorkStatusTypes,
getErrorTypes,
// 🔽 마스터 데이터 CRUD
// 마스터 데이터 CRUD
createWorkType,
updateWorkType,
deleteWorkType,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
const vacationBalanceModel = require('../models/vacationBalanceModel');
const vacationTypeModel = require('../models/vacationTypeModel');
const logger = require('../utils/logger');
const vacationBalanceController = {
/**
@@ -14,27 +15,11 @@ const vacationBalanceController = {
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
});
});
const results = await vacationBalanceModel.getByWorkerAndYear(workerId, year);
res.json({ success: true, data: results });
} catch (error) {
console.error('getByWorkerAndYear 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 잔액 조회 오류:', error);
res.status(500).json({ success: false, message: '휴가 잔액을 조회하는 중 오류가 발생했습니다' });
}
},
@@ -45,27 +30,11 @@ const vacationBalanceController = {
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
});
});
const results = await vacationBalanceModel.getAllByYear(year);
res.json({ success: true, data: results });
} catch (error) {
console.error('getAllByYear 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('전체 휴가 잔액 조회 오류:', error);
res.status(500).json({ success: false, message: '전체 휴가 잔액을 조회하는 중 오류가 발생했습니다' });
}
},
@@ -75,17 +44,9 @@ const vacationBalanceController = {
*/
async createBalance(req, res) {
try {
const {
worker_id,
vacation_type_id,
year,
total_days,
used_days,
notes
} = req.body;
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,
@@ -94,15 +55,7 @@ const vacationBalanceController = {
}
// 중복 체크
vacationBalanceModel.getByWorkerTypeYear(worker_id, vacation_type_id, year, (err, existing) => {
if (err) {
console.error('중복 체크 오류:', err);
return res.status(500).json({
success: false,
message: '중복 체크 중 오류가 발생했습니다'
});
}
const existing = await vacationBalanceModel.getByWorkerTypeYear(worker_id, vacation_type_id, year);
if (existing && existing.length > 0) {
return res.status(400).json({
success: false,
@@ -120,28 +73,15 @@ const vacationBalanceController = {
created_by
};
vacationBalanceModel.create(balanceData, (err, result) => {
if (err) {
console.error('휴가 잔액 생성 오류:', err);
return res.status(500).json({
success: false,
message: '휴가 잔액을 생성하는 중 오류가 발생했습니다'
});
}
const result = await vacationBalanceModel.create(balanceData);
res.status(201).json({
success: true,
message: '휴가 잔액이 생성되었습니다',
data: { id: result.insertId }
});
});
});
} catch (error) {
console.error('createBalance 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 잔액 생성 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -161,39 +101,18 @@ const vacationBalanceController = {
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: '휴가 잔액을 수정하는 중 오류가 발생했습니다'
});
return res.status(400).json({ success: false, message: '수정할 데이터가 없습니다' });
}
const result = await vacationBalanceModel.update(id, updateData);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '휴가 잔액을 찾을 수 없습니다'
});
return res.status(404).json({ success: false, message: '휴가 잔액을 찾을 수 없습니다' });
}
res.json({
success: true,
message: '휴가 잔액이 수정되었습니다'
});
});
res.json({ success: true, message: '휴가 잔액이 수정되었습니다' });
} catch (error) {
console.error('updateBalance 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 잔액 수정 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -204,34 +123,16 @@ const vacationBalanceController = {
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: '휴가 잔액을 삭제하는 중 오류가 발생했습니다'
});
}
const result = await vacationBalanceModel.delete(id);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '휴가 잔액을 찾을 수 없습니다'
});
return res.status(404).json({ success: false, message: '휴가 잔액을 찾을 수 없습니다' });
}
res.json({
success: true,
message: '휴가 잔액이 삭제되었습니다'
});
});
res.json({ success: true, message: '휴가 잔액이 삭제되었습니다' });
} catch (error) {
console.error('deleteBalance 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 잔액 삭제 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -251,31 +152,18 @@ const vacationBalanceController = {
});
}
// 연차 일수 계산
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 types = await vacationTypeModel.getByCode('ANNUAL');
if (!types || types.length === 0) {
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: '중복 체크 중 오류가 발생했습니다'
});
}
const existing = await vacationBalanceModel.getByWorkerTypeYear(worker_id, annualTypeId, year);
if (existing && existing.length > 0) {
return res.status(400).json({
success: false,
@@ -293,32 +181,15 @@ const vacationBalanceController = {
created_by
};
vacationBalanceModel.create(balanceData, (err, result) => {
if (err) {
console.error('휴가 잔액 생성 오류:', err);
return res.status(500).json({
success: false,
message: '휴가 잔액을 생성하는 중 오류가 발생했습니다'
});
}
const result = await vacationBalanceModel.create(balanceData);
res.status(201).json({
success: true,
message: `${annualDays}일의 연차가 자동으로 생성되었습니다`,
data: {
id: result.insertId,
calculated_days: annualDays
}
});
});
});
data: { id: result.insertId, calculated_days: annualDays }
});
} catch (error) {
console.error('autoCalculateAndCreate 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('연차 자동 계산 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -332,10 +203,7 @@ const vacationBalanceController = {
const created_by = req.user.user_id;
if (!balances || !Array.isArray(balances) || balances.length === 0) {
return res.status(400).json({
success: false,
message: '저장할 데이터가 없습니다'
});
return res.status(400).json({ success: false, message: '저장할 데이터가 없습니다' });
}
const { getDb } = require('../dbPool');
@@ -353,7 +221,6 @@ const vacationBalanceController = {
}
try {
// Upsert 쿼리
const query = `
INSERT INTO vacation_balance_details
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
@@ -367,7 +234,7 @@ const vacationBalanceController = {
await db.query(query, [worker_id, vacation_type_id, year, total_days, notes || null, created_by]);
successCount++;
} catch (err) {
console.error('휴가 잔액 저장 오류:', err);
logger.error('휴가 잔액 저장 오류:', err);
errorCount++;
}
}
@@ -378,11 +245,8 @@ const vacationBalanceController = {
data: { successCount, errorCount }
});
} catch (error) {
console.error('bulkUpsert 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('bulkUpsert 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -393,27 +257,11 @@ const vacationBalanceController = {
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
});
});
const results = await vacationBalanceModel.getAvailableVacationDays(workerId, year);
res.json({ success: true, data: results });
} catch (error) {
console.error('getAvailableDays 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('사용 가능 휴가 조회 오류:', error);
res.status(500).json({ success: false, message: '사용 가능 휴가를 조회하는 중 오류가 발생했습니다' });
}
}
};

View File

@@ -4,6 +4,7 @@
*/
const vacationTypeModel = require('../models/vacationTypeModel');
const logger = require('../utils/logger');
const vacationTypeController = {
/**
@@ -12,26 +13,11 @@ const vacationTypeController = {
*/
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
});
});
const results = await vacationTypeModel.getAll();
res.json({ success: true, data: results });
} catch (error) {
console.error('getAllTypes 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 유형 조회 오류:', error);
res.status(500).json({ success: false, message: '휴가 유형을 조회하는 중 오류가 발생했습니다' });
}
},
@@ -41,26 +27,11 @@ const vacationTypeController = {
*/
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
});
});
const results = await vacationTypeModel.getSystemTypes();
res.json({ success: true, data: results });
} catch (error) {
console.error('getSystemTypes 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('시스템 휴가 유형 조회 오류:', error);
res.status(500).json({ success: false, message: '시스템 휴가 유형을 조회하는 중 오류가 발생했습니다' });
}
},
@@ -70,26 +41,11 @@ const vacationTypeController = {
*/
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
});
});
const results = await vacationTypeModel.getSpecialTypes();
res.json({ success: true, data: results });
} catch (error) {
console.error('getSpecialTypes 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('특별 휴가 유형 조회 오류:', error);
res.status(500).json({ success: false, message: '특별 휴가 유형을 조회하는 중 오류가 발생했습니다' });
}
},
@@ -99,15 +55,8 @@ const vacationTypeController = {
*/
async createType(req, res) {
try {
const {
type_code,
type_name,
deduct_days,
priority,
description
} = req.body;
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,
@@ -116,23 +65,11 @@ const vacationTypeController = {
}
// 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 중복 체크 중 오류가 발생했습니다'
});
}
const existingTypes = await vacationTypeModel.getByCode(type_code);
if (existingTypes && existingTypes.length > 0) {
return res.status(400).json({
success: false,
message: '이미 존재하는 type_code입니다'
});
return res.status(400).json({ success: false, message: '이미 존재하는 type_code입니다' });
}
// 특별 휴가 유형으로 생성
const typeData = {
type_code,
type_name,
@@ -144,28 +81,15 @@ const vacationTypeController = {
is_active: true
};
vacationTypeModel.create(typeData, (err, result) => {
if (err) {
console.error('휴가 유형 생성 오류:', err);
return res.status(500).json({
success: false,
message: '휴가 유형을 생성하는 중 오류가 발생했습니다'
});
}
const result = await vacationTypeModel.create(typeData);
res.status(201).json({
success: true,
message: '특별 휴가 유형이 생성되었습니다',
data: { id: result.insertId }
});
});
});
} catch (error) {
console.error('createType 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 유형 생성 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -176,41 +100,20 @@ const vacationTypeController = {
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: '휴가 유형을 조회하는 중 오류가 발생했습니다'
});
}
const { type_name, deduct_days, priority, description, is_active } = req.body;
const types = await vacationTypeModel.getById(id);
if (!types || types.length === 0) {
return res.status(404).json({
success: false,
message: '휴가 유형을 찾을 수 없습니다'
});
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;
@@ -219,35 +122,15 @@ const vacationTypeController = {
}
if (Object.keys(updateData).length === 0) {
return res.status(400).json({
success: false,
message: '수정할 데이터가 없습니다'
});
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: '휴가 유형이 수정되었습니다'
});
});
});
await vacationTypeModel.update(id, updateData);
res.json({ success: true, message: '휴가 유형이 수정되었습니다' });
} catch (error) {
console.error('updateType 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 유형 수정 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -258,15 +141,7 @@ const vacationTypeController = {
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: '휴가 유형을 삭제하는 중 오류가 발생했습니다'
});
}
const result = await vacationTypeModel.delete(id);
if (result.affectedRows === 0) {
return res.status(400).json({
@@ -275,17 +150,10 @@ const vacationTypeController = {
});
}
res.json({
success: true,
message: '휴가 유형이 삭제되었습니다'
});
});
res.json({ success: true, message: '휴가 유형이 삭제되었습니다' });
} catch (error) {
console.error('deleteType 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('휴가 유형 삭제 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
},
@@ -297,35 +165,22 @@ const vacationTypeController = {
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 배열이 필요합니다'
});
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: '우선순위를 업데이트하는 중 오류가 발생했습니다'
});
for (const { id, priority } of priorities) {
await vacationTypeModel.updatePriority(id, priority);
}
res.json({
success: true,
message: '우선순위가 업데이트되었습니다',
data: { updated: result.affectedRows }
});
data: { updated: priorities.length }
});
} catch (error) {
console.error('updatePriorities 오류:', error);
res.status(500).json({
success: false,
message: '서버 오류가 발생했습니다'
});
logger.error('우선순위 업데이트 오류:', error);
res.status(500).json({ success: false, message: '서버 오류가 발생했습니다' });
}
}
};

View File

@@ -1,50 +1,34 @@
const visitRequestModel = require('../models/visitRequestModel');
const logger = require('../utils/logger');
// ==================== 출입 신청 관리 ====================
/**
* 출입 신청 생성
*/
exports.createVisitRequest = (req, res) => {
exports.createVisitRequest = async (req, res) => {
try {
const requester_id = req.user.user_id;
const requestData = {
requester_id,
...req.body
};
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}는 필수 입력 항목입니다.`
});
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
});
}
const requestId = await visitRequestModel.createVisitRequest(requestData);
res.status(201).json({
success: true,
message: '출입 신청이 성공적으로 생성되었습니다.',
data: { request_id: requestId }
});
});
} catch (err) {
logger.error('출입 신청 생성 오류:', err);
res.status(500).json({ success: false, message: '출입 신청 생성 중 오류가 발생했습니다.' });
}
};
/**
* 출입 신청 목록 조회
*/
exports.getAllVisitRequests = (req, res) => {
exports.getAllVisitRequests = async (req, res) => {
try {
const filters = {
status: req.query.status,
visit_date: req.query.visit_date,
@@ -54,359 +38,168 @@ exports.getAllVisitRequests = (req, res) => {
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
});
const requests = await visitRequestModel.getAllVisitRequests(filters);
res.json({ success: true, data: requests });
} catch (err) {
logger.error('출입 신청 목록 조회 오류:', err);
res.status(500).json({ success: false, 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
});
}
exports.getVisitRequestById = async (req, res) => {
try {
const request = await visitRequestModel.getVisitRequestById(req.params.id);
if (!request) {
return res.status(404).json({
success: false,
message: '출입 신청을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, data: request });
} catch (err) {
logger.error('출입 신청 조회 오류:', err);
res.status(500).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
});
}
exports.updateVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.updateVisitRequest(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '출입 신청을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 수정되었습니다.' });
} catch (err) {
logger.error('출입 신청 수정 오류:', err);
res.status(500).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
});
}
exports.deleteVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.deleteVisitRequest(req.params.id);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '출입 신청을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 삭제되었습니다.' });
} catch (err) {
logger.error('출입 신청 삭제 오류:', err);
res.status(500).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
});
}
exports.approveVisitRequest = async (req, res) => {
try {
const result = await visitRequestModel.approveVisitRequest(req.params.id, req.user.user_id);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '출입 신청을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 승인되었습니다.' });
} catch (err) {
logger.error('출입 신청 승인 오류:', err);
res.status(500).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 || '사유 없음';
exports.rejectVisitRequest = async (req, res) => {
try {
const rejectionData = {
approved_by: approvedBy,
rejection_reason: rejectionReason
approved_by: req.user.user_id,
rejection_reason: req.body.rejection_reason || '사유 없음'
};
visitRequestModel.rejectVisitRequest(requestId, rejectionData, (err, result) => {
if (err) {
console.error('출입 신청 반려 오류:', err);
return res.status(500).json({
success: false,
message: '출입 신청 반려 중 오류가 발생했습니다.',
error: err.message
});
}
const result = await visitRequestModel.rejectVisitRequest(req.params.id, rejectionData);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '출입 신청을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '출입 신청을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '출입 신청이 반려되었습니다.' });
} catch (err) {
logger.error('출입 신청 반려 오류:', err);
res.status(500).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
});
exports.getAllVisitPurposes = async (req, res) => {
try {
const purposes = await visitRequestModel.getAllVisitPurposes();
res.json({ success: true, data: purposes });
} catch (err) {
logger.error('방문 목적 조회 오류:', err);
res.status(500).json({ success: false, 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
});
exports.getActiveVisitPurposes = async (req, res) => {
try {
const purposes = await visitRequestModel.getActiveVisitPurposes();
res.json({ success: true, data: purposes });
} catch (err) {
logger.error('활성 방문 목적 조회 오류:', err);
res.status(500).json({ success: false, 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은 필수 입력 항목입니다.'
});
exports.createVisitPurpose = async (req, res) => {
try {
if (!req.body.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
});
}
const purposeId = await visitRequestModel.createVisitPurpose(req.body);
res.status(201).json({
success: true,
message: '방문 목적이 추가되었습니다.',
data: { purpose_id: purposeId }
});
});
} catch (err) {
logger.error('방문 목적 추가 오류:', err);
res.status(500).json({ success: false, message: '방문 목적 추가 중 오류가 발생했습니다.' });
}
};
/**
* 방문 목적 수정
*/
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
});
}
exports.updateVisitPurpose = async (req, res) => {
try {
const result = await visitRequestModel.updateVisitPurpose(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '방문 목적을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '방문 목적이 수정되었습니다.' });
} catch (err) {
logger.error('방문 목적 수정 오류:', err);
res.status(500).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
});
}
exports.deleteVisitPurpose = async (req, res) => {
try {
const result = await visitRequestModel.deleteVisitPurpose(req.params.id);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '방문 목적을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '방문 목적을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '방문 목적이 삭제되었습니다.' });
} catch (err) {
logger.error('방문 목적 삭제 오류:', err);
res.status(500).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
};
exports.createTrainingRecord = async (req, res) => {
try {
const trainingData = { trainer_id: req.user.user_id, ...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}는 필수 입력 항목입니다.`
});
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
});
}
const trainingId = await visitRequestModel.createTrainingRecord(trainingData);
// 안전교육 기록 생성되면 출입 신청 상태를 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} 상태 변경 성공`);
// 안전교육 기록 생성 출입 신청 상태를 training_completed로 변경
try {
await visitRequestModel.updateVisitRequestStatus(trainingData.request_id, 'training_completed');
} catch (statusErr) {
logger.error('출입 신청 상태 업데이트 오류:', statusErr);
}
res.status(201).json({
@@ -414,142 +207,78 @@ exports.createTrainingRecord = (req, res) => {
message: '안전교육 기록이 생성되었습니다.',
data: { training_id: trainingId }
});
});
});
} catch (err) {
logger.error('안전교육 기록 생성 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 생성 중 오류가 발생했습니다.' });
}
};
/**
* 특정 출입 신청의 안전교육 기록 조회
*/
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
});
exports.getTrainingRecordByRequestId = async (req, res) => {
try {
const record = await visitRequestModel.getTrainingRecordByRequestId(req.params.requestId);
res.json({ success: true, data: record || null });
} catch (err) {
logger.error('안전교육 기록 조회 오류:', err);
res.status(500).json({ success: false, 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
});
}
exports.updateTrainingRecord = async (req, res) => {
try {
const result = await visitRequestModel.updateTrainingRecord(req.params.id, req.body);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '안전교육 기록을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
}
res.json({ success: true, message: '안전교육 기록이 수정되었습니다.' });
} catch (err) {
logger.error('안전교육 기록 수정 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 수정 중 오류가 발생했습니다.' });
}
res.json({
success: true,
message: '안전교육 기록이 수정되었습니다.'
});
});
};
/**
* 안전교육 완료 (서명 포함)
*/
exports.completeTraining = (req, res) => {
exports.completeTraining = async (req, res) => {
try {
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
});
return res.status(400).json({ success: false, message: '서명 데이터가 필요합니다.' });
}
const result = await visitRequestModel.completeTraining(trainingId, signatureData);
if (result.affectedRows === 0) {
return res.status(404).json({
success: false,
message: '안전교육 기록을 찾을 수 없습니다.'
});
return res.status(404).json({ success: false, message: '안전교육 기록을 찾을 수 없습니다.' });
}
// 교육 완료 후 출입 신청 상태를 'training_completed'로 변경
visitRequestModel.getTrainingRecordByRequestId(trainingId, (err, record) => {
if (err || !record) {
return res.json({
success: true,
message: '안전교육이 완료되었습니다.'
});
// 교육 완료 후 출입 신청 상태 변경
try {
const record = await visitRequestModel.getTrainingRecordByRequestId(trainingId);
if (record) {
await visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed');
}
} catch (statusErr) {
logger.error('출입 신청 상태 업데이트 오류:', statusErr);
}
visitRequestModel.updateVisitRequestStatus(record.request_id, 'training_completed', (err) => {
if (err) {
console.error('출입 신청 상태 업데이트 오류:', err);
res.json({ success: true, message: '안전교육이 완료되었습니다.' });
} catch (err) {
logger.error('안전교육 완료 처리 오류:', err);
res.status(500).json({ success: false, message: '안전교육 완료 처리 중 오류가 발생했습니다.' });
}
res.json({
success: true,
message: '안전교육이 완료되었습니다.'
});
});
});
});
};
/**
* 안전교육 기록 목록 조회
*/
exports.getTrainingRecords = (req, res) => {
exports.getTrainingRecords = async (req, res) => {
try {
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
});
const records = await visitRequestModel.getTrainingRecords(filters);
res.json({ success: true, data: records });
} catch (err) {
logger.error('안전교육 기록 목록 조회 오류:', err);
res.status(500).json({ success: false, message: '안전교육 기록 목록 조회 중 오류가 발생했습니다.' });
}
res.json({
success: true,
data: records
});
});
};

View File

@@ -4,198 +4,149 @@
const workIssueModel = require('../models/workIssueModel');
const imageUploadService = require('../services/imageUploadService');
const logger = require('../utils/logger');
// ==================== 신고 카테고리 관리 ====================
/**
* 모든 카테고리 조회
*/
exports.getAllCategories = (req, res) => {
workIssueModel.getAllCategories((err, categories) => {
if (err) {
console.error('카테고리 조회 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
exports.getAllCategories = async (req, res) => {
try {
const categories = await workIssueModel.getAllCategories();
res.json({ success: true, data: categories });
});
} catch (err) {
logger.error('카테고리 조회 실패:', err);
res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
};
/**
* 타입별 카테고리 조회
*/
exports.getCategoriesByType = (req, res) => {
exports.getCategoriesByType = async (req, res) => {
try {
const { type } = req.params;
if (!['nonconformity', 'safety', 'facility'].includes(type)) {
return res.status(400).json({ success: false, error: '유효하지 않은 카테고리 타입입니다.' });
}
workIssueModel.getCategoriesByType(type, (err, categories) => {
if (err) {
console.error('카테고리 조회 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
const categories = await workIssueModel.getCategoriesByType(type);
res.json({ success: true, data: categories });
});
} catch (err) {
logger.error('카테고리 조회 실패:', err);
res.status(500).json({ success: false, error: '카테고리 조회 실패' });
}
};
/**
* 카테고리 생성
*/
exports.createCategory = (req, res) => {
exports.createCategory = async (req, res) => {
try {
const { category_type, category_name, description, display_order } = req.body;
if (!category_type || !category_name) {
return res.status(400).json({ success: false, error: '카테고리 타입과 이름은 필수입니다.' });
}
workIssueModel.createCategory(
{ category_type, category_name, description, display_order },
(err, categoryId) => {
if (err) {
console.error('카테고리 생성 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 생성 실패' });
}
const categoryId = await workIssueModel.createCategory({ category_type, category_name, description, display_order });
res.status(201).json({
success: true,
message: '카테고리가 생성되었습니다.',
data: { category_id: categoryId }
});
} catch (err) {
logger.error('카테고리 생성 실패:', err);
res.status(500).json({ success: false, error: '카테고리 생성 실패' });
}
);
};
/**
* 카테고리 수정
*/
exports.updateCategory = (req, res) => {
exports.updateCategory = async (req, res) => {
try {
const { id } = req.params;
const { category_name, description, display_order, is_active } = req.body;
workIssueModel.updateCategory(
id,
{ category_name, description, display_order, is_active },
(err, result) => {
if (err) {
console.error('카테고리 수정 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 수정 실패' });
}
await workIssueModel.updateCategory(id, { category_name, description, display_order, is_active });
res.json({ success: true, message: '카테고리가 수정되었습니다.' });
} catch (err) {
logger.error('카테고리 수정 실패:', err);
res.status(500).json({ success: false, error: '카테고리 수정 실패' });
}
);
};
/**
* 카테고리 삭제
*/
exports.deleteCategory = (req, res) => {
exports.deleteCategory = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.deleteCategory(id, (err, result) => {
if (err) {
console.error('카테고리 삭제 실패:', err);
return res.status(500).json({ success: false, error: '카테고리 삭제 실패' });
}
await workIssueModel.deleteCategory(id);
res.json({ success: true, message: '카테고리가 삭제되었습니다.' });
});
} catch (err) {
logger.error('카테고리 삭제 실패:', err);
res.status(500).json({ success: false, error: '카테고리 삭제 실패' });
}
};
// ==================== 사전 정의 항목 관리 ====================
/**
* 카테고리별 항목 조회
*/
exports.getItemsByCategory = (req, res) => {
exports.getItemsByCategory = async (req, res) => {
try {
const { categoryId } = req.params;
workIssueModel.getItemsByCategory(categoryId, (err, items) => {
if (err) {
console.error('항목 조회 실패:', err);
return res.status(500).json({ success: false, error: '항목 조회 실패' });
}
const items = await workIssueModel.getItemsByCategory(categoryId);
res.json({ success: true, data: items });
});
} catch (err) {
logger.error('항목 조회 실패:', err);
res.status(500).json({ success: false, error: '항목 조회 실패' });
}
};
/**
* 모든 항목 조회
*/
exports.getAllItems = (req, res) => {
workIssueModel.getAllItems((err, items) => {
if (err) {
console.error('항목 조회 실패:', err);
return res.status(500).json({ success: false, error: '항목 조회 실패' });
}
exports.getAllItems = async (req, res) => {
try {
const items = await workIssueModel.getAllItems();
res.json({ success: true, data: items });
});
} catch (err) {
logger.error('항목 조회 실패:', err);
res.status(500).json({ success: false, error: '항목 조회 실패' });
}
};
/**
* 항목 생성
*/
exports.createItem = (req, res) => {
exports.createItem = async (req, res) => {
try {
const { category_id, item_name, description, severity, display_order } = req.body;
if (!category_id || !item_name) {
return res.status(400).json({ success: false, error: '카테고리 ID와 항목명은 필수입니다.' });
}
workIssueModel.createItem(
{ category_id, item_name, description, severity, display_order },
(err, itemId) => {
if (err) {
console.error('항목 생성 실패:', err);
return res.status(500).json({ success: false, error: '항목 생성 실패' });
}
const itemId = await workIssueModel.createItem({ category_id, item_name, description, severity, display_order });
res.status(201).json({
success: true,
message: '항목이 생성되었습니다.',
data: { item_id: itemId }
});
} catch (err) {
logger.error('항목 생성 실패:', err);
res.status(500).json({ success: false, error: '항목 생성 실패' });
}
);
};
/**
* 항목 수정
*/
exports.updateItem = (req, res) => {
exports.updateItem = async (req, res) => {
try {
const { id } = req.params;
const { item_name, description, severity, display_order, is_active } = req.body;
workIssueModel.updateItem(
id,
{ item_name, description, severity, display_order, is_active },
(err, result) => {
if (err) {
console.error('항목 수정 실패:', err);
return res.status(500).json({ success: false, error: '항목 수정 실패' });
}
await workIssueModel.updateItem(id, { item_name, description, severity, display_order, is_active });
res.json({ success: true, message: '항목이 수정되었습니다.' });
} catch (err) {
logger.error('항목 수정 실패:', err);
res.status(500).json({ success: false, error: '항목 수정 실패' });
}
);
};
/**
* 항목 삭제
*/
exports.deleteItem = (req, res) => {
exports.deleteItem = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.deleteItem(id, (err, result) => {
if (err) {
console.error('항목 삭제 실패:', err);
return res.status(500).json({ success: false, error: '항목 삭제 실패' });
}
await workIssueModel.deleteItem(id);
res.json({ success: true, message: '항목이 삭제되었습니다.' });
});
} catch (err) {
logger.error('항목 삭제 실패:', err);
res.status(500).json({ success: false, error: '항목 삭제 실패' });
}
};
// ==================== 문제 신고 관리 ====================
/**
* 신고 생성
*/
exports.createReport = async (req, res) => {
try {
const {
@@ -206,7 +157,7 @@ exports.createReport = async (req, res) => {
visit_request_id,
issue_category_id,
issue_item_id,
custom_item_name, // 직접 입력한 항목명
custom_item_name,
additional_description,
photos = []
} = req.body;
@@ -217,42 +168,25 @@ exports.createReport = async (req, res) => {
return res.status(400).json({ success: false, error: '신고 카테고리는 필수입니다.' });
}
// 위치 정보 검증 (지도 선택 또는 기타 위치)
if (!factory_category_id && !custom_location) {
return res.status(400).json({ success: false, error: '위치 정보는 필수입니다.' });
}
// 항목 검증 (기존 항목 또는 직접 입력)
if (!issue_item_id && !custom_item_name) {
return res.status(400).json({ success: false, error: '신고 항목은 필수입니다.' });
}
// 직접 입력한 항목이 있으면 DB에 저장
let finalItemId = issue_item_id;
if (custom_item_name && !issue_item_id) {
try {
finalItemId = await new Promise((resolve, reject) => {
workIssueModel.createItem(
{
finalItemId = await workIssueModel.createItem({
category_id: issue_category_id,
item_name: custom_item_name,
description: '사용자 직접 입력',
severity: 'medium',
display_order: 999 // 마지막에 표시
},
(err, itemId) => {
if (err) reject(err);
else resolve(itemId);
}
);
display_order: 999
});
} catch (itemErr) {
console.error('커스텀 항목 생성 실패:', itemErr);
return res.status(500).json({ success: false, error: '항목 저장 실패' });
}
}
// 사진 저장 (최대 5장)
const photoPaths = {
photo_path1: null,
photo_path2: null,
@@ -283,27 +217,20 @@ exports.createReport = async (req, res) => {
...photoPaths
};
workIssueModel.createReport(reportData, (err, reportId) => {
if (err) {
console.error('신고 생성 실패:', err);
return res.status(500).json({ success: false, error: '신고 생성 실패' });
}
const reportId = await workIssueModel.createReport(reportData);
res.status(201).json({
success: true,
message: '문제 신고가 등록되었습니다.',
data: { report_id: reportId }
});
});
} catch (error) {
console.error('신고 생성 에러:', error);
} catch (err) {
logger.error('신고 생성 실패:', err);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
}
};
/**
* 신고 목록 조회
*/
exports.getAllReports = (req, res) => {
exports.getAllReports = async (req, res) => {
try {
const filters = {
status: req.query.status,
category_type: req.query.category_type,
@@ -318,38 +245,28 @@ exports.getAllReports = (req, res) => {
offset: req.query.offset
};
// 일반 사용자는 자신의 신고만 조회 (관리자 제외)
const userLevel = req.user.access_level;
if (!['admin', 'system', 'support_team'].includes(userLevel)) {
filters.reporter_id = req.user.user_id;
}
workIssueModel.getAllReports(filters, (err, reports) => {
if (err) {
console.error('신고 목록 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 목록 조회 실패' });
}
const reports = await workIssueModel.getAllReports(filters);
res.json({ success: true, data: reports });
});
} catch (err) {
logger.error('신고 목록 조회 실패:', err);
res.status(500).json({ success: false, error: '신고 목록 조회 실패' });
}
};
/**
* 신고 상세 조회
*/
exports.getReportById = (req, res) => {
exports.getReportById = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.getReportById(id, (err, report) => {
if (err) {
console.error('신고 상세 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 상세 조회 실패' });
}
const report = await workIssueModel.getReportById(id);
if (!report) {
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
}
// 권한 확인: 본인, 담당자, 또는 관리자
const userLevel = req.user.access_level;
const isOwner = report.reporter_id === req.user.user_id;
const isAssignee = report.assigned_user_id === req.user.user_id;
@@ -360,28 +277,21 @@ exports.getReportById = (req, res) => {
}
res.json({ success: true, data: report });
});
} catch (err) {
logger.error('신고 상세 조회 실패:', err);
res.status(500).json({ success: false, error: '신고 상세 조회 실패' });
}
};
/**
* 신고 수정
*/
exports.updateReport = async (req, res) => {
try {
const { id } = req.params;
// 기존 신고 확인
workIssueModel.getReportById(id, async (err, report) => {
if (err) {
console.error('신고 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 조회 실패' });
}
const report = await workIssueModel.getReportById(id);
if (!report) {
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
}
// 권한 확인
const userLevel = req.user.access_level;
const isOwner = report.reporter_id === req.user.user_id;
const isManager = ['admin', 'system'].includes(userLevel);
@@ -390,7 +300,6 @@ exports.updateReport = async (req, res) => {
return res.status(403).json({ success: false, error: '수정 권한이 없습니다.' });
}
// 상태 확인: reported 상태에서만 수정 가능 (관리자 제외)
if (!isManager && report.status !== 'reported') {
return res.status(400).json({ success: false, error: '이미 접수된 신고는 수정할 수 없습니다.' });
}
@@ -405,16 +314,13 @@ exports.updateReport = async (req, res) => {
photos = []
} = req.body;
// 사진 업데이트 처리
const photoPaths = {};
for (let i = 0; i < Math.min(photos.length, 5); i++) {
if (photos[i]) {
// 기존 사진 삭제
const oldPath = report[`photo_path${i + 1}`];
if (oldPath) {
await imageUploadService.deleteFile(oldPath);
}
// 새 사진 저장
const savedPath = await imageUploadService.saveBase64Image(photos[i], 'issue');
if (savedPath) {
photoPaths[`photo_path${i + 1}`] = savedPath;
@@ -432,37 +338,23 @@ exports.updateReport = async (req, res) => {
...photoPaths
};
workIssueModel.updateReport(id, updateData, req.user.user_id, (updateErr, result) => {
if (updateErr) {
console.error('신고 수정 실패:', updateErr);
return res.status(500).json({ success: false, error: '신고 수정 실패' });
}
await workIssueModel.updateReport(id, updateData, req.user.user_id);
res.json({ success: true, message: '신고가 수정되었습니다.' });
});
});
} catch (error) {
console.error('신고 수정 에러:', error);
} catch (err) {
logger.error('신고 수정 실패:', err);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
}
};
/**
* 신고 삭제
*/
exports.deleteReport = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.getReportById(id, async (err, report) => {
if (err) {
console.error('신고 조회 실패:', err);
return res.status(500).json({ success: false, error: '신고 조회 실패' });
}
const report = await workIssueModel.getReportById(id);
if (!report) {
return res.status(404).json({ success: false, error: '신고를 찾을 수 없습니다.' });
}
// 권한 확인
const userLevel = req.user.access_level;
const isOwner = report.reporter_id === req.user.user_id;
const isManager = ['admin', 'system'].includes(userLevel);
@@ -471,13 +363,8 @@ exports.deleteReport = async (req, res) => {
return res.status(403).json({ success: false, error: '삭제 권한이 없습니다.' });
}
workIssueModel.deleteReport(id, async (deleteErr, { result, photos }) => {
if (deleteErr) {
console.error('신고 삭제 실패:', deleteErr);
return res.status(500).json({ success: false, error: '신고 삭제 실패' });
}
const { photos } = await workIssueModel.deleteReport(id);
// 사진 파일 삭제
if (photos) {
const allPhotos = [
photos.photo_path1, photos.photo_path2, photos.photo_path3,
@@ -488,31 +375,27 @@ exports.deleteReport = async (req, res) => {
}
res.json({ success: true, message: '신고가 삭제되었습니다.' });
});
});
} catch (err) {
logger.error('신고 삭제 실패:', err);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
}
};
// ==================== 상태 관리 ====================
/**
* 신고 접수
*/
exports.receiveReport = (req, res) => {
exports.receiveReport = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.receiveReport(id, req.user.user_id, (err, result) => {
if (err) {
console.error('신고 접수 실패:', err);
return res.status(400).json({ success: false, error: err.message || '신고 접수 실패' });
}
await workIssueModel.receiveReport(id, req.user.user_id);
res.json({ success: true, message: '신고가 접수되었습니다.' });
});
} catch (err) {
logger.error('신고 접수 실패:', err);
res.status(400).json({ success: false, error: err.message || '신고 접수 실패' });
}
};
/**
* 담당자 배정
*/
exports.assignReport = (req, res) => {
exports.assignReport = async (req, res) => {
try {
const { id } = req.params;
const { assigned_department, assigned_user_id } = req.body;
@@ -520,43 +403,34 @@ exports.assignReport = (req, res) => {
return res.status(400).json({ success: false, error: '담당자는 필수입니다.' });
}
workIssueModel.assignReport(id, {
await workIssueModel.assignReport(id, {
assigned_department,
assigned_user_id,
assigned_by: req.user.user_id
}, (err, result) => {
if (err) {
console.error('담당자 배정 실패:', err);
return res.status(400).json({ success: false, error: err.message || '담당자 배정 실패' });
}
});
res.json({ success: true, message: '담당자가 배정되었습니다.' });
});
};
/**
* 처리 시작
*/
exports.startProcessing = (req, res) => {
const { id } = req.params;
workIssueModel.startProcessing(id, req.user.user_id, (err, result) => {
if (err) {
console.error('처리 시작 실패:', err);
return res.status(400).json({ success: false, error: err.message || '처리 시작 실패' });
} catch (err) {
logger.error('담당자 배정 실패:', err);
res.status(400).json({ success: false, error: err.message || '담당자 배정 실패' });
}
};
exports.startProcessing = async (req, res) => {
try {
const { id } = req.params;
await workIssueModel.startProcessing(id, req.user.user_id);
res.json({ success: true, message: '처리가 시작되었습니다.' });
} catch (err) {
logger.error('처리 시작 실패:', err);
res.status(400).json({ success: false, error: err.message || '처리 시작 실패' });
}
res.json({ success: true, message: '처리가 시작되었습니다.' });
});
};
/**
* 처리 완료
*/
exports.completeReport = async (req, res) => {
try {
const { id } = req.params;
const { resolution_notes, resolution_photos = [] } = req.body;
// 완료 사진 저장
let resolution_photo_path1 = null;
let resolution_photo_path2 = null;
@@ -567,108 +441,83 @@ exports.completeReport = async (req, res) => {
resolution_photo_path2 = await imageUploadService.saveBase64Image(resolution_photos[1], 'resolution');
}
workIssueModel.completeReport(id, {
await workIssueModel.completeReport(id, {
resolution_notes,
resolution_photo_path1,
resolution_photo_path2,
resolved_by: req.user.user_id
}, (err, result) => {
if (err) {
console.error('처리 완료 실패:', err);
return res.status(400).json({ success: false, error: err.message || '처리 완료 실패' });
}
});
res.json({ success: true, message: '처리가 완료되었습니다.' });
});
} catch (error) {
console.error('처리 완료 에러:', error);
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다.' });
} catch (err) {
logger.error('처리 완료 실패:', err);
res.status(400).json({ success: false, error: err.message || '처리 완료 실패' });
}
};
/**
* 신고 종료
*/
exports.closeReport = (req, res) => {
exports.closeReport = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.closeReport(id, req.user.user_id, (err, result) => {
if (err) {
console.error('신고 종료 실패:', err);
return res.status(400).json({ success: false, error: err.message || '신고 종료 실패' });
}
await workIssueModel.closeReport(id, req.user.user_id);
res.json({ success: true, message: '신고가 종료되었습니다.' });
});
} catch (err) {
logger.error('신고 종료 실패:', err);
res.status(400).json({ success: false, error: err.message || '신고 종료 실패' });
}
};
/**
* 상태 변경 이력 조회
*/
exports.getStatusLogs = (req, res) => {
exports.getStatusLogs = async (req, res) => {
try {
const { id } = req.params;
workIssueModel.getStatusLogs(id, (err, logs) => {
if (err) {
console.error('상태 이력 조회 실패:', err);
return res.status(500).json({ success: false, error: '상태 이력 조회 실패' });
}
const logs = await workIssueModel.getStatusLogs(id);
res.json({ success: true, data: logs });
});
} catch (err) {
logger.error('상태 이력 조회 실패:', err);
res.status(500).json({ success: false, error: '상태 이력 조회 실패' });
}
};
// ==================== 통계 ====================
/**
* 통계 요약
*/
exports.getStatsSummary = (req, res) => {
exports.getStatsSummary = async (req, res) => {
try {
const filters = {
start_date: req.query.start_date,
end_date: req.query.end_date,
factory_category_id: req.query.factory_category_id
};
workIssueModel.getStatsSummary(filters, (err, stats) => {
if (err) {
console.error('통계 조회 실패:', err);
return res.status(500).json({ success: false, error: '통계 조회 실패' });
}
const stats = await workIssueModel.getStatsSummary(filters);
res.json({ success: true, data: stats });
});
} catch (err) {
logger.error('통계 조회 실패:', err);
res.status(500).json({ success: false, error: '통계 조회 실패' });
}
};
/**
* 카테고리별 통계
*/
exports.getStatsByCategory = (req, res) => {
exports.getStatsByCategory = async (req, res) => {
try {
const filters = {
start_date: req.query.start_date,
end_date: req.query.end_date
};
workIssueModel.getStatsByCategory(filters, (err, stats) => {
if (err) {
console.error('카테고리별 통계 조회 실패:', err);
return res.status(500).json({ success: false, error: '통계 조회 실패' });
}
const stats = await workIssueModel.getStatsByCategory(filters);
res.json({ success: true, data: stats });
});
} catch (err) {
logger.error('카테고리별 통계 조회 실패:', err);
res.status(500).json({ success: false, error: '통계 조회 실패' });
}
};
/**
* 작업장별 통계
*/
exports.getStatsByWorkplace = (req, res) => {
exports.getStatsByWorkplace = async (req, res) => {
try {
const filters = {
start_date: req.query.start_date,
end_date: req.query.end_date,
factory_category_id: req.query.factory_category_id
};
workIssueModel.getStatsByWorkplace(filters, (err, stats) => {
if (err) {
console.error('작업장별 통계 조회 실패:', err);
return res.status(500).json({ success: false, error: '통계 조회 실패' });
}
const stats = await workIssueModel.getStatsByWorkplace(filters);
res.json({ success: true, data: stats });
});
} catch (err) {
logger.error('작업장별 통계 조회 실패:', err);
res.status(500).json({ success: false, error: '통계 조회 실패' });
}
};

View File

@@ -8,15 +8,12 @@
*/
const workplaceModel = require('../models/workplaceModel');
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
const { ValidationError, NotFoundError } = require('../utils/errors');
const { asyncHandler } = require('../middlewares/errorHandler');
const logger = require('../utils/logger');
// ==================== 카테고리(공장) 관련 ====================
/**
* 카테고리 생성
*/
exports.createCategory = asyncHandler(async (req, res) => {
const categoryData = req.body;
@@ -26,12 +23,7 @@ exports.createCategory = asyncHandler(async (req, res) => {
logger.info('카테고리 생성 요청', { name: categoryData.category_name });
const id = await new Promise((resolve, reject) => {
workplaceModel.createCategory(categoryData, (err, lastID) => {
if (err) reject(new DatabaseError('카테고리 생성 중 오류가 발생했습니다'));
else resolve(lastID);
});
});
const id = await workplaceModel.createCategory(categoryData);
logger.info('카테고리 생성 성공', { category_id: id });
@@ -42,16 +34,8 @@ exports.createCategory = asyncHandler(async (req, res) => {
});
});
/**
* 전체 카테고리 조회
*/
exports.getAllCategories = asyncHandler(async (req, res) => {
const rows = await new Promise((resolve, reject) => {
workplaceModel.getAllCategories((err, data) => {
if (err) reject(new DatabaseError('카테고리 목록 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const rows = await workplaceModel.getAllCategories();
res.json({
success: true,
@@ -60,16 +44,8 @@ exports.getAllCategories = asyncHandler(async (req, res) => {
});
});
/**
* 활성 카테고리만 조회
*/
exports.getActiveCategories = asyncHandler(async (req, res) => {
const rows = await new Promise((resolve, reject) => {
workplaceModel.getActiveCategories((err, data) => {
if (err) reject(new DatabaseError('활성 카테고리 목록 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const rows = await workplaceModel.getActiveCategories();
res.json({
success: true,
@@ -78,18 +54,9 @@ exports.getActiveCategories = asyncHandler(async (req, res) => {
});
});
/**
* 단일 카테고리 조회
*/
exports.getCategoryById = asyncHandler(async (req, res) => {
const categoryId = req.params.id;
const category = await new Promise((resolve, reject) => {
workplaceModel.getCategoryById(categoryId, (err, data) => {
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const category = await workplaceModel.getCategoryById(categoryId);
if (!category) {
throw new NotFoundError('카테고리를 찾을 수 없습니다');
@@ -102,9 +69,6 @@ exports.getCategoryById = asyncHandler(async (req, res) => {
});
});
/**
* 카테고리 수정
*/
exports.updateCategory = asyncHandler(async (req, res) => {
const categoryId = req.params.id;
const categoryData = req.body;
@@ -115,19 +79,11 @@ exports.updateCategory = asyncHandler(async (req, res) => {
logger.info('카테고리 수정 요청', { category_id: categoryId });
// 기존 카테고리 정보 가져오기
const existingCategory = await new Promise((resolve, reject) => {
workplaceModel.getCategoryById(categoryId, (err, data) => {
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const existingCategory = await workplaceModel.getCategoryById(categoryId);
if (!existingCategory) {
throw new NotFoundError('카테고리를 찾을 수 없습니다');
}
// layout_image가 요청에 없거나 null이면 기존 값 보존
const updateData = {
...categoryData,
layout_image: (categoryData.layout_image !== undefined && categoryData.layout_image !== null)
@@ -135,12 +91,7 @@ exports.updateCategory = asyncHandler(async (req, res) => {
: existingCategory.layout_image
};
await new Promise((resolve, reject) => {
workplaceModel.updateCategory(categoryId, updateData, (err, result) => {
if (err) reject(new DatabaseError('카테고리 수정 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.updateCategory(categoryId, updateData);
logger.info('카테고리 수정 성공', { category_id: categoryId });
@@ -150,20 +101,12 @@ exports.updateCategory = asyncHandler(async (req, res) => {
});
});
/**
* 카테고리 삭제
*/
exports.deleteCategory = asyncHandler(async (req, res) => {
const categoryId = req.params.id;
logger.info('카테고리 삭제 요청', { category_id: categoryId });
await new Promise((resolve, reject) => {
workplaceModel.deleteCategory(categoryId, (err, result) => {
if (err) reject(new DatabaseError('카테고리 삭제 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.deleteCategory(categoryId);
logger.info('카테고리 삭제 성공', { category_id: categoryId });
@@ -175,9 +118,6 @@ exports.deleteCategory = asyncHandler(async (req, res) => {
// ==================== 작업장 관련 ====================
/**
* 작업장 생성
*/
exports.createWorkplace = asyncHandler(async (req, res) => {
const workplaceData = req.body;
@@ -187,12 +127,7 @@ exports.createWorkplace = asyncHandler(async (req, res) => {
logger.info('작업장 생성 요청', { name: workplaceData.workplace_name });
const id = await new Promise((resolve, reject) => {
workplaceModel.createWorkplace(workplaceData, (err, lastID) => {
if (err) reject(new DatabaseError('작업장 생성 중 오류가 발생했습니다'));
else resolve(lastID);
});
});
const id = await workplaceModel.createWorkplace(workplaceData);
logger.info('작업장 생성 성공', { workplace_id: id });
@@ -203,35 +138,12 @@ exports.createWorkplace = asyncHandler(async (req, res) => {
});
});
/**
* 전체 작업장 조회
*/
exports.getAllWorkplaces = asyncHandler(async (req, res) => {
const categoryId = req.query.category_id;
// 카테고리별 필터링
if (categoryId) {
const rows = await new Promise((resolve, reject) => {
workplaceModel.getWorkplacesByCategory(categoryId, (err, data) => {
if (err) reject(new DatabaseError('작업장 목록 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
return res.json({
success: true,
data: rows,
message: '작업장 목록 조회 성공'
});
}
// 전체 조회
const rows = await new Promise((resolve, reject) => {
workplaceModel.getAllWorkplaces((err, data) => {
if (err) reject(new DatabaseError('작업장 목록 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const rows = categoryId
? await workplaceModel.getWorkplacesByCategory(categoryId)
: await workplaceModel.getAllWorkplaces();
res.json({
success: true,
@@ -240,16 +152,8 @@ exports.getAllWorkplaces = asyncHandler(async (req, res) => {
});
});
/**
* 활성 작업장만 조회
*/
exports.getActiveWorkplaces = asyncHandler(async (req, res) => {
const rows = await new Promise((resolve, reject) => {
workplaceModel.getActiveWorkplaces((err, data) => {
if (err) reject(new DatabaseError('활성 작업장 목록 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const rows = await workplaceModel.getActiveWorkplaces();
res.json({
success: true,
@@ -258,18 +162,9 @@ exports.getActiveWorkplaces = asyncHandler(async (req, res) => {
});
});
/**
* 단일 작업장 조회
*/
exports.getWorkplaceById = asyncHandler(async (req, res) => {
const workplaceId = req.params.id;
const workplace = await new Promise((resolve, reject) => {
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const workplace = await workplaceModel.getWorkplaceById(workplaceId);
if (!workplace) {
throw new NotFoundError('작업장을 찾을 수 없습니다');
@@ -282,9 +177,6 @@ exports.getWorkplaceById = asyncHandler(async (req, res) => {
});
});
/**
* 작업장 수정
*/
exports.updateWorkplace = asyncHandler(async (req, res) => {
const workplaceId = req.params.id;
const workplaceData = req.body;
@@ -295,19 +187,11 @@ exports.updateWorkplace = asyncHandler(async (req, res) => {
logger.info('작업장 수정 요청', { workplace_id: workplaceId });
// 기존 작업장 정보 가져오기
const existingWorkplace = await new Promise((resolve, reject) => {
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const existingWorkplace = await workplaceModel.getWorkplaceById(workplaceId);
if (!existingWorkplace) {
throw new NotFoundError('작업장을 찾을 수 없습니다');
}
// layout_image가 요청에 없거나 null이면 기존 값 보존
const updateData = {
...workplaceData,
layout_image: (workplaceData.layout_image !== undefined && workplaceData.layout_image !== null)
@@ -315,12 +199,7 @@ exports.updateWorkplace = asyncHandler(async (req, res) => {
: existingWorkplace.layout_image
};
await new Promise((resolve, reject) => {
workplaceModel.updateWorkplace(workplaceId, updateData, (err, result) => {
if (err) reject(new DatabaseError('작업장 수정 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.updateWorkplace(workplaceId, updateData);
logger.info('작업장 수정 성공', { workplace_id: workplaceId });
@@ -330,20 +209,12 @@ exports.updateWorkplace = asyncHandler(async (req, res) => {
});
});
/**
* 작업장 삭제
*/
exports.deleteWorkplace = asyncHandler(async (req, res) => {
const workplaceId = req.params.id;
logger.info('작업장 삭제 요청', { workplace_id: workplaceId });
await new Promise((resolve, reject) => {
workplaceModel.deleteWorkplace(workplaceId, (err, result) => {
if (err) reject(new DatabaseError('작업장 삭제 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.deleteWorkplace(workplaceId);
logger.info('작업장 삭제 성공', { workplace_id: workplaceId });
@@ -355,9 +226,6 @@ exports.deleteWorkplace = asyncHandler(async (req, res) => {
// ==================== 작업장 지도 영역 관련 ====================
/**
* 카테고리 레이아웃 이미지 업로드
*/
exports.uploadCategoryLayoutImage = asyncHandler(async (req, res) => {
const categoryId = req.params.id;
@@ -369,19 +237,11 @@ exports.uploadCategoryLayoutImage = asyncHandler(async (req, res) => {
logger.info('카테고리 레이아웃 이미지 업로드 요청', { category_id: categoryId, path: imagePath });
// 현재 카테고리 정보 가져오기
const category = await new Promise((resolve, reject) => {
workplaceModel.getCategoryById(categoryId, (err, data) => {
if (err) reject(new DatabaseError('카테고리 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const category = await workplaceModel.getCategoryById(categoryId);
if (!category) {
throw new NotFoundError('카테고리를 찾을 수 없습니다');
}
// 카테고리 정보 업데이트 (이미지 경로만 변경)
const updatedData = {
category_name: category.category_name,
description: category.description,
@@ -390,12 +250,7 @@ exports.uploadCategoryLayoutImage = asyncHandler(async (req, res) => {
layout_image: imagePath
};
await new Promise((resolve, reject) => {
workplaceModel.updateCategory(categoryId, updatedData, (err, result) => {
if (err) reject(new DatabaseError('이미지 경로 저장 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.updateCategory(categoryId, updatedData);
logger.info('레이아웃 이미지 업로드 성공', { category_id: categoryId });
@@ -406,9 +261,6 @@ exports.uploadCategoryLayoutImage = asyncHandler(async (req, res) => {
});
});
/**
* 작업장 레이아웃 이미지 업로드
*/
exports.uploadWorkplaceLayoutImage = asyncHandler(async (req, res) => {
const workplaceId = req.params.id;
@@ -420,19 +272,11 @@ exports.uploadWorkplaceLayoutImage = asyncHandler(async (req, res) => {
logger.info('작업장 레이아웃 이미지 업로드 요청', { workplace_id: workplaceId, path: imagePath });
// 현재 작업장 정보 가져오기
const workplace = await new Promise((resolve, reject) => {
workplaceModel.getWorkplaceById(workplaceId, (err, data) => {
if (err) reject(new DatabaseError('작업장 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const workplace = await workplaceModel.getWorkplaceById(workplaceId);
if (!workplace) {
throw new NotFoundError('작업장을 찾을 수 없습니다');
}
// 작업장 정보 업데이트 (이미지 경로만 변경)
const updatedData = {
workplace_name: workplace.workplace_name,
category_id: workplace.category_id,
@@ -443,12 +287,7 @@ exports.uploadWorkplaceLayoutImage = asyncHandler(async (req, res) => {
layout_image: imagePath
};
await new Promise((resolve, reject) => {
workplaceModel.updateWorkplace(workplaceId, updatedData, (err, result) => {
if (err) reject(new DatabaseError('이미지 경로 저장 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.updateWorkplace(workplaceId, updatedData);
logger.info('작업장 레이아웃 이미지 업로드 성공', { workplace_id: workplaceId });
@@ -459,9 +298,6 @@ exports.uploadWorkplaceLayoutImage = asyncHandler(async (req, res) => {
});
});
/**
* 지도 영역 생성
*/
exports.createMapRegion = asyncHandler(async (req, res) => {
const regionData = req.body;
@@ -471,12 +307,7 @@ exports.createMapRegion = asyncHandler(async (req, res) => {
logger.info('지도 영역 생성 요청', { workplace_id: regionData.workplace_id });
const id = await new Promise((resolve, reject) => {
workplaceModel.createMapRegion(regionData, (err, lastID) => {
if (err) reject(new DatabaseError('지도 영역 생성 중 오류가 발생했습니다'));
else resolve(lastID);
});
});
const id = await workplaceModel.createMapRegion(regionData);
logger.info('지도 영역 생성 성공', { region_id: id });
@@ -487,18 +318,9 @@ exports.createMapRegion = asyncHandler(async (req, res) => {
});
});
/**
* 카테고리별 지도 영역 조회 (작업장 정보 포함)
*/
exports.getMapRegionsByCategory = asyncHandler(async (req, res) => {
const categoryId = req.params.categoryId;
const rows = await new Promise((resolve, reject) => {
workplaceModel.getMapRegionsByCategory(categoryId, (err, data) => {
if (err) reject(new DatabaseError('지도 영역 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const rows = await workplaceModel.getMapRegionsByCategory(categoryId);
res.json({
success: true,
@@ -507,18 +329,9 @@ exports.getMapRegionsByCategory = asyncHandler(async (req, res) => {
});
});
/**
* 작업장별 지도 영역 조회
*/
exports.getMapRegionByWorkplace = asyncHandler(async (req, res) => {
const workplaceId = req.params.workplaceId;
const region = await new Promise((resolve, reject) => {
workplaceModel.getMapRegionByWorkplace(workplaceId, (err, data) => {
if (err) reject(new DatabaseError('지도 영역 조회 중 오류가 발생했습니다'));
else resolve(data);
});
});
const region = await workplaceModel.getMapRegionByWorkplace(workplaceId);
res.json({
success: true,
@@ -527,21 +340,13 @@ exports.getMapRegionByWorkplace = asyncHandler(async (req, res) => {
});
});
/**
* 지도 영역 수정
*/
exports.updateMapRegion = asyncHandler(async (req, res) => {
const regionId = req.params.id;
const regionData = req.body;
logger.info('지도 영역 수정 요청', { region_id: regionId });
await new Promise((resolve, reject) => {
workplaceModel.updateMapRegion(regionId, regionData, (err, result) => {
if (err) reject(new DatabaseError('지도 영역 수정 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.updateMapRegion(regionId, regionData);
logger.info('지도 영역 수정 성공', { region_id: regionId });
@@ -551,20 +356,12 @@ exports.updateMapRegion = asyncHandler(async (req, res) => {
});
});
/**
* 지도 영역 삭제
*/
exports.deleteMapRegion = asyncHandler(async (req, res) => {
const regionId = req.params.id;
logger.info('지도 영역 삭제 요청', { region_id: regionId });
await new Promise((resolve, reject) => {
workplaceModel.deleteMapRegion(regionId, (err, result) => {
if (err) reject(new DatabaseError('지도 영역 삭제 중 오류가 발생했습니다'));
else resolve(result);
});
});
await workplaceModel.deleteMapRegion(regionId);
logger.info('지도 영역 삭제 성공', { region_id: regionId });

View File

@@ -2,8 +2,6 @@ const { getDb } = require('../dbPool');
/**
* 1. 여러 개의 이슈 보고서를 트랜잭션으로 생성합니다.
* @param {Array<object>} reports - 생성할 보고서 데이터 배열
* @returns {Promise<Array<number>>} - 삽입된 ID 배열
*/
const createMany = async (reports) => {
const db = await getDb();
@@ -36,10 +34,9 @@ const createMany = async (reports) => {
};
/**
* 2. 특정 날짜의 전체 이슈 목록 조회 (Promise 기반)
* 2. 특정 날짜의 전체 이슈 목록 조회
*/
const getAllByDate = async (date) => {
try {
const db = await getDb();
const [rows] = await db.query(
`SELECT
@@ -54,30 +51,21 @@ const getAllByDate = async (date) => {
[date]
);
return rows;
} catch (err) {
console.error(`[Model] ${date} 이슈 목록 조회 오류:`, err);
throw new Error('데이터베이스에서 이슈 목록을 조회하는 중 오류가 발생했습니다.');
}
};
/**
* 3. 단일 조회 (선택사항: 컨트롤러에서 사용 중)
* 3. 단일 조회
*/
const getById = async (id, callback) => {
try {
const getById = async (id) => {
const db = await getDb();
const [rows] = await db.query(`SELECT id, date, worker_id, project_id, issue_type_id, description, created_at, start_time, end_time FROM DailyIssueReports WHERE id = ?`, [id]);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 4. 수정
*/
const update = async (id, data, callback) => {
try {
const update = async (id, data) => {
const db = await getDb();
const fields = [];
@@ -88,67 +76,29 @@ const update = async (id, data, callback) => {
values.push(data[key]);
}
values.push(id); // 마지막에 id
values.push(id);
const [result] = await db.query(
`UPDATE DailyIssueReports SET ${fields.join(', ')} WHERE id = ?`,
values
);
callback(null, result.affectedRows);
} catch (err) {
callback(err);
}
return result.affectedRows;
};
/**
* 5. 삭제 (Promise 기반)
* 5. 삭제
*/
const remove = async (id) => {
try {
const db = await getDb();
const [result] = await db.query(`DELETE FROM DailyIssueReports WHERE id = ?`, [id]);
return result.affectedRows;
} catch (err) {
console.error(`[Model] 이슈(id: ${id}) 삭제 오류:`, err);
throw new Error('데이터베이스에서 이슈를 삭제하는 중 오류가 발생했습니다.');
}
};
// V1 함수들은 점진적으로 제거 예정
const create = async (report, callback) => {
try {
const db = await getDb();
const {
date,
worker_id,
project_id,
start_time,
end_time,
issue_type_id,
description = null // 선택값 처리
} = report;
const [result] = await db.query(
`INSERT INTO DailyIssueReports
(date, worker_id, project_id, start_time, end_time, issue_type_id, description)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[date, worker_id, project_id, start_time, end_time, issue_type_id, description]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
};
module.exports = {
createMany, // 신규
createMany,
getAllByDate,
remove,
// 레거시 호환성을 위해 V1 함수들 임시 유지
create: (report, callback) => createMany([report]).then(ids => callback(null, ids[0])).catch(err => callback(err)),
getById,
update,
remove
};

View File

@@ -2,34 +2,22 @@
const { getDb } = require('../dbPool');
/**
* 📋 마스터 데이터 조회 함수들
* 마스터 데이터 조회 함수들
*/
const getAllWorkTypes = async (callback) => {
try {
const getAllWorkTypes = async () => {
const db = await getDb();
const [rows] = await db.query('SELECT id, name, description, category, created_at, updated_at FROM work_types ORDER BY name ASC');
callback(null, rows);
} catch (err) {
console.error('작업 유형 조회 오류:', err);
callback(err);
}
return rows;
};
const getAllWorkStatusTypes = async (callback) => {
try {
const getAllWorkStatusTypes = async () => {
const db = await getDb();
const [rows] = await db.query('SELECT id, name, description, is_error, created_at FROM work_status_types ORDER BY id ASC');
callback(null, rows);
} catch (err) {
console.error('업무 상태 유형 조회 오류:', err);
callback(err);
}
return rows;
};
const getAllErrorTypes = async (callback) => {
try {
const getAllErrorTypes = async () => {
const db = await getDb();
// issue_report_items에서 부적합(nonconformity) 타입의 항목만 조회
const [rows] = await db.query(`
SELECT
iri.item_id as id,
@@ -44,17 +32,13 @@ const getAllErrorTypes = async (callback) => {
WHERE irc.category_type = 'nonconformity' AND iri.is_active = TRUE
ORDER BY irc.display_order, iri.display_order, iri.item_name ASC
`);
callback(null, rows);
} catch (err) {
console.error('에러 유형 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 🔄 누적 추가 전용 함수 (createDailyReport 대체) - 절대 삭제 안함!
* 누적 추가 전용 함수 (createDailyReport 대체) - 절대 삭제 안함!
*/
const createDailyReport = async (reportData, callback) => {
const createDailyReport = async (reportData) => {
const { report_date, worker_id, work_entries, created_by, created_by_name, total_hours } = reportData;
const db = await getDb();
const conn = await db.getConnection();
@@ -62,9 +46,8 @@ const createDailyReport = async (reportData, callback) => {
try {
await conn.beginTransaction();
console.log(`📝 ${created_by_name}${report_date} ${worker_id}번 작업자에게 데이터 추가 중...`);
console.log(`${created_by_name}${report_date} ${worker_id}번 작업자에게 데이터 추가 중...`);
// ✅ 수정된 쿼리 (테이블 alias 추가):
const [existingReports] = await conn.query(
`SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours
FROM daily_work_reports dwr
@@ -74,10 +57,9 @@ const createDailyReport = async (reportData, callback) => {
[report_date, worker_id]
);
console.log('기존 데이터 (삭제하지 않음):', existingReports);
// 2. ✅ 삭제 없이 새로운 데이터만 추가!
// 삭제 없이 새로운 데이터만 추가!
const insertedIds = [];
for (const entry of work_entries) {
const { project_id, work_type_id, work_status_id, error_type_id, work_hours } = entry;
@@ -92,7 +74,6 @@ const createDailyReport = async (reportData, callback) => {
insertedIds.push(insertResult.insertId);
}
// ✅ 수정된 쿼리:
const [finalReports] = await conn.query(
`SELECT dwr.created_by, u.name as created_by_name, COUNT(*) as count, SUM(dwr.work_hours) as total_hours
FROM daily_work_reports dwr
@@ -109,9 +90,9 @@ const createDailyReport = async (reportData, callback) => {
finalReports.forEach(report => {
console.log(` - ${report.created_by_name}: ${report.total_hours}시간 (${report.count}개 항목)`);
});
console.log(` 📊 총합: ${grandTotal}시간`);
console.log(` 총합: ${grandTotal}시간`);
// 4. 감사 로그 추가
// 감사 로그 추가
try {
await conn.query(
`INSERT INTO work_report_audit_log
@@ -139,16 +120,15 @@ const createDailyReport = async (reportData, callback) => {
await conn.commit();
// 5. 근태 기록 동기화 (추가)
// 근태 기록 동기화
try {
const AttendanceModel = require('./attendanceModel');
await AttendanceModel.syncWithWorkReports(worker_id, report_date);
} catch (syncErr) {
console.error('근태 기록 동기화 실패:', syncErr);
// 메인 트랜잭션은 성공했으므로 동기화 실패로 롤백하지 않음 (비동기 처리 또는 무시)
}
callback(null, {
return {
success: true,
inserted_count: insertedIds.length,
deleted_count: 0, // 항상 0 (삭제 안함)
@@ -160,22 +140,20 @@ const createDailyReport = async (reportData, callback) => {
total_contributors: finalReports.length,
contributors: finalReports
}
});
};
} catch (err) {
await conn.rollback();
console.error('작업보고서 누적 추가 오류:', err);
callback(err);
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
};
/**
* 📊 특정 날짜 + 작업자 + 작성자의 누적 현황 조회
* 특정 날짜 + 작업자 + 작성자의 누적 현황 조회
*/
const getMyAccumulatedHours = async (date, worker_id, created_by, callback) => {
try {
const getMyAccumulatedHours = async (date, worker_id, created_by) => {
const db = await getDb();
const sql = `
@@ -192,18 +170,13 @@ const getMyAccumulatedHours = async (date, worker_id, created_by, callback) => {
`;
const [rows] = await db.query(sql, [date, worker_id, created_by]);
callback(null, rows[0] || { my_total_hours: 0, my_entry_count: 0, my_entries: null });
} catch (err) {
console.error('개인 누적 현황 조회 오류:', err);
callback(err);
}
return rows[0] || { my_total_hours: 0, my_entry_count: 0, my_entries: null };
};
/**
* 📊 누적 현황 조회 - 날짜+작업자별 (모든 기여자)
* 누적 현황 조회 - 날짜+작업자별 (모든 기여자)
*/
const getAccumulatedReportsByDate = async (date, worker_id, callback) => {
try {
const getAccumulatedReportsByDate = async (date, worker_id) => {
const db = await getDb();
const sql = getSelectQuery() + `
@@ -212,18 +185,13 @@ const getAccumulatedReportsByDate = async (date, worker_id, callback) => {
`;
const [rows] = await db.query(sql, [date, worker_id]);
callback(null, rows);
} catch (err) {
console.error('누적 현황 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 📊 기여자별 요약 조회
* 기여자별 요약 조회
*/
const getContributorsByDate = async (date, worker_id, callback) => {
try {
const getContributorsByDate = async (date, worker_id) => {
const db = await getDb();
const sql = `
@@ -247,17 +215,13 @@ const getContributorsByDate = async (date, worker_id, callback) => {
`;
const [rows] = await db.query(sql, [date, worker_id]);
callback(null, rows);
} catch (err) {
console.error('기여자별 요약 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 🗑️ 특정 작업 항목만 삭제 (개별 삭제)
* 특정 작업 항목만 삭제 (개별 삭제)
*/
const removeSpecificEntry = async (entry_id, deleted_by, callback) => {
const removeSpecificEntry = async (entry_id, deleted_by) => {
const db = await getDb();
const conn = await db.getConnection();
@@ -276,16 +240,14 @@ const removeSpecificEntry = async (entry_id, deleted_by, callback) => {
);
if (entryInfo.length === 0) {
await conn.rollback();
return callback(new Error('삭제할 항목을 찾을 수 없습니다.'));
throw new Error('삭제할 항목을 찾을 수 없습니다.');
}
const entry = entryInfo[0];
// 권한 확인: 본인이 작성한 것만 삭제 가능
if (entry.created_by !== deleted_by) {
await conn.rollback();
return callback(new Error('본인이 작성한 항목만 삭제할 수 있습니다.'));
throw new Error('본인이 작성한 항목만 삭제할 수 있습니다.');
}
// 개별 항목 삭제
@@ -295,19 +257,18 @@ const removeSpecificEntry = async (entry_id, deleted_by, callback) => {
console.log(`[삭제 로그] 작업자: ${entry.worker_name}, 프로젝트: ${entry.project_name}, 작업시간: ${entry.work_hours}시간, 삭제자: ${deleted_by}`);
await conn.commit();
callback(null, {
return {
success: true,
deleted_entry: {
worker_name: entry.worker_name,
project_name: entry.project_name,
work_hours: entry.work_hours
}
});
};
} catch (err) {
await conn.rollback();
console.error('개별 항목 삭제 오류:', err);
callback(err);
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
@@ -350,115 +311,84 @@ const getSelectQuery = () => `
/**
* 7. ID로 작업보고서 조회
*/
const getById = async (id, callback) => {
try {
const getById = async (id) => {
const db = await getDb();
const sql = getSelectQuery() + 'WHERE dwr.id = ?';
const [rows] = await db.query(sql, [id]);
callback(null, rows[0] || null);
} catch (err) {
console.error('ID로 작업보고서 조회 오류:', err);
callback(err);
}
return rows[0] || null;
};
/**
* 8. 일일 작업보고서 조회 (날짜별)
*/
const getByDate = async (date, callback) => {
try {
const getByDate = async (date) => {
const db = await getDb();
const sql = getSelectQuery() + `
WHERE dwr.report_date = ?
ORDER BY w.worker_name ASC, p.project_name ASC, dwr.id ASC
`;
const [rows] = await db.query(sql, [date]);
callback(null, rows);
} catch (err) {
console.error('날짜별 작업보고서 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 9. 일일 작업보고서 조회 (날짜 + 작성자별)
*/
const getByDateAndCreator = async (date, created_by, callback) => {
try {
const getByDateAndCreator = async (date, created_by) => {
const db = await getDb();
const sql = getSelectQuery() + `
WHERE dwr.report_date = ? AND dwr.created_by = ?
ORDER BY w.worker_name ASC, p.project_name ASC, dwr.id ASC
`;
const [rows] = await db.query(sql, [date, created_by]);
callback(null, rows);
} catch (err) {
console.error('날짜+작성자별 작업보고서 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 10. 일일 작업보고서 조회 (작업자별)
*/
const getByWorker = async (worker_id, callback) => {
try {
const getByWorker = async (worker_id) => {
const db = await getDb();
const sql = getSelectQuery() + `
WHERE dwr.worker_id = ?
ORDER BY dwr.report_date DESC, dwr.id ASC
`;
const [rows] = await db.query(sql, [worker_id]);
callback(null, rows);
} catch (err) {
console.error('작업자별 작업보고서 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 11. 일일 작업보고서 조회 (날짜 + 작업자)
*/
const getByDateAndWorker = async (date, worker_id, callback) => {
try {
const getByDateAndWorker = async (date, worker_id) => {
const db = await getDb();
const sql = getSelectQuery() + `
WHERE dwr.report_date = ? AND dwr.worker_id = ?
ORDER BY dwr.id ASC
`;
const [rows] = await db.query(sql, [date, worker_id]);
callback(null, rows);
} catch (err) {
console.error('날짜+작업자별 작업보고서 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 12. 기간별 조회
*/
const getByRange = async (start_date, end_date, callback) => {
try {
const getByRange = async (start_date, end_date) => {
const db = await getDb();
const sql = getSelectQuery() + `
WHERE dwr.report_date BETWEEN ? AND ?
ORDER BY dwr.report_date DESC, w.worker_name ASC, dwr.id ASC
`;
const [rows] = await db.query(sql, [start_date, end_date]);
callback(null, rows);
} catch (err) {
console.error('기간별 작업보고서 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 13. 상세 검색 (페이지네이션 포함)
*/
const searchWithDetails = async (params, callback) => {
const searchWithDetails = async (params) => {
const { start_date, end_date, worker_id, project_id, work_status_id, created_by, page, limit } = params;
try {
const db = await getDb();
// 조건 구성
@@ -507,18 +437,13 @@ const searchWithDetails = async (params, callback) => {
const dataParams = [...queryParams, limit, offset];
const [rows] = await db.query(dataQuery, dataParams);
callback(null, { reports: rows, total });
} catch (err) {
console.error('상세 검색 오류:', err);
callback(err);
}
return { reports: rows, total };
};
/**
* 14. 일일 근무 요약 조회 (날짜별)
*/
const getSummaryByDate = async (date, callback) => {
try {
const getSummaryByDate = async (date) => {
const db = await getDb();
const sql = `
@@ -536,18 +461,13 @@ const getSummaryByDate = async (date, callback) => {
ORDER BY w.worker_name ASC
`;
const [rows] = await db.query(sql, [date]);
callback(null, rows);
} catch (err) {
console.error('일일 근무 요약 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 15. 일일 근무 요약 조회 (작업자별)
*/
const getSummaryByWorker = async (worker_id, callback) => {
try {
const getSummaryByWorker = async (worker_id) => {
const db = await getDb();
const sql = `
@@ -565,18 +485,13 @@ const getSummaryByWorker = async (worker_id, callback) => {
ORDER BY dwr.report_date DESC
`;
const [rows] = await db.query(sql, [worker_id]);
callback(null, rows);
} catch (err) {
console.error('작업자별 근무 요약 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 16. 월간 요약
*/
const getMonthlySummary = async (year, month, callback) => {
try {
const getMonthlySummary = async (year, month) => {
const db = await getDb();
const start = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-01`;
const end = `${year.padStart(4, '0')}-${month.padStart(2, '0')}-31`;
@@ -601,18 +516,13 @@ const getMonthlySummary = async (year, month, callback) => {
ORDER BY dwr.report_date DESC, w.worker_name ASC
`;
const [rows] = await db.query(sql, [start, end]);
callback(null, rows);
} catch (err) {
console.error('월간 요약 조회 오류:', err);
callback(err);
}
return rows;
};
/**
* 17. 작업보고서 수정
*/
const updateById = async (id, updateData, callback) => {
try {
const updateById = async (id, updateData) => {
const db = await getDb();
const setFields = [];
@@ -655,8 +565,6 @@ const updateById = async (id, updateData, callback) => {
const sql = `UPDATE daily_work_reports SET ${setFields.join(', ')} WHERE id = ?`;
const [result] = await db.query(sql, values);
// [Sync] 근태 기록 동기화
try {
const [targetReport] = await db.query('SELECT worker_id, report_date FROM daily_work_reports WHERE id = ?', [id]);
@@ -669,17 +577,13 @@ const updateById = async (id, updateData, callback) => {
console.error('근태 기록 동기화 실패 (Update):', syncErr);
}
callback(null, result.affectedRows);
} catch (err) {
console.error('작업보고서 수정 오류:', err);
callback(err);
}
return result.affectedRows;
};
/**
* 18. 특정 작업보고서 삭제
*/
const removeById = async (id, deletedBy, callback) => {
const removeById = async (id, deletedBy) => {
const db = await getDb();
const conn = await db.getConnection();
@@ -708,7 +612,6 @@ const removeById = async (id, deletedBy, callback) => {
await conn.commit();
// [Sync] 근태 기록 동기화
if (reportInfo.length > 0) {
try {
@@ -720,11 +623,10 @@ const removeById = async (id, deletedBy, callback) => {
}
}
callback(null, result.affectedRows);
return result.affectedRows;
} catch (err) {
await conn.rollback();
console.error('작업보고서 삭제 오류:', err);
callback(err);
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
@@ -733,7 +635,7 @@ const removeById = async (id, deletedBy, callback) => {
/**
* 19. 작업자의 특정 날짜 전체 삭제
*/
const removeByDateAndWorker = async (date, worker_id, deletedBy, callback) => {
const removeByDateAndWorker = async (date, worker_id, deletedBy) => {
const db = await getDb();
const conn = await db.getConnection();
@@ -768,7 +670,6 @@ const removeByDateAndWorker = async (date, worker_id, deletedBy, callback) => {
await conn.commit();
// [Sync] 근태 기록 동기화
try {
const AttendanceModel = require('./attendanceModel');
@@ -777,21 +678,19 @@ const removeByDateAndWorker = async (date, worker_id, deletedBy, callback) => {
console.error('근태 기록 동기화 실패 (Batch Delete):', syncErr);
}
callback(null, result.affectedRows);
return result.affectedRows;
} catch (err) {
await conn.rollback();
console.error('작업보고서 전체 삭제 오류:', err);
callback(err);
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
};
/**
* 20. 통계 조회 (Promise 기반)
* 20. 통계 조회
*/
const getStatistics = async (start_date, end_date) => {
try {
const db = await getDb();
const overallSql = `
@@ -821,14 +720,10 @@ const getStatistics = async (start_date, end_date) => {
overall: overallRows[0],
daily_breakdown: dailyStats
};
} catch (err) {
console.error('통계 조회 오류:', err);
throw new Error('데이터베이스에서 통계 정보를 조회하는 중 오류가 발생했습니다.');
}
};
/**
* [V2] 여러 작업 보고서 항목을 트랜잭션으로 생성합니다. (Promise 기반)
* [V2] 여러 작업 보고서 항목을 트랜잭션으로 생성합니다.
* @param {object} modelData - 서비스 레이어에서 전달된 데이터
* @returns {Promise<object>} 삽입된 항목의 ID 배열과 개수
*/
@@ -862,8 +757,6 @@ const createReportEntries = async ({ report_date, worker_id, entries }) => {
await conn.commit();
// [Sync] 근태 기록 동기화
try {
const AttendanceModel = require('./attendanceModel');
@@ -879,9 +772,7 @@ const createReportEntries = async ({ report_date, worker_id, entries }) => {
};
} catch (err) {
await conn.rollback();
console.error('[Model] 작업 보고서 생성 중 오류 발생:', err);
// 여기서 발생한 에러는 서비스 레이어로 전파됩니다.
try { await conn.rollback(); } catch (e) {}
throw new Error('데이터베이스에 작업 보고서를 생성하는 중 오류가 발생했습니다.');
} finally {
conn.release();
@@ -925,7 +816,7 @@ const getSelectQueryV2 = () => `
`;
/**
* [V2] 옵션 기반으로 작업 보고서를 조회합니다. (Promise 기반)
* [V2] 옵션 기반으로 작업 보고서를 조회합니다.
* @param {object} options - 조회 조건 (date, worker_id, created_by_user_id 등)
* @returns {Promise<Array>} 조회된 작업 보고서 배열
*/
@@ -951,7 +842,6 @@ const getReportsWithOptions = async (options) => {
whereConditions.push('dwr.created_by = ?');
queryParams.push(options.created_by_user_id);
}
// 필요에 따라 다른 조건 추가 가능 (project_id 등)
if (whereConditions.length === 0) {
throw new Error('조회 조건이 하나 이상 필요합니다.');
@@ -960,17 +850,12 @@ const getReportsWithOptions = async (options) => {
const whereClause = whereConditions.join(' AND ');
const sql = `${getSelectQueryV2()} WHERE ${whereClause} ORDER BY w.worker_name ASC, p.project_name ASC`;
try {
const [rows] = await db.query(sql, queryParams);
return rows;
} catch (err) {
console.error('[Model] 작업 보고서 조회 오류:', err);
throw new Error('데이터베이스에서 작업 보고서를 조회하는 중 오류가 발생했습니다.');
}
};
/**
* [V2] ID를 기준으로 특정 작업 보고서 항목을 수정합니다. (Promise 기반)
* [V2] ID를 기준으로 특정 작업 보고서 항목을 수정합니다.
* @param {string} reportId - 수정할 보고서의 ID
* @param {object} updateData - 수정할 필드와 값
* @returns {Promise<number>} 영향을 받은 행의 수
@@ -1011,7 +896,6 @@ const updateReportById = async (reportId, updateData) => {
const sql = `UPDATE daily_work_reports SET ${setClauses.join(', ')} WHERE id = ?`;
try {
const [result] = await db.query(sql, queryParams);
// [Sync] 근태 기록 동기화
@@ -1025,14 +909,10 @@ const updateReportById = async (reportId, updateData) => {
}
return result.affectedRows;
} catch (err) {
console.error(`[Model] 작업 보고서 수정 오류 (id: ${reportId}):`, err);
throw new Error('데이터베이스에서 작업 보고서를 수정하는 중 오류가 발생했습니다.');
}
};
/**
* [V2] ID를 기준으로 특정 작업 보고서를 삭제합니다. (Promise 기반)
* [V2] ID를 기준으로 특정 작업 보고서를 삭제합니다.
* @param {string} reportId - 삭제할 보고서 ID
* @param {number} deletedByUserId - 삭제를 수행하는 사용자 ID
* @returns {Promise<number>} 영향을 받은 행의 수
@@ -1071,9 +951,8 @@ const removeReportById = async (reportId, deletedByUserId) => {
return result.affectedRows;
} catch (err) {
await conn.rollback();
console.error(`[Model] 작업 보고서 삭제 오류 (id: ${reportId}):`, err);
throw new Error('데이터베이스에서 작업 보고서를 삭제하는 중 오류가 발생했습니다.');
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
@@ -1082,153 +961,108 @@ const removeReportById = async (reportId, deletedByUserId) => {
// ========== 마스터 데이터 CRUD 메서드들 ==========
/**
* 📝 작업 유형 생성
* 작업 유형 생성
*/
const createWorkType = async (data, callback) => {
try {
const createWorkType = async (data) => {
const db = await getDb();
const { name, description, category } = data;
const [result] = await db.query(
'INSERT INTO work_types (name, description, category) VALUES (?, ?, ?)',
[name, description, category]
);
callback(null, { id: result.insertId, ...data });
} catch (err) {
console.error('작업 유형 생성 오류:', err);
callback(err);
}
return { id: result.insertId, ...data };
};
/**
* ✏️ 작업 유형 수정
* 작업 유형 수정
*/
const updateWorkType = async (id, data, callback) => {
try {
const updateWorkType = async (id, data) => {
const db = await getDb();
const { name, description, category } = data;
const [result] = await db.query(
'UPDATE work_types SET name = ?, description = ?, category = ? WHERE id = ?',
[name, description, category, id]
);
callback(null, result);
} catch (err) {
console.error('작업 유형 수정 오류:', err);
callback(err);
}
return result;
};
/**
* 🗑️ 작업 유형 삭제
* 작업 유형 삭제
*/
const deleteWorkType = async (id, callback) => {
try {
const deleteWorkType = async (id) => {
const db = await getDb();
const [result] = await db.query('DELETE FROM work_types WHERE id = ?', [id]);
callback(null, result);
} catch (err) {
console.error('작업 유형 삭제 오류:', err);
callback(err);
}
return result;
};
/**
* 📝 작업 상태 생성
* 작업 상태 생성
*/
const createWorkStatus = async (data, callback) => {
try {
const createWorkStatus = async (data) => {
const db = await getDb();
const { name, description, is_error } = data;
const [result] = await db.query(
'INSERT INTO work_status_types (name, description, is_error) VALUES (?, ?, ?)',
[name, description, is_error || 0]
);
callback(null, { id: result.insertId, ...data });
} catch (err) {
console.error('작업 상태 생성 오류:', err);
callback(err);
}
return { id: result.insertId, ...data };
};
/**
* ✏️ 작업 상태 수정
* 작업 상태 수정
*/
const updateWorkStatus = async (id, data, callback) => {
try {
const updateWorkStatus = async (id, data) => {
const db = await getDb();
const { name, description, is_error } = data;
const [result] = await db.query(
'UPDATE work_status_types SET name = ?, description = ?, is_error = ? WHERE id = ?',
[name, description, is_error || 0, id]
);
callback(null, result);
} catch (err) {
console.error('작업 상태 수정 오류:', err);
callback(err);
}
return result;
};
/**
* 🗑️ 작업 상태 삭제
* 작업 상태 삭제
*/
const deleteWorkStatus = async (id, callback) => {
try {
const deleteWorkStatus = async (id) => {
const db = await getDb();
const [result] = await db.query('DELETE FROM work_status_types WHERE id = ?', [id]);
callback(null, result);
} catch (err) {
console.error('작업 상태 삭제 오류:', err);
callback(err);
}
return result;
};
/**
* 📝 오류 유형 생성
* 오류 유형 생성
*/
const createErrorType = async (data, callback) => {
try {
const createErrorType = async (data) => {
const db = await getDb();
const { name, description, severity } = data;
const [result] = await db.query(
'INSERT INTO error_types (name, description, severity) VALUES (?, ?, ?)',
[name, description, severity || 'medium']
);
callback(null, { id: result.insertId, ...data });
} catch (err) {
console.error('오류 유형 생성 오류:', err);
callback(err);
}
return { id: result.insertId, ...data };
};
/**
* ✏️ 오류 유형 수정
* 오류 유형 수정
*/
const updateErrorType = async (id, data, callback) => {
try {
const updateErrorType = async (id, data) => {
const db = await getDb();
const { name, description, severity } = data;
const [result] = await db.query(
'UPDATE error_types SET name = ?, description = ?, severity = ? WHERE id = ?',
[name, description, severity || 'medium', id]
);
callback(null, result);
} catch (err) {
console.error('오류 유형 수정 오류:', err);
callback(err);
}
return result;
};
/**
* 🗑️ 오류 유형 삭제
* 오류 유형 삭제
*/
const deleteErrorType = async (id, callback) => {
try {
const deleteErrorType = async (id) => {
const db = await getDb();
const [result] = await db.query('DELETE FROM error_types WHERE id = ?', [id]);
callback(null, result);
} catch (err) {
console.error('오류 유형 삭제 오류:', err);
callback(err);
}
return result;
};
@@ -1331,30 +1165,28 @@ const createFromTbmAssignment = async (reportData) => {
};
} catch (err) {
await conn.rollback();
console.error('[Model] TBM 작업보고서 생성 중 오류 발생:', err);
throw new Error('TBM 작업보고서 생성 중 오류가 발생했습니다.');
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
};
// 모든 함수 내보내기 (Promise 기반 함수 위주로 재구성)
// 모든 함수 내보내기
module.exports = {
// 새로 추가된 V2 함수 (Promise 기반)
// V2 함수
createReportEntries,
getReportsWithOptions,
updateReportById,
removeReportById,
createFromTbmAssignment,
// Promise 기반으로 리팩토링된 함수
// 통계/요약
getStatistics,
getSummaryByDate,
getSummaryByWorker,
// 아직 리팩토링되지 않았지만 필요한 기존 함수들...
// (점진적으로 아래 함수들도 Promise 기반으로 전환해야 함)
// 마스터 데이터 조회
getAllWorkTypes,
getAllWorkStatusTypes,
getAllErrorTypes,
@@ -1369,6 +1201,8 @@ module.exports = {
createErrorType,
updateErrorType,
deleteErrorType,
// 레거시 함수 (콜백 제거 완료)
createDailyReport,
getMyAccumulatedHours,
getAccumulatedReportsByDate,

View File

@@ -4,8 +4,7 @@ const notificationModel = require('./notificationModel');
const EquipmentModel = {
// CREATE - 설비 생성
create: async (equipmentData, callback) => {
try {
create: async (equipmentData) => {
const db = await getDb();
const query = `
INSERT INTO equipments (
@@ -37,18 +36,14 @@ const EquipmentModel = {
];
const [result] = await db.query(query, values);
callback(null, {
return {
equipment_id: result.insertId,
...equipmentData
});
} catch (error) {
callback(error);
}
};
},
// READ ALL - 모든 설비 조회 (필터링 옵션 포함)
getAll: async (filters, callback) => {
try {
getAll: async (filters) => {
const db = await getDb();
let query = `
SELECT
@@ -63,25 +58,21 @@ const EquipmentModel = {
const values = [];
// 필터링: 작업장 ID
if (filters.workplace_id) {
query += ' AND e.workplace_id = ?';
values.push(filters.workplace_id);
}
// 필터링: 설비 유형
if (filters.equipment_type) {
query += ' AND e.equipment_type = ?';
values.push(filters.equipment_type);
}
// 필터링: 상태
if (filters.status) {
query += ' AND e.status = ?';
values.push(filters.status);
}
// 필터링: 검색어 (설비명, 설비코드)
if (filters.search) {
query += ' AND (e.equipment_name LIKE ? OR e.equipment_code LIKE ?)';
const searchTerm = `%${filters.search}%`;
@@ -91,58 +82,44 @@ const EquipmentModel = {
query += ' ORDER BY e.equipment_code ASC';
const [rows] = await db.query(query, values);
callback(null, rows);
} catch (error) {
callback(error);
}
return rows;
},
// READ ONE - 특정 설비 조회
getById: async (equipmentId, callback) => {
try {
getById: async (equipmentId) => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
e.*,
w.workplace_name,
wc.category_name
FROM equipments e
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
WHERE e.equipment_id = ?
`;
const [rows] = await db.query(query, [equipmentId]);
callback(null, rows[0]);
} catch (error) {
callback(error);
}
WHERE e.equipment_id = ?`,
[equipmentId]
);
return rows[0];
},
// READ BY WORKPLACE - 특정 작업장의 설비 조회
getByWorkplace: async (workplaceId, callback) => {
try {
getByWorkplace: async (workplaceId) => {
const db = await getDb();
const query = `
SELECT e.*
const [rows] = await db.query(
`SELECT e.*
FROM equipments e
WHERE e.workplace_id = ?
ORDER BY e.equipment_code ASC
`;
const [rows] = await db.query(query, [workplaceId]);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY e.equipment_code ASC`,
[workplaceId]
);
return rows;
},
// READ ACTIVE - 활성 설비만 조회
getActive: async (callback) => {
try {
getActive: async () => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
e.*,
w.workplace_name,
wc.category_name
@@ -150,43 +127,14 @@ const EquipmentModel = {
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
WHERE e.status = 'active'
ORDER BY e.equipment_code ASC
`;
const [rows] = await db.query(query);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY e.equipment_code ASC`
);
return rows;
},
// UPDATE - 설비 수정
update: async (equipmentId, equipmentData, callback) => {
try {
update: async (equipmentId, equipmentData) => {
const db = await getDb();
const query = `
UPDATE equipments SET
equipment_code = ?,
equipment_name = ?,
equipment_type = ?,
model_name = ?,
manufacturer = ?,
supplier = ?,
purchase_price = ?,
installation_date = ?,
serial_number = ?,
specifications = ?,
status = ?,
notes = ?,
workplace_id = ?,
map_x_percent = ?,
map_y_percent = ?,
map_width_percent = ?,
map_height_percent = ?,
updated_at = NOW()
WHERE equipment_id = ?
`;
const values = [
equipmentData.equipment_code,
equipmentData.equipment_name,
@@ -208,22 +156,40 @@ const EquipmentModel = {
equipmentId
];
const [result] = await db.query(query, values);
const [result] = await db.query(
`UPDATE equipments SET
equipment_code = ?,
equipment_name = ?,
equipment_type = ?,
model_name = ?,
manufacturer = ?,
supplier = ?,
purchase_price = ?,
installation_date = ?,
serial_number = ?,
specifications = ?,
status = ?,
notes = ?,
workplace_id = ?,
map_x_percent = ?,
map_y_percent = ?,
map_width_percent = ?,
map_height_percent = ?,
updated_at = NOW()
WHERE equipment_id = ?`,
values
);
if (result.affectedRows === 0) {
return callback(new Error('Equipment not found'));
}
callback(null, { equipment_id: equipmentId, ...equipmentData });
} catch (error) {
callback(error);
throw new Error('Equipment not found');
}
return { equipment_id: equipmentId, ...equipmentData };
},
// UPDATE MAP POSITION - 지도상 위치 업데이트 (선택적으로 workplace_id도 업데이트)
updateMapPosition: async (equipmentId, positionData, callback) => {
try {
// UPDATE MAP POSITION - 지도상 위치 업데이트
updateMapPosition: async (equipmentId, positionData) => {
const db = await getDb();
// workplace_id가 포함된 경우 함께 업데이트
const hasWorkplaceId = positionData.workplace_id !== undefined;
const query = hasWorkplaceId ? `
@@ -262,33 +228,23 @@ const EquipmentModel = {
const [result] = await db.query(query, values);
if (result.affectedRows === 0) {
return callback(new Error('Equipment not found'));
}
callback(null, { equipment_id: equipmentId, ...positionData });
} catch (error) {
callback(error);
throw new Error('Equipment not found');
}
return { equipment_id: equipmentId, ...positionData };
},
// DELETE - 설비 삭제
delete: async (equipmentId, callback) => {
try {
delete: async (equipmentId) => {
const db = await getDb();
const query = 'DELETE FROM equipments WHERE equipment_id = ?';
const [result] = await db.query(query, [equipmentId]);
const [result] = await db.query('DELETE FROM equipments WHERE equipment_id = ?', [equipmentId]);
if (result.affectedRows === 0) {
return callback(new Error('Equipment not found'));
}
callback(null, { equipment_id: equipmentId });
} catch (error) {
callback(error);
throw new Error('Equipment not found');
}
return { equipment_id: equipmentId };
},
// CHECK DUPLICATE CODE - 설비 코드 중복 확인
checkDuplicateCode: async (equipmentCode, excludeEquipmentId, callback) => {
try {
checkDuplicateCode: async (equipmentCode, excludeEquipmentId) => {
const db = await getDb();
let query = 'SELECT equipment_id FROM equipments WHERE equipment_code = ?';
const values = [equipmentCode];
@@ -299,48 +255,35 @@ const EquipmentModel = {
}
const [rows] = await db.query(query, values);
callback(null, rows.length > 0);
} catch (error) {
callback(error);
}
return rows.length > 0;
},
// GET EQUIPMENT TYPES - 사용 중인 설비 유형 목록 조회
getEquipmentTypes: async (callback) => {
try {
getEquipmentTypes: async () => {
const db = await getDb();
const query = `
SELECT DISTINCT equipment_type
const [rows] = await db.query(
`SELECT DISTINCT equipment_type
FROM equipments
WHERE equipment_type IS NOT NULL
ORDER BY equipment_type ASC
`;
const [rows] = await db.query(query);
callback(null, rows.map(row => row.equipment_type));
} catch (error) {
callback(error);
}
ORDER BY equipment_type ASC`
);
return rows.map(row => row.equipment_type);
},
// GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성 (TKP-001 형식)
getNextEquipmentCode: async (prefix = 'TKP', callback) => {
try {
// GET NEXT EQUIPMENT CODE - 다음 관리번호 자동 생성
getNextEquipmentCode: async (prefix = 'TKP') => {
const db = await getDb();
// 해당 접두사로 시작하는 가장 큰 번호 찾기
const query = `
SELECT equipment_code
const [rows] = await db.query(
`SELECT equipment_code
FROM equipments
WHERE equipment_code LIKE ?
ORDER BY equipment_code DESC
LIMIT 1
`;
const [rows] = await db.query(query, [`${prefix}-%`]);
LIMIT 1`,
[`${prefix}-%`]
);
let nextNumber = 1;
if (rows.length > 0) {
// TKP-001 형식에서 숫자 부분 추출
const lastCode = rows[0].equipment_code;
const match = lastCode.match(new RegExp(`^${prefix}-(\\d+)$`));
if (match) {
@@ -348,28 +291,15 @@ const EquipmentModel = {
}
}
// 3자리로 패딩 (001, 002, ...)
const nextCode = `${prefix}-${String(nextNumber).padStart(3, '0')}`;
callback(null, nextCode);
} catch (error) {
callback(error);
}
return `${prefix}-${String(nextNumber).padStart(3, '0')}`;
},
// ==========================================
// 설비 사진 관리
// ==========================================
// ADD PHOTO - 설비 사진 추가
addPhoto: async (equipmentId, photoData, callback) => {
try {
addPhoto: async (equipmentId, photoData) => {
const db = await getDb();
const query = `
INSERT INTO equipment_photos (
equipment_id, photo_path, description, display_order, uploaded_by
) VALUES (?, ?, ?, ?, ?)
`;
const values = [
equipmentId,
photoData.photo_path,
@@ -378,72 +308,59 @@ const EquipmentModel = {
photoData.uploaded_by || null
];
const [result] = await db.query(query, values);
callback(null, {
const [result] = await db.query(
`INSERT INTO equipment_photos (
equipment_id, photo_path, description, display_order, uploaded_by
) VALUES (?, ?, ?, ?, ?)`,
values
);
return {
photo_id: result.insertId,
equipment_id: equipmentId,
...photoData
});
} catch (error) {
callback(error);
}
};
},
// GET PHOTOS - 설비 사진 조회
getPhotos: async (equipmentId, callback) => {
try {
getPhotos: async (equipmentId) => {
const db = await getDb();
const query = `
SELECT ep.*, u.name AS uploaded_by_name
const [rows] = await db.query(
`SELECT ep.*, u.name AS uploaded_by_name
FROM equipment_photos ep
LEFT JOIN users u ON ep.uploaded_by = u.user_id
WHERE ep.equipment_id = ?
ORDER BY ep.display_order ASC, ep.created_at ASC
`;
const [rows] = await db.query(query, [equipmentId]);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY ep.display_order ASC, ep.created_at ASC`,
[equipmentId]
);
return rows;
},
// DELETE PHOTO - 설비 사진 삭제
deletePhoto: async (photoId, callback) => {
try {
deletePhoto: async (photoId) => {
const db = await getDb();
// 먼저 사진 정보 조회 (파일 삭제용)
const [photo] = await db.query(
'SELECT photo_path FROM equipment_photos WHERE photo_id = ?',
[photoId]
);
const query = 'DELETE FROM equipment_photos WHERE photo_id = ?';
const [result] = await db.query(query, [photoId]);
const [result] = await db.query('DELETE FROM equipment_photos WHERE photo_id = ?', [photoId]);
if (result.affectedRows === 0) {
return callback(new Error('Photo not found'));
throw new Error('Photo not found');
}
callback(null, { photo_id: photoId, photo_path: photo[0]?.photo_path });
} catch (error) {
callback(error);
}
return { photo_id: photoId, photo_path: photo[0]?.photo_path };
},
// ==========================================
// 설비 임시 이동
// ==========================================
// MOVE TEMPORARILY - 설비 임시 이동
moveTemporarily: async (equipmentId, moveData, callback) => {
try {
moveTemporarily: async (equipmentId, moveData) => {
const db = await getDb();
// 1. 설비 현재 위치 업데이트
const updateQuery = `
UPDATE equipments SET
const [result] = await db.query(
`UPDATE equipments SET
current_workplace_id = ?,
current_map_x_percent = ?,
current_map_y_percent = ?,
@@ -453,10 +370,8 @@ const EquipmentModel = {
moved_at = NOW(),
moved_by = ?,
updated_at = NOW()
WHERE equipment_id = ?
`;
const updateValues = [
WHERE equipment_id = ?`,
[
moveData.target_workplace_id,
moveData.target_x_percent,
moveData.target_y_percent,
@@ -464,26 +379,22 @@ const EquipmentModel = {
moveData.target_height_percent || null,
moveData.moved_by || null,
equipmentId
];
const [result] = await db.query(updateQuery, updateValues);
]
);
if (result.affectedRows === 0) {
return callback(new Error('Equipment not found'));
throw new Error('Equipment not found');
}
// 2. 이동 이력 기록
const logQuery = `
INSERT INTO equipment_move_logs (
await db.query(
`INSERT INTO equipment_move_logs (
equipment_id, move_type,
from_workplace_id, to_workplace_id,
from_x_percent, from_y_percent,
to_x_percent, to_y_percent,
reason, moved_by
) VALUES (?, 'temporary', ?, ?, ?, ?, ?, ?, ?, ?)
`;
await db.query(logQuery, [
) VALUES (?, 'temporary', ?, ?, ?, ?, ?, ?, ?, ?)`,
[
equipmentId,
moveData.from_workplace_id || null,
moveData.target_workplace_id,
@@ -493,32 +404,26 @@ const EquipmentModel = {
moveData.target_y_percent,
moveData.reason || null,
moveData.moved_by || null
]);
]
);
callback(null, { equipment_id: equipmentId, moved: true });
} catch (error) {
callback(error);
}
return { equipment_id: equipmentId, moved: true };
},
// RETURN TO ORIGINAL - 설비 원위치 복귀
returnToOriginal: async (equipmentId, userId, callback) => {
try {
returnToOriginal: async (equipmentId, userId) => {
const db = await getDb();
// 1. 현재 임시 위치 정보 조회
const [equipment] = await db.query(
'SELECT current_workplace_id, current_map_x_percent, current_map_y_percent FROM equipments WHERE equipment_id = ?',
[equipmentId]
);
if (!equipment[0]) {
return callback(new Error('Equipment not found'));
throw new Error('Equipment not found');
}
// 2. 임시 위치 필드 초기화
const updateQuery = `
UPDATE equipments SET
await db.query(
`UPDATE equipments SET
current_workplace_id = NULL,
current_map_x_percent = NULL,
current_map_y_percent = NULL,
@@ -528,40 +433,32 @@ const EquipmentModel = {
moved_at = NULL,
moved_by = NULL,
updated_at = NOW()
WHERE equipment_id = ?
`;
WHERE equipment_id = ?`,
[equipmentId]
);
await db.query(updateQuery, [equipmentId]);
// 3. 복귀 이력 기록
const logQuery = `
INSERT INTO equipment_move_logs (
await db.query(
`INSERT INTO equipment_move_logs (
equipment_id, move_type,
from_workplace_id, from_x_percent, from_y_percent,
reason, moved_by
) VALUES (?, 'return', ?, ?, ?, '원위치 복귀', ?)
`;
await db.query(logQuery, [
) VALUES (?, 'return', ?, ?, ?, '원위치 복귀', ?)`,
[
equipmentId,
equipment[0].current_workplace_id,
equipment[0].current_map_x_percent,
equipment[0].current_map_y_percent,
userId || null
]);
]
);
callback(null, { equipment_id: equipmentId, returned: true });
} catch (error) {
callback(error);
}
return { equipment_id: equipmentId, returned: true };
},
// GET TEMPORARILY MOVED - 임시 이동된 설비 목록
getTemporarilyMoved: async (callback) => {
try {
getTemporarilyMoved: async () => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
e.*,
w_orig.workplace_name AS original_workplace_name,
w_curr.workplace_name AS current_workplace_name,
@@ -571,22 +468,15 @@ const EquipmentModel = {
LEFT JOIN workplaces w_curr ON e.current_workplace_id = w_curr.workplace_id
LEFT JOIN users u ON e.moved_by = u.user_id
WHERE e.is_temporarily_moved = TRUE
ORDER BY e.moved_at DESC
`;
const [rows] = await db.query(query);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY e.moved_at DESC`
);
return rows;
},
// GET MOVE LOGS - 설비 이동 이력 조회
getMoveLogs: async (equipmentId, callback) => {
try {
getMoveLogs: async (equipmentId) => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
eml.*,
w_from.workplace_name AS from_workplace_name,
w_to.workplace_name AS to_workplace_name,
@@ -596,34 +486,25 @@ const EquipmentModel = {
LEFT JOIN workplaces w_to ON eml.to_workplace_id = w_to.workplace_id
LEFT JOIN users u ON eml.moved_by = u.user_id
WHERE eml.equipment_id = ?
ORDER BY eml.moved_at DESC
`;
const [rows] = await db.query(query, [equipmentId]);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY eml.moved_at DESC`,
[equipmentId]
);
return rows;
},
// ==========================================
// 설비 외부 반출/반입
// ==========================================
// EXPORT EQUIPMENT - 설비 외부 반출
exportEquipment: async (exportData, callback) => {
try {
exportEquipment: async (exportData) => {
const db = await getDb();
// 1. 반출 로그 생성
const logQuery = `
INSERT INTO equipment_external_logs (
const [logResult] = await db.query(
`INSERT INTO equipment_external_logs (
equipment_id, log_type, export_date, expected_return_date,
destination, reason, notes, exported_by
) VALUES (?, 'export', ?, ?, ?, ?, ?, ?)
`;
const logValues = [
) VALUES (?, 'export', ?, ?, ?, ?, ?, ?)`,
[
exportData.equipment_id,
exportData.export_date || new Date().toISOString().slice(0, 10),
exportData.expected_return_date || null,
@@ -631,45 +512,36 @@ const EquipmentModel = {
exportData.reason || null,
exportData.notes || null,
exportData.exported_by || null
];
]
);
const [logResult] = await db.query(logQuery, logValues);
// 2. 설비 상태 업데이트
const status = exportData.is_repair ? 'repair_external' : 'external';
await db.query(
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
[status, exportData.equipment_id]
);
callback(null, {
return {
log_id: logResult.insertId,
equipment_id: exportData.equipment_id,
exported: true
});
} catch (error) {
callback(error);
}
};
},
// RETURN EQUIPMENT - 설비 반입 (외부에서 복귀)
returnEquipment: async (logId, returnData, callback) => {
try {
returnEquipment: async (logId, returnData) => {
const db = await getDb();
// 1. 반출 로그 조회
const [logs] = await db.query(
'SELECT equipment_id FROM equipment_external_logs WHERE log_id = ?',
[logId]
);
if (!logs[0]) {
return callback(new Error('Export log not found'));
throw new Error('Export log not found');
}
const equipmentId = logs[0].equipment_id;
// 2. 반출 로그 업데이트
await db.query(
`UPDATE equipment_external_logs SET
actual_return_date = ?,
@@ -685,28 +557,22 @@ const EquipmentModel = {
]
);
// 3. 설비 상태 복원
await db.query(
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
[returnData.new_status || 'active', equipmentId]
);
callback(null, {
return {
log_id: logId,
equipment_id: equipmentId,
returned: true
});
} catch (error) {
callback(error);
}
};
},
// GET EXTERNAL LOGS - 설비 외부 반출 이력 조회
getExternalLogs: async (equipmentId, callback) => {
try {
getExternalLogs: async (equipmentId) => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
eel.*,
u_exp.name AS exported_by_name,
u_ret.name AS returned_by_name
@@ -714,22 +580,16 @@ const EquipmentModel = {
LEFT JOIN users u_exp ON eel.exported_by = u_exp.user_id
LEFT JOIN users u_ret ON eel.returned_by = u_ret.user_id
WHERE eel.equipment_id = ?
ORDER BY eel.created_at DESC
`;
const [rows] = await db.query(query, [equipmentId]);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY eel.created_at DESC`,
[equipmentId]
);
return rows;
},
// GET EXPORTED EQUIPMENTS - 현재 외부 반출 중인 설비 목록
getExportedEquipments: async (callback) => {
try {
getExportedEquipments: async () => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
e.*,
w.workplace_name,
eel.export_date,
@@ -748,37 +608,28 @@ const EquipmentModel = {
LEFT JOIN workplaces w ON e.workplace_id = w.workplace_id
LEFT JOIN users u ON eel.exported_by = u.user_id
WHERE e.status IN ('external', 'repair_external')
ORDER BY eel.export_date DESC
`;
const [rows] = await db.query(query);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY eel.export_date DESC`
);
return rows;
},
// ==========================================
// 설비 수리 신청 (work_issue_reports 연동)
// 설비 수리 신청
// ==========================================
// CREATE REPAIR REQUEST - 수리 신청 (신고 시스템 활용)
createRepairRequest: async (requestData, callback) => {
try {
createRepairRequest: async (requestData) => {
const db = await getDb();
// 설비 수리 카테고리 ID 조회
const [categories] = await db.query(
"SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리' LIMIT 1"
);
if (!categories[0]) {
return callback(new Error('설비 수리 카테고리가 없습니다'));
throw new Error('설비 수리 카테고리가 없습니다');
}
const categoryId = categories[0].category_id;
// 항목 ID 조회 (지정된 항목이 없으면 첫번째 항목 사용)
let itemId = requestData.item_id;
if (!itemId) {
const [items] = await db.query(
@@ -788,21 +639,17 @@ const EquipmentModel = {
itemId = items[0]?.item_id;
}
// 사진 경로 분리 (최대 5장)
const photos = requestData.photo_paths || [];
// work_issue_reports에 삽입
const query = `
INSERT INTO work_issue_reports (
const [result] = await db.query(
`INSERT INTO work_issue_reports (
reporter_id, issue_category_id, issue_item_id,
workplace_id, equipment_id,
additional_description,
photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reported')
`;
const values = [
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'reported')`,
[
requestData.reported_by || null,
categoryId,
itemId,
@@ -814,17 +661,14 @@ const EquipmentModel = {
photos[2] || null,
photos[3] || null,
photos[4] || null
];
]
);
const [result] = await db.query(query, values);
// 설비 상태를 repair_needed로 업데이트
await db.query(
'UPDATE equipments SET status = ?, updated_at = NOW() WHERE equipment_id = ?',
['repair_needed', requestData.equipment_id]
);
// 알림 생성
try {
await notificationModel.createRepairNotification({
equipment_id: requestData.equipment_id,
@@ -834,26 +678,20 @@ const EquipmentModel = {
created_by: requestData.reported_by
});
} catch (notifError) {
console.error('알림 생성 실패:', notifError);
// 알림 생성 실패해도 수리 신청은 성공으로 처리
}
callback(null, {
return {
report_id: result.insertId,
equipment_id: requestData.equipment_id,
created: true
});
} catch (error) {
callback(error);
}
};
},
// GET REPAIR HISTORY - 설비 수리 이력 조회
getRepairHistory: async (equipmentId, callback) => {
try {
getRepairHistory: async (equipmentId) => {
const db = await getDb();
const query = `
SELECT
const [rows] = await db.query(
`SELECT
wir.*,
irc.category_name,
iri.item_name,
@@ -867,82 +705,59 @@ const EquipmentModel = {
LEFT JOIN users u_res ON wir.resolved_by = u_res.user_id
LEFT JOIN workplaces w ON wir.workplace_id = w.workplace_id
WHERE wir.equipment_id = ?
ORDER BY wir.created_at DESC
`;
const [rows] = await db.query(query, [equipmentId]);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY wir.created_at DESC`,
[equipmentId]
);
return rows;
},
// GET REPAIR CATEGORIES - 설비 수리 항목 목록 조회
getRepairCategories: async (callback) => {
try {
getRepairCategories: async () => {
const db = await getDb();
// 설비 수리 카테고리의 항목들 조회
const query = `
SELECT iri.item_id, iri.item_name, iri.description, iri.severity
const [rows] = await db.query(
`SELECT iri.item_id, iri.item_name, iri.description, iri.severity
FROM issue_report_items iri
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
WHERE irc.category_name = '설비 수리' AND iri.is_active = 1
ORDER BY iri.display_order ASC
`;
const [rows] = await db.query(query);
callback(null, rows);
} catch (error) {
callback(error);
}
ORDER BY iri.display_order ASC`
);
return rows;
},
// ADD REPAIR CATEGORY - 새 수리 항목 추가
addRepairCategory: async (itemName, callback) => {
try {
addRepairCategory: async (itemName) => {
const db = await getDb();
// 설비 수리 카테고리 ID 조회
const [categories] = await db.query(
"SELECT category_id FROM issue_report_categories WHERE category_name = '설비 수리'"
);
if (categories.length === 0) {
return callback(new Error('설비 수리 카테고리가 없습니다.'));
throw new Error('설비 수리 카테고리가 없습니다.');
}
const categoryId = categories[0].category_id;
// 중복 확인
const [existing] = await db.query(
'SELECT item_id FROM issue_report_items WHERE category_id = ? AND item_name = ?',
[categoryId, itemName]
);
if (existing.length > 0) {
// 이미 존재하면 해당 ID 반환
return callback(null, { item_id: existing[0].item_id, item_name: itemName, isNew: false });
return { item_id: existing[0].item_id, item_name: itemName, isNew: false };
}
// 다음 display_order 구하기
const [maxOrder] = await db.query(
'SELECT MAX(display_order) as max_order FROM issue_report_items WHERE category_id = ?',
[categoryId]
);
const nextOrder = (maxOrder[0].max_order || 0) + 1;
// 새 항목 추가
const [result] = await db.query(
`INSERT INTO issue_report_items (category_id, item_name, display_order, is_active)
VALUES (?, ?, ?, 1)`,
[categoryId, itemName, nextOrder]
);
callback(null, { item_id: result.insertId, item_name: itemName, isNew: true });
} catch (error) {
callback(error);
}
return { item_id: result.insertId, item_name: itemName, isNew: true };
}
};

View File

@@ -1,160 +0,0 @@
// models/pageAccessModel.js
const db = require('../db/connection');
const PageAccessModel = {
// 사용자의 페이지 권한 조회
getUserPageAccess: (userId, callback) => {
const sql = `
SELECT
p.id,
p.page_key,
p.page_name,
p.page_path,
p.category,
p.is_admin_only,
COALESCE(upa.can_access, 0) as can_access,
upa.granted_at,
upa.granted_by,
granter.username as granted_by_username
FROM pages p
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
LEFT JOIN users granter ON upa.granted_by = granter.user_id
WHERE p.is_admin_only = 0
ORDER BY p.category, p.display_order
`;
db.query(sql, [userId], callback);
},
// 모든 페이지 목록 조회
getAllPages: (callback) => {
const sql = `
SELECT
id,
page_key,
page_name,
page_path,
category,
description,
is_admin_only,
display_order
FROM pages
WHERE is_admin_only = 0
ORDER BY category, display_order
`;
db.query(sql, callback);
},
// 페이지 권한 부여
grantPageAccess: (userId, pageId, grantedBy, callback) => {
const sql = `
INSERT INTO user_page_access (user_id, page_id, can_access, granted_by, granted_at)
VALUES (?, ?, 1, ?, NOW())
ON DUPLICATE KEY UPDATE
can_access = 1,
granted_by = ?,
granted_at = NOW()
`;
db.query(sql, [userId, pageId, grantedBy, grantedBy], callback);
},
// 페이지 권한 회수
revokePageAccess: (userId, pageId, callback) => {
const sql = `
DELETE FROM user_page_access
WHERE user_id = ? AND page_id = ?
`;
db.query(sql, [userId, pageId], callback);
},
// 여러 페이지 권한 일괄 설정
setUserPageAccess: (userId, pageIds, grantedBy, callback) => {
db.beginTransaction((err) => {
if (err) return callback(err);
// 기존 권한 모두 삭제
const deleteSql = 'DELETE FROM user_page_access WHERE user_id = ?';
db.query(deleteSql, [userId], (err) => {
if (err) {
return db.rollback(() => callback(err));
}
// 새 권한이 없으면 커밋하고 종료
if (!pageIds || pageIds.length === 0) {
return db.commit((err) => {
if (err) return db.rollback(() => callback(err));
callback(null, { affectedRows: 0 });
});
}
// 새 권한 추가
const values = pageIds.map(pageId => [userId, pageId, 1, grantedBy]);
const insertSql = `
INSERT INTO user_page_access (user_id, page_id, can_access, granted_by, granted_at)
VALUES ?
`;
db.query(insertSql, [values], (err, result) => {
if (err) {
return db.rollback(() => callback(err));
}
db.commit((err) => {
if (err) return db.rollback(() => callback(err));
callback(null, result);
});
});
});
});
},
// 특정 페이지 접근 권한 확인
checkPageAccess: (userId, pageKey, callback) => {
const sql = `
SELECT
COALESCE(upa.can_access, 0) as can_access,
p.is_admin_only
FROM pages p
LEFT JOIN user_page_access upa ON p.id = upa.page_id AND upa.user_id = ?
WHERE p.page_key = ?
`;
db.query(sql, [userId, pageKey], (err, results) => {
if (err) return callback(err);
if (results.length === 0) return callback(null, { can_access: false });
callback(null, results[0]);
});
},
// 계정이 있는 작업자 목록 조회 (권한 관리용)
getUsersWithAccounts: (callback) => {
const sql = `
SELECT
u.user_id,
u.username,
u.name,
u.role_id,
r.name as role_name,
u.worker_id,
w.worker_name,
w.job_type,
COUNT(upa.page_id) as granted_pages_count
FROM users u
LEFT JOIN roles r ON u.role_id = r.id
LEFT JOIN workers w ON u.worker_id = w.worker_id
LEFT JOIN user_page_access upa ON u.user_id = upa.user_id AND upa.can_access = 1
WHERE u.is_active = 1
AND u.role_id IN (4, 5)
GROUP BY u.user_id
ORDER BY w.worker_name, u.username
`;
db.query(sql, callback);
}
};
module.exports = PageAccessModel;

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,10 @@ const TbmTransferModel = {
* 작업자 이동 실행 (보내기/빼오기)
* 트랜잭션: source work_hours 업데이트 + dest INSERT + 로그 INSERT
*/
createTransfer: async (transferData, callback) => {
let conn;
try {
async createTransfer(transferData) {
const db = await getDb();
conn = await db.getConnection();
const conn = await db.getConnection();
try {
await conn.beginTransaction();
const {
@@ -27,8 +26,7 @@ const TbmTransferModel = {
if (sourceRows.length === 0) {
await conn.rollback();
conn.release();
return callback(null, { success: false, message: '원본 세션에서 해당 작업자를 찾을 수 없습니다.' });
return { success: false, message: '원본 세션에서 해당 작업자를 찾을 수 없습니다.' };
}
const currentHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours);
@@ -36,8 +34,7 @@ const TbmTransferModel = {
if (newSourceHours < 0) {
await conn.rollback();
conn.release();
return callback(null, { success: false, message: '이동 시간이 현재 배정 시간보다 큽니다.' });
return { success: false, message: '이동 시간이 현재 배정 시간보다 큽니다.' };
}
await conn.query(
@@ -52,14 +49,12 @@ const TbmTransferModel = {
);
if (destRows.length > 0) {
// 이미 있으면 시간만 추가
const existingHours = destRows[0].work_hours === null ? 8 : parseFloat(destRows[0].work_hours);
await conn.query(
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?',
[existingHours + parseFloat(hours), dest_session_id, worker_id]
);
} else {
// 새로 INSERT
await conn.query(
`INSERT INTO tbm_team_assignments
(session_id, worker_id, work_hours, project_id, work_type_id, task_id, workplace_category_id, workplace_id, is_present)
@@ -89,7 +84,6 @@ const TbmTransferModel = {
const totalHours = totalRows[0] ? parseFloat(totalRows[0].total_hours) : 0;
await conn.commit();
conn.release();
const result = {
success: true,
@@ -101,24 +95,22 @@ const TbmTransferModel = {
result.warning = `해당 작업자의 당일 합계가 ${totalHours}h입니다 (8h 초과).`;
}
callback(null, result);
return result;
} catch (err) {
if (conn) {
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
callback(err);
}
},
/**
* 이동 취소 (원복)
*/
cancelTransfer: async (transferId, callback) => {
let conn;
try {
async cancelTransfer(transferId) {
const db = await getDb();
conn = await db.getConnection();
const conn = await db.getConnection();
try {
await conn.beginTransaction();
// 1. 이동 로그 조회
@@ -129,8 +121,7 @@ const TbmTransferModel = {
if (transfers.length === 0) {
await conn.rollback();
conn.release();
return callback(null, { success: false, message: '이동 기록을 찾을 수 없습니다.' });
return { success: false, message: '이동 기록을 찾을 수 없습니다.' };
}
const t = transfers[0];
@@ -146,7 +137,6 @@ const TbmTransferModel = {
const newDestHours = destHours - parseFloat(t.hours);
if (newDestHours <= 0) {
// 삭제
await conn.query(
'DELETE FROM tbm_team_assignments WHERE session_id = ? AND worker_id = ?',
[t.dest_session_id, t.worker_id]
@@ -168,7 +158,6 @@ const TbmTransferModel = {
if (sourceRows.length > 0) {
const sourceHours = sourceRows[0].work_hours === null ? 8 : parseFloat(sourceRows[0].work_hours);
const restoredHours = sourceHours + parseFloat(t.hours);
// 8이면 NULL로 복원 (종일)
await conn.query(
'UPDATE tbm_team_assignments SET work_hours = ? WHERE session_id = ? AND worker_id = ?',
[restoredHours >= 8 ? null : restoredHours, t.source_session_id, t.worker_id]
@@ -179,23 +168,19 @@ const TbmTransferModel = {
await conn.query('DELETE FROM tbm_transfers WHERE transfer_id = ?', [transferId]);
await conn.commit();
conn.release();
callback(null, { success: true });
return { success: true };
} catch (err) {
if (conn) {
try { await conn.rollback(); } catch (e) {}
throw err;
} finally {
conn.release();
}
callback(err);
}
},
/**
* 당일 이동 내역 조회
*/
getTransfersByDate: async (date, callback) => {
try {
async getTransfersByDate(date) {
const db = await getDb();
const sql = `
SELECT
@@ -216,17 +201,13 @@ const TbmTransferModel = {
ORDER BY t.created_at DESC
`;
const [rows] = await db.query(sql, [date]);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
},
/**
* 당일 전 작업자 배정 현황 조회
*/
getWorkerAssignmentsByDate: async (date, callback) => {
try {
async getWorkerAssignmentsByDate(date) {
const db = await getDb();
// 1. 해당 날짜의 모든 배정 가져오기
@@ -279,15 +260,11 @@ const TbmTransferModel = {
}
});
// available 판단
Object.values(workerMap).forEach(w => {
w.available = w.total_hours < 8;
});
callback(null, Object.values(workerMap));
} catch (err) {
callback(err);
}
return Object.values(workerMap);
}
};

View File

@@ -9,10 +9,9 @@ const vacationBalanceModel = {
/**
* 특정 작업자의 모든 휴가 잔액 조회 (특정 연도)
*/
async getByWorkerAndYear(workerId, year, callback) {
try {
async getByWorkerAndYear(workerId, year) {
const db = await getDb();
const query = `
const [rows] = await db.query(`
SELECT
vbd.*,
vt.type_name,
@@ -23,21 +22,16 @@ const vacationBalanceModel = {
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);
}
`, [workerId, year]);
return rows;
},
/**
* 특정 작업자의 특정 휴가 유형 잔액 조회
*/
async getByWorkerTypeYear(workerId, vacationTypeId, year, callback) {
try {
async getByWorkerTypeYear(workerId, vacationTypeId, year) {
const db = await getDb();
const query = `
const [rows] = await db.query(`
SELECT
vbd.*,
vt.type_name,
@@ -47,22 +41,16 @@ const vacationBalanceModel = {
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);
}
`, [workerId, vacationTypeId, year]);
return rows;
},
/**
* 모든 작업자의 휴가 잔액 조회 (특정 연도)
* - 연간 연차 현황 차트용
*/
async getAllByYear(year, callback) {
try {
async getAllByYear(year) {
const db = await getDb();
const query = `
const [rows] = await db.query(`
SELECT
vbd.*,
w.worker_name,
@@ -76,108 +64,75 @@ const vacationBalanceModel = {
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);
}
`, [year]);
return rows;
},
/**
* 휴가 잔액 생성
*/
async create(balanceData, callback) {
try {
async create(balanceData) {
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);
}
const [result] = await db.query(`INSERT INTO vacation_balance_details SET ?`, balanceData);
return result;
},
/**
* 휴가 잔액 수정
*/
async update(id, updateData, callback) {
try {
async update(id, updateData) {
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);
}
const [result] = await db.query(`UPDATE vacation_balance_details SET ? WHERE id = ?`, [updateData, id]);
return result;
},
/**
* 휴가 잔액 삭제
*/
async delete(id, callback) {
try {
async delete(id) {
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);
}
const [result] = await db.query(`DELETE FROM vacation_balance_details WHERE id = ?`, [id]);
return result;
},
/**
* 작업자의 휴가 사용 일수 업데이트 (차감)
* - 휴가 신청 승인 시 호출
*/
async deductDays(workerId, vacationTypeId, year, daysToDeduct, callback) {
try {
async deductDays(workerId, vacationTypeId, year, daysToDeduct) {
const db = await getDb();
const query = `
const [result] = await db.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);
}
`, [daysToDeduct, workerId, vacationTypeId, year]);
return result;
},
/**
* 작업자의 휴가 사용 일수 복구 (취소)
* - 휴가 신청 취소/거부 시 호출
*/
async restoreDays(workerId, vacationTypeId, year, daysToRestore, callback) {
try {
async restoreDays(workerId, vacationTypeId, year, daysToRestore) {
const db = await getDb();
const query = `
const [result] = await db.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);
}
`, [daysToRestore, workerId, vacationTypeId, year]);
return result;
},
/**
* 특정 작업자의 사용 가능한 휴가 일수 확인
* - 우선순위가 높은 순서대로 차감 가능 여부 확인
*/
async getAvailableVacationDays(workerId, year, callback) {
try {
async getAvailableVacationDays(workerId, year) {
const db = await getDb();
const query = `
const [rows] = await db.query(`
SELECT
vbd.id,
vbd.vacation_type_id,
@@ -193,26 +148,19 @@ const vacationBalanceModel = {
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);
}
`, [workerId, year]);
return rows;
},
/**
* 작업자별 휴가 잔액 일괄 생성 (연도별)
* - 매년 초 또는 입사 시 사용
*/
async bulkCreate(balances, callback) {
try {
const db = await getDb();
async bulkCreate(balances) {
if (!balances || balances.length === 0) {
return callback(new Error('생성할 휴가 잔액 데이터가 없습니다'));
throw new Error('생성할 휴가 잔액 데이터가 없습니다');
}
const db = await getDb();
const query = `INSERT INTO vacation_balance_details
(worker_id, vacation_type_id, year, total_days, used_days, notes, created_by)
VALUES ?`;
@@ -227,33 +175,24 @@ const vacationBalanceModel = {
b.created_by
]);
const [rows] = await db.query(query, [values]);
callback(null, rows);
} catch (error) {
callback(error);
}
const [result] = await db.query(query, [values]);
return result;
},
/**
* 근속년수 기반 연차 일수 계산 (한국 근로기준법)
* @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);
@@ -261,17 +200,11 @@ const vacationBalanceModel = {
},
/**
* 휴가 사용 시 우선순위에 따라 잔액에서 차감 (Promise 버전)
* - 일일 근태 기록 저장 시 호출
* @param {number} workerId - 작업자 ID
* @param {number} year - 연도
* @param {number} daysToDeduct - 차감할 일수 (1, 0.5, 0.25)
* @returns {Promise<Object>} - 차감 결과
* 휴가 사용 시 우선순위에 따라 잔액에서 차감
*/
async deductByPriority(workerId, year, daysToDeduct) {
const db = await getDb();
// 우선순위순으로 잔여 일수가 있는 잔액 조회
const [balances] = await db.query(`
SELECT vbd.id, vbd.vacation_type_id, vbd.total_days, vbd.used_days,
(vbd.total_days - vbd.used_days) as remaining_days,
@@ -284,7 +217,6 @@ const vacationBalanceModel = {
`, [workerId, year]);
if (balances.length === 0) {
// 잔액이 없어도 일단 기록은 저장 (경고만)
console.warn(`[VacationBalance] 작업자 ${workerId}${year}년 휴가 잔액이 없습니다`);
return { success: false, message: '휴가 잔액이 없습니다', deducted: 0 };
}
@@ -321,16 +253,11 @@ const vacationBalanceModel = {
},
/**
* 휴가 취소 시 우선순위 역순으로 복구 (Promise 버전)
* @param {number} workerId - 작업자 ID
* @param {number} year - 연도
* @param {number} daysToRestore - 복구할 일수
* @returns {Promise<Object>} - 복구 결과
* 휴가 취소 시 우선순위 역순으로 복구
*/
async restoreByPriority(workerId, year, daysToRestore) {
const db = await getDb();
// 우선순위 역순으로 사용 일수가 있는 잔액 조회 (나중에 차감된 것부터 복구)
const [balances] = await db.query(`
SELECT vbd.id, vbd.vacation_type_id, vbd.used_days,
vt.type_code, vt.type_name, vt.priority
@@ -375,10 +302,9 @@ const vacationBalanceModel = {
/**
* 특정 ID로 휴가 잔액 조회
*/
async getById(id, callback) {
try {
async getById(id) {
const db = await getDb();
const query = `
const [rows] = await db.query(`
SELECT
vbd.*,
w.worker_name,
@@ -388,12 +314,8 @@ const vacationBalanceModel = {
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);
}
`, [id]);
return rows;
}
};

View File

@@ -9,123 +9,97 @@ const vacationTypeModel = {
/**
* 모든 활성 휴가 유형 조회 (우선순위 순서대로)
*/
async getAll(callback) {
try {
async getAll() {
const db = await getDb();
const query = `
const [rows] = await db.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);
}
`);
return rows;
},
/**
* 시스템 기본 휴가 유형만 조회
*/
async getSystemTypes(callback) {
try {
async getSystemTypes() {
const db = await getDb();
const query = `
const [rows] = await db.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);
}
`);
return rows;
},
/**
* 특별 휴가 유형만 조회
*/
async getSpecialTypes() {
const db = await getDb();
const [rows] = await db.query(`
SELECT *
FROM vacation_types
WHERE is_special = 1 AND is_active = 1
ORDER BY priority ASC
`);
return rows;
},
/**
* 특정 ID로 휴가 유형 조회
*/
async getById(id, callback) {
try {
async getById(id) {
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);
}
const [rows] = await db.query(`SELECT * FROM vacation_types WHERE id = ?`, [id]);
return rows;
},
/**
* 휴가 유형 코드로 조회
*/
async getByCode(code, callback) {
try {
async getByCode(code) {
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);
}
const [rows] = await db.query(`SELECT * FROM vacation_types WHERE type_code = ?`, [code]);
return rows;
},
/**
* 휴가 유형 생성
*/
async create(typeData, callback) {
try {
async create(typeData) {
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);
}
const [result] = await db.query(`INSERT INTO vacation_types SET ?`, typeData);
return result;
},
/**
* 휴가 유형 수정
*/
async update(id, updateData, callback) {
try {
async update(id, updateData) {
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);
}
const [result] = await db.query(`UPDATE vacation_types SET ? WHERE id = ?`, [updateData, id]);
return result;
},
/**
* 휴가 유형 삭제 (논리적 삭제 - is_active = 0)
*/
async delete(id, callback) {
try {
async delete(id) {
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);
}
const [result] = await db.query(`UPDATE vacation_types SET is_active = 0, updated_at = NOW() WHERE id = ?`, [id]);
return result;
},
/**
* 우선순위 업데이트
*/
async updatePriority(id, priority, callback) {
try {
async updatePriority(id, priority) {
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);
}
const [result] = await db.query(`UPDATE vacation_types SET priority = ?, updated_at = NOW() WHERE id = ?`, [priority, id]);
return result;
}
};

View File

@@ -2,11 +2,7 @@ const { getDb } = require('../dbPool');
// ==================== 출입 신청 관리 ====================
/**
* 출입 신청 생성
*/
const createVisitRequest = async (requestData, callback) => {
try {
const createVisitRequest = async (requestData) => {
const db = await getDb();
const {
requester_id,
@@ -29,17 +25,10 @@ const createVisitRequest = async (requestData, callback) => {
visit_date, visit_time, purpose_id, notes]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 출입 신청 목록 조회 (필터 옵션 포함)
*/
const getAllVisitRequests = async (filters = {}, callback) => {
try {
const getAllVisitRequests = async (filters = {}) => {
const db = await getDb();
let query = `
SELECT
@@ -63,27 +52,22 @@ const getAllVisitRequests = async (filters = {}, callback) => {
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);
@@ -92,17 +76,10 @@ const getAllVisitRequests = async (filters = {}, callback) => {
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);
}
return rows;
};
/**
* 출입 신청 상세 조회
*/
const getVisitRequestById = async (requestId, callback) => {
try {
const getVisitRequestById = async (requestId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
@@ -124,28 +101,14 @@ const getVisitRequestById = async (requestId, callback) => {
WHERE vr.request_id = ?`,
[requestId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 출입 신청 수정
*/
const updateVisitRequest = async (requestId, requestData, callback) => {
try {
const updateVisitRequest = async (requestId, requestData) => {
const db = await getDb();
const {
visitor_company,
visitor_count,
category_id,
workplace_id,
visit_date,
visit_time,
purpose_id,
notes
visitor_company, visitor_count, category_id, workplace_id,
visit_date, visit_time, purpose_id, notes
} = requestData;
const [result] = await db.query(
@@ -156,34 +119,19 @@ const updateVisitRequest = async (requestId, requestData, callback) => {
[visitor_company, visitor_count, category_id, workplace_id,
visit_date, visit_time, purpose_id, notes, requestId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 출입 신청 삭제
*/
const deleteVisitRequest = async (requestId, callback) => {
try {
const deleteVisitRequest = async (requestId) => {
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);
}
return result;
};
/**
* 출입 신청 승인
*/
const approveVisitRequest = async (requestId, approvedBy, callback) => {
try {
const approveVisitRequest = async (requestId, approvedBy) => {
const db = await getDb();
const [result] = await db.query(
`UPDATE workplace_visit_requests
@@ -191,18 +139,10 @@ const approveVisitRequest = async (requestId, approvedBy, callback) => {
WHERE request_id = ?`,
[approvedBy, requestId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 출입 신청 반려
*/
const rejectVisitRequest = async (requestId, rejectionData, callback) => {
try {
const rejectVisitRequest = async (requestId, rejectionData) => {
const db = await getDb();
const { approved_by, rejection_reason } = rejectionData;
@@ -213,18 +153,10 @@ const rejectVisitRequest = async (requestId, rejectionData, callback) => {
WHERE request_id = ?`,
[approved_by, rejection_reason, requestId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 출입 신청 상태 변경
*/
const updateVisitRequestStatus = async (requestId, status, callback) => {
try {
const updateVisitRequestStatus = async (requestId, status) => {
const db = await getDb();
const [result] = await db.query(
`UPDATE workplace_visit_requests
@@ -232,37 +164,22 @@ const updateVisitRequestStatus = async (requestId, status, callback) => {
WHERE request_id = ?`,
[status, requestId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
// ==================== 방문 목적 관리 ====================
/**
* 모든 방문 목적 조회
*/
const getAllVisitPurposes = async (callback) => {
try {
const getAllVisitPurposes = async () => {
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);
}
return rows;
};
/**
* 활성 방문 목적만 조회
*/
const getActiveVisitPurposes = async (callback) => {
try {
const getActiveVisitPurposes = async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT purpose_id, purpose_name, display_order, is_active, created_at
@@ -270,17 +187,10 @@ const getActiveVisitPurposes = async (callback) => {
WHERE is_active = TRUE
ORDER BY display_order ASC, purpose_id ASC`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 방문 목적 추가
*/
const createVisitPurpose = async (purposeData, callback) => {
try {
const createVisitPurpose = async (purposeData) => {
const db = await getDb();
const { purpose_name, display_order = 0, is_active = true } = purposeData;
@@ -289,18 +199,10 @@ const createVisitPurpose = async (purposeData, callback) => {
VALUES (?, ?, ?)`,
[purpose_name, display_order, is_active]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 방문 목적 수정
*/
const updateVisitPurpose = async (purposeId, purposeData, callback) => {
try {
const updateVisitPurpose = async (purposeId, purposeData) => {
const db = await getDb();
const { purpose_name, display_order, is_active } = purposeData;
@@ -310,44 +212,25 @@ const updateVisitPurpose = async (purposeId, purposeData, callback) => {
WHERE purpose_id = ?`,
[purpose_name, display_order, is_active, purposeId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 방문 목적 삭제
*/
const deleteVisitPurpose = async (purposeId, callback) => {
try {
const deleteVisitPurpose = async (purposeId) => {
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);
}
return result;
};
// ==================== 안전교육 기록 관리 ====================
/**
* 안전교육 기록 생성
*/
const createTrainingRecord = async (trainingData, callback) => {
try {
const createTrainingRecord = async (trainingData) => {
const db = await getDb();
const {
request_id,
trainer_id,
training_date,
training_start_time,
training_end_time = null,
training_topics = null
request_id, trainer_id, training_date,
training_start_time, training_end_time = null, training_topics = null
} = trainingData;
const [result] = await db.query(
@@ -356,18 +239,10 @@ const createTrainingRecord = async (trainingData, callback) => {
VALUES (?, ?, ?, ?, ?, ?)`,
[request_id, trainer_id, training_date, training_start_time, training_end_time, training_topics]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 특정 출입 신청의 안전교육 기록 조회
*/
const getTrainingRecordByRequestId = async (requestId, callback) => {
try {
const getTrainingRecordByRequestId = async (requestId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
@@ -380,25 +255,12 @@ const getTrainingRecordByRequestId = async (requestId, callback) => {
WHERE str.request_id = ?`,
[requestId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 안전교육 기록 수정
*/
const updateTrainingRecord = async (trainingId, trainingData, callback) => {
try {
const updateTrainingRecord = async (trainingId, trainingData) => {
const db = await getDb();
const {
training_date,
training_start_time,
training_end_time,
training_topics
} = trainingData;
const { training_date, training_start_time, training_end_time, training_topics } = trainingData;
const [result] = await db.query(
`UPDATE safety_training_records
@@ -407,18 +269,10 @@ const updateTrainingRecord = async (trainingId, trainingData, callback) => {
WHERE training_id = ?`,
[training_date, training_start_time, training_end_time, training_topics, trainingId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 안전교육 완료 (서명 포함)
*/
const completeTraining = async (trainingId, signatureData, callback) => {
try {
const completeTraining = async (trainingId, signatureData) => {
const db = await getDb();
const [result] = await db.query(
`UPDATE safety_training_records
@@ -426,18 +280,10 @@ const completeTraining = async (trainingId, signatureData, callback) => {
WHERE training_id = ?`,
[signatureData, trainingId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 안전교육 목록 조회 (날짜별 필터)
*/
const getTrainingRecords = async (filters = {}, callback) => {
try {
const getTrainingRecords = async (filters = {}) => {
const db = await getDb();
let query = `
SELECT
@@ -458,12 +304,10 @@ const getTrainingRecords = async (filters = {}, callback) => {
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);
@@ -472,14 +316,10 @@ const getTrainingRecords = async (filters = {}, callback) => {
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);
}
return rows;
};
module.exports = {
// 출입 신청
createVisitRequest,
getAllVisitRequests,
getVisitRequestById,
@@ -488,15 +328,11 @@ module.exports = {
approveVisitRequest,
rejectVisitRequest,
updateVisitRequestStatus,
// 방문 목적
getAllVisitPurposes,
getActiveVisitPurposes,
createVisitPurpose,
updateVisitPurpose,
deleteVisitPurpose,
// 안전교육
createTrainingRecord,
getTrainingRecordByRequestId,
updateTrainingRecord,

View File

@@ -7,28 +7,17 @@ const { getDb } = require('../dbPool');
// ==================== 신고 카테고리 관리 ====================
/**
* 모든 신고 카테고리 조회
*/
const getAllCategories = async (callback) => {
try {
const getAllCategories = async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description, display_order, is_active, created_at
FROM issue_report_categories
ORDER BY category_type, display_order, category_id`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 타입별 활성 카테고리 조회 (nonconformity/safety)
*/
const getCategoriesByType = async (categoryType, callback) => {
try {
const getCategoriesByType = async (categoryType) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_type, category_name, description, display_order
@@ -37,17 +26,10 @@ const getCategoriesByType = async (categoryType, callback) => {
ORDER BY display_order, category_id`,
[categoryType]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 카테고리 생성
*/
const createCategory = async (categoryData, callback) => {
try {
const createCategory = async (categoryData) => {
const db = await getDb();
const { category_type, category_name, description = null, display_order = 0 } = categoryData;
@@ -57,17 +39,10 @@ const createCategory = async (categoryData, callback) => {
[category_type, category_name, description, display_order]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 카테고리 수정
*/
const updateCategory = async (categoryId, categoryData, callback) => {
try {
const updateCategory = async (categoryId, categoryData) => {
const db = await getDb();
const { category_name, description, display_order, is_active } = categoryData;
@@ -78,35 +53,21 @@ const updateCategory = async (categoryId, categoryData, callback) => {
[category_name, description, display_order, is_active, categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 카테고리 삭제
*/
const deleteCategory = async (categoryId, callback) => {
try {
const deleteCategory = async (categoryId) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM issue_report_categories WHERE category_id = ?`,
[categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
// ==================== 사전 정의 신고 항목 관리 ====================
/**
* 카테고리별 활성 항목 조회
*/
const getItemsByCategory = async (categoryId, callback) => {
try {
const getItemsByCategory = async (categoryId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT item_id, category_id, item_name, description, severity, display_order
@@ -115,17 +76,10 @@ const getItemsByCategory = async (categoryId, callback) => {
ORDER BY display_order, item_id`,
[categoryId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 모든 항목 조회 (관리용)
*/
const getAllItems = async (callback) => {
try {
const getAllItems = async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT iri.item_id, iri.category_id, iri.item_name, iri.description,
@@ -135,17 +89,10 @@ const getAllItems = async (callback) => {
INNER JOIN issue_report_categories irc ON iri.category_id = irc.category_id
ORDER BY irc.category_type, irc.display_order, iri.display_order, iri.item_id`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 항목 생성
*/
const createItem = async (itemData, callback) => {
try {
const createItem = async (itemData) => {
const db = await getDb();
const { category_id, item_name, description = null, severity = 'medium', display_order = 0 } = itemData;
@@ -155,17 +102,10 @@ const createItem = async (itemData, callback) => {
[category_id, item_name, description, severity, display_order]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 항목 수정
*/
const updateItem = async (itemId, itemData, callback) => {
try {
const updateItem = async (itemId, itemData) => {
const db = await getDb();
const { item_name, description, severity, display_order, is_active } = itemData;
@@ -176,26 +116,16 @@ const updateItem = async (itemId, itemData, callback) => {
[item_name, description, severity, display_order, is_active, itemId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 항목 삭제
*/
const deleteItem = async (itemId, callback) => {
try {
const deleteItem = async (itemId) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM issue_report_items WHERE item_id = ?`,
[itemId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
// ==================== 문제 신고 관리 ====================
@@ -203,11 +133,7 @@ const deleteItem = async (itemId, callback) => {
// 한국 시간 유틸리티 import
const { getKoreaDatetime } = require('../utils/dateUtils');
/**
* 신고 생성
*/
const createReport = async (reportData, callback) => {
try {
const createReport = async (reportData) => {
const db = await getDb();
const {
reporter_id,
@@ -226,7 +152,6 @@ const createReport = async (reportData, callback) => {
photo_path5 = null
} = reportData;
// 한국 시간 기준으로 신고 일시 설정
const reportDate = getKoreaDatetime();
const [result] = await db.query(
@@ -240,24 +165,16 @@ const createReport = async (reportData, callback) => {
additional_description, photo_path1, photo_path2, photo_path3, photo_path4, photo_path5]
);
// 상태 변경 로그 기록
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, NULL, 'reported', ?)`,
[result.insertId, reporter_id]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 신고 목록 조회 (필터 옵션 포함)
*/
const getAllReports = async (filters = {}, callback) => {
try {
const getAllReports = async (filters = {}) => {
const db = await getDb();
let query = `
SELECT
@@ -287,7 +204,6 @@ const getAllReports = async (filters = {}, callback) => {
const params = [];
// 필터 적용
if (filters.status) {
query += ` AND wir.status = ?`;
params.push(filters.status);
@@ -336,7 +252,6 @@ const getAllReports = async (filters = {}, callback) => {
query += ` ORDER BY wir.report_date DESC, wir.report_id DESC`;
// 페이지네이션
if (filters.limit) {
query += ` LIMIT ?`;
params.push(parseInt(filters.limit));
@@ -348,17 +263,10 @@ const getAllReports = async (filters = {}, callback) => {
}
const [rows] = await db.query(query, params);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 신고 상세 조회
*/
const getReportById = async (reportId, callback) => {
try {
const getReportById = async (reportId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT
@@ -393,32 +301,23 @@ const getReportById = async (reportId, callback) => {
[reportId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 신고 수정
*/
const updateReport = async (reportId, reportData, userId, callback) => {
try {
const updateReport = async (reportId, reportData, userId) => {
const db = await getDb();
// 기존 데이터 조회
const [existing] = await db.query(
`SELECT * FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (existing.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
const current = existing[0];
// 수정 이력 생성
const modifications = [];
const now = new Date().toISOString();
@@ -434,7 +333,6 @@ const updateReport = async (reportId, reportData, userId, callback) => {
}
}
// 기존 이력과 병합
const existingHistory = current.modification_history ? JSON.parse(current.modification_history) : [];
const newHistory = [...existingHistory, ...modifications];
@@ -474,20 +372,12 @@ const updateReport = async (reportId, reportData, userId, callback) => {
JSON.stringify(newHistory), reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 신고 삭제
*/
const deleteReport = async (reportId, callback) => {
try {
const deleteReport = async (reportId) => {
const db = await getDb();
// 먼저 사진 경로 조회 (삭제용)
const [photos] = await db.query(
`SELECT photo_path1, photo_path2, photo_path3, photo_path4, photo_path5,
resolution_photo_path1, resolution_photo_path2
@@ -500,34 +390,25 @@ const deleteReport = async (reportId, callback) => {
[reportId]
);
// 삭제할 사진 경로 반환
callback(null, { result, photos: photos[0] });
} catch (err) {
callback(err);
}
return { result, photos: photos[0] };
};
// ==================== 상태 관리 ====================
/**
* 신고 접수 (reported → received)
*/
const receiveReport = async (reportId, userId, callback) => {
try {
const receiveReport = async (reportId, userId) => {
const db = await getDb();
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'reported') {
return callback(new Error('접수 대기 상태가 아닙니다.'));
throw new Error('접수 대기 상태가 아닙니다.');
}
const [result] = await db.query(
@@ -537,41 +418,31 @@ const receiveReport = async (reportId, userId, callback) => {
[reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, 'reported', 'received', ?)`,
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 담당자 배정
*/
const assignReport = async (reportId, assignData, callback) => {
try {
const assignReport = async (reportId, assignData) => {
const db = await getDb();
const { assigned_department, assigned_user_id, assigned_by } = assignData;
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
// 접수 상태 이상이어야 배정 가능
const validStatuses = ['received', 'in_progress'];
if (!validStatuses.includes(current[0].status)) {
return callback(new Error('접수된 상태에서만 담당자 배정이 가능합니다.'));
throw new Error('접수된 상태에서만 담당자 배정이 가능합니다.');
}
const [result] = await db.query(
@@ -582,31 +453,23 @@ const assignReport = async (reportId, assignData, callback) => {
[assigned_department, assigned_user_id, assigned_by, reportId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 처리 시작 (received → in_progress)
*/
const startProcessing = async (reportId, userId, callback) => {
try {
const startProcessing = async (reportId, userId) => {
const db = await getDb();
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'received') {
return callback(new Error('접수된 상태에서만 처리를 시작할 수 있습니다.'));
throw new Error('접수된 상태에서만 처리를 시작할 수 있습니다.');
}
const [result] = await db.query(
@@ -616,39 +479,30 @@ const startProcessing = async (reportId, userId, callback) => {
[reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, 'received', 'in_progress', ?)`,
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 처리 완료 (in_progress → completed)
*/
const completeReport = async (reportId, completionData, callback) => {
try {
const completeReport = async (reportId, completionData) => {
const db = await getDb();
const { resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by } = completionData;
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'in_progress') {
return callback(new Error('처리 중 상태에서만 완료할 수 있습니다.'));
throw new Error('처리 중 상태에서만 완료할 수 있습니다.');
}
const [result] = await db.query(
@@ -660,38 +514,29 @@ const completeReport = async (reportId, completionData, callback) => {
[resolution_notes, resolution_photo_path1, resolution_photo_path2, resolved_by, reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by, change_reason)
VALUES (?, 'in_progress', 'completed', ?, ?)`,
[reportId, resolved_by, resolution_notes]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 신고 종료 (completed → closed)
*/
const closeReport = async (reportId, userId, callback) => {
try {
const closeReport = async (reportId, userId) => {
const db = await getDb();
// 현재 상태 확인
const [current] = await db.query(
`SELECT status FROM work_issue_reports WHERE report_id = ?`,
[reportId]
);
if (current.length === 0) {
return callback(new Error('신고를 찾을 수 없습니다.'));
throw new Error('신고를 찾을 수 없습니다.');
}
if (current[0].status !== 'completed') {
return callback(new Error('완료된 상태에서만 종료할 수 있습니다.'));
throw new Error('완료된 상태에서만 종료할 수 있습니다.');
}
const [result] = await db.query(
@@ -701,24 +546,16 @@ const closeReport = async (reportId, userId, callback) => {
[reportId]
);
// 상태 변경 로그
await db.query(
`INSERT INTO work_issue_status_logs (report_id, previous_status, new_status, changed_by)
VALUES (?, 'completed', 'closed', ?)`,
[reportId, userId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 상태 변경 이력 조회
*/
const getStatusLogs = async (reportId, callback) => {
try {
const getStatusLogs = async (reportId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT wisl.log_id, wisl.report_id, wisl.previous_status, wisl.new_status,
@@ -730,19 +567,12 @@ const getStatusLogs = async (reportId, callback) => {
ORDER BY wisl.changed_at ASC`,
[reportId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
// ==================== 통계 ====================
/**
* 신고 통계 요약
*/
const getStatsSummary = async (filters = {}, callback) => {
try {
const getStatsSummary = async (filters = {}) => {
const db = await getDb();
let whereClause = '1=1';
@@ -771,17 +601,10 @@ const getStatsSummary = async (filters = {}, callback) => {
params
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 카테고리별 통계
*/
const getStatsByCategory = async (filters = {}, callback) => {
try {
const getStatsByCategory = async (filters = {}) => {
const db = await getDb();
let whereClause = '1=1';
@@ -804,17 +627,10 @@ const getStatsByCategory = async (filters = {}, callback) => {
params
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 작업장별 통계
*/
const getStatsByWorkplace = async (filters = {}, callback) => {
try {
const getStatsByWorkplace = async (filters = {}) => {
const db = await getDb();
let whereClause = 'wir.workplace_id IS NOT NULL';
@@ -844,10 +660,7 @@ const getStatsByWorkplace = async (filters = {}, callback) => {
params
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
module.exports = {

View File

@@ -3,7 +3,7 @@ const { getDb } = require('../dbPool');
/**
* 1. 여러 건 등록 (트랜잭션 사용)
*/
const createBatch = async (reports, callback) => {
const createBatch = async (reports) => {
const db = await getDb();
const conn = await db.getConnection();
@@ -30,10 +30,9 @@ const createBatch = async (reports, callback) => {
}
await conn.commit();
callback(null);
} catch (err) {
await conn.rollback();
callback(err);
throw err;
} finally {
conn.release();
}
@@ -42,8 +41,7 @@ const createBatch = async (reports, callback) => {
/**
* 2. 단일 등록
*/
const create = async (report, callback) => {
try {
const create = async (report) => {
const db = await getDb();
const {
date, worker_id, project_id,
@@ -66,21 +64,17 @@ const create = async (report, callback) => {
]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 3. 날짜별 조회
*/
const getAllByDate = async (date, callback) => {
try {
const getAllByDate = async (date) => {
const db = await getDb();
const sql = `
SELECT
wr.worker_id, -- 이 줄을 추가했습니다
wr.worker_id,
wr.id,
wr.\`date\`,
w.worker_name,
@@ -97,17 +91,13 @@ const create = async (report, callback) => {
ORDER BY w.worker_name ASC
`;
const [rows] = await db.query(sql, [date]);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 4. 기간 조회
*/
const getByRange = async (start, end, callback) => {
try {
const getByRange = async (start, end) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT id, \`date\`, worker_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports
@@ -115,33 +105,25 @@ const getByRange = async (start, end, callback) => {
ORDER BY \`date\` ASC`,
[start, end]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 5. ID로 조회
*/
const getById = async (id, callback) => {
try {
const getById = async (id) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT id, \`date\`, worker_id, project_id, morning_task_id, afternoon_task_id, overtime_hours, overtime_task_id, work_details, note, memo, created_at, updated_at, morning_project_id, afternoon_project_id, overtime_project_id, task_id FROM WorkReports WHERE id = ?`,
[id]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 6. 수정
*/
const update = async (id, report, callback) => {
try {
const update = async (id, report) => {
const db = await getDb();
const {
date, worker_id, project_id,
@@ -172,45 +154,33 @@ const update = async (id, report, callback) => {
]
);
callback(null, result.affectedRows);
} catch (err) {
callback(err);
}
return result.affectedRows;
};
/**
* 7. 삭제
*/
const remove = async (id, callback) => {
try {
const remove = async (id) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM WorkReports WHERE id = ?`,
[id]
);
callback(null, result.affectedRows);
} catch (err) {
callback(new Error(err.message || String(err)));
}
return result.affectedRows;
};
/**
* 8. 중복 확인
*/
const existsByDateAndWorker = async (date, worker_id, callback) => {
try {
const existsByDateAndWorker = async (date, worker_id) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT 1 FROM WorkReports WHERE \`date\` = ? AND worker_id = ? LIMIT 1`,
[date, worker_id]
);
callback(null, rows.length > 0);
} catch (err) {
callback(err);
}
return rows.length > 0;
};
// ✅ 내보내기
module.exports = {
create,
createBatch,

View File

@@ -2,11 +2,7 @@ const { getDb } = require('../dbPool');
// ==================== 카테고리(공장) 관련 ====================
/**
* 카테고리 생성
*/
const createCategory = async (category, callback) => {
try {
const createCategory = async (category) => {
const db = await getDb();
const {
category_name,
@@ -22,34 +18,20 @@ const createCategory = async (category, callback) => {
[category_name, description, display_order, is_active]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 모든 카테고리 조회
*/
const getAllCategories = async (callback) => {
try {
const getAllCategories = async () => {
const db = await getDb();
const [rows] = await db.query(
`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`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 활성 카테고리만 조회
*/
const getActiveCategories = async (callback) => {
try {
const getActiveCategories = async () => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
@@ -57,17 +39,10 @@ const getActiveCategories = async (callback) => {
WHERE is_active = TRUE
ORDER BY display_order ASC, category_id ASC`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* ID로 카테고리 조회
*/
const getCategoryById = async (categoryId, callback) => {
try {
const getCategoryById = async (categoryId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT category_id, category_name, description, display_order, is_active, layout_image, created_at, updated_at
@@ -75,17 +50,10 @@ const getCategoryById = async (categoryId, callback) => {
WHERE category_id = ?`,
[categoryId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 카테고리 수정
*/
const updateCategory = async (categoryId, category, callback) => {
try {
const updateCategory = async (categoryId, category) => {
const db = await getDb();
const {
category_name,
@@ -102,35 +70,21 @@ const updateCategory = async (categoryId, category, callback) => {
[category_name, description, display_order, is_active, layout_image, categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 카테고리 삭제
*/
const deleteCategory = async (categoryId, callback) => {
try {
const deleteCategory = async (categoryId) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM workplace_categories WHERE category_id = ?`,
[categoryId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
// ==================== 작업장 관련 ====================
/**
* 작업장 생성
*/
const createWorkplace = async (workplace, callback) => {
try {
const createWorkplace = async (workplace) => {
const db = await getDb();
const {
category_id = null,
@@ -148,17 +102,10 @@ const createWorkplace = async (workplace, callback) => {
[category_id, workplace_name, description, is_active, workplace_purpose, display_priority]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 모든 작업장 조회 (카테고리 정보 포함)
*/
const getAllWorkplaces = async (callback) => {
try {
const getAllWorkplaces = async () => {
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.workplace_purpose, w.display_priority,
@@ -168,17 +115,10 @@ const getAllWorkplaces = async (callback) => {
LEFT JOIN workplace_categories wc ON w.category_id = wc.category_id
ORDER BY wc.display_order ASC, w.display_priority ASC, w.workplace_id DESC`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 활성 작업장만 조회
*/
const getActiveWorkplaces = async (callback) => {
try {
const getActiveWorkplaces = async () => {
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.workplace_purpose, w.display_priority,
@@ -189,17 +129,10 @@ const getActiveWorkplaces = async (callback) => {
WHERE w.is_active = TRUE
ORDER BY wc.display_order ASC, w.workplace_id DESC`
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 카테고리별 작업장 조회
*/
const getWorkplacesByCategory = async (categoryId, callback) => {
try {
const getWorkplacesByCategory = async (categoryId) => {
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.workplace_purpose, w.display_priority,
@@ -211,17 +144,10 @@ const getWorkplacesByCategory = async (categoryId, callback) => {
ORDER BY w.workplace_id DESC`,
[categoryId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* ID로 작업장 조회
*/
const getWorkplaceById = async (workplaceId, callback) => {
try {
const getWorkplaceById = async (workplaceId) => {
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.workplace_purpose, w.display_priority,
@@ -232,17 +158,10 @@ const getWorkplaceById = async (workplaceId, callback) => {
WHERE w.workplace_id = ?`,
[workplaceId]
);
callback(null, rows[0]);
} catch (err) {
callback(err);
}
return rows[0];
};
/**
* 작업장 수정
*/
const updateWorkplace = async (workplaceId, workplace, callback) => {
try {
const updateWorkplace = async (workplaceId, workplace) => {
const db = await getDb();
const {
category_id,
@@ -262,35 +181,21 @@ const updateWorkplace = async (workplaceId, workplace, callback) => {
[category_id, workplace_name, description, is_active, workplace_purpose, display_priority, layout_image, workplaceId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 작업장 삭제
*/
const deleteWorkplace = async (workplaceId, callback) => {
try {
const deleteWorkplace = async (workplaceId) => {
const db = await getDb();
const [result] = await db.query(
`DELETE FROM workplaces WHERE workplace_id = ?`,
[workplaceId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
// ==================== 작업장 지도 영역 관련 ====================
/**
* 작업장 지도 영역 생성
*/
const createMapRegion = async (region, callback) => {
try {
const createMapRegion = async (region) => {
const db = await getDb();
const {
workplace_id,
@@ -310,17 +215,10 @@ const createMapRegion = async (region, callback) => {
[workplace_id, category_id, x_start, y_start, x_end, y_end, shape, polygon_points]
);
callback(null, result.insertId);
} catch (err) {
callback(err);
}
return result.insertId;
};
/**
* 카테고리(공장)별 지도 영역 조회
*/
const getMapRegionsByCategory = async (categoryId, callback) => {
try {
const getMapRegionsByCategory = async (categoryId) => {
const db = await getDb();
const [rows] = await db.query(
`SELECT mr.*, w.workplace_name, w.description
@@ -330,33 +228,19 @@ const getMapRegionsByCategory = async (categoryId, callback) => {
ORDER BY mr.region_id ASC`,
[categoryId]
);
callback(null, rows);
} catch (err) {
callback(err);
}
return rows;
};
/**
* 작업장별 지도 영역 조회
*/
const getMapRegionByWorkplace = async (workplaceId, callback) => {
try {
const getMapRegionByWorkplace = async (workplaceId) => {
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);
}
return rows[0];
};
/**
* 지도 영역 수정
*/
const updateMapRegion = async (regionId, region, callback) => {
try {
const updateMapRegion = async (regionId, region) => {
const db = await getDb();
const {
x_start,
@@ -374,54 +258,34 @@ const updateMapRegion = async (regionId, region, callback) => {
[x_start, y_start, x_end, y_end, shape, polygon_points, regionId]
);
callback(null, result);
} catch (err) {
callback(err);
}
return result;
};
/**
* 지도 영역 삭제
*/
const deleteMapRegion = async (regionId, callback) => {
try {
const deleteMapRegion = async (regionId) => {
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);
}
return result;
};
/**
* 작업장 영역 일괄 삭제 (카테고리별)
*/
const deleteMapRegionsByCategory = async (categoryId, callback) => {
try {
const deleteMapRegionsByCategory = async (categoryId) => {
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);
}
return result;
};
module.exports = {
// 카테고리
createCategory,
getAllCategories,
getActiveCategories,
getCategoryById,
updateCategory,
deleteCategory,
// 작업장
createWorkplace,
getAllWorkplaces,
getActiveWorkplaces,
@@ -429,8 +293,6 @@ module.exports = {
getWorkplaceById,
updateWorkplace,
deleteWorkplace,
// 지도 영역
createMapRegion,
getMapRegionsByCategory,
getMapRegionByWorkplace,

View File

@@ -24,16 +24,10 @@ const createWorkReportService = async (reportData) => {
logger.info('작업 보고서 생성 요청', { count: reports.length });
const workReport_ids = [];
try {
const workReport_ids = [];
for (const report of reports) {
const id = await new Promise((resolve, reject) => {
workReportModel.create(report, (err, insertId) => {
if (err) reject(err);
else resolve(insertId);
});
});
const id = await workReportModel.create(report);
workReport_ids.push(id);
}
@@ -66,15 +60,8 @@ const getWorkReportsByDateService = async (date) => {
logger.info('작업 보고서 날짜별 조회 요청', { date });
try {
const rows = await new Promise((resolve, reject) => {
workReportModel.getAllByDate(date, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const rows = await workReportModel.getAllByDate(date);
logger.info('작업 보고서 조회 성공', { date, count: rows.length });
return rows;
} catch (error) {
logger.error('작업 보고서 조회 실패', { date, error: error.message });
@@ -96,15 +83,8 @@ const getWorkReportsInRangeService = async (start, end) => {
logger.info('작업 보고서 기간별 조회 요청', { start, end });
try {
const rows = await new Promise((resolve, reject) => {
workReportModel.getByRange(start, end, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const rows = await workReportModel.getByRange(start, end);
logger.info('작업 보고서 조회 성공', { start, end, count: rows.length });
return rows;
} catch (error) {
logger.error('작업 보고서 조회 실패', { start, end, error: error.message });
@@ -123,12 +103,7 @@ const getWorkReportByIdService = async (id) => {
logger.info('작업 보고서 조회 요청', { report_id: id });
try {
const row = await new Promise((resolve, reject) => {
workReportModel.getById(id, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const row = await workReportModel.getById(id);
if (!row) {
logger.warn('작업 보고서를 찾을 수 없음', { report_id: id });
@@ -136,13 +111,9 @@ const getWorkReportByIdService = async (id) => {
}
logger.info('작업 보고서 조회 성공', { report_id: id });
return row;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
if (error instanceof NotFoundError) throw error;
logger.error('작업 보고서 조회 실패', { report_id: id, error: error.message });
throw new DatabaseError('작업 보고서 조회 중 오류가 발생했습니다');
}
@@ -159,12 +130,7 @@ const updateWorkReportService = async (id, updateData) => {
logger.info('작업 보고서 수정 요청', { report_id: id });
try {
const changes = await new Promise((resolve, reject) => {
workReportModel.update(id, updateData, (err, affectedRows) => {
if (err) reject(err);
else resolve(affectedRows);
});
});
const changes = await workReportModel.update(id, updateData);
if (changes === 0) {
logger.warn('작업 보고서를 찾을 수 없거나 변경사항 없음', { report_id: id });
@@ -172,13 +138,9 @@ const updateWorkReportService = async (id, updateData) => {
}
logger.info('작업 보고서 수정 성공', { report_id: id, changes });
return { changes };
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
if (error instanceof NotFoundError) throw error;
logger.error('작업 보고서 수정 실패', { report_id: id, error: error.message });
throw new DatabaseError('작업 보고서 수정 중 오류가 발생했습니다');
}
@@ -195,12 +157,7 @@ const removeWorkReportService = async (id) => {
logger.info('작업 보고서 삭제 요청', { report_id: id });
try {
const changes = await new Promise((resolve, reject) => {
workReportModel.remove(id, (err, affectedRows) => {
if (err) reject(err);
else resolve(affectedRows);
});
});
const changes = await workReportModel.remove(id);
if (changes === 0) {
logger.warn('작업 보고서를 찾을 수 없음', { report_id: id });
@@ -208,13 +165,9 @@ const removeWorkReportService = async (id) => {
}
logger.info('작업 보고서 삭제 성공', { report_id: id, changes });
return { changes };
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
if (error instanceof NotFoundError) throw error;
logger.error('작업 보고서 삭제 실패', { report_id: id, error: error.message });
throw new DatabaseError('작업 보고서 삭제 중 오류가 발생했습니다');
}
@@ -237,35 +190,18 @@ const getSummaryService = async (year, month) => {
logger.info('작업 보고서 월간 요약 조회 요청', { year, month, start, end });
try {
const rows = await new Promise((resolve, reject) => {
workReportModel.getByRange(start, end, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const rows = await workReportModel.getByRange(start, end);
if (!rows || rows.length === 0) {
logger.warn('월간 요약 데이터 없음', { year, month });
throw new NotFoundError('해당 기간의 작업 보고서가 없습니다');
}
logger.info('작업 보고서 월간 요약 조회 성공', {
year,
month,
count: rows.length
});
logger.info('작업 보고서 월간 요약 조회 성공', { year, month, count: rows.length });
return rows;
} catch (error) {
if (error instanceof NotFoundError) {
throw error;
}
logger.error('작업 보고서 월간 요약 조회 실패', {
year,
month,
error: error.message
});
if (error instanceof NotFoundError) throw error;
logger.error('작업 보고서 월간 요약 조회 실패', { year, month, error: error.message });
throw new DatabaseError('월간 요약 조회 중 오류가 발생했습니다');
}
};
@@ -274,7 +210,6 @@ const getSummaryService = async (year, month) => {
/**
* 작업 보고서의 부적합 원인 목록 조회
* - error_type_id 또는 issue_report_id 중 하나로 연결
*/
const getReportDefectsService = async (reportId) => {
const db = await getDb();
@@ -313,22 +248,16 @@ const getReportDefectsService = async (reportId) => {
/**
* 부적합 원인 저장 (전체 교체)
* - error_type_id 또는 issue_report_id 중 하나 사용 가능
* - issue_report_id: 신고된 이슈와 연결
* - error_type_id: 기존 오류 유형 (레거시)
*/
const saveReportDefectsService = async (reportId, defects) => {
const db = await getDb();
try {
await db.query('START TRANSACTION');
// 기존 부적합 원인 삭제
await db.execute('DELETE FROM work_report_defects WHERE report_id = ?', [reportId]);
// 새 부적합 원인 추가
if (defects && defects.length > 0) {
for (const defect of defects) {
// issue_report_id > category_id/item_id > error_type_id 순으로 우선
await db.execute(`
INSERT INTO work_report_defects (report_id, error_type_id, issue_report_id, category_id, item_id, defect_hours, note)
VALUES (?, ?, ?, ?, ?, ?, ?)
@@ -344,12 +273,10 @@ const saveReportDefectsService = async (reportId, defects) => {
}
}
// 총 부적합 시간 계산 및 daily_work_reports 업데이트
const totalErrorHours = defects
? defects.reduce((sum, d) => sum + (parseFloat(d.defect_hours) || 0), 0)
: 0;
// 첫 번째 defect의 error_type_id를 대표값으로 (레거시 호환)
const firstErrorTypeId = defects && defects.length > 0
? (defects.find(d => d.error_type_id)?.error_type_id || null)
: null;
@@ -380,7 +307,6 @@ const saveReportDefectsService = async (reportId, defects) => {
/**
* 부적합 원인 추가 (단일)
* - issue_report_id 또는 error_type_id 중 하나 사용
*/
const addReportDefectService = async (reportId, defectData) => {
const db = await getDb();
@@ -396,7 +322,6 @@ const addReportDefectService = async (reportId, defectData) => {
defectData.note || null
]);
// 총 부적합 시간 업데이트
await updateTotalErrorHours(reportId);
logger.info('부적합 원인 추가 성공', { reportId, defectId: result.insertId });
@@ -413,7 +338,6 @@ const addReportDefectService = async (reportId, defectData) => {
const removeReportDefectService = async (defectId) => {
const db = await getDb();
try {
// report_id 먼저 조회
const [defect] = await db.execute('SELECT report_id FROM work_report_defects WHERE defect_id = ?', [defectId]);
if (defect.length === 0) {
throw new NotFoundError('부적합 원인을 찾을 수 없습니다');
@@ -421,10 +345,7 @@ const removeReportDefectService = async (defectId) => {
const reportId = defect[0].report_id;
// 삭제
await db.execute('DELETE FROM work_report_defects WHERE defect_id = ?', [defectId]);
// 총 부적합 시간 업데이트
await updateTotalErrorHours(reportId);
logger.info('부적합 원인 삭제 성공', { defectId, reportId });
@@ -438,7 +359,6 @@ const removeReportDefectService = async (defectId) => {
/**
* 총 부적합 시간 업데이트 헬퍼
* - issue_report_id가 있는 경우도 고려
*/
const updateTotalErrorHours = async (reportId) => {
const db = await getDb();
@@ -450,8 +370,6 @@ const updateTotalErrorHours = async (reportId) => {
const totalErrorHours = result[0].total || 0;
// 첫 번째 부적합 원인의 error_type_id를 대표값으로 사용 (레거시 호환)
// issue_report_id만 있는 경우 error_type_id는 null
const [firstDefect] = await db.execute(`
SELECT error_type_id FROM work_report_defects
WHERE report_id = ? AND error_type_id IS NOT NULL
@@ -480,7 +398,6 @@ module.exports = {
updateWorkReportService,
removeWorkReportService,
getSummaryService,
// 부적합 원인 관리
getReportDefectsService,
saveReportDefectsService,
addReportDefectService,

View File

@@ -19,7 +19,6 @@ describe('WorkReportService', () => {
describe('createWorkReportService', () => {
it('단일 보고서를 성공적으로 생성해야 함', async () => {
// Arrange
const reportData = {
report_date: '2025-12-11',
worker_id: 1,
@@ -29,46 +28,32 @@ describe('WorkReportService', () => {
work_content: '기능 개발'
};
// workReportModel.create가 콜백 형태이므로 모킹 설정
workReportModel.create = jest.fn((data, callback) => {
callback(null, 123); // insertId = 123
});
workReportModel.create.mockResolvedValue(123);
// Act
const result = await workReportService.createWorkReportService(reportData);
// Assert
expect(result).toEqual({ workReport_ids: [123] });
expect(workReportModel.create).toHaveBeenCalledTimes(1);
expect(workReportModel.create).toHaveBeenCalledWith(
reportData,
expect.any(Function)
);
expect(workReportModel.create).toHaveBeenCalledWith(reportData);
});
it('다중 보고서를 성공적으로 생성해야 함', async () => {
// Arrange
const reportsData = [
{ report_date: '2025-12-11', worker_id: 1, work_hours: 8 },
{ report_date: '2025-12-11', worker_id: 2, work_hours: 7 }
];
let callCount = 0;
workReportModel.create = jest.fn((data, callback) => {
callCount++;
callback(null, 100 + callCount);
});
workReportModel.create
.mockResolvedValueOnce(101)
.mockResolvedValueOnce(102);
// Act
const result = await workReportService.createWorkReportService(reportsData);
// Assert
expect(result).toEqual({ workReport_ids: [101, 102] });
expect(workReportModel.create).toHaveBeenCalledTimes(2);
});
it('빈 배열이면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.createWorkReportService([]))
.rejects.toThrow(ValidationError);
@@ -77,14 +62,10 @@ describe('WorkReportService', () => {
});
it('DB 오류 시 DatabaseError를 던져야 함', async () => {
// Arrange
const reportData = { report_date: '2025-12-11', worker_id: 1 };
workReportModel.create = jest.fn((data, callback) => {
callback(new Error('DB connection failed'), null);
});
workReportModel.create.mockRejectedValue(new Error('DB connection failed'));
// Act & Assert
await expect(workReportService.createWorkReportService(reportData))
.rejects.toThrow(DatabaseError);
});
@@ -92,24 +73,18 @@ describe('WorkReportService', () => {
describe('getWorkReportsByDateService', () => {
it('날짜로 보고서를 조회해야 함', async () => {
// Arrange
const date = '2025-12-11';
const mockReports = mockWorkReports.filter(r => r.report_date === date);
workReportModel.getAllByDate = jest.fn((date, callback) => {
callback(null, mockReports);
});
workReportModel.getAllByDate.mockResolvedValue(mockReports);
// Act
const result = await workReportService.getWorkReportsByDateService(date);
// Assert
expect(result).toEqual(mockReports);
expect(workReportModel.getAllByDate).toHaveBeenCalledWith(date, expect.any(Function));
expect(workReportModel.getAllByDate).toHaveBeenCalledWith(date);
});
it('날짜가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportsByDateService(null))
.rejects.toThrow(ValidationError);
@@ -118,12 +93,8 @@ describe('WorkReportService', () => {
});
it('DB 오류 시 DatabaseError를 던져야 함', async () => {
// Arrange
workReportModel.getAllByDate = jest.fn((date, callback) => {
callback(new Error('DB error'), null);
});
workReportModel.getAllByDate.mockRejectedValue(new Error('DB error'));
// Act & Assert
await expect(workReportService.getWorkReportsByDateService('2025-12-11'))
.rejects.toThrow(DatabaseError);
});
@@ -131,28 +102,19 @@ describe('WorkReportService', () => {
describe('getWorkReportByIdService', () => {
it('ID로 보고서를 조회해야 함', async () => {
// Arrange
const mockReport = mockWorkReports[0];
workReportModel.getById = jest.fn((id, callback) => {
callback(null, mockReport);
});
workReportModel.getById.mockResolvedValue(mockReport);
// Act
const result = await workReportService.getWorkReportByIdService(1);
// Assert
expect(result).toEqual(mockReport);
expect(workReportModel.getById).toHaveBeenCalledWith(1, expect.any(Function));
expect(workReportModel.getById).toHaveBeenCalledWith(1);
});
it('보고서가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.getById = jest.fn((id, callback) => {
callback(null, null);
});
workReportModel.getById.mockResolvedValue(null);
// Act & Assert
await expect(workReportService.getWorkReportByIdService(999))
.rejects.toThrow(NotFoundError);
@@ -161,7 +123,6 @@ describe('WorkReportService', () => {
});
it('ID가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportByIdService(null))
.rejects.toThrow(ValidationError);
});
@@ -169,38 +130,24 @@ describe('WorkReportService', () => {
describe('updateWorkReportService', () => {
it('보고서를 성공적으로 수정해야 함', async () => {
// Arrange
const updateData = { work_hours: 9 };
workReportModel.update = jest.fn((id, data, callback) => {
callback(null, 1); // affectedRows = 1
});
workReportModel.update.mockResolvedValue(1);
// Act
const result = await workReportService.updateWorkReportService(123, updateData);
// Assert
expect(result).toEqual({ changes: 1 });
expect(workReportModel.update).toHaveBeenCalledWith(
123,
updateData,
expect.any(Function)
);
expect(workReportModel.update).toHaveBeenCalledWith(123, updateData);
});
it('수정할 보고서가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.update = jest.fn((id, data, callback) => {
callback(null, 0); // affectedRows = 0
});
workReportModel.update.mockResolvedValue(0);
// Act & Assert
await expect(workReportService.updateWorkReportService(999, {}))
.rejects.toThrow(NotFoundError);
});
it('ID가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.updateWorkReportService(null, {}))
.rejects.toThrow(ValidationError);
});
@@ -208,32 +155,22 @@ describe('WorkReportService', () => {
describe('removeWorkReportService', () => {
it('보고서를 성공적으로 삭제해야 함', async () => {
// Arrange
workReportModel.remove = jest.fn((id, callback) => {
callback(null, 1);
});
workReportModel.remove.mockResolvedValue(1);
// Act
const result = await workReportService.removeWorkReportService(123);
// Assert
expect(result).toEqual({ changes: 1 });
expect(workReportModel.remove).toHaveBeenCalledWith(123, expect.any(Function));
expect(workReportModel.remove).toHaveBeenCalledWith(123);
});
it('삭제할 보고서가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.remove = jest.fn((id, callback) => {
callback(null, 0);
});
workReportModel.remove.mockResolvedValue(0);
// Act & Assert
await expect(workReportService.removeWorkReportService(999))
.rejects.toThrow(NotFoundError);
});
it('ID가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.removeWorkReportService(null))
.rejects.toThrow(ValidationError);
});
@@ -241,34 +178,23 @@ describe('WorkReportService', () => {
describe('getWorkReportsInRangeService', () => {
it('기간으로 보고서를 조회해야 함', async () => {
// Arrange
const start = '2025-12-01';
const end = '2025-12-31';
workReportModel.getByRange = jest.fn((start, end, callback) => {
callback(null, mockWorkReports);
});
workReportModel.getByRange.mockResolvedValue(mockWorkReports);
// Act
const result = await workReportService.getWorkReportsInRangeService(start, end);
// Assert
expect(result).toEqual(mockWorkReports);
expect(workReportModel.getByRange).toHaveBeenCalledWith(
start,
end,
expect.any(Function)
);
expect(workReportModel.getByRange).toHaveBeenCalledWith(start, end);
});
it('시작일이 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportsInRangeService(null, '2025-12-31'))
.rejects.toThrow(ValidationError);
});
it('종료일이 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getWorkReportsInRangeService('2025-12-01', null))
.rejects.toThrow(ValidationError);
});
@@ -276,41 +202,30 @@ describe('WorkReportService', () => {
describe('getSummaryService', () => {
it('월간 요약을 조회해야 함', async () => {
// Arrange
const year = '2025';
const month = '12';
workReportModel.getByRange = jest.fn((start, end, callback) => {
callback(null, mockWorkReports);
});
workReportModel.getByRange.mockResolvedValue(mockWorkReports);
// Act
const result = await workReportService.getSummaryService(year, month);
// Assert
expect(result).toEqual(mockWorkReports);
expect(workReportModel.getByRange).toHaveBeenCalled();
});
it('연도가 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getSummaryService(null, '12'))
.rejects.toThrow(ValidationError);
});
it('월이 없으면 ValidationError를 던져야 함', async () => {
// Act & Assert
await expect(workReportService.getSummaryService('2025', null))
.rejects.toThrow(ValidationError);
});
it('데이터가 없으면 NotFoundError를 던져야 함', async () => {
// Arrange
workReportModel.getByRange = jest.fn((start, end, callback) => {
callback(null, []);
});
workReportModel.getByRange.mockResolvedValue([]);
// Act & Assert
await expect(workReportService.getSummaryService('2025', '12'))
.rejects.toThrow(NotFoundError);