From a1b7d3c5dfc59e60e8d5a9fc62d96b0e5882de30 Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 28 Jul 2025 15:21:19 +0900 Subject: [PATCH] =?UTF-8?q?perf(frontend):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=84=EC=84=9D=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드 집계 API(/api/analysis)를 연동하여 프론트엔드 데이터 처리 로직 제거 - 불필요해진 project-analysis-data.js 파일 삭제 - 페이지 로딩 및 데이터 분석 속도를 획기적으로 개선 --- web-ui/js/project-analysis-api.js | 25 +++---- web-ui/js/project-analysis-data.js | 114 ----------------------------- web-ui/js/project-analysis-ui.js | 56 +++++++------- web-ui/js/project-analysis.js | 48 ++++-------- 4 files changed, 49 insertions(+), 194 deletions(-) delete mode 100644 web-ui/js/project-analysis-data.js diff --git a/web-ui/js/project-analysis-api.js b/web-ui/js/project-analysis-api.js index 239f8d5..2518597 100644 --- a/web-ui/js/project-analysis-api.js +++ b/web-ui/js/project-analysis-api.js @@ -3,6 +3,7 @@ import { apiGet } from './api-helper.js'; /** * 분석 페이지에 필요한 모든 초기 데이터(마스터 데이터)를 병렬로 가져옵니다. + * 이 데이터는 필터 옵션을 채우는 데 사용됩니다. * @returns {Promise<{workers: Array, projects: Array, tasks: Array}>} */ export async function getMasterData() { @@ -15,30 +16,22 @@ export async function getMasterData() { return { workers, projects, tasks }; } catch (error) { console.error('마스터 데이터 로딩 실패:', error); - // 하나라도 실패하면 페이지 기능에 문제가 생길 수 있으므로 에러를 던집니다. - throw new Error('페이지 초기화에 필요한 데이터를 불러오는 데 실패했습니다.'); + throw new Error('필터링에 필요한 데이터를 불러오는 데 실패했습니다.'); } } /** - * 지정된 기간의 작업 보고서 데이터를 가져옵니다. - * 백엔드에 집계 API가 있다면 그쪽을 사용하는 것이 더 효율적입니다. - * (예: /api/analysis/reports?startDate=...&endDate=...) - * 현재는 기존 방식을 유지합니다. + * 지정된 기간의 모든 분석 데이터를 백엔드에서 직접 가져옵니다. * @param {string} startDate - 시작일 (YYYY-MM-DD) * @param {string} endDate - 종료일 (YYYY-MM-DD) - * @returns {Promise} + * @returns {Promise} - 요약, 집계, 상세 데이터가 모두 포함된 분석 결과 객체 */ -export async function getWorkReports(startDate, endDate) { +export async function getAnalysisReport(startDate, endDate) { try { - // API 엔드포인트를 명확하게 수정합니다. - // 기존: /workreports?start=... - // 변경: /daily-work-reports/search?startDate=...&endDate=... (가정) - // 우선 기존 URL 구조를 최대한 따르되, 좀 더 명시적인 경로로 변경 제안 - const reports = await apiGet(`/workreports/search?startDate=${startDate}&endDate=${endDate}`); - return reports; + const analysisData = await apiGet(`/analysis?startDate=${startDate}&endDate=${endDate}`); + return analysisData; } catch (error) { - console.error('작업 보고서 데이터 로딩 실패:', error); - throw new Error(`작업 보고서 데이터를 불러오는 데 실패했습니다: ${error.message}`); + console.error('분석 보고서 데이터 로딩 실패:', error); + throw new Error(`분석 데이터를 불러오는 데 실패했습니다: ${error.message}`); } } \ No newline at end of file diff --git a/web-ui/js/project-analysis-data.js b/web-ui/js/project-analysis-data.js deleted file mode 100644 index 7df5838..0000000 --- a/web-ui/js/project-analysis-data.js +++ /dev/null @@ -1,114 +0,0 @@ -// /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 }; -} \ No newline at end of file diff --git a/web-ui/js/project-analysis-ui.js b/web-ui/js/project-analysis-ui.js index f03efe1..75c49e6 100644 --- a/web-ui/js/project-analysis-ui.js +++ b/web-ui/js/project-analysis-ui.js @@ -68,20 +68,20 @@ export function setUIState(state) { /** - * 필터링된 데이터에서 고유한 값을 추출하여 필터 옵션을 채웁니다. - * @param {Array} data - 가공된 전체 데이터 + * 마스터 데이터를 기반으로 필터 옵션을 채웁니다. + * @param {{workers: Array, projects: Array, tasks: Array}} masterData - 마스터 데이터 */ -export function updateFilterOptions(data) { - const createOptions = (items) => { +export function updateFilterOptions(masterData) { + const createOptions = (items, key, value) => { let html = ''; - [...new Set(items)].sort().forEach(item => { - html += ``; + items.forEach(item => { + html += ``; }); return html; }; - DOM.projectFilter.innerHTML = createOptions(data.map(d => d.project_name)); - DOM.workerFilter.innerHTML = createOptions(data.map(d => d.worker_name)); - DOM.taskFilter.innerHTML = createOptions(data.map(d => d.task_category)); + DOM.projectFilter.innerHTML = createOptions(masterData.projects, 'project_id', 'project_name'); + DOM.workerFilter.innerHTML = createOptions(masterData.workers, 'worker_id', 'worker_name'); + DOM.taskFilter.innerHTML = createOptions(masterData.tasks, 'task_id', 'category'); } /** @@ -90,10 +90,10 @@ export function updateFilterOptions(data) { */ export function renderSummary(summary) { DOM.summaryCards.innerHTML = ` -

총 투입 시간

${summary.totalHours.toFixed(1)}h
-

참여 프로젝트

${summary.totalProjects}개
-

참여 인원

${summary.totalWorkers}명
-

작업 분류

${summary.totalTasks}개
+

총 투입 시간

${(summary.totalHours || 0).toFixed(1)}h
+

참여 프로젝트

${summary.totalProjects || 0}개
+

참여 인원

${summary.totalWorkers || 0}명
+

작업 분류

${summary.totalTasks || 0}개
`; } @@ -104,12 +104,11 @@ export function renderSummary(summary) { * @param {function} rowRenderer - 각 행을 렌더링하는 함수 */ function renderTable(tableBodyEl, data, rowRenderer) { - if (data.length === 0) { + if (!data || data.length === 0) { tableBodyEl.innerHTML = '데이터가 없습니다'; return; } - const totalHours = data.reduce((sum, item) => sum + item.hours, 0); - tableBodyEl.innerHTML = data.map((item, index) => rowRenderer(item, index, totalHours)).join(''); + tableBodyEl.innerHTML = data.map(rowRenderer).join(''); } /** @@ -117,17 +116,17 @@ function renderTable(tableBodyEl, data, rowRenderer) { * @param {object} analysis - 프로젝트/작업자/작업별 집계 데이터 */ export function renderAnalysisTables(analysis) { - renderTable(DOM.projectTableBody, analysis.byProject, (p, i, total) => ` - ${i + 1}${p.name}${p.hours.toFixed(1)}h - ${(p.hours / total * 100).toFixed(1)}%${p.participants.size}명`); + renderTable(DOM.projectTableBody, analysis.byProject, (p, i) => ` + ${i + 1}${p.name}${p.hours}h + ${p.percentage}%${p.participants}명`); - renderTable(DOM.workerTableBody, analysis.byWorker, (w, i, total) => ` - ${i + 1}${w.name}${w.hours.toFixed(1)}h - ${(w.hours / total * 100).toFixed(1)}%${w.participants.size}개`); + renderTable(DOM.workerTableBody, analysis.byWorker, (w, i) => ` + ${i + 1}${w.name}${w.hours}h + ${w.percentage}%${w.participants}개`); - renderTable(DOM.taskTableBody, analysis.byTask, (t, i, total) => ` - ${i + 1}${t.name}${t.hours.toFixed(1)}h - ${(t.hours / total * 100).toFixed(1)}%${t.participants.size}명`); + renderTable(DOM.taskTableBody, analysis.byTask, (t, i) => ` + ${i + 1}${t.name}${t.hours}h + ${t.percentage}%${t.participants}명`); } /** @@ -135,17 +134,16 @@ export function renderAnalysisTables(analysis) { * @param {Array} detailData - 필터링된 상세 데이터 */ export function renderDetailTable(detailData) { - if (detailData.length === 0) { + if (!detailData || detailData.length === 0) { DOM.detailTableBody.innerHTML = '데이터가 없습니다'; return; } - const sorted = [...detailData].sort((a, b) => new Date(b.date) - new Date(a.date)); - DOM.detailTableBody.innerHTML = sorted.map((item, index) => ` + DOM.detailTableBody.innerHTML = detailData.map((item, index) => ` ${index + 1}${formatDate(new Date(item.date))} ${item.project_name} ${item.worker_name}${item.task_category} ${item.work_details || '정상근무'} - ${(item.work_hours || 0).toFixed(1)}h + ${item.work_hours}h ${(item.memo || '-').substring(0, 20)}` ).join(''); } diff --git a/web-ui/js/project-analysis.js b/web-ui/js/project-analysis.js index 66c0b4a..fcad431 100644 --- a/web-ui/js/project-analysis.js +++ b/web-ui/js/project-analysis.js @@ -1,6 +1,5 @@ // /js/project-analysis.js -import { getMasterData, getWorkReports } from './project-analysis-api.js'; -import { processRawData, applyFilters, getAnalysis } from './project-analysis-data.js'; +import { getMasterData, getAnalysisReport } from './project-analysis-api.js'; import { setDefaultDates, setUIState, @@ -9,16 +8,8 @@ import { renderAnalysisTables, renderDetailTable, switchTab, - getCurrentFilters, } from './project-analysis-ui.js'; -// 애플리케이션 상태 (전역 변수 최소화) -const state = { - masterData: null, - processedData: [], - filteredData: [], -}; - // DOM 요소 참조 (이벤트 리스너 설정용) const DOM = { startDate: document.getElementById('startDate'), @@ -26,7 +17,8 @@ const DOM = { analyzeBtn: document.getElementById('analyzeBtn'), quickMonthBtn: document.getElementById('quickMonth'), quickLastMonthBtn: document.getElementById('quickLastMonth'), - applyFilterBtn: document.getElementById('applyFilter'), + // 필터 버튼은 현재 아무 기능도 하지 않으므로 주석 처리 또는 제거 가능 + // applyFilterBtn: document.getElementById('applyFilter'), tabButtons: document.querySelectorAll('.tab-button'), }; @@ -44,17 +36,16 @@ async function handleAnalysis() { setUIState('loading'); try { - const rawReports = await getWorkReports(startDate, endDate); - state.processedData = processRawData(rawReports, state.masterData); + const analysisResult = await getAnalysisReport(startDate, endDate); - if (state.processedData.length === 0) { + if (!analysisResult.summary.totalHours) { setUIState('no-data'); - updateFilterOptions([]); return; } - updateFilterOptions(state.processedData); - handleFilterChange(); // 필터 적용 및 렌더링 + renderSummary(analysisResult.summary); + renderAnalysisTables(analysisResult); + renderDetailTable(analysisResult.details); setUIState('data'); } catch (error) { @@ -64,23 +55,8 @@ async function handleAnalysis() { } } -/** - * 필터 적용 버튼 클릭 또는 분석 후 자동 실행되는 핸들러 - */ -function handleFilterChange() { - const filters = getCurrentFilters(); - state.filteredData = applyFilters(state.processedData, filters); - - const analysisResult = getAnalysis(state.filteredData); - - renderSummary(analysisResult.summary); - renderAnalysisTables(analysisResult); - renderDetailTable(state.filteredData); -} - /** * 빠른 날짜 설정 버튼 핸들러 - * @param {'this' | 'last'} monthType - 이번 달 또는 지난 달 */ function handleQuickDate(monthType) { const now = new Date(); @@ -98,13 +74,15 @@ function handleQuickDate(monthType) { */ function setupEventListeners() { DOM.analyzeBtn.addEventListener('click', handleAnalysis); - DOM.applyFilterBtn.addEventListener('click', handleFilterChange); DOM.quickMonthBtn.addEventListener('click', () => handleQuickDate('this')); DOM.quickLastMonthBtn.addEventListener('click', () => handleQuickDate('last')); DOM.tabButtons.forEach(btn => { btn.addEventListener('click', () => switchTab(btn.dataset.tab)); }); + + // 프론트엔드 필터링은 제거되었으므로 관련 이벤트 리스너는 주석 처리합니다. + // DOM.applyFilterBtn.addEventListener('click', ...); } /** @@ -115,8 +93,8 @@ async function initialize() { setupEventListeners(); try { - state.masterData = await getMasterData(); - // 페이지 로드 시 바로 분석 실행 + const masterData = await getMasterData(); + updateFilterOptions(masterData); await handleAnalysis(); } catch (error) { alert(error.message);