Files
TK-FB-Project/web-ui/js/project-analysis-data.js
hyungi 7c5a985166 refactor(frontend): 프로젝트 분석 페이지 전체 리팩토링
- 600줄에 달하는 project-analysis.js 파일을 API, Data, UI, Controller 네 개의 모듈로 분리
- 복잡한 데이터 처리 로직을 data 모듈로 위임하고, UI 렌더링 코드를 ui 모듈로 분리하여 관심사 분리 원칙(SoC) 적용
- 전역 상태를 최소화하고 데이터 흐름을 명확하게 개선하여 유지보수성 및 안정성 향상
2025-07-28 14:22:36 +09:00

114 lines
4.5 KiB
JavaScript

// /js/project-analysis-data.js
/**
* 근무 형태에 따른 실제 투입 시간을 계산합니다.
* 잔업(OT)은 1.5배 가산됩니다.
* @param {string} workDetails - 근무 형태 (예: '정상', '연차', '반차')
* @param {number} overtimeHours - 잔업 시간
* @returns {number} - 실제 투입 시간
*/
function calculateActualWorkHours(workDetails, overtimeHours) {
let baseHours = 8;
switch(workDetails) {
case '연차':
case '휴무':
case '유급': baseHours = 0; break;
case '반차': baseHours = 4; break;
case '반반차': baseHours = 6; break;
case '조퇴': baseHours = 2; break;
default: baseHours = 8; // 정상근무
}
return baseHours + (overtimeHours || 0) * 1.5;
}
/**
* 원본 작업 보고서 데이터에 마스터 데이터를 매핑하고 가공합니다.
* @param {Array} rawReports - 원본 작업 보고서
* @param {{workers: Array, projects: Array, tasks: Array}} masterData - 마스터 데이터
* @returns {Array} - 가공된 데이터
*/
export function processRawData(rawReports, masterData) {
if (!rawReports || !masterData) return [];
const { workers, projects, tasks } = masterData;
const workerMap = new Map(workers.map(w => [w.worker_id, w.worker_name]));
const projectMap = new Map(projects.map(p => [p.project_id, p.project_name]));
const taskMap = new Map(tasks.map(t => [t.task_id, t.category]));
return rawReports
.map(item => ({
...item,
worker_name: workerMap.get(item.worker_id) || '알 수 없음',
project_name: projectMap.get(item.project_id) || `프로젝트 ID ${item.project_id}`,
task_category: taskMap.get(item.task_id) || `작업 ID ${item.task_id}`,
work_hours: calculateActualWorkHours(item.work_details, item.overtime_hours),
}))
// 실제 투입 시간이 있고, 완전 휴가가 아닌 유효한 데이터만 필터링
.filter(item => item.work_hours > 0 && !['연차', '휴무', '유급'].includes(item.work_details));
}
/**
* 주어진 데이터셋을 필터링합니다.
* @param {Array} data - 가공된 전체 데이터
* @param {{project: string, worker: string, task: string}} filters - 필터 조건
* @returns {Array} - 필터링된 데이터
*/
export function applyFilters(data, filters) {
return data.filter(item => {
const projectMatch = !filters.project || item.project_name === filters.project;
const workerMatch = !filters.worker || item.worker_name === filters.worker;
const taskMatch = !filters.task || item.task_category === filters.task;
return projectMatch && workerMatch && taskMatch;
});
}
/**
* 데이터를 특정 키(프로젝트, 작업자, 작업)로 집계합니다.
* @param {Array} data - 집계할 데이터
* @param {'project_name' | 'worker_name' | 'task_category'} key - 집계 기준 키
* @returns {Array} - 집계된 데이터
*/
function aggregateData(data, key) {
const aggregated = data.reduce((acc, item) => {
const group = item[key];
if (!acc[group]) {
acc[group] = {
name: group,
hours: 0,
// 참여자 또는 참여 프로젝트를 추적하기 위한 Set
participants: new Set(),
};
}
acc[group].hours += item.work_hours;
// 집계 키에 따라 다른 종류의 참여자를 추가
if (key === 'project_name') acc[group].participants.add(item.worker_name);
else if (key === 'worker_name') acc[group].participants.add(item.project_name);
else if (key === 'task_category') acc[group].participants.add(item.worker_name);
return acc;
}, {});
return Object.values(aggregated).sort((a, b) => b.hours - a.hours);
}
/**
* 필터링된 데이터를 기반으로 모든 분석 데이터를 생성합니다.
* @param {Array} filteredData - 필터링된 데이터
* @returns {object} - 요약, 프로젝트별, 작업자별, 작업별 집계 데이터
*/
export function getAnalysis(filteredData) {
const totalHours = filteredData.reduce((sum, item) => sum + (item.work_hours || 0), 0);
const summary = {
totalHours,
totalProjects: new Set(filteredData.map(item => item.project_name)).size,
totalWorkers: new Set(filteredData.map(item => item.worker_name)).size,
totalTasks: new Set(filteredData.map(item => item.task_category)).size,
};
const byProject = aggregateData(filteredData, 'project_name');
const byWorker = aggregateData(filteredData, 'worker_name');
const byTask = aggregateData(filteredData, 'task_category');
return { summary, byProject, byWorker, byTask };
}