Files
TK-FB-Project/api.hyungi.net/models/WorkAnalysis.js
hyungi 5ff0c7cd60 feat: 모델 및 데이터베이스 구조 개선
- 모델 파일들의 데이터베이스 연결 및 쿼리 최적화
- 마이그레이션 파일 정리 및 통합
- Docker 설정 파일 추가
- 프로젝트 시작/중지 스크립트 추가
- README 및 문서 업데이트
2025-10-27 10:01:12 +09:00

430 lines
18 KiB
JavaScript

// models/WorkAnalysis.js - 향상된 버전
class WorkAnalysis {
constructor(db) {
this.db = db;
}
// 기본 통계 조회
async getBasicStats(startDate, endDate) {
const query = `
SELECT
COALESCE(SUM(work_hours), 0) as total_hours,
COUNT(*) as total_reports,
COUNT(DISTINCT project_id) as active_projects,
COUNT(DISTINCT worker_id) as active_workers,
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_reports,
ROUND(AVG(work_hours), 2) as avg_hours_per_report
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
const stats = results[0];
const errorRate = stats.total_reports > 0
? (stats.error_reports / stats.total_reports) * 100
: 0;
return {
totalHours: parseFloat(stats.total_hours) || 0,
totalReports: parseInt(stats.total_reports) || 0,
activeProjects: parseInt(stats.active_projects) || 0,
activeworkers: parseInt(stats.active_workers) || 0,
errorRate: parseFloat(errorRate.toFixed(2)) || 0,
avgHoursPerReport: parseFloat(stats.avg_hours_per_report) || 0
};
} catch (error) {
throw new Error(`기본 통계 조회 실패: ${error.message}`);
}
}
// 일별 작업시간 추이
async getDailyTrend(startDate, endDate) {
const query = `
SELECT
report_date as date,
SUM(work_hours) as hours,
COUNT(*) as reports,
COUNT(DISTINCT worker_id) as workers,
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as errors
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
GROUP BY report_date
ORDER BY report_date
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({
date: row.date,
hours: parseFloat(row.hours) || 0,
reports: parseInt(row.reports) || 0,
workers: parseInt(row.workers) || 0,
errors: parseInt(row.errors) || 0
}));
} catch (error) {
throw new Error(`일별 추이 조회 실패: ${error.message}`);
}
}
// 작업자별 통계
async getWorkerStats(startDate, endDate) {
const query = `
SELECT
dwr.worker_id,
w.worker_name,
SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports,
ROUND(AVG(dwr.work_hours), 2) as avgHours,
COUNT(DISTINCT dwr.project_id) as projectCount,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
COUNT(DISTINCT dwr.report_date) as workingDays
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.worker_id, w.worker_name
ORDER BY totalHours DESC
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({
worker_id: row.worker_id,
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
totalHours: parseFloat(row.totalHours) || 0,
totalReports: parseInt(row.totalReports) || 0,
avgHours: parseFloat(row.avgHours) || 0,
projectCount: parseInt(row.projectCount) || 0,
errorCount: parseInt(row.errorCount) || 0,
workingDays: parseInt(row.workingDays) || 0,
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
}));
} catch (error) {
throw new Error(`작업자별 통계 조회 실패: ${error.message}`);
}
}
// 프로젝트별 통계
async getProjectStats(startDate, endDate) {
const query = `
SELECT
dwr.project_id,
p.project_name,
SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports,
COUNT(DISTINCT dwr.worker_id) as workerCount,
ROUND(AVG(dwr.work_hours), 2) as avgHours,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
COUNT(DISTINCT dwr.report_date) as activeDays
FROM daily_work_reports dwr
LEFT JOIN Projects p ON dwr.project_id = p.project_id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.project_id, p.project_name
ORDER BY totalHours DESC
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({
project_id: row.project_id,
project_name: row.project_name || `프로젝트 ${row.project_id}`,
totalHours: parseFloat(row.totalHours) || 0,
totalReports: parseInt(row.totalReports) || 0,
workerCount: parseInt(row.workerCount) || 0,
avgHours: parseFloat(row.avgHours) || 0,
errorCount: parseInt(row.errorCount) || 0,
activeDays: parseInt(row.activeDays) || 0,
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
}));
} catch (error) {
throw new Error(`프로젝트별 통계 조회 실패: ${error.message}`);
}
}
// 작업유형별 통계
async getWorkTypeStats(startDate, endDate) {
const query = `
SELECT
dwr.work_type_id,
wt.name as work_type_name,
SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports,
ROUND(AVG(dwr.work_hours), 2) as avgHours,
COUNT(DISTINCT dwr.worker_id) as workerCount,
SUM(CASE WHEN dwr.work_status_id = 2 THEN 1 ELSE 0 END) as errorCount,
COUNT(DISTINCT dwr.project_id) as projectCount
FROM daily_work_reports dwr
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.work_type_id, wt.name
ORDER BY totalHours DESC
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({
work_type_id: row.work_type_id,
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
totalHours: parseFloat(row.totalHours) || 0,
totalReports: parseInt(row.totalReports) || 0,
avgHours: parseFloat(row.avgHours) || 0,
workerCount: parseInt(row.workerCount) || 0,
errorCount: parseInt(row.errorCount) || 0,
projectCount: parseInt(row.projectCount) || 0,
errorRate: row.totalReports > 0 ? parseFloat(((row.errorCount / row.totalReports) * 100).toFixed(2)) : 0
}));
} catch (error) {
throw new Error(`작업유형별 통계 조회 실패: ${error.message}`);
}
}
// 최근 작업 현황
async getRecentWork(startDate, endDate, limit = 50) {
const query = `
SELECT
dwr.id,
dwr.report_date,
dwr.worker_id,
w.worker_name,
dwr.project_id,
p.project_name,
dwr.work_type_id,
wt.name as work_type_name,
dwr.work_status_id,
wst.name as work_status_name,
dwr.error_type_id,
et.name as error_type_name,
dwr.work_hours,
dwr.created_by,
u.name as created_by_name,
dwr.created_at
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
LEFT JOIN Projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
LEFT JOIN error_types et ON dwr.error_type_id = et.id
LEFT JOIN Users u ON dwr.created_by = u.user_id
WHERE dwr.report_date BETWEEN ? AND ?
ORDER BY dwr.created_at DESC
LIMIT ?
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate, parseInt(limit)]);
return results.map(row => ({
id: row.id,
report_date: row.report_date,
worker_id: row.worker_id,
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
project_id: row.project_id,
project_name: row.project_name || `프로젝트 ${row.project_id}`,
work_type_id: row.work_type_id,
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
work_status_id: row.work_status_id,
work_status_name: row.work_status_name || '정상',
error_type_id: row.error_type_id,
error_type_name: row.error_type_name || null,
work_hours: parseFloat(row.work_hours) || 0,
created_by: row.created_by,
created_by_name: row.created_by_name || '미지정',
created_at: row.created_at
}));
} catch (error) {
throw new Error(`최근 작업 현황 조회 실패: ${error.message}`);
}
}
// 요일별 패턴 분석
async getWeekdayPattern(startDate, endDate) {
const query = `
SELECT
DAYOFWEEK(report_date) as day_of_week,
CASE DAYOFWEEK(report_date)
WHEN 1 THEN '일요일'
WHEN 2 THEN '월요일'
WHEN 3 THEN '화요일'
WHEN 4 THEN '수요일'
WHEN 5 THEN '목요일'
WHEN 6 THEN '금요일'
WHEN 7 THEN '토요일'
END as day_name,
SUM(work_hours) as total_hours,
COUNT(*) as total_reports,
ROUND(AVG(work_hours), 2) as avg_hours,
COUNT(DISTINCT worker_id) as active_workers
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
GROUP BY DAYOFWEEK(report_date)
ORDER BY day_of_week
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({
dayOfWeek: row.day_of_week,
dayName: row.day_name,
totalHours: parseFloat(row.total_hours) || 0,
totalReports: parseInt(row.total_reports) || 0,
avgHours: parseFloat(row.avg_hours) || 0,
activeworkers: parseInt(row.active_workers) || 0
}));
} catch (error) {
throw new Error(`요일별 패턴 분석 실패: ${error.message}`);
}
}
// 에러 분석
async getErrorAnalysis(startDate, endDate) {
const query = `
SELECT
dwr.error_type_id,
et.name as error_type_name,
COUNT(*) as error_count,
SUM(dwr.work_hours) as total_hours,
COUNT(DISTINCT dwr.worker_id) as affected_workers,
COUNT(DISTINCT dwr.project_id) as affected_projects
FROM daily_work_reports dwr
LEFT JOIN error_types et ON dwr.error_type_id = et.id
WHERE dwr.report_date BETWEEN ? AND ?
AND dwr.work_status_id = 2
GROUP BY dwr.error_type_id, et.name
ORDER BY error_count DESC
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate]);
return results.map(row => ({
error_type_id: row.error_type_id,
error_type_name: row.error_type_name || `에러유형 ${row.error_type_id}`,
errorCount: parseInt(row.error_count) || 0,
totalHours: parseFloat(row.total_hours) || 0,
affectedworkers: parseInt(row.affected_workers) || 0,
affectedProjects: parseInt(row.affected_projects) || 0
}));
} catch (error) {
throw new Error(`에러 분석 실패: ${error.message}`);
}
}
// 월별 비교 분석
async getMonthlyComparison(year) {
const query = `
SELECT
MONTH(report_date) as month,
MONTHNAME(report_date) as month_name,
SUM(work_hours) as total_hours,
COUNT(*) as total_reports,
COUNT(DISTINCT worker_id) as active_workers,
COUNT(DISTINCT project_id) as active_projects,
SUM(CASE WHEN work_status_id = 2 THEN 1 ELSE 0 END) as error_count
FROM daily_work_reports
WHERE YEAR(report_date) = ?
GROUP BY MONTH(report_date), MONTHNAME(report_date)
ORDER BY month
`;
try {
const [results] = await this.db.execute(query, [year]);
return results.map(row => ({
month: row.month,
monthName: row.month_name,
totalHours: parseFloat(row.total_hours) || 0,
totalReports: parseInt(row.total_reports) || 0,
activeworkers: parseInt(row.active_workers) || 0,
activeProjects: parseInt(row.active_projects) || 0,
errorCount: parseInt(row.error_count) || 0,
errorRate: row.total_reports > 0 ? parseFloat(((row.error_count / row.total_reports) * 100).toFixed(2)) : 0
}));
} catch (error) {
throw new Error(`월별 비교 분석 실패: ${error.message}`);
}
}
// 작업자별 전문분야 분석
async getWorkerSpecialization(startDate, endDate) {
const query = `
SELECT
dwr.worker_id,
w.worker_name,
dwr.work_type_id,
wt.name as work_type_name,
dwr.project_id,
p.project_name,
SUM(dwr.work_hours) as totalHours,
COUNT(*) as totalReports,
ROUND((SUM(dwr.work_hours) / (
SELECT SUM(work_hours)
FROM daily_work_reports
WHERE worker_id = dwr.worker_id
AND report_date BETWEEN ? AND ?
)) * 100, 2) as percentage
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN Projects p ON dwr.project_id = p.project_id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY dwr.worker_id, w.worker_name, dwr.work_type_id, wt.name, dwr.project_id, p.project_name
HAVING totalHours > 0
ORDER BY dwr.worker_id, totalHours DESC
`;
try {
const [results] = await this.db.execute(query, [startDate, endDate, startDate, endDate]);
return results.map(row => ({
worker_id: row.worker_id,
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
work_type_id: row.work_type_id,
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
project_id: row.project_id,
project_name: row.project_name || `프로젝트 ${row.project_id}`,
totalHours: parseFloat(row.totalHours) || 0,
totalReports: parseInt(row.totalReports) || 0,
percentage: parseFloat(row.percentage) || 0
}));
} catch (error) {
throw new Error(`작업자별 전문분야 분석 실패: ${error.message}`);
}
}
// 대시보드용 종합 데이터
async getDashboardData(startDate, endDate) {
try {
// 병렬로 모든 데이터 조회
const [
stats,
dailyTrend,
workerStats,
projectStats,
workTypeStats,
recentWork
] = await Promise.all([
this.getBasicStats(startDate, endDate),
this.getDailyTrend(startDate, endDate),
this.getWorkerStats(startDate, endDate),
this.getProjectStats(startDate, endDate),
this.getWorkTypeStats(startDate, endDate),
this.getRecentWork(startDate, endDate, 20)
]);
return {
stats,
dailyTrend,
workerStats,
projectStats,
workTypeStats,
recentWork,
metadata: {
period: `${startDate} ~ ${endDate}`,
timestamp: new Date().toISOString()
}
};
} catch (error) {
throw new Error(`대시보드 데이터 조회 실패: ${error.message}`);
}
}
}
module.exports = WorkAnalysis;