feat: 오류 분석 시스템 완전 재구조화 및 개선
✨ 주요 기능 - Production Report 스타일 오류 분석 테이블 구현 - 작업 형태 중심의 데이터 집계 및 표시 - 프로젝트별 그룹화 (rowspan 적용) - 연차/휴무 통합 처리 및 주말 제외 📊 테이블 구조 개선 - Job No. → 프로젝트명 표시 - 작업내용 → 작업 형태 (Base, Vessel, Piping Assembly) - 총 시간 → 작업 형태별 총 시간 - 세부시간 → 정규/오류 유형별 세분화 - 백분율 → 오류율로 변경 🎨 UI/UX 개선 - 프로젝트별 rowspan 그룹화 - 정규(녹색)/오류(빨간색) 시각적 구분 - 연차/휴무 오렌지 색상 테마 - 프로젝트 그룹 경계선 추가 🔧 데이터 처리 로직 - 작업 형태별 오류 데이터 집계 - 오류 유형별 세분화 (설계미스, 발주미스 등) - 주말 연차/휴무 자동 제외 - 모든 연차/휴무 하나로 통합 🛡️ 안정성 개선 - DOM 요소 null 체크 및 안전한 접근 - 디버깅 로그 추가 - 에러 핸들링 강화
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/work-analysis.css?v=33">
|
||||
<link rel="stylesheet" href="/css/work-analysis.css?v=41">
|
||||
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||
<script src="/js/auth-check.js?v=1" defer></script>
|
||||
<script src="/js/api-config.js?v=1" defer></script>
|
||||
@@ -296,18 +296,47 @@
|
||||
|
||||
<!-- 오류 분석 탭 -->
|
||||
<div id="error-analysis-tab" class="tab-content">
|
||||
<div class="chart-container chart-type">
|
||||
<div class="chart-container table-type">
|
||||
<div class="chart-header">
|
||||
<h3 class="chart-title">
|
||||
<span class="icon">⚠️</span>
|
||||
오류 유형별 분석
|
||||
오류 분석
|
||||
</h3>
|
||||
<button class="chart-analyze-btn" onclick="analyzeErrorTypes()" disabled>
|
||||
<button class="chart-analyze-btn" onclick="analyzeErrorAnalysis()" disabled>
|
||||
<span class="icon">🔍</span>
|
||||
분석 실행
|
||||
</button>
|
||||
</div>
|
||||
<canvas id="errorAnalysisChart" class="chart-canvas"></canvas>
|
||||
<div class="table-container">
|
||||
<table class="error-analysis-table" id="errorAnalysisTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job No.</th>
|
||||
<th>작업내용<br><small>Contents</small></th>
|
||||
<th>총 시간<br><small>Total Hours</small></th>
|
||||
<th>세부시간<br><small>Detail Hours</small></th>
|
||||
<th>작업 타입<br><small>Work Type</small></th>
|
||||
<th>오류율<br><small>Error Rate</small></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="errorAnalysisTableBody">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 2rem; color: #666;">
|
||||
분석을 실행하여 데이터를 확인하세요
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot id="errorAnalysisTableFooter" style="display: none;">
|
||||
<tr class="total-row">
|
||||
<td colspan="2"><strong>합계<br><small>Total</small></strong></td>
|
||||
<td><strong id="totalErrorHours">-</strong></td>
|
||||
<td><strong>-</strong></td>
|
||||
<td><strong>-</strong></td>
|
||||
<td><strong id="totalErrorRate">-</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 2rem; color: #666;">
|
||||
해당 기간에 오류 데이터가 없습니다
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
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(`<span class="regular-hours">정규: ${workType.regularHours}h</span>`);
|
||||
}
|
||||
|
||||
// 오류 세부사항 추가
|
||||
workType.errorDetails.forEach(error => {
|
||||
detailHours.push(`<span class="error-hours">오류: ${error.type} ${error.hours}h</span>`);
|
||||
});
|
||||
|
||||
// 작업 타입 구성 (단순화)
|
||||
let workTypeDisplay = '';
|
||||
if (workType.regularHours > 0) {
|
||||
workTypeDisplay += `
|
||||
<div class="work-type-item regular">
|
||||
<span class="work-type-status">정규시간</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
workType.errorDetails.forEach(error => {
|
||||
workTypeDisplay += `
|
||||
<div class="work-type-item error">
|
||||
<span class="work-type-status">오류: ${error.type}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
tableRows.push(`
|
||||
<tr class="${rowClass}">
|
||||
${isFirstWorkType ? `<td class="project-name" rowspan="${rowspan}">${workType.isVacation ? '연차/휴무' : (workType.project_name || 'N/A')}</td>` : ''}
|
||||
<td class="work-content">${workType.work_type_name}</td>
|
||||
<td class="total-hours">${workType.totalHours}h</td>
|
||||
<td class="detail-hours">
|
||||
${detailHours.join('<br>')}
|
||||
</td>
|
||||
<td class="work-type">
|
||||
<div class="work-type-breakdown">
|
||||
${workTypeDisplay}
|
||||
</div>
|
||||
</td>
|
||||
<td class="error-percentage ${workType.errorHours > 0 ? 'has-error' : ''}">${workType.errorRate}%</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
if (tableRows.length === 0) {
|
||||
tableBody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 2rem; color: #666;">
|
||||
해당 기간에 작업 데이터가 없습니다
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
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 = `
|
||||
<strong>정규: ${grandTotalRegularHours}h<br>오류: ${grandTotalErrorHours}h</strong>
|
||||
`;
|
||||
}
|
||||
|
||||
if (errorRateCell) {
|
||||
errorRateCell.innerHTML = `<strong>${totalErrorRate}%</strong>`;
|
||||
}
|
||||
|
||||
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 || '');
|
||||
});
|
||||
}
|
||||
|
||||
// 프로젝트별 작업 분포 테이블 렌더링
|
||||
|
||||
Reference in New Issue
Block a user