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 {
|
.data-table {
|
||||||
min-width: 600px;
|
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) {
|
@media (max-width: 480px) {
|
||||||
@@ -1267,6 +1290,200 @@ select:focus {
|
|||||||
outline-offset: 2px;
|
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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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 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">
|
<link rel="icon" type="image/png" href="/img/favicon.png">
|
||||||
<script src="/js/auth-check.js?v=1" defer></script>
|
<script src="/js/auth-check.js?v=1" defer></script>
|
||||||
<script src="/js/api-config.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 id="error-analysis-tab" class="tab-content">
|
||||||
<div class="chart-container chart-type">
|
<div class="chart-container table-type">
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<h3 class="chart-title">
|
<h3 class="chart-title">
|
||||||
<span class="icon">⚠️</span>
|
<span class="icon">⚠️</span>
|
||||||
오류 유형별 분석
|
오류 분석
|
||||||
</h3>
|
</h3>
|
||||||
<button class="chart-analyze-btn" onclick="analyzeErrorTypes()" disabled>
|
<button class="chart-analyze-btn" onclick="analyzeErrorAnalysis()" disabled>
|
||||||
<span class="icon">🔍</span>
|
<span class="icon">🔍</span>
|
||||||
분석 실행
|
분석 실행
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -574,27 +603,317 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function analyzeErrorTypes() {
|
// 오류 분석 함수
|
||||||
if (!isAnalysisEnabled) {
|
async function analyzeErrorAnalysis() {
|
||||||
alert('먼저 기간을 확정해주세요.');
|
console.log('⚠️ 오류 분석 시작');
|
||||||
|
|
||||||
|
if (!confirmedStartDate || !confirmedEndDate) {
|
||||||
|
alert('기간을 먼저 확정해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('⚠️ 오류 유형별 분석 시작');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
// 병렬로 API 호출
|
||||||
start: confirmedStartDate,
|
const [recentWorkResponse, errorAnalysisResponse] = await Promise.all([
|
||||||
end: confirmedEndDate
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await apiCall(`/work-analysis/error-analysis?${params}`, 'GET');
|
// 프로젝트별로 렌더링
|
||||||
renderErrorAnalysisChart(response.data);
|
Array.from(projectGroups.entries()).forEach(([projectKey, workTypes]) => {
|
||||||
console.log('✅ 오류 유형별 분석 완료');
|
workTypes.forEach((workType, index) => {
|
||||||
} catch (error) {
|
grandTotalHours += workType.totalHours;
|
||||||
console.error('❌ 오류 유형별 분석 오류:', error);
|
grandTotalRegularHours += workType.regularHours;
|
||||||
alert('오류 유형별 분석에 실패했습니다.');
|
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