feat: 3-System 분리 프로젝트 초기 코드 작성
TK-FB(공장관리+신고)와 M-Project(부적합관리)를 3개 독립 시스템으로 분리하기 위한 전체 코드 구조 작성. - SSO 인증 서비스 (bcrypt + pbkdf2 이중 해시 지원) - System 1: 공장관리 (TK-FB 기반, 신고 코드 제거) - System 2: 신고 (TK-FB에서 workIssue 코드 추출) - System 3: 부적합관리 (M-Project 기반) - Gateway 포털 (path-based 라우팅) - 통합 docker-compose.yml 및 배포 스크립트 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
429
system1-factory/api/controllers/workReportAnalysisController.js
Normal file
429
system1-factory/api/controllers/workReportAnalysisController.js
Normal file
@@ -0,0 +1,429 @@
|
||||
/**
|
||||
* 데일리 워크 레포트 분석 컨트롤러
|
||||
*
|
||||
* 작업 보고서 종합 분석 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.worker_id, w.worker_name
|
||||
FROM workers w
|
||||
INNER JOIN daily_work_reports dwr ON w.worker_id = dwr.worker_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, worker_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,
|
||||
worker_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 (worker_id) {
|
||||
whereConditions.push('dwr.worker_id = ?');
|
||||
queryParams.push(worker_id);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
// 1. 전체 요약 통계
|
||||
const overallSql = `
|
||||
SELECT
|
||||
COUNT(*) as total_entries,
|
||||
SUM(dwr.work_hours) as total_hours,
|
||||
COUNT(DISTINCT dwr.worker_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.worker_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.worker_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.worker_id = w.worker_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY w.worker_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.worker_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, worker_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.worker_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, worker_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,
|
||||
worker_id
|
||||
});
|
||||
|
||||
const db = await getDb();
|
||||
|
||||
try {
|
||||
let whereConditions = ['dwr.report_date BETWEEN ? AND ?'];
|
||||
let queryParams = [start_date, end_date];
|
||||
|
||||
if (worker_id) {
|
||||
whereConditions.push('dwr.worker_id = ?');
|
||||
queryParams.push(worker_id);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(' AND ');
|
||||
|
||||
const workerStatsSql = `
|
||||
SELECT
|
||||
dwr.worker_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.worker_id = w.worker_id
|
||||
WHERE ${whereClause}
|
||||
GROUP BY dwr.worker_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
|
||||
};
|
||||
Reference in New Issue
Block a user