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

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,64 @@
// 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) => {
logger.info('분석 필터 데이터 조회 요청');
const db = await getDb();
try { try {
const db = await getDb();
// 프로젝트 목록 // 프로젝트 목록
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
FROM projects p FROM projects p
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
ORDER BY p.project_name ORDER BY p.project_name
`); `);
// 작업자 목록 // 작업자 목록
const [workers] = await db.query(` const [workers] = await db.query(`
SELECT DISTINCT w.worker_id, w.worker_name SELECT DISTINCT w.worker_id, w.worker_name
FROM workers w FROM workers w
INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_id
ORDER BY w.worker_name ORDER BY w.worker_name
`); `);
// 작업 유형 목록 // 작업 유형 목록
const [workTypes] = await db.query(` const [workTypes] = await db.query(`
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
FROM work_types wt FROM work_types wt
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
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,
MAX(report_date) as max_date MAX(report_date) as max_date
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,57 +66,58 @@ 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) {
return res.status(400).json({
success: false,
error: 'start_date와 end_date가 필요합니다.',
example: 'start_date=2025-08-01&end_date=2025-08-31'
});
}
const db = await getDb(); if (!start_date || !end_date) {
throw new ValidationError('start_date와 end_date가 필요합니다', {
required: ['start_date', 'end_date'],
received: { start_date, end_date },
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();
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);
} }
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,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.worker_id) as unique_workers, COUNT(DISTINCT dwr.worker_id) as unique_workers,
@@ -111,12 +130,12 @@ const getAnalyticsByPeriod = async (req, res) => {
FROM daily_work_reports dwr FROM daily_work_reports dwr
WHERE ${whereClause} WHERE ${whereClause}
`; `;
const [overallStats] = await db.query(overallSql, queryParams); const [overallStats] = await db.query(overallSql, queryParams);
// 2. 일별 통계 // 2. 일별 통계
const dailyStatsSql = ` const dailyStatsSql = `
SELECT SELECT
dwr.report_date, dwr.report_date,
SUM(dwr.work_hours) as daily_hours, SUM(dwr.work_hours) as daily_hours,
COUNT(*) as daily_entries, COUNT(*) as daily_entries,
@@ -126,12 +145,12 @@ const getAnalyticsByPeriod = async (req, res) => {
GROUP BY dwr.report_date GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC ORDER BY dwr.report_date ASC
`; `;
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,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors, COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
COUNT(*) as daily_total, COUNT(*) as daily_total,
@@ -141,12 +160,12 @@ const getAnalyticsByPeriod = async (req, res) => {
GROUP BY dwr.report_date GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC ORDER BY dwr.report_date ASC
`; `;
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,
et.name as error_type_name, et.name as error_type_name,
COUNT(*) as error_count, COUNT(*) as error_count,
@@ -158,12 +177,12 @@ const getAnalyticsByPeriod = async (req, res) => {
GROUP BY et.id, et.name GROUP BY et.id, et.name
ORDER BY error_count DESC ORDER BY error_count DESC
`; `;
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,
wt.name as work_type_name, wt.name as work_type_name,
COUNT(*) as work_count, COUNT(*) as work_count,
@@ -177,12 +196,12 @@ const getAnalyticsByPeriod = async (req, res) => {
GROUP BY wt.id, wt.name GROUP BY wt.id, wt.name
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;
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,
w.worker_name, w.worker_name,
COUNT(*) as total_entries, COUNT(*) as total_entries,
@@ -198,12 +217,12 @@ const getAnalyticsByPeriod = async (req, res) => {
GROUP BY w.worker_id, w.worker_name GROUP BY w.worker_id, w.worker_name
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;
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,
p.project_name, p.project_name,
COUNT(*) as total_entries, COUNT(*) as total_entries,
@@ -219,9 +238,16 @@ const getAnalyticsByPeriod = async (req, res) => {
GROUP BY p.project_id, p.project_name GROUP BY p.project_id, p.project_name
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;
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,48 +260,53 @@ 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) {
return res.status(400).json({
success: false,
error: 'start_date와 end_date가 필요합니다.'
});
}
const db = await getDb(); if (!start_date || !end_date) {
throw new ValidationError('start_date와 end_date가 필요합니다', {
required: ['start_date', 'end_date'],
received: { start_date, end_date }
});
}
logger.info('프로젝트별 분석 조회 요청', {
start_date,
end_date,
project_id
});
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);
} }
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
// 프로젝트별 통계
const projectStatsSql = ` const projectStatsSql = `
SELECT SELECT
dwr.project_id, dwr.project_id,
p.project_name, p.project_name,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
@@ -289,56 +320,67 @@ const getProjectAnalysis = async (req, res) => {
GROUP BY dwr.project_id GROUP BY dwr.project_id
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;
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) {
return res.status(400).json({
success: false,
error: 'start_date와 end_date가 필요합니다.'
});
}
const db = await getDb(); if (!start_date || !end_date) {
throw new ValidationError('start_date와 end_date가 필요합니다', {
required: ['start_date', 'end_date'],
received: { start_date, end_date }
});
}
logger.info('작업자별 분석 조회 요청', {
start_date,
end_date,
worker_id
});
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 (worker_id) { if (worker_id) {
whereConditions.push('dwr.worker_id = ?'); whereConditions.push('dwr.worker_id = ?');
queryParams.push(worker_id); queryParams.push(worker_id);
} }
const whereClause = whereConditions.join(' AND '); const whereClause = whereConditions.join(' AND ');
// 작업자별 통계
const workerStatsSql = ` const workerStatsSql = `
SELECT SELECT
dwr.worker_id, dwr.worker_id,
w.worker_name, w.worker_name,
SUM(dwr.work_hours) as total_hours, SUM(dwr.work_hours) as total_hours,
@@ -352,30 +394,36 @@ const getWorkerAnalysis = async (req, res) => {
GROUP BY dwr.worker_id GROUP BY dwr.worker_id
ORDER BY total_hours DESC ORDER BY total_hours DESC
`; `;
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,
getAnalyticsByPeriod, getAnalyticsByPeriod,
getProjectAnalysis, getProjectAnalysis,
getWorkerAnalysis getWorkerAnalysis
}; };