feat: Phase 3.8 - 복잡한 분석 컨트롤러 개선

두 개의 복잡한 분석 컨트롤러를 현대적인 패턴으로 전면 개선:

## workReportAnalysisController.js (381 → 430 lines)
- 7개 SQL 쿼리 기반 복합 분석 엔드포인트 개선
- console.error → logger.info/error/warn 전환
- try-catch → asyncHandler 미들웨어 적용
- Error → ValidationError, DatabaseError 전환
- JSDoc 문서화 및 구조화된 로깅 추가
- 4개 함수: getAnalysisFilters, getAnalyticsByPeriod, getProjectAnalysis, getWorkerAnalysis

## workAnalysisController.js (523 → 622 lines)
- 클래스 기반 → 함수 기반 컨트롤러 전환
- console.error → logger.info/error/debug 전환
- try-catch → asyncHandler 미들웨어 적용
- Error → ValidationError, DatabaseError 전환
- validateDateRange 헬퍼 함수 개선 (상세한 에러 컨텍스트)
- JSDoc 문서화 및 구조화된 로깅 추가
- 12개 함수: getStats, getDailyTrend, getWorkerStats, getProjectStats,
  getWorkTypeStats, getRecentWork, getWeekdayPattern, getErrorAnalysis,
  getMonthlyComparison, getWorkerSpecialization, getDashboardData,
  getProjectWorkTypeAnalysis

## 기술적 개선사항
- 통합 에러 처리: 커스텀 에러 클래스로 일관된 에러 핸들링
- 구조화된 로깅: 모든 API 호출에 컨텍스트 정보 포함
- 자동 에러 전파: asyncHandler로 보일러플레이트 코드 제거
- 향상된 유효성 검사: 상세한 에러 메시지와 컨텍스트
- 프로덕션 준비: 표준화된 응답 형식 및 에러 처리

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2025-12-11 13:30:42 +09:00
parent 146854e8fe
commit 9206672b63
2 changed files with 734 additions and 588 deletions

View File

@@ -1,292 +1,352 @@
// controllers/workAnalysisController.js /**
* 작업 분석 컨트롤러
*
* 작업 보고서 다차원 분석 API 엔드포인트 핸들러
*
* @author TK-FB-Project
* @since 2025-12-11
*/
const WorkAnalysis = require('../models/WorkAnalysis'); const WorkAnalysis = require('../models/WorkAnalysis');
const { getDb } = require('../dbPool'); // 기존 프로젝트의 DB 연결 방식 사용 const { getDb } = require('../dbPool');
const { ValidationError, DatabaseError } = require('../utils/errors');
const { asyncHandler } = require('../middlewares/errorHandler');
const logger = require('../utils/logger');
class WorkAnalysisController { /**
constructor() { * 날짜 유효성 검사 헬퍼 함수
// 메서드 바인딩 */
this.getStats = this.getStats.bind(this); const validateDateRange = (startDate, endDate) => {
this.getDailyTrend = this.getDailyTrend.bind(this);
this.getWorkerStats = this.getWorkerStats.bind(this);
this.getProjectStats = this.getProjectStats.bind(this);
this.getWorkTypeStats = this.getWorkTypeStats.bind(this);
this.getRecentWork = this.getRecentWork.bind(this);
this.getWeekdayPattern = this.getWeekdayPattern.bind(this);
this.getErrorAnalysis = this.getErrorAnalysis.bind(this);
this.getMonthlyComparison = this.getMonthlyComparison.bind(this);
this.getWorkerSpecialization = this.getWorkerSpecialization.bind(this);
this.getProjectWorkTypeAnalysis = this.getProjectWorkTypeAnalysis.bind(this);
}
// 날짜 유효성 검사
validateDateRange(startDate, endDate) {
if (!startDate || !endDate) { if (!startDate || !endDate) {
throw new Error('시작일과 종료일을 입력해주세요.'); throw new ValidationError('시작일과 종료일을 입력해주세요', {
required: ['start', 'end'],
received: { start: startDate, end: endDate }
});
} }
const start = new Date(startDate); const start = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
if (isNaN(start.getTime()) || isNaN(end.getTime())) { if (isNaN(start.getTime()) || isNaN(end.getTime())) {
throw new Error('올바른 날짜 형식을 입력해주세요. (YYYY-MM-DD)'); throw new ValidationError('올바른 날짜 형식을 입력해주세요', {
format: 'YYYY-MM-DD',
received: { start: startDate, end: endDate }
});
} }
if (start > end) { if (start > end) {
throw new Error('시작일이 종료일보다 늦을 수 없습니다.'); throw new ValidationError('시작일이 종료일보다 늦을 수 없습니다', {
start: startDate,
end: endDate
});
} }
// 너무 긴 기간 방지 (1년 제한) // 너무 긴 기간 방지 (1년 제한)
const diffTime = Math.abs(end - start); const diffTime = Math.abs(end - start);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays > 365) { if (diffDays > 365) {
throw new Error('조회 기간은 1년을 초과할 수 없습니다.'); throw new ValidationError('조회 기간은 1년을 초과할 수 없습니다', {
days: diffDays,
max: 365
});
} }
return { start, end }; return { start, end };
} };
// 기본 통계 조회 /**
async getStats(req, res) { * 기본 통계 조회
try { */
const getStats = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('기본 통계 조회 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const stats = await workAnalysis.getBasicStats(start, end); const stats = await workAnalysis.getBasicStats(start, end);
res.status(200).json({ logger.info('기본 통계 조회 성공', { start, end });
res.json({
success: true, success: true,
data: stats, data: stats,
message: '기본 통계 조회 완료' message: '기본 통계 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('기본 통계 조회 오류:', error); logger.error('기본 통계 조회 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('기본 통계 조회 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 일별 작업시간 추이 /**
async getDailyTrend(req, res) { * 일별 작업시간 추이 조회
try { */
const getDailyTrend = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('일별 추이 조회 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const trendData = await workAnalysis.getDailyTrend(start, end); const trendData = await workAnalysis.getDailyTrend(start, end);
res.status(200).json({ logger.info('일별 추이 조회 성공', { start, end, dataPoints: trendData.length });
res.json({
success: true, success: true,
data: trendData, data: trendData,
message: '일별 추이 조회 완료' message: '일별 추이 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('일별 추이 조회 오류:', error); logger.error('일별 추이 조회 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('일별 추이 조회 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 작업자별 통계 /**
async getWorkerStats(req, res) { * 작업자별 통계 조회
try { */
const getWorkerStats = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('작업자별 통계 조회 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const workerStats = await workAnalysis.getWorkerStats(start, end); const workerStats = await workAnalysis.getWorkerStats(start, end);
res.status(200).json({ logger.info('작업자별 통계 조회 성공', {
start,
end,
workerCount: workerStats.length
});
res.json({
success: true, success: true,
data: workerStats, data: workerStats,
message: '작업자별 통계 조회 완료' message: '작업자별 통계 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('작업자별 통계 조회 오류:', error); logger.error('작업자별 통계 조회 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('작업자별 통계 조회 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 프로젝트별 통계 /**
async getProjectStats(req, res) { * 프로젝트별 통계 조회
try { */
const getProjectStats = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('프로젝트별 통계 조회 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const projectStats = await workAnalysis.getProjectStats(start, end); const projectStats = await workAnalysis.getProjectStats(start, end);
res.status(200).json({ logger.info('프로젝트별 통계 조회 성공', {
start,
end,
projectCount: projectStats.length
});
res.json({
success: true, success: true,
data: projectStats, data: projectStats,
message: '프로젝트별 통계 조회 완료' message: '프로젝트별 통계 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('프로젝트별 통계 조회 오류:', error); logger.error('프로젝트별 통계 조회 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('프로젝트별 통계 조회 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 작업유형별 통계 /**
async getWorkTypeStats(req, res) { * 작업유형별 통계 조회
try { */
const getWorkTypeStats = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('작업유형별 통계 조회 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const workTypeStats = await workAnalysis.getWorkTypeStats(start, end); const workTypeStats = await workAnalysis.getWorkTypeStats(start, end);
res.status(200).json({ logger.info('작업유형별 통계 조회 성공', {
start,
end,
workTypeCount: workTypeStats.length
});
res.json({
success: true, success: true,
data: workTypeStats, data: workTypeStats,
message: '작업유형별 통계 조회 완료' message: '작업유형별 통계 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('작업유형별 통계 조회 오류:', error); logger.error('작업유형별 통계 조회 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('작업유형별 통계 조회 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 최근 작업 현황 /**
async getRecentWork(req, res) { * 최근 작업 현황 조회
try { */
const getRecentWork = asyncHandler(async (req, res) => {
const { start, end, limit = 10 } = req.query; const { start, end, limit = 10 } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
// limit 유효성 검사 (최대 5000까지 허용) // limit 유효성 검사 (최대 5000까지 허용)
const limitNum = parseInt(limit); const limitNum = parseInt(limit);
if (isNaN(limitNum) || limitNum < 1 || limitNum > 5000) { if (isNaN(limitNum) || limitNum < 1 || limitNum > 5000) {
throw new Error('limit은 1~5000 사이의 숫자여야 합니다.'); throw new ValidationError('limit은 1~5000 사이의 숫자여야 합니다', {
received: limit,
min: 1,
max: 5000
});
} }
logger.info('최근 작업 현황 조회 요청', { start, end, limit: limitNum });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const recentWork = await workAnalysis.getRecentWork(start, end, limitNum); const recentWork = await workAnalysis.getRecentWork(start, end, limitNum);
res.status(200).json({ logger.info('최근 작업 현황 조회 성공', {
start,
end,
limit: limitNum,
resultCount: recentWork.length
});
res.json({
success: true, success: true,
data: recentWork, data: recentWork,
message: '최근 작업 현황 조회 완료' message: '최근 작업 현황 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('최근 작업 현황 조회 오류:', error); logger.error('최근 작업 현황 조회 실패', {
res.status(400).json({ start,
success: false, end,
limit: limitNum,
error: error.message error: error.message
}); });
throw new DatabaseError('최근 작업 현황 조회 중 오류가 발생했습니다');
} }
} });
// 요일별 패턴 분석 /**
async getWeekdayPattern(req, res) { * 요일별 패턴 분석 조회
try { */
const getWeekdayPattern = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('요일별 패턴 분석 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const weekdayPattern = await workAnalysis.getWeekdayPattern(start, end); const weekdayPattern = await workAnalysis.getWeekdayPattern(start, end);
res.status(200).json({ logger.info('요일별 패턴 분석 성공', { start, end });
res.json({
success: true, success: true,
data: weekdayPattern, data: weekdayPattern,
message: '요일별 패턴 분석 완료' message: '요일별 패턴 분석 완료'
}); });
} catch (error) { } catch (error) {
console.error('요일별 패턴 분석 오류:', error); logger.error('요일별 패턴 분석 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('요일별 패턴 분석 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 에러 분석 /**
async getErrorAnalysis(req, res) { * 에러 분석 조회
try { */
const getErrorAnalysis = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('에러 분석 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const errorAnalysis = await workAnalysis.getErrorAnalysis(start, end); const errorAnalysis = await workAnalysis.getErrorAnalysis(start, end);
res.status(200).json({ logger.info('에러 분석 성공', { start, end });
res.json({
success: true, success: true,
data: errorAnalysis, data: errorAnalysis,
message: '에러 분석 완료' message: '에러 분석 완료'
}); });
} catch (error) { } catch (error) {
console.error('에러 분석 오류:', error); logger.error('에러 분석 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('에러 분석 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 월별 비교 분석 /**
async getMonthlyComparison(req, res) { * 월별 비교 분석 조회
try { */
const getMonthlyComparison = asyncHandler(async (req, res) => {
const { year = new Date().getFullYear() } = req.query; const { year = new Date().getFullYear() } = req.query;
const yearNum = parseInt(year); const yearNum = parseInt(year);
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2050) { if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2050) {
throw new Error('올바른 연도를 입력해주세요. (2000-2050)'); throw new ValidationError('올바른 연도를 입력해주세요', {
received: year,
min: 2000,
max: 2050
});
} }
logger.info('월별 비교 분석 요청', { year: yearNum });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const monthlyData = await workAnalysis.getMonthlyComparison(yearNum); const monthlyData = await workAnalysis.getMonthlyComparison(yearNum);
res.status(200).json({ logger.info('월별 비교 분석 성공', { year: yearNum });
res.json({
success: true, success: true,
data: monthlyData, data: monthlyData,
message: '월별 비교 분석 완료' message: '월별 비교 분석 완료'
}); });
} catch (error) { } catch (error) {
console.error('월별 비교 분석 오류:', error); logger.error('월별 비교 분석 실패', { year: yearNum, error: error.message });
res.status(400).json({ throw new DatabaseError('월별 비교 분석 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 작업자별 전문분야 분석 /**
async getWorkerSpecialization(req, res) { * 작업자별 전문분야 분석 조회
try { */
const getWorkerSpecialization = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('작업자별 전문분야 분석 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
const specializationData = await workAnalysis.getWorkerSpecialization(start, end); const specializationData = await workAnalysis.getWorkerSpecialization(start, end);
@@ -306,27 +366,33 @@ class WorkAnalysisController {
return acc; return acc;
}, {}); }, {});
res.status(200).json({ logger.info('작업자별 전문분야 분석 성공', {
start,
end,
workerCount: Object.keys(groupedData).length
});
res.json({
success: true, success: true,
data: groupedData, data: groupedData,
message: '작업자별 전문분야 분석 완료' message: '작업자별 전문분야 분석 완료'
}); });
} catch (error) { } catch (error) {
console.error('작업자별 전문분야 분석 오류:', error); logger.error('작업자별 전문분야 분석 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('작업자별 전문분야 분석 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 대시보드용 종합 데이터 /**
async getDashboardData(req, res) { * 대시보드용 종합 데이터 조회
try { */
const getDashboardData = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('대시보드 데이터 조회 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
const workAnalysis = new WorkAnalysis(db); const workAnalysis = new WorkAnalysis(db);
@@ -347,7 +413,9 @@ class WorkAnalysisController {
workAnalysis.getRecentWork(start, end, 10) workAnalysis.getRecentWork(start, end, 10)
]); ]);
res.status(200).json({ logger.info('대시보드 데이터 조회 성공', { start, end });
res.json({
success: true, success: true,
data: { data: {
stats, stats,
@@ -359,22 +427,22 @@ class WorkAnalysisController {
}, },
message: '대시보드 데이터 조회 완료' message: '대시보드 데이터 조회 완료'
}); });
} catch (error) { } catch (error) {
console.error('대시보드 데이터 조회 오류:', error); logger.error('대시보드 데이터 조회 실패', { start, end, error: error.message });
res.status(400).json({ throw new DatabaseError('대시보드 데이터 조회 중 오류가 발생했습니다');
success: false, }
error: error.message
}); });
}
}
// 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간) /**
async getProjectWorkTypeAnalysis(req, res) { * 프로젝트별-작업별 시간 분석 (총시간, 정규시간, 에러시간)
try { */
const getProjectWorkTypeAnalysis = asyncHandler(async (req, res) => {
const { start, end } = req.query; const { start, end } = req.query;
this.validateDateRange(start, end); validateDateRange(start, end);
logger.info('프로젝트별-작업별 시간 분석 요청', { start, end });
try {
const db = await getDb(); const db = await getDb();
// 먼저 데이터 존재 여부 확인 // 먼저 데이터 존재 여부 확인
@@ -389,7 +457,11 @@ class WorkAnalysisController {
`; `;
const testResults = await db.query(testQuery, [start, end]); const testResults = await db.query(testQuery, [start, end]);
console.log('📊 데이터 확인:', testResults[0]); logger.debug('데이터 확인', {
start,
end,
count: testResults[0][0]?.total_count
});
// 먼저 간단한 테스트 쿼리로 데이터 확인 // 먼저 간단한 테스트 쿼리로 데이터 확인
const simpleQuery = ` const simpleQuery = `
@@ -399,7 +471,11 @@ class WorkAnalysisController {
`; `;
const simpleResult = await db.query(simpleQuery, [start, end]); const simpleResult = await db.query(simpleQuery, [start, end]);
console.log('📊 기간 내 데이터 확인:', simpleResult[0][0]); logger.debug('기간 내 데이터 확인', {
start,
end,
result: simpleResult[0][0]
});
// 프로젝트별-작업별 시간 분석 쿼리 (work_types 테이블과 조인) // 프로젝트별-작업별 시간 분석 쿼리 (work_types 테이블과 조인)
const query = ` const query = `
@@ -439,9 +515,11 @@ class WorkAnalysisController {
`; `;
const results = await db.query(query, [start, end]); const results = await db.query(query, [start, end]);
console.log('📊 쿼리 결과 개수:', results[0].length); logger.debug('쿼리 결과', {
console.log('📊 첫 번째 결과:', results[0][0]); start,
console.log('📊 모든 결과:', JSON.stringify(results[0], null, 2)); end,
resultCount: results[0].length
});
// 데이터를 프로젝트별로 그룹화 // 데이터를 프로젝트별로 그룹화
const groupedData = {}; const groupedData = {};
@@ -500,7 +578,15 @@ class WorkAnalysisController {
? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100 ? Math.round((totalStats.grand_error_hours / totalStats.grand_total_hours) * 100 * 100) / 100
: 0; : 0;
res.status(200).json({ logger.info('프로젝트별-작업별 시간 분석 성공', {
start,
end,
projectCount: totalStats.total_projects,
workTypeCount: totalStats.total_work_types,
totalHours: totalStats.grand_total_hours
});
res.json({
success: true, success: true,
data: { data: {
summary: totalStats, summary: totalStats,
@@ -509,15 +595,27 @@ class WorkAnalysisController {
}, },
message: '프로젝트별-작업별 시간 분석 완료' message: '프로젝트별-작업별 시간 분석 완료'
}); });
} catch (error) { } catch (error) {
console.error('프로젝트별-작업별 시간 분석 오류:', error); logger.error('프로젝트별-작업별 시간 분석 실패', {
res.status(400).json({ start,
success: false, end,
error: error.message error: error.message
}); });
throw new DatabaseError('프로젝트별-작업별 시간 분석 중 오류가 발생했습니다');
} }
} });
}
module.exports = new WorkAnalysisController(); module.exports = {
getStats,
getDailyTrend,
getWorkerStats,
getProjectStats,
getWorkTypeStats,
getRecentWork,
getWeekdayPattern,
getErrorAnalysis,
getMonthlyComparison,
getWorkerSpecialization,
getDashboardData,
getProjectWorkTypeAnalysis
};

View File

@@ -1,14 +1,26 @@
// controllers/workReportAnalysisController.js - 데일리 워크 레포트 분석 전용 컨트롤러 /**
const dailyWorkReportModel = require('../models/dailyWorkReportModel'); * 데일리 워크 레포트 분석 컨트롤러
*
* 작업 보고서 종합 분석 API 엔드포인트 핸들러
*
* @author TK-FB-Project
* @since 2025-12-11
*/
const { getDb } = require('../dbPool'); const { getDb } = require('../dbPool');
const { ValidationError, DatabaseError } = require('../utils/errors');
const { asyncHandler } = require('../middlewares/errorHandler');
const logger = require('../utils/logger');
/** /**
* 📋 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록) * 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록)
*/ */
const getAnalysisFilters = async (req, res) => { const getAnalysisFilters = asyncHandler(async (req, res) => {
try { logger.info('분석 필터 데이터 조회 요청');
const db = await getDb(); const db = await getDb();
try {
// 프로젝트 목록 // 프로젝트 목록
const [projects] = await db.query(` const [projects] = await db.query(`
SELECT DISTINCT p.project_id, p.project_name SELECT DISTINCT p.project_id, p.project_name
@@ -33,7 +45,7 @@ const getAnalysisFilters = async (req, res) => {
ORDER BY wt.name ORDER BY wt.name
`); `);
// 날짜 범위 (최초/최신 데이터) // 날짜 범위
const [dateRange] = await db.query(` const [dateRange] = await db.query(`
SELECT SELECT
MIN(report_date) as min_date, MIN(report_date) as min_date,
@@ -41,6 +53,12 @@ const getAnalysisFilters = async (req, res) => {
FROM daily_work_reports FROM daily_work_reports
`); `);
logger.info('분석 필터 데이터 조회 성공', {
projects: projects.length,
workers: workers.length,
workTypes: workTypes.length
});
res.json({ res.json({
success: true, success: true,
data: { data: {
@@ -48,47 +66,48 @@ const getAnalysisFilters = async (req, res) => {
workers, workers,
workTypes, workTypes,
dateRange: dateRange[0] dateRange: dateRange[0]
} },
message: '분석 필터 데이터 조회 성공'
}); });
} catch (error) { } catch (error) {
console.error('필터 데이터 조회 오류:', error); logger.error('분석 필터 데이터 조회 실패', { error: error.message });
res.status(500).json({ throw new DatabaseError('필터 데이터 조회 중 오류가 발생했습니다');
success: false,
error: '필터 데이터 조회 중 오류가 발생했습니다.',
detail: error.message
});
} }
}; });
/** /**
* 📊 기간별 작업 분석 데이터 조회 * 기간별 작업 분석 데이터 조회
*/ */
const getAnalyticsByPeriod = async (req, res) => { const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
try {
const { start_date, end_date, project_id, worker_id } = req.query; const { start_date, end_date, project_id, worker_id } = req.query;
if (!start_date || !end_date) { if (!start_date || !end_date) {
return res.status(400).json({ throw new ValidationError('start_date와 end_date가 필요합니다', {
success: false, required: ['start_date', 'end_date'],
error: 'start_dateend_date가 필요합니다.', received: { start_date, end_date },
example: 'start_date=2025-08-01&end_date=2025-08-31' example: 'start_date=2025-08-01&end_date=2025-08-31'
}); });
} }
logger.info('기간별 분석 데이터 조회 요청', {
start_date,
end_date,
project_id,
worker_id
});
const db = await getDb(); const db = await getDb();
try {
// 기본 조건 // 기본 조건
let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date]; let queryParams = [start_date, end_date];
// 프로젝트 필터
if (project_id) { if (project_id) {
whereConditions.push('dwr.project_id = ?'); whereConditions.push('dwr.project_id = ?');
queryParams.push(project_id); queryParams.push(project_id);
} }
// 작업자 필터
if (worker_id) { if (worker_id) {
whereConditions.push('dwr.worker_id = ?'); whereConditions.push('dwr.worker_id = ?');
queryParams.push(worker_id); queryParams.push(worker_id);
@@ -96,7 +115,7 @@ const getAnalyticsByPeriod = async (req, res) => {
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
// 1. 전체 요약 통계 (에러 분석 포함) // 1. 전체 요약 통계
const overallSql = ` const overallSql = `
SELECT SELECT
COUNT(*) as total_entries, COUNT(*) as total_entries,
@@ -129,7 +148,7 @@ const getAnalyticsByPeriod = async (req, res) => {
const [dailyStats] = await db.query(dailyStatsSql, queryParams); const [dailyStats] = await db.query(dailyStatsSql, queryParams);
// 2.5. 일별 에러 발생 통계 // 3. 일별 에러 통계
const dailyErrorStatsSql = ` const dailyErrorStatsSql = `
SELECT SELECT
dwr.report_date, dwr.report_date,
@@ -144,7 +163,7 @@ const getAnalyticsByPeriod = async (req, res) => {
const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams); const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams);
// 3. 에러 유형별 분석 (간단한 방식으로 수정) // 4. 에러 유형별 분석
const errorAnalysisSql = ` const errorAnalysisSql = `
SELECT SELECT
et.id as error_type_id, et.id as error_type_id,
@@ -161,7 +180,7 @@ const getAnalyticsByPeriod = async (req, res) => {
const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams); const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams);
// 4. 작업 유형별 분석 // 5. 작업 유형별 분석
const workTypeAnalysisSql = ` const workTypeAnalysisSql = `
SELECT SELECT
wt.id as work_type_id, wt.id as work_type_id,
@@ -180,7 +199,7 @@ const getAnalyticsByPeriod = async (req, res) => {
const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams); const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams);
// 5. 작업자별 성과 분석 // 6. 작업자별 성과 분석
const workerAnalysisSql = ` const workerAnalysisSql = `
SELECT SELECT
w.worker_id, w.worker_id,
@@ -201,7 +220,7 @@ const getAnalyticsByPeriod = async (req, res) => {
const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams); const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams);
// 6. 프로젝트별 분석 // 7. 프로젝트별 분석
const projectAnalysisSql = ` const projectAnalysisSql = `
SELECT SELECT
p.project_id, p.project_id,
@@ -222,6 +241,13 @@ const getAnalyticsByPeriod = async (req, res) => {
const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams); const [projectAnalysis] = await db.query(projectAnalysisSql, queryParams);
logger.info('기간별 분석 데이터 조회 성공', {
start_date,
end_date,
total_entries: overallStats[0].total_entries,
total_hours: overallStats[0].total_hours
});
res.json({ res.json({
success: true, success: true,
data: { data: {
@@ -234,35 +260,41 @@ const getAnalyticsByPeriod = async (req, res) => {
projectAnalysis, projectAnalysis,
period: { start_date, end_date }, period: { start_date, end_date },
filters: { project_id, worker_id } filters: { project_id, worker_id }
} },
message: '기간별 분석 데이터 조회 성공'
}); });
} catch (error) { } catch (error) {
console.error('기간별 분석 데이터 조회 오류:', error); logger.error('기간별 분석 데이터 조회 실패', {
res.status(500).json({ start_date,
success: false, end_date,
error: '기간별 분석 데이터 조회 중 오류가 발생했습니다.', error: error.message
detail: error.message
}); });
throw new DatabaseError('기간별 분석 데이터 조회 중 오류가 발생했습니다');
} }
}; });
/** /**
* 📈 프로젝트별 상세 분석 * 프로젝트별 상세 분석
*/ */
const getProjectAnalysis = async (req, res) => { const getProjectAnalysis = asyncHandler(async (req, res) => {
try {
const { start_date, end_date, project_id } = req.query; const { start_date, end_date, project_id } = req.query;
if (!start_date || !end_date) { if (!start_date || !end_date) {
return res.status(400).json({ throw new ValidationError('start_date와 end_date가 필요합니다', {
success: false, required: ['start_date', 'end_date'],
error: 'start_dateend_date가 필요합니다.' received: { start_date, end_date }
}); });
} }
logger.info('프로젝트별 분석 조회 요청', {
start_date,
end_date,
project_id
});
const db = await getDb(); const db = await getDb();
try {
let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date]; let queryParams = [start_date, end_date];
@@ -273,7 +305,6 @@ const getProjectAnalysis = async (req, res) => {
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
// 프로젝트별 통계
const projectStatsSql = ` const projectStatsSql = `
SELECT SELECT
dwr.project_id, dwr.project_id,
@@ -292,40 +323,52 @@ const getProjectAnalysis = async (req, res) => {
const [projectStats] = await db.query(projectStatsSql, queryParams); const [projectStats] = await db.query(projectStatsSql, queryParams);
logger.info('프로젝트별 분석 조회 성공', {
start_date,
end_date,
projectCount: projectStats.length
});
res.json({ res.json({
success: true, success: true,
data: { data: {
projectStats, projectStats,
period: { start_date, end_date } period: { start_date, end_date }
} },
message: '프로젝트별 분석 조회 성공'
}); });
} catch (error) { } catch (error) {
console.error('프로젝트별 분석 데이터 조회 오류:', error); logger.error('프로젝트별 분석 조회 실패', {
res.status(500).json({ start_date,
success: false, end_date,
error: '프로젝트별 분석 데이터 조회 중 오류가 발생했습니다.', error: error.message
detail: error.message
}); });
throw new DatabaseError('프로젝트별 분석 데이터 조회 중 오류가 발생했습니다');
} }
}; });
/** /**
* 👤 작업자별 상세 분석 * 작업자별 상세 분석
*/ */
const getWorkerAnalysis = async (req, res) => { const getWorkerAnalysis = asyncHandler(async (req, res) => {
try {
const { start_date, end_date, worker_id } = req.query; const { start_date, end_date, worker_id } = req.query;
if (!start_date || !end_date) { if (!start_date || !end_date) {
return res.status(400).json({ throw new ValidationError('start_date와 end_date가 필요합니다', {
success: false, required: ['start_date', 'end_date'],
error: 'start_dateend_date가 필요합니다.' received: { start_date, end_date }
}); });
} }
logger.info('작업자별 분석 조회 요청', {
start_date,
end_date,
worker_id
});
const db = await getDb(); const db = await getDb();
try {
let whereConditions = ['dwr.report_date BETWEEN ? AND ?']; let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date]; let queryParams = [start_date, end_date];
@@ -336,7 +379,6 @@ const getWorkerAnalysis = async (req, res) => {
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
// 작업자별 통계
const workerStatsSql = ` const workerStatsSql = `
SELECT SELECT
dwr.worker_id, dwr.worker_id,
@@ -355,23 +397,29 @@ const getWorkerAnalysis = async (req, res) => {
const [workerStats] = await db.query(workerStatsSql, queryParams); const [workerStats] = await db.query(workerStatsSql, queryParams);
logger.info('작업자별 분석 조회 성공', {
start_date,
end_date,
workerCount: workerStats.length
});
res.json({ res.json({
success: true, success: true,
data: { data: {
workerStats, workerStats,
period: { start_date, end_date } period: { start_date, end_date }
} },
message: '작업자별 분석 조회 성공'
}); });
} catch (error) { } catch (error) {
console.error('작업자별 분석 데이터 조회 오류:', error); logger.error('작업자별 분석 조회 실패', {
res.status(500).json({ start_date,
success: false, end_date,
error: '작업자별 분석 데이터 조회 중 오류가 발생했습니다.', error: error.message
detail: error.message
}); });
throw new DatabaseError('작업자별 분석 데이터 조회 중 오류가 발생했습니다');
} }
}; });
module.exports = { module.exports = {
getAnalysisFilters, getAnalysisFilters,