Files
tk-factory-services/system1-factory/api/controllers/workReportAnalysisController.js
Hyungi Ahn abd7564e6b refactor: worker_id → user_id 전체 마이그레이션 (Phase 1-4)
sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거,
department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러,
4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:13:10 +09:00

430 lines
12 KiB
JavaScript

/**
* 데일리 워크 레포트 분석 컨트롤러
*
* 작업 보고서 종합 분석 API 엔드포인트 핸들러
*
* @author TK-FB-Project
* @since 2025-12-11
*/
const { getDb } = require('../dbPool');
const { ValidationError, DatabaseError } = require('../utils/errors');
const { asyncHandler } = require('../middlewares/errorHandler');
const logger = require('../utils/logger');
/**
* 분석용 필터 데이터 조회 (프로젝트, 작업자, 작업유형 목록)
*/
const getAnalysisFilters = asyncHandler(async (req, res) => {
logger.info('분석 필터 데이터 조회 요청');
const db = await getDb();
try {
// 프로젝트 목록
const [projects] = await db.query(`
SELECT DISTINCT p.project_id, p.project_name
FROM projects p
INNER JOIN daily_work_reports dwr ON p.project_id = dwr.project_id
ORDER BY p.project_name
`);
// 작업자 목록
const [workers] = await db.query(`
SELECT DISTINCT w.user_id, w.worker_name
FROM workers w
INNER JOIN daily_work_reports dwr ON w.user_id = dwr.user_id
ORDER BY w.worker_name
`);
// 작업 유형 목록
const [workTypes] = await db.query(`
SELECT DISTINCT wt.id as work_type_id, wt.name as work_type_name
FROM work_types wt
INNER JOIN daily_work_reports dwr ON wt.id = dwr.work_type_id
ORDER BY wt.name
`);
// 날짜 범위
const [dateRange] = await db.query(`
SELECT
MIN(report_date) as min_date,
MAX(report_date) as max_date
FROM daily_work_reports
`);
logger.info('분석 필터 데이터 조회 성공', {
projects: projects.length,
workers: workers.length,
workTypes: workTypes.length
});
res.json({
success: true,
data: {
projects,
workers,
workTypes,
dateRange: dateRange[0]
},
message: '분석 필터 데이터 조회 성공'
});
} catch (error) {
logger.error('분석 필터 데이터 조회 실패', { error: error.message });
throw new DatabaseError('필터 데이터 조회 중 오류가 발생했습니다');
}
});
/**
* 기간별 작업 분석 데이터 조회
*/
const getAnalyticsByPeriod = asyncHandler(async (req, res) => {
const { start_date, end_date, project_id, user_id } = req.query;
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,
user_id
});
const db = await getDb();
try {
// 기본 조건
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date];
if (project_id) {
whereConditions.push('dwr.project_id = ?');
queryParams.push(project_id);
}
if (user_id) {
whereConditions.push('dwr.user_id = ?');
queryParams.push(user_id);
}
const whereClause = whereConditions.join(' AND ');
// 1. 전체 요약 통계
const overallSql = `
SELECT
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.user_id) as unique_workers,
COUNT(DISTINCT dwr.project_id) as unique_projects,
COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(DISTINCT dwr.created_by) as contributors,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_entries,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
WHERE ${whereClause}
`;
const [overallStats] = await db.query(overallSql, queryParams);
// 2. 일별 통계
const dailyStatsSql = `
SELECT
dwr.report_date,
SUM(dwr.work_hours) as daily_hours,
COUNT(*) as daily_entries,
COUNT(DISTINCT dwr.user_id) as daily_workers
FROM daily_work_reports dwr
WHERE ${whereClause}
GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC
`;
const [dailyStats] = await db.query(dailyStatsSql, queryParams);
// 3. 일별 에러 통계
const dailyErrorStatsSql = `
SELECT
dwr.report_date,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as daily_errors,
COUNT(*) as daily_total,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as daily_error_rate
FROM daily_work_reports dwr
WHERE ${whereClause}
GROUP BY dwr.report_date
ORDER BY dwr.report_date ASC
`;
const [dailyErrorStats] = await db.query(dailyErrorStatsSql, queryParams);
// 4. 에러 유형별 분석
const errorAnalysisSql = `
SELECT
et.id as error_type_id,
et.name as error_type_name,
COUNT(*) as error_count,
SUM(dwr.work_hours) as error_hours,
ROUND((COUNT(*) / (SELECT COUNT(*) FROM daily_work_reports WHERE error_type_id IS NOT NULL)) * 100, 2) as error_percentage
FROM daily_work_reports dwr
LEFT JOIN error_types et ON dwr.error_type_id = et.id
WHERE ${whereClause} AND dwr.error_type_id IS NOT NULL
GROUP BY et.id, et.name
ORDER BY error_count DESC
`;
const [errorAnalysis] = await db.query(errorAnalysisSql, queryParams);
// 5. 작업 유형별 분석
const workTypeAnalysisSql = `
SELECT
wt.id as work_type_id,
wt.name as work_type_name,
COUNT(*) as work_count,
SUM(dwr.work_hours) as total_hours,
AVG(dwr.work_hours) as avg_hours,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE ${whereClause}
GROUP BY wt.id, wt.name
ORDER BY total_hours DESC
`;
const [workTypeAnalysis] = await db.query(workTypeAnalysisSql, queryParams);
// 6. 작업자별 성과 분석
const workerAnalysisSql = `
SELECT
w.user_id,
w.worker_name,
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(DISTINCT dwr.project_id) as projects_worked,
COUNT(DISTINCT dwr.report_date) as working_days,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE ${whereClause}
GROUP BY w.user_id, w.worker_name
ORDER BY total_hours DESC
`;
const [workerAnalysis] = await db.query(workerAnalysisSql, queryParams);
// 7. 프로젝트별 분석
const projectAnalysisSql = `
SELECT
p.project_id,
p.project_name,
COUNT(*) as total_entries,
SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.user_id) as workers_count,
COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry,
COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) as error_count,
ROUND((COUNT(CASE WHEN dwr.error_type_id IS NOT NULL THEN 1 END) / COUNT(*)) * 100, 2) as error_rate
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE ${whereClause}
GROUP BY p.project_id, p.project_name
ORDER BY total_hours DESC
`;
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({
success: true,
data: {
summary: overallStats[0],
dailyStats,
dailyErrorStats,
errorAnalysis,
workTypeAnalysis,
workerAnalysis,
projectAnalysis,
period: { start_date, end_date },
filters: { project_id, user_id }
},
message: '기간별 분석 데이터 조회 성공'
});
} catch (error) {
logger.error('기간별 분석 데이터 조회 실패', {
start_date,
end_date,
error: error.message
});
throw new DatabaseError('기간별 분석 데이터 조회 중 오류가 발생했습니다');
}
});
/**
* 프로젝트별 상세 분석
*/
const getProjectAnalysis = asyncHandler(async (req, res) => {
const { start_date, end_date, project_id } = req.query;
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 queryParams = [start_date, end_date];
if (project_id) {
whereConditions.push('dwr.project_id = ?');
queryParams.push(project_id);
}
const whereClause = whereConditions.join(' AND ');
const projectStatsSql = `
SELECT
dwr.project_id,
p.project_name,
SUM(dwr.work_hours) as total_hours,
COUNT(*) as total_entries,
COUNT(DISTINCT dwr.user_id) as workers_count,
COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
WHERE ${whereClause}
GROUP BY dwr.project_id
ORDER BY total_hours DESC
`;
const [projectStats] = await db.query(projectStatsSql, queryParams);
logger.info('프로젝트별 분석 조회 성공', {
start_date,
end_date,
projectCount: projectStats.length
});
res.json({
success: true,
data: {
projectStats,
period: { start_date, end_date }
},
message: '프로젝트별 분석 조회 성공'
});
} catch (error) {
logger.error('프로젝트별 분석 조회 실패', {
start_date,
end_date,
error: error.message
});
throw new DatabaseError('프로젝트별 분석 데이터 조회 중 오류가 발생했습니다');
}
});
/**
* 작업자별 상세 분석
*/
const getWorkerAnalysis = asyncHandler(async (req, res) => {
const { start_date, end_date, user_id } = req.query;
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,
user_id
});
const db = await getDb();
try {
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
let queryParams = [start_date, end_date];
if (user_id) {
whereConditions.push('dwr.user_id = ?');
queryParams.push(user_id);
}
const whereClause = whereConditions.join(' AND ');
const workerStatsSql = `
SELECT
dwr.user_id,
w.worker_name,
SUM(dwr.work_hours) as total_hours,
COUNT(*) as total_entries,
COUNT(DISTINCT dwr.project_id) as projects_worked,
COUNT(DISTINCT dwr.report_date) as working_days,
AVG(dwr.work_hours) as avg_hours_per_entry
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.user_id = w.user_id
WHERE ${whereClause}
GROUP BY dwr.user_id
ORDER BY total_hours DESC
`;
const [workerStats] = await db.query(workerStatsSql, queryParams);
logger.info('작업자별 분석 조회 성공', {
start_date,
end_date,
workerCount: workerStats.length
});
res.json({
success: true,
data: {
workerStats,
period: { start_date, end_date }
},
message: '작업자별 분석 조회 성공'
});
} catch (error) {
logger.error('작업자별 분석 조회 실패', {
start_date,
end_date,
error: error.message
});
throw new DatabaseError('작업자별 분석 데이터 조회 중 오류가 발생했습니다');
}
});
module.exports = {
getAnalysisFilters,
getAnalyticsByPeriod,
getProjectAnalysis,
getWorkerAnalysis
};