diff --git a/web-ui/css/work-analysis.css b/web-ui/css/work-analysis.css
index 9eeea8e..b144181 100644
--- a/web-ui/css/work-analysis.css
+++ b/web-ui/css/work-analysis.css
@@ -1159,6 +1159,29 @@ body {
.data-table {
min-width: 600px;
}
+
+ .error-analysis-table {
+ font-size: 0.8rem;
+ }
+
+ .error-analysis-table th,
+ .error-analysis-table td {
+ padding: 0.5rem 0.25rem;
+ }
+
+ .error-analysis-table .job-no {
+ width: 80px;
+ min-width: 80px;
+ font-size: 0.75rem;
+ }
+
+ .error-analysis-table .work-type-breakdown {
+ gap: 0.15rem;
+ }
+
+ .error-analysis-table .work-type-item {
+ padding: 0.15rem;
+ }
}
@media (max-width: 480px) {
@@ -1267,6 +1290,200 @@ select:focus {
outline-offset: 2px;
}
+/* ========== 오류 분석 테이블 스타일 ========== */
+.error-analysis-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 1rem;
+ font-size: 0.9rem;
+ background: var(--white);
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.error-analysis-table th {
+ background: #FF6B6B; /* 오류 분석 전용 빨간색 헤더 */
+ color: var(--white);
+ font-weight: 600;
+ text-align: center;
+ padding: 1rem 0.75rem;
+ border-bottom: 2px solid #E74C3C;
+ font-size: 0.85rem;
+ line-height: 1.3;
+}
+
+.error-analysis-table td {
+ padding: 0.75rem;
+ border-bottom: 1px solid #F8F9FA;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.error-analysis-table .job-no,
+.error-analysis-table .project-name {
+ background: #FFE5E5; /* 연한 빨간색 배경 */
+ color: #C0392B;
+ font-weight: 600;
+ text-align: center;
+ width: 120px;
+ min-width: 120px;
+ vertical-align: middle;
+}
+
+.error-analysis-table .work-content {
+ text-align: left;
+ font-weight: 500;
+ color: var(--gray-800);
+}
+
+.error-analysis-table .total-hours {
+ font-weight: 600;
+ color: var(--gray-900);
+}
+
+.error-analysis-table .detail-hours {
+ text-align: left;
+ line-height: 1.4;
+}
+
+.error-analysis-table .regular-hours {
+ color: #27AE60;
+ font-weight: 500;
+}
+
+.error-analysis-table .error-hours {
+ color: #E74C3C;
+ font-weight: 600;
+}
+
+.error-analysis-table .work-type {
+ text-align: left;
+ padding: 0.5rem;
+}
+
+.error-analysis-table .work-type-breakdown {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.error-analysis-table .work-type-item {
+ display: flex;
+ flex-direction: column;
+ gap: 0.1rem;
+ padding: 0.25rem;
+ background: #F8F9FA;
+ border-radius: 4px;
+ margin-bottom: 0.25rem;
+}
+
+/* 정규 작업 스타일 */
+.error-analysis-table .work-type-item.regular {
+ background: #E8F5E8;
+ border-left: 3px solid #4CAF50;
+}
+
+.error-analysis-table .work-type-item.regular .work-type-name {
+ color: #2E7D32;
+}
+
+.error-analysis-table .work-type-item.regular .regular-time {
+ color: #4CAF50;
+ font-weight: 600;
+}
+
+.error-analysis-table .work-type-item.regular .work-type-status {
+ color: #388E3C;
+ font-size: 0.7rem;
+ font-weight: 500;
+}
+
+/* 오류 작업 스타일 */
+.error-analysis-table .work-type-item.error {
+ background: #FFEBEE;
+ border-left: 3px solid #F44336;
+}
+
+.error-analysis-table .work-type-item.error .work-type-name {
+ color: #C62828;
+}
+
+.error-analysis-table .work-type-item.error .error-time {
+ color: #F44336;
+ font-weight: 600;
+}
+
+.error-analysis-table .work-type-item.error .work-type-status {
+ color: #D32F2F;
+ font-size: 0.7rem;
+ font-weight: 600;
+}
+
+.error-analysis-table .work-type-name {
+ font-weight: 600;
+ color: var(--gray-800);
+ font-size: 0.85rem;
+}
+
+.error-analysis-table .work-type-hours {
+ font-size: 0.75rem;
+ color: var(--gray-600);
+}
+
+.error-analysis-table .error-percentage {
+ font-weight: 600;
+ font-size: 1rem;
+}
+
+.error-analysis-table .error-percentage.has-error {
+ color: #E74C3C;
+ background: #FFE5E5;
+ border-radius: 4px;
+ padding: 0.25rem;
+}
+
+/* 프로젝트 그룹 스타일 */
+.error-analysis-table .project-group {
+ border-top: 1px solid #E0E0E0;
+}
+
+.error-analysis-table .project-group:first-child {
+ border-top: none;
+}
+
+/* 연차/휴무 오류 스타일 */
+.error-analysis-table .vacation-project {
+ background: #FFF3E0 !important; /* 연한 오렌지 배경 */
+ border-top: 3px solid #FF9800 !important;
+}
+
+.error-analysis-table .vacation-project .job-no,
+.error-analysis-table .vacation-project .project-name {
+ background: #FF9800 !important; /* 오렌지 배경 */
+ color: var(--white) !important;
+}
+
+.error-analysis-table .vacation-project td {
+ background: #FFF3E0 !important;
+ color: #E65100 !important;
+}
+
+/* 총계 행 스타일 */
+.error-analysis-table .total-row {
+ background: #FFEBEE !important;
+ color: #C62828;
+ border-top: 2px solid #E74C3C;
+ font-weight: 600;
+}
+
+.error-analysis-table .total-row td {
+ background: #FFEBEE !important;
+ color: #C62828 !important;
+ font-weight: 600;
+ border-bottom: none;
+}
+
/* 다크 모드 지원 (선택사항) */
@media (prefers-color-scheme: dark) {
:root {
diff --git a/web-ui/pages/analysis/work-analysis.html b/web-ui/pages/analysis/work-analysis.html
index b73a74c..f74457f 100644
--- a/web-ui/pages/analysis/work-analysis.html
+++ b/web-ui/pages/analysis/work-analysis.html
@@ -7,7 +7,7 @@
-
+
@@ -296,18 +296,47 @@
-
+
-
+
+
+
+
+ | Job No. |
+ 작업내용 Contents |
+ 총 시간 Total Hours |
+ 세부시간 Detail Hours |
+ 작업 타입 Work Type |
+ 오류율 Error Rate |
+
+
+
+
+ |
+ 분석을 실행하여 데이터를 확인하세요
+ |
+
+
+
+
+
@@ -574,27 +603,317 @@
}
}
- async function analyzeErrorTypes() {
- if (!isAnalysisEnabled) {
- alert('먼저 기간을 확정해주세요.');
+ // 오류 분석 함수
+ async function analyzeErrorAnalysis() {
+ console.log('⚠️ 오류 분석 시작');
+
+ if (!confirmedStartDate || !confirmedEndDate) {
+ alert('기간을 먼저 확정해주세요.');
+ return;
+ }
+
+ try {
+ // 병렬로 API 호출
+ const [recentWorkResponse, errorAnalysisResponse] = await Promise.all([
+ apiCall(`/work-analysis/recent-work?start=${confirmedStartDate}&end=${confirmedEndDate}&limit=2000`, 'GET'),
+ apiCall(`/work-analysis/error-analysis?start=${confirmedStartDate}&end=${confirmedEndDate}`, 'GET')
+ ]);
+
+ console.log('🔍 최근 작업 API 응답:', recentWorkResponse);
+ console.log('🔍 오류 분석 API 응답:', errorAnalysisResponse);
+
+ if (recentWorkResponse.success && recentWorkResponse.data) {
+ renderErrorAnalysisTable(recentWorkResponse.data);
+ console.log('✅ 오류 분석 완료');
+ } else {
+ throw new Error(recentWorkResponse.message || '오류 분석 실패');
+ }
+ } catch (error) {
+ console.error('❌ 오류 분석 실패:', error);
+ alert('오류 분석에 실패했습니다.');
+ }
+ }
+
+ // 오류 분석 테이블 렌더링
+ function renderErrorAnalysisTable(recentWorkData) {
+ console.log('📊 오류 분석 테이블 렌더링 시작');
+ console.log('📊 받은 데이터:', recentWorkData);
+
+ const tableBody = document.getElementById('errorAnalysisTableBody');
+ const tableFooter = document.getElementById('errorAnalysisTableFooter');
+
+ console.log('📊 DOM 요소 확인:', { tableBody, tableFooter });
+
+ // DOM 요소 존재 확인
+ if (!tableBody) {
+ console.error('❌ errorAnalysisTableBody 요소를 찾을 수 없습니다');
return;
}
- console.log('⚠️ 오류 유형별 분석 시작');
-
- try {
- const params = new URLSearchParams({
- start: confirmedStartDate,
- end: confirmedEndDate
- });
-
- const response = await apiCall(`/work-analysis/error-analysis?${params}`, 'GET');
- renderErrorAnalysisChart(response.data);
- console.log('✅ 오류 유형별 분석 완료');
- } catch (error) {
- console.error('❌ 오류 유형별 분석 오류:', error);
- alert('오류 유형별 분석에 실패했습니다.');
+ if (!recentWorkData || recentWorkData.length === 0) {
+ tableBody.innerHTML = `
+
+ |
+ 해당 기간에 오류 데이터가 없습니다
+ |
+
+ `;
+ if (tableFooter) {
+ tableFooter.style.display = 'none';
+ }
+ return;
}
+
+ // 작업 형태별 오류 데이터 집계
+ const errorData = aggregateErrorData(recentWorkData);
+
+ let tableRows = [];
+ let grandTotalHours = 0;
+ let grandTotalRegularHours = 0;
+ let grandTotalErrorHours = 0;
+
+ // 프로젝트별로 그룹화
+ const projectGroups = new Map();
+ errorData.forEach(workType => {
+ const projectKey = workType.isVacation ? 'vacation' : workType.project_id;
+ if (!projectGroups.has(projectKey)) {
+ projectGroups.set(projectKey, []);
+ }
+ projectGroups.get(projectKey).push(workType);
+ });
+
+ // 프로젝트별로 렌더링
+ Array.from(projectGroups.entries()).forEach(([projectKey, workTypes]) => {
+ workTypes.forEach((workType, index) => {
+ grandTotalHours += workType.totalHours;
+ grandTotalRegularHours += workType.regularHours;
+ grandTotalErrorHours += workType.errorHours;
+
+ const rowClass = workType.isVacation ? 'vacation-project' : 'project-group';
+ const isFirstWorkType = index === 0;
+ const rowspan = workTypes.length;
+
+ // 세부시간 구성
+ let detailHours = [];
+ if (workType.regularHours > 0) {
+ detailHours.push(`정규: ${workType.regularHours}h`);
+ }
+
+ // 오류 세부사항 추가
+ workType.errorDetails.forEach(error => {
+ detailHours.push(`오류: ${error.type} ${error.hours}h`);
+ });
+
+ // 작업 타입 구성 (단순화)
+ let workTypeDisplay = '';
+ if (workType.regularHours > 0) {
+ workTypeDisplay += `
+
+ 정규시간
+
+ `;
+ }
+
+ workType.errorDetails.forEach(error => {
+ workTypeDisplay += `
+
+ 오류: ${error.type}
+
+ `;
+ });
+
+ tableRows.push(`
+
+ ${isFirstWorkType ? `| ${workType.isVacation ? '연차/휴무' : (workType.project_name || 'N/A')} | ` : ''}
+ ${workType.work_type_name} |
+ ${workType.totalHours}h |
+
+ ${detailHours.join(' ')}
+ |
+
+
+ ${workTypeDisplay}
+
+ |
+ ${workType.errorRate}% |
+
+ `);
+ });
+ });
+
+ if (tableRows.length === 0) {
+ tableBody.innerHTML = `
+
+ |
+ 해당 기간에 작업 데이터가 없습니다
+ |
+
+ `;
+ if (tableFooter) {
+ tableFooter.style.display = 'none';
+ }
+ } else {
+ tableBody.innerHTML = tableRows.join('');
+
+ // 총계 업데이트
+ const totalErrorRate = grandTotalHours > 0 ? ((grandTotalErrorHours / grandTotalHours) * 100).toFixed(1) : '0.0';
+
+ // 안전한 DOM 요소 접근
+ const totalErrorHoursElement = document.getElementById('totalErrorHours');
+ if (totalErrorHoursElement) {
+ totalErrorHoursElement.textContent = `${grandTotalHours}h`;
+ }
+
+ if (tableFooter) {
+ const detailHoursCell = tableFooter.querySelector('.total-row td:nth-child(4)');
+ const errorRateCell = tableFooter.querySelector('.total-row td:nth-child(6)');
+
+ if (detailHoursCell) {
+ detailHoursCell.innerHTML = `
+ 정규: ${grandTotalRegularHours}h
오류: ${grandTotalErrorHours}h
+ `;
+ }
+
+ if (errorRateCell) {
+ errorRateCell.innerHTML = `${totalErrorRate}%`;
+ }
+
+ tableFooter.style.display = 'table-footer-group';
+ }
+ }
+ }
+
+ // 헬퍼 함수들
+ function isWeekendDate(dateString) {
+ const date = new Date(dateString);
+ const dayOfWeek = date.getDay();
+ return dayOfWeek === 0 || dayOfWeek === 6; // 일요일(0) 또는 토요일(6)
+ }
+
+ function isVacationProject(projectName) {
+ if (!projectName) return false;
+ const vacationKeywords = ['연차', '휴무', '휴가', '병가', '특별휴가'];
+ return vacationKeywords.some(keyword => projectName.includes(keyword));
+ }
+
+ // 작업 형태별 오류 데이터 집계 함수
+ function aggregateErrorData(recentWorkData) {
+ const workTypeMap = new Map();
+ let vacationData = null; // 연차/휴무 통합 데이터
+
+ recentWorkData.forEach(work => {
+ const isWeekend = isWeekendDate(work.report_date);
+ const isVacation = 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
+ };
+ }
+
+ const hours = parseFloat(work.work_hours) || 0;
+ if (work.work_status === 'error' || work.error_type_id) {
+ vacationData.errorHours += hours;
+ const errorTypeName = work.error_type_name || work.error_description || '설계미스';
+ if (!vacationData.errorDetails.has(errorTypeName)) {
+ vacationData.errorDetails.set(errorTypeName, 0);
+ }
+ vacationData.errorDetails.set(errorTypeName,
+ vacationData.errorDetails.get(errorTypeName) + hours
+ );
+ } else {
+ vacationData.regularHours += hours;
+ }
+ } else {
+ // 일반 프로젝트 처리
+ const workTypeKey = work.work_type_id || 'unknown';
+ const projectName = work.project_name || `프로젝트 ${work.project_id}`;
+ const workTypeName = work.work_type_name || `작업유형 ${workTypeKey}`;
+
+ // 작업 형태별로 집계 (프로젝트별로 구분)
+ const combinedKey = `${work.project_id || 'unknown'}_${workTypeKey}`;
+
+ if (!workTypeMap.has(combinedKey)) {
+ workTypeMap.set(combinedKey, {
+ project_id: work.project_id,
+ project_name: projectName,
+ job_no: work.job_no,
+ work_type_id: workTypeKey,
+ work_type_name: workTypeName,
+ regularHours: 0,
+ errorHours: 0,
+ errorDetails: new Map(), // 오류 유형별 세분화
+ isVacation: false
+ });
+ }
+
+ const workTypeData = workTypeMap.get(combinedKey);
+ 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;
+ }
+ }
+ });
+
+ // 결과 배열 생성
+ const result = Array.from(workTypeMap.values());
+
+ // 연차/휴무 데이터가 있으면 추가
+ if (vacationData && (vacationData.regularHours > 0 || vacationData.errorHours > 0)) {
+ result.push(vacationData);
+ }
+
+ // 최종 데이터 처리
+ return 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 || '');
+ });
}
// 프로젝트별 작업 분포 테이블 렌더링