🔧 그룹 리더 대시보드 수정사항: - API 호출 방식 수정 (modern-dashboard.js) - 서버 API 요구사항에 맞는 데이터 구조 변경 - work_entries 배열 구조로 변경 - work_type_id → task_id 필드명 매핑 - 400 Bad Request 오류 해결 ⚡ 작업 분석 시스템 성능 최적화: - 중복 함수 제거 (isWeekend, isVacationProject 통합) - WorkAnalysisAPI 캐싱 시스템 구현 (5분 만료) - 네임스페이스 조직화 (utils, ui, analysis, render) - ErrorHandler 통합 에러 처리 시스템 - 성능 모니터링 및 메모리 누수 방지 - GPU 가속 CSS 애니메이션 추가 - 디바운스/스로틀 함수 적용 - 의미 없는 통계 카드 제거 📊 작업 분석 페이지 개선: - 프로그레스 바 애니메이션 - 토스트 알림 시스템 - 부드러운 전환 효과 - 반응형 최적화 - 메모리 사용량 모니터링
356 lines
12 KiB
JavaScript
356 lines
12 KiB
JavaScript
/**
|
|
* Work Analysis Data Processor Module
|
|
* 작업 분석 데이터 가공 및 변환을 담당하는 모듈
|
|
*/
|
|
|
|
class WorkAnalysisDataProcessor {
|
|
|
|
// ========== 유틸리티 함수 ==========
|
|
|
|
/**
|
|
* 주말 여부 확인
|
|
* @param {string} dateString - 날짜 문자열
|
|
* @returns {boolean} 주말 여부
|
|
*/
|
|
isWeekendDate(dateString) {
|
|
const date = new Date(dateString);
|
|
const dayOfWeek = date.getDay();
|
|
return dayOfWeek === 0 || dayOfWeek === 6; // 일요일(0) 또는 토요일(6)
|
|
}
|
|
|
|
/**
|
|
* 연차/휴무 프로젝트 여부 확인
|
|
* @param {string} projectName - 프로젝트명
|
|
* @returns {boolean} 연차/휴무 여부
|
|
*/
|
|
isVacationProject(projectName) {
|
|
if (!projectName) return false;
|
|
const vacationKeywords = ['연차', '휴무', '휴가', '병가', '특별휴가'];
|
|
return vacationKeywords.some(keyword => projectName.includes(keyword));
|
|
}
|
|
|
|
/**
|
|
* 날짜 포맷팅 (간단한 형식)
|
|
* @param {string} dateString - 날짜 문자열
|
|
* @returns {string} 포맷된 날짜
|
|
*/
|
|
formatSimpleDate(dateString) {
|
|
if (!dateString) return '';
|
|
return dateString.split('T')[0]; // 시간 부분 제거
|
|
}
|
|
|
|
// ========== 프로젝트 분포 데이터 처리 ==========
|
|
|
|
/**
|
|
* 프로젝트별 데이터 집계
|
|
* @param {Array} recentWorkData - 최근 작업 데이터
|
|
* @returns {Object} 집계된 프로젝트 데이터
|
|
*/
|
|
aggregateProjectData(recentWorkData) {
|
|
console.log('📊 프로젝트 데이터 집계 시작');
|
|
|
|
if (!recentWorkData || recentWorkData.length === 0) {
|
|
return { projects: [], totalHours: 0 };
|
|
}
|
|
|
|
const projectMap = new Map();
|
|
let vacationData = null;
|
|
|
|
recentWorkData.forEach(work => {
|
|
const isWeekend = this.isWeekendDate(work.report_date);
|
|
const isVacation = this.isVacationProject(work.project_name);
|
|
|
|
// 주말 연차는 제외
|
|
if (isWeekend && isVacation) {
|
|
console.log('🏖️ 주말 연차/휴무 제외:', work.report_date, work.project_name);
|
|
return;
|
|
}
|
|
|
|
if (isVacation) {
|
|
// 연차/휴무 통합 처리
|
|
if (!vacationData) {
|
|
vacationData = {
|
|
project_id: 'vacation',
|
|
project_name: '연차/휴무',
|
|
job_no: null,
|
|
totalHours: 0,
|
|
workTypes: new Map()
|
|
};
|
|
}
|
|
this._addWorkToProject(vacationData, work, '연차/휴무');
|
|
} else {
|
|
// 일반 프로젝트 처리
|
|
const projectKey = work.project_id || 'unknown';
|
|
|
|
if (!projectMap.has(projectKey)) {
|
|
projectMap.set(projectKey, {
|
|
project_id: projectKey,
|
|
project_name: work.project_name || `프로젝트 ${projectKey}`,
|
|
job_no: work.job_no,
|
|
totalHours: 0,
|
|
workTypes: new Map()
|
|
});
|
|
}
|
|
|
|
const project = projectMap.get(projectKey);
|
|
this._addWorkToProject(project, work);
|
|
}
|
|
});
|
|
|
|
// 결과 배열 생성
|
|
const projects = Array.from(projectMap.values());
|
|
if (vacationData && vacationData.totalHours > 0) {
|
|
projects.push(vacationData);
|
|
}
|
|
|
|
// 작업유형을 배열로 변환하고 정렬
|
|
projects.forEach(project => {
|
|
project.workTypes = Array.from(project.workTypes.values())
|
|
.sort((a, b) => b.totalHours - a.totalHours);
|
|
});
|
|
|
|
// 프로젝트를 총 시간 순으로 정렬 (연차/휴무는 맨 아래)
|
|
projects.sort((a, b) => {
|
|
if (a.project_id === 'vacation') return 1;
|
|
if (b.project_id === 'vacation') return -1;
|
|
return b.totalHours - a.totalHours;
|
|
});
|
|
|
|
const totalHours = projects.reduce((sum, p) => sum + p.totalHours, 0);
|
|
|
|
console.log('✅ 프로젝트 데이터 집계 완료:', projects.length, '개 프로젝트');
|
|
return { projects, totalHours };
|
|
}
|
|
|
|
/**
|
|
* 프로젝트에 작업 데이터 추가 (내부 헬퍼)
|
|
*/
|
|
_addWorkToProject(project, work, overrideWorkTypeName = null) {
|
|
const hours = parseFloat(work.work_hours) || 0;
|
|
project.totalHours += hours;
|
|
|
|
const workTypeKey = work.work_type_id || 'unknown';
|
|
const workTypeName = overrideWorkTypeName || work.work_type_name || `작업유형 ${workTypeKey}`;
|
|
|
|
if (!project.workTypes.has(workTypeKey)) {
|
|
project.workTypes.set(workTypeKey, {
|
|
work_type_id: workTypeKey,
|
|
work_type_name: workTypeName,
|
|
totalHours: 0
|
|
});
|
|
}
|
|
|
|
project.workTypes.get(workTypeKey).totalHours += hours;
|
|
}
|
|
|
|
// ========== 오류 분석 데이터 처리 ==========
|
|
|
|
/**
|
|
* 작업 형태별 오류 데이터 집계
|
|
* @param {Array} recentWorkData - 최근 작업 데이터
|
|
* @returns {Array} 집계된 오류 데이터
|
|
*/
|
|
aggregateErrorData(recentWorkData) {
|
|
console.log('📊 오류 분석 데이터 집계 시작');
|
|
|
|
if (!recentWorkData || recentWorkData.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const workTypeMap = new Map();
|
|
let vacationData = null;
|
|
|
|
recentWorkData.forEach(work => {
|
|
const isWeekend = this.isWeekendDate(work.report_date);
|
|
const isVacation = this.isVacationProject(work.project_name);
|
|
|
|
// 주말 연차는 완전히 제외
|
|
if (isWeekend && isVacation) {
|
|
console.log('🏖️ 주말 연차/휴무 제외:', work.report_date, work.project_name);
|
|
return;
|
|
}
|
|
|
|
if (isVacation) {
|
|
// 모든 연차/휴무를 하나로 통합
|
|
if (!vacationData) {
|
|
vacationData = {
|
|
project_id: 'vacation',
|
|
project_name: '연차/휴무',
|
|
job_no: null,
|
|
work_type_id: 'vacation',
|
|
work_type_name: '연차/휴무',
|
|
regularHours: 0,
|
|
errorHours: 0,
|
|
errorDetails: new Map(),
|
|
isVacation: true
|
|
};
|
|
}
|
|
|
|
this._addWorkToErrorData(vacationData, work);
|
|
} else {
|
|
// 일반 프로젝트 처리
|
|
const workTypeKey = work.work_type_id || 'unknown';
|
|
const combinedKey = `${work.project_id || 'unknown'}_${workTypeKey}`;
|
|
|
|
if (!workTypeMap.has(combinedKey)) {
|
|
workTypeMap.set(combinedKey, {
|
|
project_id: work.project_id,
|
|
project_name: work.project_name || `프로젝트 ${work.project_id}`,
|
|
job_no: work.job_no,
|
|
work_type_id: workTypeKey,
|
|
work_type_name: work.work_type_name || `작업유형 ${workTypeKey}`,
|
|
regularHours: 0,
|
|
errorHours: 0,
|
|
errorDetails: new Map(),
|
|
isVacation: false
|
|
});
|
|
}
|
|
|
|
const workTypeData = workTypeMap.get(combinedKey);
|
|
this._addWorkToErrorData(workTypeData, work);
|
|
}
|
|
});
|
|
|
|
// 결과 배열 생성
|
|
const result = Array.from(workTypeMap.values());
|
|
|
|
// 연차/휴무 데이터가 있으면 추가
|
|
if (vacationData && (vacationData.regularHours > 0 || vacationData.errorHours > 0)) {
|
|
result.push(vacationData);
|
|
}
|
|
|
|
// 최종 데이터 처리
|
|
const processedResult = result.map(wt => ({
|
|
...wt,
|
|
totalHours: wt.regularHours + wt.errorHours,
|
|
errorRate: wt.regularHours + wt.errorHours > 0 ?
|
|
((wt.errorHours / (wt.regularHours + wt.errorHours)) * 100).toFixed(1) : '0.0',
|
|
errorDetails: Array.from(wt.errorDetails.entries()).map(([type, hours]) => ({
|
|
type, hours
|
|
}))
|
|
})).filter(wt => wt.totalHours > 0) // 시간이 있는 것만 표시
|
|
.sort((a, b) => {
|
|
// 연차/휴무를 맨 아래로
|
|
if (a.isVacation && !b.isVacation) return 1;
|
|
if (!a.isVacation && b.isVacation) return -1;
|
|
|
|
// 같은 프로젝트 내에서는 오류 시간 순으로 정렬
|
|
if (a.project_id === b.project_id) {
|
|
return b.errorHours - a.errorHours;
|
|
}
|
|
|
|
// 다른 프로젝트는 프로젝트명 순으로 정렬
|
|
return (a.project_name || '').localeCompare(b.project_name || '');
|
|
});
|
|
|
|
console.log('✅ 오류 분석 데이터 집계 완료:', processedResult.length, '개 항목');
|
|
return processedResult;
|
|
}
|
|
|
|
/**
|
|
* 작업 데이터를 오류 분석 데이터에 추가 (내부 헬퍼)
|
|
*/
|
|
_addWorkToErrorData(workTypeData, work) {
|
|
const hours = parseFloat(work.work_hours) || 0;
|
|
|
|
if (work.work_status === 'error' || work.error_type_id) {
|
|
workTypeData.errorHours += hours;
|
|
|
|
// 오류 유형별 세분화
|
|
const errorTypeName = work.error_type_name || work.error_description || '설계미스';
|
|
if (!workTypeData.errorDetails.has(errorTypeName)) {
|
|
workTypeData.errorDetails.set(errorTypeName, 0);
|
|
}
|
|
workTypeData.errorDetails.set(errorTypeName,
|
|
workTypeData.errorDetails.get(errorTypeName) + hours
|
|
);
|
|
} else {
|
|
workTypeData.regularHours += hours;
|
|
}
|
|
}
|
|
|
|
// ========== 차트 데이터 처리 ==========
|
|
|
|
/**
|
|
* 시계열 차트 데이터 변환
|
|
* @param {Array} dailyData - 일별 데이터
|
|
* @returns {Object} 차트 데이터
|
|
*/
|
|
processTimeSeriesData(dailyData) {
|
|
if (!dailyData || dailyData.length === 0) {
|
|
return { labels: [], datasets: [] };
|
|
}
|
|
|
|
const labels = dailyData.map(item => this.formatSimpleDate(item.date));
|
|
const hours = dailyData.map(item => item.total_hours || 0);
|
|
const workers = dailyData.map(item => item.worker_count || 0);
|
|
|
|
return {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: '총 작업시간',
|
|
data: hours,
|
|
borderColor: '#3b82f6',
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
tension: 0.4
|
|
},
|
|
{
|
|
label: '참여 작업자 수',
|
|
data: workers,
|
|
borderColor: '#10b981',
|
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
tension: 0.4,
|
|
yAxisID: 'y1'
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 도넛 차트 데이터 변환
|
|
* @param {Array} projectData - 프로젝트 데이터
|
|
* @returns {Object} 차트 데이터
|
|
*/
|
|
processDonutChartData(projectData) {
|
|
if (!projectData || projectData.length === 0) {
|
|
return { labels: [], datasets: [] };
|
|
}
|
|
|
|
const labels = projectData.map(item => item.project_name || item.name);
|
|
const data = projectData.map(item => item.total_hours || item.hours || 0);
|
|
const colors = this._generateColors(data.length);
|
|
|
|
return {
|
|
labels,
|
|
datasets: [{
|
|
data,
|
|
backgroundColor: colors,
|
|
borderWidth: 2,
|
|
borderColor: '#ffffff'
|
|
}]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 색상 생성 헬퍼
|
|
*/
|
|
_generateColors(count) {
|
|
const baseColors = [
|
|
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
|
|
'#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1'
|
|
];
|
|
|
|
const colors = [];
|
|
for (let i = 0; i < count; i++) {
|
|
colors.push(baseColors[i % baseColors.length]);
|
|
}
|
|
return colors;
|
|
}
|
|
}
|
|
|
|
// 전역 인스턴스 생성
|
|
window.WorkAnalysisDataProcessor = new WorkAnalysisDataProcessor();
|
|
|
|
// Export는 브라우저 환경에서 제거됨
|