feat: 다수 기능 개선 - 순찰, 출근, 작업분석, 모바일 UI 등
- 순찰/점검 기능 개선 (zone-detail 페이지 추가) - 출근/근태 시스템 개선 (연차 조회, 근무현황) - 작업분석 대분류 그룹화 및 마이그레이션 스크립트 - 모바일 네비게이션 UI 추가 - NAS 배포 도구 및 문서 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
801
deploy/tkfb-package/web-ui/js/work-analysis.js
Normal file
801
deploy/tkfb-package/web-ui/js/work-analysis.js
Normal file
@@ -0,0 +1,801 @@
|
||||
// 작업 분석 페이지 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 hours = String(now.getHours()).padStart(2, '0');
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||
const timeString = `${hours}시 ${minutes}분 ${seconds}초`;
|
||||
|
||||
const timeElement = document.getElementById('timeValue');
|
||||
if (timeElement) {
|
||||
timeElement.textContent = timeString;
|
||||
}
|
||||
}
|
||||
|
||||
// 사용자 정보 업데이트 - navbar/sidebar는 app-init.js에서 공통 처리
|
||||
function updateUserInfo() {
|
||||
// app-init.js가 navbar 사용자 정보를 처리하므로 여기서는 아무것도 하지 않음
|
||||
}
|
||||
|
||||
// 프로필 메뉴 설정
|
||||
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 = '<option value="">전체 프로젝트</option>';
|
||||
projects.forEach(project => {
|
||||
projectFilter.innerHTML += `<option value="${project.project_id}">${project.project_name}</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
if (projectModeSelect) {
|
||||
projectModeSelect.innerHTML = '<option value="">프로젝트를 선택하세요</option>';
|
||||
projects.forEach(project => {
|
||||
projectModeSelect.innerHTML += `<option value="${project.project_id}">${project.project_name}</option>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 분석 모드 전환
|
||||
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 = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">👥</div>
|
||||
<h3>분석할 작업자 데이터가 없습니다.</h3>
|
||||
<p>선택한 기간에 등록된 작업이 없습니다.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let gridHtml = '';
|
||||
|
||||
workerStats.forEach(worker => {
|
||||
const workerName = worker.worker_name || worker.name || '알 수 없음';
|
||||
const totalHours = worker.total_hours || worker.totalHours || 0;
|
||||
|
||||
gridHtml += `
|
||||
<div class="worker-card">
|
||||
<div class="worker-header">
|
||||
<div class="worker-info">
|
||||
<div class="worker-avatar">${workerName.charAt(0)}</div>
|
||||
<div class="worker-name">${workerName}</div>
|
||||
</div>
|
||||
<div class="worker-total-hours">${totalHours}h</div>
|
||||
</div>
|
||||
<div class="worker-projects">
|
||||
`;
|
||||
|
||||
// API 응답 구조에 따라 프로젝트 데이터 처리
|
||||
const projects = worker.projects || worker.project_details || [];
|
||||
if (projects.length > 0) {
|
||||
projects.forEach(project => {
|
||||
const projectName = project.project_name || project.name || '프로젝트';
|
||||
gridHtml += `
|
||||
<div class="project-item">
|
||||
<div class="project-name">${projectName}</div>
|
||||
<div class="work-items">
|
||||
`;
|
||||
|
||||
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 += `
|
||||
<div class="work-item">
|
||||
<div class="work-name">${workName}</div>
|
||||
<div class="work-hours">${workHours}h</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
gridHtml += `
|
||||
<div class="work-item">
|
||||
<div class="work-name">총 작업시간</div>
|
||||
<div class="work-hours">${project.total_hours || project.hours || 0}h</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
gridHtml += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
gridHtml += `
|
||||
<div class="project-item">
|
||||
<div class="project-name">전체 작업</div>
|
||||
<div class="work-items">
|
||||
<div class="work-item">
|
||||
<div class="work-name">총 작업시간</div>
|
||||
<div class="work-hours">${totalHours}h</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
gridHtml += `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
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 = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">📁</div>
|
||||
<h3>분석할 프로젝트 데이터가 없습니다.</h3>
|
||||
</div>
|
||||
`;
|
||||
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 += `
|
||||
<div class="project-detail-card">
|
||||
<div class="project-detail-header">
|
||||
<div class="project-detail-name">${projectName}</div>
|
||||
<div class="project-percentage">${percentage}%</div>
|
||||
</div>
|
||||
<div class="project-hours">${totalHours}시간</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
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 = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">⚠️</div>
|
||||
<h3>오류 유형 데이터가 없습니다.</h3>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<h4>🔍 오류 유형별 상세 분석</h4>';
|
||||
|
||||
errorTypes.forEach(errorType => {
|
||||
html += `
|
||||
<div class="error-type-item">
|
||||
<div class="error-type-info">
|
||||
<div class="error-type-icon">⚠️</div>
|
||||
<div class="error-type-name">${errorType.error_name}</div>
|
||||
</div>
|
||||
<div class="error-type-stats">
|
||||
<div class="error-type-count">${errorType.count}건</div>
|
||||
<div class="error-type-percentage">${errorType.percentage}%</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
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 = `
|
||||
<div class="project-mode-analysis">
|
||||
<h3>📁 ${data.project_name} 분석 결과</h3>
|
||||
<!-- 프로젝트별 상세 분석 내용 -->
|
||||
</div>
|
||||
`;
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user