feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
355
deploy/tkfb-package/web-ui/js/work-analysis/data-processor.js
Normal file
355
deploy/tkfb-package/web-ui/js/work-analysis/data-processor.js
Normal file
@@ -0,0 +1,355 @@
|
||||
/**
|
||||
* 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는 브라우저 환경에서 제거됨
|
||||
Reference in New Issue
Block a user