// 작업 분석 페이지 JavaScript // API 설정 import import './api-config.js'; // 전역 변수 let currentMode = 'period'; let currentTab = 'worker'; let analysisData = null; let projectChart = null; let errorByProjectChart = null; let errorTimelineChart = null; // 페이지 초기화 document.addEventListener('DOMContentLoaded', function() { console.log('📈 작업 분석 페이지 초기화 시작'); initializePage(); loadInitialData(); }); // 페이지 초기화 function initializePage() { // 시간 업데이트 시작 updateCurrentTime(); setInterval(updateCurrentTime, 1000); // 사용자 정보 업데이트 updateUserInfo(); // 프로필 메뉴 토글 setupProfileMenu(); // 로그아웃 버튼 setupLogoutButton(); // 기본 날짜 설정은 HTML에서 처리됨 (새로운 UI) console.log('✅ 작업 분석 페이지 초기화 완료'); } // 현재 시간 업데이트 function updateCurrentTime() { const now = new Date(); const timeString = now.toLocaleTimeString('ko-KR', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); const timeElement = document.getElementById('timeValue'); if (timeElement) { timeElement.textContent = timeString; } } // 사용자 정보 업데이트 function updateUserInfo() { let userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}'); let authUser = JSON.parse(localStorage.getItem('user') || '{}'); const finalUserInfo = { worker_name: userInfo.worker_name || authUser.username || authUser.worker_name, job_type: userInfo.job_type || authUser.role || authUser.job_type, username: authUser.username || userInfo.username }; const userNameElement = document.getElementById('userName'); const userRoleElement = document.getElementById('userRole'); const userInitialElement = document.getElementById('userInitial'); if (userNameElement) { userNameElement.textContent = finalUserInfo.worker_name || '사용자'; } if (userRoleElement) { const roleMap = { 'leader': '그룹장', 'worker': '작업자', 'admin': '관리자', 'system': '시스템 관리자' }; userRoleElement.textContent = roleMap[finalUserInfo.job_type] || finalUserInfo.job_type || '작업자'; } if (userInitialElement) { const name = finalUserInfo.worker_name || '사용자'; userInitialElement.textContent = name.charAt(0); } } // 프로필 메뉴 설정 function setupProfileMenu() { const userProfile = document.getElementById('userProfile'); const profileMenu = document.getElementById('profileMenu'); if (userProfile && profileMenu) { userProfile.addEventListener('click', function(e) { e.stopPropagation(); const isVisible = profileMenu.style.display === 'block'; profileMenu.style.display = isVisible ? 'none' : 'block'; }); // 외부 클릭 시 메뉴 닫기 document.addEventListener('click', function() { profileMenu.style.display = 'none'; }); } } // 로그아웃 버튼 설정 function setupLogoutButton() { const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) { logoutBtn.addEventListener('click', function() { if (confirm('로그아웃 하시겠습니까?')) { localStorage.removeItem('token'); localStorage.removeItem('user'); localStorage.removeItem('userInfo'); window.location.href = '/index.html'; } }); } } // 초기 데이터 로드 async function loadInitialData() { try { console.log('📊 초기 데이터 로딩 시작'); // 프로젝트 목록 로드 const projects = await apiCall('/projects/active/list', 'GET'); const projectData = Array.isArray(projects) ? projects : (projects.data || []); // 프로젝트 필터 옵션 업데이트 updateProjectFilters(projectData); console.log('✅ 초기 데이터 로딩 완료'); } catch (error) { console.error('초기 데이터 로딩 오류:', error); showToast('초기 데이터를 불러오는데 실패했습니다.', 'error'); } } // 프로젝트 필터 업데이트 function updateProjectFilters(projects) { const projectFilter = document.getElementById('projectFilter'); const projectModeSelect = document.getElementById('projectModeSelect'); if (projectFilter) { projectFilter.innerHTML = ''; projects.forEach(project => { projectFilter.innerHTML += ``; }); } if (projectModeSelect) { projectModeSelect.innerHTML = ''; projects.forEach(project => { projectModeSelect.innerHTML += ``; }); } } // 분석 모드 전환 function switchAnalysisMode(mode) { currentMode = mode; // 탭 버튼 활성화 상태 변경 document.querySelectorAll('.mode-tab').forEach(tab => { tab.classList.remove('active'); }); document.querySelector(`[data-mode="${mode}"]`).classList.add('active'); // 모드 콘텐츠 표시/숨김 document.querySelectorAll('.analysis-mode').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${mode}-mode`).classList.add('active'); console.log(`🔄 분석 모드 전환: ${mode}`); } // 분석 탭 전환 function switchAnalysisTab(tab) { currentTab = tab; // 탭 버튼 활성화 상태 변경 document.querySelectorAll('.analysis-tab').forEach(tabBtn => { tabBtn.classList.remove('active'); }); document.querySelector(`[data-tab="${tab}"]`).classList.add('active'); // 탭 콘텐츠 표시/숨김 document.querySelectorAll('.analysis-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`${tab}-analysis`).classList.add('active'); console.log(`🔄 분석 탭 전환: ${tab}`); } // 기간별 분석 로드 async function loadPeriodAnalysis() { const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; const projectId = document.getElementById('projectFilter').value; if (!startDate || !endDate) { showToast('시작일과 종료일을 모두 선택해주세요.', 'error'); return; } if (new Date(startDate) > new Date(endDate)) { showToast('시작일이 종료일보다 늦을 수 없습니다.', 'error'); return; } showLoading(true); try { console.log('📊 기간별 분석 데이터 로딩 시작'); // API 호출 파라미터 구성 const params = new URLSearchParams({ start: startDate, end: endDate }); if (projectId) { params.append('project_id', projectId); } // 여러 API를 병렬로 호출하여 종합 분석 데이터 구성 console.log('📡 API 파라미터:', params.toString()); const [statsRes, workerStatsRes, projectStatsRes, errorAnalysisRes] = await Promise.all([ apiCall(`/work-analysis/stats?${params}`, 'GET').catch(err => { console.error('❌ stats API 오류:', err); return { data: null }; }), apiCall(`/work-analysis/worker-stats?${params}`, 'GET').catch(err => { console.error('❌ worker-stats API 오류:', err); return { data: [] }; }), apiCall(`/work-analysis/project-stats?${params}`, 'GET').catch(err => { console.error('❌ project-stats API 오류:', err); return { data: [] }; }), apiCall(`/work-analysis/error-analysis?${params}`, 'GET').catch(err => { console.error('❌ error-analysis API 오류:', err); return { data: {} }; }) ]); console.log('📊 개별 API 응답:'); console.log(' - stats:', statsRes); console.log(' - worker-stats:', workerStatsRes); console.log(' - project-stats:', projectStatsRes); console.log(' - error-analysis:', errorAnalysisRes); // 종합 분석 데이터 구성 analysisData = { summary: statsRes.data || statsRes, workerStats: workerStatsRes.data || workerStatsRes, projectStats: projectStatsRes.data || projectStatsRes, errorStats: errorAnalysisRes.data || errorAnalysisRes }; console.log('📊 분석 데이터:', analysisData); console.log('📊 요약 통계:', analysisData.summary); console.log('👥 작업자 통계:', analysisData.workerStats); console.log('📁 프로젝트 통계:', analysisData.projectStats); console.log('⚠️ 오류 통계:', analysisData.errorStats); // 결과 표시 displayPeriodAnalysis(analysisData); // 결과 섹션 표시 document.getElementById('periodResults').style.display = 'block'; showToast('분석이 완료되었습니다.', 'success'); } catch (error) { console.error('기간별 분석 오류:', error); showToast('분석 중 오류가 발생했습니다.', 'error'); } finally { showLoading(false); } } // 기간별 분석 결과 표시 function displayPeriodAnalysis(data) { // 요약 통계 업데이트 updateSummaryStats(data.summary || {}); // 작업자별 분석 표시 displayWorkerAnalysis(data.workerStats || []); // 프로젝트별 분석 표시 displayProjectAnalysis(data.projectStats || []); // 오류 분석 표시 (전체 분석 데이터도 함께 전달) displayErrorAnalysis(data.errorStats || {}, data); } // 요약 통계 업데이트 function updateSummaryStats(summary) { // API 응답 구조에 맞게 필드명 조정 document.getElementById('totalHours').textContent = `${summary.totalHours || summary.total_hours || 0}h`; document.getElementById('totalWorkers').textContent = `${summary.activeworkers || summary.activeWorkers || summary.total_workers || 0}명`; document.getElementById('totalProjects').textContent = `${summary.activeProjects || summary.active_projects || summary.total_projects || 0}개`; document.getElementById('errorRate').textContent = `${summary.errorRate || summary.error_rate || 0}%`; } // 작업자별 분석 표시 function displayWorkerAnalysis(workerStats) { const grid = document.getElementById('workerAnalysisGrid'); console.log('👥 작업자 분석 데이터 확인:', workerStats); console.log('👥 데이터 타입:', typeof workerStats); console.log('👥 배열 여부:', Array.isArray(workerStats)); console.log('👥 길이:', workerStats ? workerStats.length : 'undefined'); if (!workerStats || (Array.isArray(workerStats) && workerStats.length === 0)) { console.log('👥 빈 데이터로 인한 empty-state 표시'); grid.innerHTML = `
👥

분석할 작업자 데이터가 없습니다.

선택한 기간에 등록된 작업이 없습니다.

`; return; } let gridHtml = ''; workerStats.forEach(worker => { const workerName = worker.worker_name || worker.name || '알 수 없음'; const totalHours = worker.total_hours || worker.totalHours || 0; gridHtml += `
${workerName.charAt(0)}
${workerName}
${totalHours}h
`; // API 응답 구조에 따라 프로젝트 데이터 처리 const projects = worker.projects || worker.project_details || []; if (projects.length > 0) { projects.forEach(project => { const projectName = project.project_name || project.name || '프로젝트'; gridHtml += `
${projectName}
`; const works = project.works || project.work_details || project.tasks || []; if (works.length > 0) { works.forEach(work => { const workName = work.work_name || work.task_name || work.name || '작업'; const workHours = work.hours || work.total_hours || work.work_hours || 0; gridHtml += `
${workName}
${workHours}h
`; }); } else { gridHtml += `
총 작업시간
${project.total_hours || project.hours || 0}h
`; } gridHtml += `
`; }); } else { gridHtml += `
전체 작업
총 작업시간
${totalHours}h
`; } gridHtml += `
`; }); grid.innerHTML = gridHtml; } // 프로젝트별 분석 표시 function displayProjectAnalysis(projectStats) { const detailsContainer = document.getElementById('projectDetails'); console.log('📁 프로젝트 분석 데이터 확인:', projectStats); console.log('📁 데이터 타입:', typeof projectStats); console.log('📁 배열 여부:', Array.isArray(projectStats)); console.log('📁 길이:', projectStats ? projectStats.length : 'undefined'); if (projectStats && projectStats.length > 0) { console.log('📁 첫 번째 프로젝트 데이터:', projectStats[0]); } if (!projectStats || projectStats.length === 0) { console.log('📁 빈 데이터로 인한 empty-state 표시'); detailsContainer.innerHTML = `
📁

분석할 프로젝트 데이터가 없습니다.

`; return; } // 프로젝트 상세 정보 표시 let detailsHtml = ''; // 전체 시간 계산 (퍼센트 계산용) const totalAllHours = projectStats.reduce((sum, p) => { return sum + (p.totalHours || p.total_hours || p.hours || 0); }, 0); projectStats.forEach(project => { console.log('📁 개별 프로젝트 처리:', project); const projectName = project.project_name || project.name || project.projectName || '프로젝트'; const totalHours = project.totalHours || project.total_hours || project.hours || 0; // 퍼센트 계산 let percentage = project.percentage || project.percent || 0; if (percentage === 0 && totalAllHours > 0) { percentage = Math.round((totalHours / totalAllHours) * 100); } detailsHtml += `
${projectName}
${percentage}%
${totalHours}시간
`; }); detailsContainer.innerHTML = detailsHtml; // 차트 업데이트 updateProjectChart(projectStats); } // 프로젝트 차트 업데이트 function updateProjectChart(projectStats) { const ctx = document.getElementById('projectChart'); if (projectChart) { projectChart.destroy(); } const labels = projectStats.map(p => p.project_name || p.name || p.projectName || '프로젝트'); const data = projectStats.map(p => p.totalHours || p.total_hours || p.hours || 0); console.log('📊 차트 라벨:', labels); console.log('📊 차트 데이터:', data); const colors = [ '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6366f1' ]; projectChart = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: colors.slice(0, data.length), borderWidth: 2, borderColor: '#ffffff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { padding: 20, usePointStyle: true } } } } }); } // 오류 분석 표시 function displayErrorAnalysis(errorStats, allData) { console.log('⚠️ 오류 분석 데이터 확인:', errorStats); console.log('⚠️ 데이터 타입:', typeof errorStats); console.log('⚠️ 배열 여부:', Array.isArray(errorStats)); // errorStats가 배열인 경우 첫 번째 요소 사용 let errorData = errorStats; if (Array.isArray(errorStats) && errorStats.length > 0) { errorData = errorStats[0]; console.log('⚠️ 배열에서 첫 번째 요소 사용:', errorData); } // 오류 요약 업데이트 - 실제 데이터 구조에 맞게 수정 const errorHours = errorData.totalHours || errorData.total_hours || errorData.error_hours || 0; // 전체 작업 시간에서 오류 시간을 빼서 정규 시간 계산 // 요약 통계에서 전체 시간을 가져와서 계산 const totalHours = allData && allData.summary ? allData.summary.totalHours : 0; const normalHours = Math.max(0, totalHours - errorHours); console.log('⚠️ 정규 시간:', normalHours, '오류 시간:', errorHours); document.getElementById('normalHours').textContent = `${normalHours}h`; document.getElementById('errorHours').textContent = `${errorHours}h`; // 프로젝트별 에러율 차트 if (errorStats.projectErrorRates) { updateErrorByProjectChart(errorStats.projectErrorRates); } // 일별 오류 추이 차트 if (errorStats.dailyErrorTrend) { updateErrorTimelineChart(errorStats.dailyErrorTrend); } // 오류 유형별 분석 if (errorStats.errorTypes) { displayErrorTypes(errorStats.errorTypes); } } // 프로젝트별 에러율 차트 업데이트 function updateErrorByProjectChart(projectErrorRates) { const ctx = document.getElementById('errorByProjectChart'); if (errorByProjectChart) { errorByProjectChart.destroy(); } const labels = projectErrorRates.map(p => p.project_name); const data = projectErrorRates.map(p => p.error_rate); errorByProjectChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: '에러율 (%)', data: data, backgroundColor: 'rgba(239, 68, 68, 0.8)', borderColor: 'rgba(239, 68, 68, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: function(value) { return value + '%'; } } } }, plugins: { legend: { display: false } } } }); } // 일별 오류 추이 차트 업데이트 function updateErrorTimelineChart(dailyErrorTrend) { const ctx = document.getElementById('errorTimelineChart'); if (errorTimelineChart) { errorTimelineChart.destroy(); } const labels = dailyErrorTrend.map(d => formatDate(new Date(d.date))); const errorData = dailyErrorTrend.map(d => d.error_count); const totalData = dailyErrorTrend.map(d => d.total_count); errorTimelineChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: '총 작업', data: totalData, borderColor: 'rgba(59, 130, 246, 1)', backgroundColor: 'rgba(59, 130, 246, 0.1)', fill: true }, { label: '오류 작업', data: errorData, borderColor: 'rgba(239, 68, 68, 1)', backgroundColor: 'rgba(239, 68, 68, 0.1)', fill: true } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } }, plugins: { legend: { position: 'top' } } } }); } // 오류 유형별 분석 표시 function displayErrorTypes(errorTypes) { const container = document.getElementById('errorTypesAnalysis'); if (!errorTypes || errorTypes.length === 0) { container.innerHTML = `
⚠️

오류 유형 데이터가 없습니다.

`; return; } let html = '

🔍 오류 유형별 상세 분석

'; errorTypes.forEach(errorType => { html += `
⚠️
${errorType.error_name}
${errorType.count}건
${errorType.percentage}%
`; }); container.innerHTML = html; } // 프로젝트별 분석 로드 async function loadProjectAnalysis() { const projectId = document.getElementById('projectModeSelect').value; const startDate = document.getElementById('projectStartDate').value; const endDate = document.getElementById('projectEndDate').value; if (!projectId) { showToast('프로젝트를 선택해주세요.', 'error'); return; } showLoading(true); try { console.log('📁 프로젝트별 분석 데이터 로딩 시작'); // API 호출 파라미터 구성 const params = new URLSearchParams({ project_id: projectId }); if (startDate) params.append('start', startDate); if (endDate) params.append('end', endDate); // 프로젝트별 상세 분석 데이터 로드 const response = await apiCall(`/work-analysis/project-worktype-analysis?${params}`, 'GET'); const projectAnalysisData = response.data || response; console.log('📁 프로젝트 분석 데이터:', projectAnalysisData); // 결과 표시 displayProjectModeAnalysis(projectAnalysisData); // 결과 섹션 표시 document.getElementById('projectModeResults').style.display = 'block'; showToast('프로젝트 분석이 완료되었습니다.', 'success'); } catch (error) { console.error('프로젝트별 분석 오류:', error); showToast('프로젝트 분석 중 오류가 발생했습니다.', 'error'); } finally { showLoading(false); } } // 프로젝트별 분석 결과 표시 function displayProjectModeAnalysis(data) { const container = document.getElementById('projectModeResults'); // 프로젝트별 분석 결과 HTML 생성 let html = `

📁 ${data.project_name} 분석 결과

`; container.innerHTML = html; } // 로딩 상태 표시/숨김 function showLoading(show) { const overlay = document.getElementById('loadingOverlay'); if (overlay) { overlay.style.display = show ? 'flex' : 'none'; } } // 날짜 포맷팅 function formatDate(date) { if (!date) return ''; const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // 토스트 메시지 표시 function showToast(message, type = 'info') { // 기존 토스트 제거 const existingToast = document.querySelector('.toast'); if (existingToast) { existingToast.remove(); } // 새 토스트 생성 const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; // 스타일 적용 Object.assign(toast.style, { position: 'fixed', top: '20px', right: '20px', padding: '12px 24px', borderRadius: '8px', color: 'white', fontWeight: '500', zIndex: '10000', transform: 'translateX(100%)', transition: 'transform 0.3s ease' }); // 타입별 배경색 const colors = { success: '#10b981', error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' }; toast.style.backgroundColor = colors[type] || colors.info; document.body.appendChild(toast); // 애니메이션 setTimeout(() => { toast.style.transform = 'translateX(0)'; }, 100); // 자동 제거 setTimeout(() => { toast.style.transform = 'translateX(100%)'; setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 300); }, 3000); } // 전역 함수로 노출 window.switchAnalysisMode = switchAnalysisMode; window.switchAnalysisTab = switchAnalysisTab; window.loadPeriodAnalysis = loadPeriodAnalysis; window.loadProjectAnalysis = loadProjectAnalysis;