feat: 프로젝트별 작업 분포 Production Report 스타일 완성

 주요 기능
- 프로젝트별 → 작업유형별 데이터 취합 및 표시
- Production Report 스타일 테이블 구현
- 연차/휴무 별도 처리 (주말 제외, 녹색 테마)
- Job No. 정확한 표시 (중복 제거)

🔧 API 개선
- recent-work API에 job_no 필드 추가
- MySQL 쿼리 결과 처리 수정 (results[0] 사용)
- Projects 테이블 대소문자 조인 문제 해결

🎨 UI/UX 개선
- 탭 기반 분석 인터페이스
- 색상 팔레트 개선 (파란색/녹색/노란색 계열)
- 텍스트 방향 수정 (가로 표시)
- 프로젝트별 합계 행 추가

📊 계산 로직
- 공수: 시간 ÷ 8
- 부하율: (개별 시간 ÷ 전체 시간) × 100%
- 인건비: 공수 × 350,000원
- 주말 연차 자동 제외
This commit is contained in:
Hyungi Ahn
2025-11-04 17:52:24 +09:00
parent de427c457b
commit 26f9a4dea2
4 changed files with 828 additions and 76 deletions

View File

@@ -391,14 +391,24 @@ class WorkAnalysisController {
const testResults = await db.query(testQuery, [start, end]);
console.log('📊 데이터 확인:', testResults[0]);
// 프로젝트별-작업별 시간 분석 쿼리 (간단한 버전으로 테스트)
// 먼저 간단한 테스트 쿼리로 데이터 확인
const simpleQuery = `
SELECT COUNT(*) as count, MIN(report_date) as min_date, MAX(report_date) as max_date
FROM daily_work_reports
WHERE report_date BETWEEN ? AND ?
`;
const simpleResult = await db.query(simpleQuery, [start, end]);
console.log('📊 기간 내 데이터 확인:', simpleResult[0][0]);
// 프로젝트별-작업별 시간 분석 쿼리 (work_types 테이블과 조인)
const query = `
SELECT
COALESCE(p.project_id, 0) as project_id,
COALESCE(p.project_name, 'Unknown Project') as project_name,
COALESCE(p.project_id, dwr.project_id) as project_id,
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
COALESCE(p.job_no, 'N/A') as job_no,
dwr.work_type_id,
CONCAT('Work Type ', dwr.work_type_id) as work_type_name,
COALESCE(wt.name, CONCAT('작업유형 ', dwr.work_type_id)) as work_type_name,
-- 총 시간
SUM(dwr.work_hours) as total_hours,
@@ -421,18 +431,22 @@ class WorkAnalysisController {
) as error_rate_percent
FROM daily_work_reports dwr
LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN Projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
WHERE dwr.report_date BETWEEN ? AND ?
GROUP BY p.project_id, p.project_name, p.job_no, dwr.work_type_id
ORDER BY p.project_name, dwr.work_type_id
GROUP BY dwr.project_id, p.project_name, p.job_no, dwr.work_type_id, wt.name
ORDER BY p.project_name, wt.name
`;
const results = await db.query(query, [start, end]);
console.log('📊 쿼리 결과 개수:', results[0].length);
console.log('📊 첫 번째 결과:', results[0][0]);
console.log('📊 모든 결과:', JSON.stringify(results[0], null, 2));
// 데이터를 프로젝트별로 그룹화
const groupedData = {};
results.forEach(row => {
results[0].forEach(row => {
const projectKey = `${row.project_id}_${row.project_name}`;
if (!groupedData[projectKey]) {
@@ -476,7 +490,7 @@ class WorkAnalysisController {
// 전체 요약 통계
const totalStats = {
total_projects: Object.keys(groupedData).length,
total_work_types: new Set(results.map(r => r.work_type_id)).size,
total_work_types: new Set(results[0].map(r => r.work_type_id)).size,
grand_total_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_project_hours, 0),
grand_regular_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_regular_hours, 0),
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)

View File

@@ -190,6 +190,7 @@ class WorkAnalysis {
w.worker_name,
dwr.project_id,
p.project_name,
p.job_no,
dwr.work_type_id,
wt.name as work_type_name,
dwr.work_status_id,
@@ -202,7 +203,7 @@ class WorkAnalysis {
dwr.created_at
FROM daily_work_reports dwr
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
LEFT JOIN projects p ON dwr.project_id = p.project_id
LEFT JOIN Projects p ON dwr.project_id = p.project_id
LEFT JOIN work_types wt ON dwr.work_type_id = wt.id
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.id
LEFT JOIN error_types et ON dwr.error_type_id = et.id
@@ -221,6 +222,7 @@ class WorkAnalysis {
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
project_id: row.project_id,
project_name: row.project_name || `프로젝트 ${row.project_id}`,
job_no: row.job_no || 'N/A',
work_type_id: row.work_type_id,
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
work_status_id: row.work_status_id,

View File

@@ -609,6 +609,152 @@ body {
color: var(--gray-600);
}
/* ========== Production Report 테이블 ========== */
.production-report-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
background: var(--white);
border: 2px solid #000;
}
.production-report-table th {
background: #4472C4;
color: var(--white);
font-weight: 600;
padding: 1rem 0.75rem;
text-align: center;
border: 1px solid #000;
vertical-align: middle;
line-height: 1.3;
}
.production-report-table td {
padding: 0.75rem;
border: 1px solid #000;
text-align: center;
vertical-align: middle;
}
/* 프로젝트명 (Job No.) 스타일 */
.production-report-table .project-name {
background: #5D9CEC;
color: var(--white);
font-weight: 600;
text-align: center;
writing-mode: horizontal-tb;
text-orientation: mixed;
width: 150px;
min-width: 150px;
padding: 0.5rem;
line-height: 1.3;
}
/* 작업 내용 스타일 */
.production-report-table .work-content {
background: var(--white);
color: var(--gray-800);
font-weight: 500;
text-align: center;
}
/* 공수 스타일 */
.production-report-table .man-days {
background: var(--white);
color: var(--gray-800);
font-weight: 600;
text-align: center;
}
/* 부하율 스타일 */
.production-report-table .load-rate {
background: var(--white);
color: var(--gray-800);
font-weight: 600;
text-align: center;
}
/* 인건비 스타일 */
.production-report-table .labor-cost {
background: var(--white);
color: var(--gray-800);
font-weight: 600;
text-align: center;
}
/* 합계 행 스타일 */
.production-report-table .total-row {
background: #FFF3CD !important;
font-weight: 700;
border-top: 2px solid #F39C12;
}
.production-report-table .total-row td {
background: #FFF3CD !important;
color: #856404;
font-weight: 700;
}
/* 프로젝트 그룹 스타일 */
.production-report-table .project-group {
border-top: 2px solid #000;
}
.production-report-table .project-group:first-child {
border-top: none;
}
/* 연차/휴무 스타일 */
.production-report-table .vacation-project {
background: #E8F5E8 !important;
border-top: 3px solid #4CAF50 !important;
}
.production-report-table .vacation-project .project-name {
background: #4CAF50 !important;
color: var(--white) !important;
font-weight: 700;
text-align: center;
font-size: 1rem;
writing-mode: horizontal-tb !important;
text-orientation: mixed !important;
}
.production-report-table .vacation-project .work-content {
background: #E8F5E8 !important;
color: #2E7D32 !important;
font-weight: 600;
text-align: center;
}
.production-report-table .vacation-project td {
background: #E8F5E8 !important;
color: #2E7D32 !important;
font-weight: 500;
}
.production-report-table .vacation-project .man-days,
.production-report-table .vacation-project .load-rate,
.production-report-table .vacation-project .labor-cost {
background: #E8F5E8 !important;
color: #2E7D32 !important;
font-weight: 600;
}
/* 프로젝트별 합계 행 스타일 */
.production-report-table .project-subtotal {
background: #E8F4FD !important;
font-weight: 600;
border-bottom: 2px solid #5D9CEC;
}
.production-report-table .project-subtotal td {
background: #E8F4FD !important;
color: #2E5BBA;
font-weight: 600;
border-bottom: 2px solid #5D9CEC;
}
.work-report-table .input-hours {
font-weight: 600;
color: var(--success);
@@ -720,6 +866,66 @@ body {
transform: none;
}
/* ========== 탭 네비게이션 ========== */
.tab-navigation {
background: var(--white);
border-radius: var(--radius-xl);
padding: var(--space-6);
box-shadow: var(--shadow-lg);
border: 1px solid var(--gray-200);
margin-bottom: var(--space-8);
}
.tab-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--space-4);
}
.tab-button {
background: var(--gray-50);
border: 2px solid var(--gray-200);
border-radius: var(--radius-lg);
padding: var(--space-4) var(--space-6);
font-size: 0.9rem;
font-weight: 600;
color: var(--gray-700);
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
text-align: center;
}
.tab-button:hover {
background: var(--gray-100);
border-color: var(--primary);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.tab-button.active {
background: var(--gradient-primary);
border-color: var(--primary);
color: var(--white);
box-shadow: var(--shadow-lg);
}
.tab-button .icon {
font-size: 1.2rem;
}
/* ========== 탭 컨텐츠 ========== */
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.chart-title {
font-size: 1.5rem;
font-weight: 700;
@@ -986,6 +1192,16 @@ body {
font-size: 0.85rem;
}
.tab-buttons {
grid-template-columns: 1fr;
gap: var(--space-3);
}
.tab-button {
padding: var(--space-3) var(--space-4);
font-size: 0.85rem;
}
.result-card {
padding: var(--space-4);
}

View File

@@ -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=21">
<link rel="stylesheet" href="/css/work-analysis.css?v=33">
<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>
@@ -83,14 +83,26 @@
<p class="loading-text">분석 중입니다...</p>
</div>
<!-- 빈 상태 -->
<div id="emptyState" class="empty-state">
<div class="empty-icon">📊</div>
<h3 class="empty-title">분석을 시작해보세요</h3>
<p class="empty-description">
기간을 설정하고 분석 실행 버튼을 클릭하여<br>
상세한 작업 현황 분석을 확인할 수 있습니다.
</p>
<!-- 분석 탭 네비게이션 -->
<div id="analysisTabNavigation" class="tab-navigation" style="display: none;">
<div class="tab-buttons">
<button class="tab-button active" data-tab="work-status">
<span class="icon">📈</span>
기간별 작업 현황
</button>
<button class="tab-button" data-tab="project-distribution">
<span class="icon">🥧</span>
프로젝트별 분포
</button>
<button class="tab-button" data-tab="worker-performance">
<span class="icon">👤</span>
작업자별 성과
</button>
<button class="tab-button" data-tab="error-analysis">
<span class="icon">⚠️</span>
오류 분석
</button>
</div>
</div>
<!-- 결과 카드 그리드 -->
@@ -172,20 +184,21 @@
</div>
</div>
<!-- 차트 영역 -->
<div id="chartsContainer" style="display: none;">
<!-- 기간별 작업 현황 테이블 -->
<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="analyzeWorkStatus()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<!-- 탭 컨텐츠 영역 -->
<div id="tabContentContainer" style="display: none;">
<!-- 기간별 작업 현황 -->
<div id="work-status-tab" class="tab-content active">
<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="analyzeWorkStatus()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<table class="work-report-table" id="workReportTable">
<thead>
@@ -217,51 +230,85 @@
</tfoot>
</table>
</div>
</div>
</div>
<!-- 프로젝트별 작업 분포 차트 -->
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">🥧</span>
프로젝트별 작업 분포
</h3>
<button class="chart-analyze-btn" onclick="analyzeProjectDistribution()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
<!-- 프로젝트별 작업 분포 -->
<div id="project-distribution-tab" class="tab-content">
<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="analyzeProjectDistribution()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<div class="table-container">
<table class="production-report-table" id="projectDistributionTable">
<thead>
<tr>
<th>Job No.</th>
<th>작업 내용<br><small>Contents</small></th>
<th>공 수<br><small>Man / Day</small></th>
<th>전체 부하율</th>
<th>인건비<br><small>Labor Cost</small></th>
</tr>
</thead>
<tbody id="projectDistributionTableBody">
<tr>
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
분석을 실행하여 데이터를 확인하세요
</td>
</tr>
</tbody>
<tfoot id="projectDistributionTableFooter" style="display: none;">
<tr class="total-row">
<td colspan="2"><strong>합계<br><small>Total</small></strong></td>
<td><strong id="totalManDays">-</strong></td>
<td><strong>100.00%</strong></td>
<td><strong id="totalLaborCost">-</strong></td>
</tr>
</tfoot>
</table>
</div>
</div>
<canvas id="projectDistributionChart" class="chart-canvas"></canvas>
</div>
<!-- 작업자별 성과 차트 -->
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">👤</span>
작업자별 성과
</h3>
<button class="chart-analyze-btn" onclick="analyzeWorkerPerformance()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
<!-- 작업자별 성과 -->
<div id="worker-performance-tab" class="tab-content">
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">👤</span>
작업자별 성과
</h3>
<button class="chart-analyze-btn" onclick="analyzeWorkerPerformance()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<canvas id="workerPerformanceChart" class="chart-canvas"></canvas>
</div>
<canvas id="workerPerformanceChart" class="chart-canvas"></canvas>
</div>
<!-- 오류 분석 차트 -->
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">⚠️</span>
오류 유형별 분석
</h3>
<button class="chart-analyze-btn" onclick="analyzeErrorTypes()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
<!-- 오류 분석 -->
<div id="error-analysis-tab" class="tab-content">
<div class="chart-container chart-type">
<div class="chart-header">
<h3 class="chart-title">
<span class="icon">⚠️</span>
오류 유형별 분석
</h3>
<button class="chart-analyze-btn" onclick="analyzeErrorTypes()" disabled>
<span class="icon">🔍</span>
분석 실행
</button>
</div>
<canvas id="errorAnalysisChart" class="chart-canvas"></canvas>
</div>
<canvas id="errorAnalysisChart" class="chart-canvas"></canvas>
</div>
</div>
@@ -406,12 +453,47 @@
btn.disabled = false;
});
// 차트 컨테이너 표시
document.getElementById('chartsContainer').style.display = 'block';
// 탭 네비게이션과 컨텐츠 표시
document.getElementById('analysisTabNavigation').style.display = 'block';
document.getElementById('tabContentContainer').style.display = 'block';
console.log(`✅ 기간 확정: ${startDate} ~ ${endDate}`);
}
// 탭 전환 함수
function switchTab(tabId) {
// 모든 탭 버튼 비활성화
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
// 모든 탭 컨텐츠 숨기기
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
// 선택된 탭 버튼 활성화
document.querySelector(`[data-tab="${tabId}"]`).classList.add('active');
// 선택된 탭 컨텐츠 표시
document.getElementById(`${tabId}-tab`).classList.add('active');
console.log(`🔄 탭 전환: ${tabId}`);
}
// 탭 버튼 이벤트 리스너 추가
document.addEventListener('DOMContentLoaded', function() {
// 기존 초기화 코드...
// 탭 버튼 클릭 이벤트
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab');
switchTab(tabId);
});
});
});
// 개별 분석 함수들
async function analyzeWorkStatus() {
if (!isAnalysisEnabled) {
@@ -437,16 +519,31 @@
return;
}
console.log('🥧 프로젝트별 작업 분포 분석 시작');
console.log('📋 프로젝트별 작업 분포 분석 시작');
try {
const params = new URLSearchParams({
start: confirmedStartDate,
end: confirmedEndDate
end: confirmedEndDate,
limit: 2000
});
const response = await apiCall(`/work-analysis/project-stats?${params}`, 'GET');
renderProjectDistributionChart(response.data);
// 기간별 작업 현황 API를 사용해서 프로젝트별 데이터 가져오기
const [projectWorktypeResponse, workerResponse, recentWorkResponse] = await Promise.all([
apiCall(`/work-analysis/project-worktype-analysis?${params}`, 'GET'),
apiCall(`/work-analysis/worker-stats?${params}`, 'GET'),
apiCall(`/work-analysis/recent-work?${params}`, 'GET')
]);
console.log('🔍 프로젝트-작업유형 API 응답:', projectWorktypeResponse);
console.log('🔍 작업자 API 응답:', workerResponse);
console.log('🔍 최근 작업 API 응답:', recentWorkResponse);
// recent-work 데이터로 프로젝트별 취합
const projectData = aggregateProjectData(recentWorkResponse.data || []);
console.log('📊 취합된 프로젝트 데이터:', projectData);
renderProjectDistributionTableFromRecentWork(projectData, workerResponse.data);
console.log('✅ 프로젝트별 작업 분포 분석 완료');
} catch (error) {
console.error('❌ 프로젝트별 작업 분포 분석 오류:', error);
@@ -499,7 +596,430 @@
alert('오류 유형별 분석에 실패했습니다.');
}
}
// 프로젝트별 작업 분포 테이블 렌더링
function renderProjectDistributionTable(projectData, workerData) {
const tbody = document.getElementById('projectDistributionTableBody');
const tfoot = document.getElementById('projectDistributionTableFooter');
if (!tbody) return;
console.log('📋 프로젝트별 분포 테이블 렌더링:', projectData);
console.log('📋 작업자 데이터:', workerData);
// 프로젝트 API 데이터가 null이면 작업자 데이터로 대체
if (!projectData || !projectData.projects || projectData.projects.length === 0 ||
(projectData.projects[0] && projectData.projects[0].total_project_hours === null)) {
console.log('⚠️ 프로젝트 API 데이터가 없어서 작업자 데이터로 대체합니다.');
renderFallbackTable(workerData);
return;
}
let tableRows = [];
let grandTotalHours = 0;
let grandTotalManDays = 0;
let grandTotalLaborCost = 0;
// 공수당 인건비 (350,000원)
const manDayRate = 350000;
// 먼저 전체 시간을 계산 (부하율 계산용)
if (projectData && projectData.projects && Array.isArray(projectData.projects)) {
projectData.projects.forEach(project => {
const workTypes = project.work_types || [];
workTypes.forEach(workType => {
grandTotalHours += parseFloat(workType.total_hours) || 0;
});
});
}
if (projectData && projectData.projects && Array.isArray(projectData.projects)) {
projectData.projects.forEach(project => {
const projectName = project.project_name || '알 수 없는 프로젝트';
const workTypes = project.work_types || [];
let projectTotalHours = 0;
let projectTotalManDays = 0;
let projectTotalLaborCost = 0;
if (workTypes.length === 0) {
// 작업유형이 없는 경우
const projectHours = parseFloat(project.total_hours) || 0;
const manDays = Math.round((projectHours / 8) * 100) / 100;
const laborCost = manDays * manDayRate;
const loadRate = grandTotalHours > 0 ? ((projectHours / grandTotalHours) * 100).toFixed(2) : '0.00';
projectTotalHours += projectHours;
projectTotalManDays += manDays;
projectTotalLaborCost += laborCost;
tableRows.push(`
<tr class="project-group">
<td class="project-name">${projectName}</td>
<td class="work-content">전체</td>
<td class="man-days">${manDays}</td>
<td class="load-rate">${loadRate}%</td>
<td class="labor-cost">₩${laborCost.toLocaleString()}</td>
</tr>
`);
} else {
// 작업유형별로 행 생성
workTypes.forEach((workType, index) => {
const workTypeName = workType.work_type_name || `작업유형 ${workType.work_type_id}`;
const workTypeHours = parseFloat(workType.total_hours) || 0;
const manDays = Math.round((workTypeHours / 8) * 100) / 100;
const laborCost = manDays * manDayRate;
const loadRate = grandTotalHours > 0 ? ((workTypeHours / grandTotalHours) * 100).toFixed(2) : '0.00';
projectTotalHours += workTypeHours;
projectTotalManDays += manDays;
projectTotalLaborCost += laborCost;
const isFirstWorkType = index === 0;
const rowspan = workTypes.length + 1; // +1 for subtotal row
tableRows.push(`
<tr class="project-group">
${isFirstWorkType ? `<td class="project-name" rowspan="${rowspan}">${projectName}</td>` : ''}
<td class="work-content">${workTypeName}</td>
<td class="man-days">${manDays}</td>
<td class="load-rate">${loadRate}%</td>
<td class="labor-cost">₩${laborCost.toLocaleString()}</td>
</tr>
`);
});
// 프로젝트별 합계 행 추가
const projectLoadRate = grandTotalHours > 0 ? ((projectTotalHours / grandTotalHours) * 100).toFixed(2) : '0.00';
tableRows.push(`
<tr class="project-subtotal">
<td class="work-content"><strong>합계</strong></td>
<td class="man-days"><strong>${Math.round(projectTotalManDays * 100) / 100}</strong></td>
<td class="load-rate"><strong>${projectLoadRate}%</strong></td>
<td class="labor-cost"><strong>₩${projectTotalLaborCost.toLocaleString()}</strong></td>
</tr>
`);
}
grandTotalManDays += projectTotalManDays;
grandTotalLaborCost += projectTotalLaborCost;
});
}
if (tableRows.length > 0) {
tbody.innerHTML = tableRows.join('');
// 전체 합계 행 업데이트
document.getElementById('totalManDays').textContent = `${Math.round(grandTotalManDays * 100) / 100} 공수`;
document.getElementById('totalLaborCost').textContent = `${grandTotalLaborCost.toLocaleString()}`;
if (tfoot) {
tfoot.style.display = 'table-footer-group';
}
} else {
tbody.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
해당 기간에 프로젝트 데이터가 없습니다
</td>
</tr>
`;
if (tfoot) {
tfoot.style.display = 'none';
}
}
}
// 작업자 데이터로 대체 테이블 렌더링
function renderFallbackTable(workerData) {
const tbody = document.getElementById('projectDistributionTableBody');
const tfoot = document.getElementById('projectDistributionTableFooter');
if (!workerData || !Array.isArray(workerData) || workerData.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
해당 기간에 데이터가 없습니다
</td>
</tr>
`;
if (tfoot) tfoot.style.display = 'none';
return;
}
let tableRows = [];
let totalHours = 0;
let totalManDays = 0;
let totalLaborCost = 0;
const manDayRate = 350000; // 공수당 인건비
// 작업자별로 행 생성 (임시 데이터)
workerData.forEach(worker => {
const hours = worker.totalHours || 0;
const manDays = Math.round((hours / 8) * 100) / 100;
const laborCost = manDays * manDayRate;
const loadRate = 0; // 나중에 계산
totalHours += hours;
totalManDays += manDays;
totalLaborCost += laborCost;
tableRows.push({
projectName: `작업자 ${worker.worker_name}`,
workContent: '전체 작업',
manDays: manDays,
hours: hours,
laborCost: laborCost
});
});
// 부하율 계산 및 HTML 생성
const htmlRows = tableRows.map(row => {
const loadRate = totalHours > 0 ? ((row.hours / totalHours) * 100).toFixed(2) : '0.00';
return `
<tr class="project-group">
<td class="project-name">${row.projectName}</td>
<td class="work-content">${row.workContent}</td>
<td class="man-days">${row.manDays}</td>
<td class="load-rate">${loadRate}%</td>
<td class="labor-cost">₩${row.laborCost.toLocaleString()}</td>
</tr>
`;
});
tbody.innerHTML = htmlRows.join('');
// 합계 행 업데이트
document.getElementById('totalManDays').textContent = `${Math.round(totalManDays * 100) / 100} 공수`;
document.getElementById('totalLaborCost').textContent = `${totalLaborCost.toLocaleString()}`;
if (tfoot) {
tfoot.style.display = 'table-footer-group';
}
}
// 주말 체크 함수
function isWeekend(dateStr) {
const date = new Date(dateStr);
const dayOfWeek = date.getDay();
return dayOfWeek === 0 || dayOfWeek === 6; // 0: 일요일, 6: 토요일
}
// 연차/휴무 프로젝트 체크 함수
function isVacationProject(projectName) {
return projectName && (projectName.includes('연차') || projectName.includes('휴무'));
}
// recent-work 데이터로 프로젝트별 취합
function aggregateProjectData(recentWorkData) {
const projectMap = new Map();
let vacationData = {
total_hours: 0,
regular_hours: 0,
error_hours: 0,
total_reports: 0
};
recentWorkData.forEach(work => {
// 연차/휴무 프로젝트인 경우
if (isVacationProject(work.project_name)) {
// 주말 연차는 제외
if (!isWeekend(work.report_date)) {
const hours = parseFloat(work.work_hours) || 0;
vacationData.total_hours += hours;
vacationData.total_reports += 1;
if (work.work_status_name === '정규') {
vacationData.regular_hours += hours;
} else if (work.work_status_name === '에러') {
vacationData.error_hours += hours;
}
}
return; // 연차/휴무는 별도 처리하므로 여기서 종료
}
const projectKey = `${work.project_id}_${work.project_name}`;
if (!projectMap.has(projectKey)) {
projectMap.set(projectKey, {
project_id: work.project_id,
project_name: work.project_name,
job_no: work.job_no || work.project_name || 'N/A',
work_types: new Map()
});
}
const project = projectMap.get(projectKey);
const workTypeKey = `${work.work_type_id}_${work.work_type_name}`;
if (!project.work_types.has(workTypeKey)) {
project.work_types.set(workTypeKey, {
work_type_id: work.work_type_id,
work_type_name: work.work_type_name,
total_hours: 0,
regular_hours: 0,
error_hours: 0,
total_reports: 0
});
}
const workType = project.work_types.get(workTypeKey);
const hours = parseFloat(work.work_hours) || 0;
workType.total_hours += hours;
workType.total_reports += 1;
if (work.work_status_name === '정규') {
workType.regular_hours += hours;
} else if (work.work_status_name === '에러') {
workType.error_hours += hours;
}
});
// Map을 배열로 변환
const projects = Array.from(projectMap.values()).map(project => ({
...project,
work_types: Array.from(project.work_types.values())
}));
// 연차/휴무 데이터가 있으면 맨 마지막에 추가
if (vacationData.total_hours > 0) {
projects.push({
project_id: 'vacation',
project_name: '연차/휴무',
job_no: '연차/휴무',
work_types: [{
work_type_id: 'vacation',
work_type_name: '-',
total_hours: vacationData.total_hours,
regular_hours: vacationData.regular_hours,
error_hours: vacationData.error_hours,
total_reports: vacationData.total_reports
}]
});
}
return { projects };
}
// recent-work 데이터로 프로젝트별 분포 테이블 렌더링
function renderProjectDistributionTableFromRecentWork(projectData, workerData) {
const tbody = document.getElementById('projectDistributionTableBody');
const tfoot = document.getElementById('projectDistributionTableFooter');
if (!tbody) return;
console.log('📋 프로젝트별 분포 테이블 렌더링 (recent-work 기반):', projectData);
if (!projectData || !projectData.projects || projectData.projects.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" style="text-align: center; padding: 2rem; color: #666;">
해당 기간에 프로젝트 데이터가 없습니다
</td>
</tr>
`;
if (tfoot) tfoot.style.display = 'none';
return;
}
let tableRows = [];
let grandTotalHours = 0;
let grandTotalManDays = 0;
let grandTotalLaborCost = 0;
const manDayRate = 350000; // 공수당 인건비
// 먼저 전체 시간 계산 (부하율 계산용)
projectData.projects.forEach(project => {
project.work_types.forEach(workType => {
grandTotalHours += workType.total_hours;
});
});
// 프로젝트별로 테이블 행 생성
projectData.projects.forEach(project => {
const projectName = project.project_name || '알 수 없는 프로젝트';
const jobNo = project.job_no || project.project_name || 'N/A';
const workTypes = project.work_types || [];
let projectTotalHours = 0;
let projectTotalManDays = 0;
let projectTotalLaborCost = 0;
if (workTypes.length === 0) {
// 작업유형이 없는 경우 (빈 프로젝트)
tableRows.push(`
<tr class="project-group">
<td class="project-name">${jobNo}</td>
<td class="work-content">데이터 없음</td>
<td class="man-days">0</td>
<td class="load-rate">0.00%</td>
<td class="labor-cost">₩0</td>
</tr>
`);
} else {
// 작업유형별로 행 생성
workTypes.forEach((workType, index) => {
const workTypeName = workType.work_type_name || `작업유형 ${workType.work_type_id}`;
const workTypeHours = workType.total_hours || 0;
const manDays = Math.round((workTypeHours / 8) * 100) / 100;
const laborCost = manDays * manDayRate;
const loadRate = grandTotalHours > 0 ? ((workTypeHours / grandTotalHours) * 100).toFixed(2) : '0.00';
projectTotalHours += workTypeHours;
projectTotalManDays += manDays;
projectTotalLaborCost += laborCost;
const isFirstWorkType = index === 0;
const rowspan = workTypes.length + 1; // +1 for subtotal row
const isVacation = project.project_id === 'vacation';
const rowClass = isVacation ? 'project-group vacation-project' : 'project-group';
// 연차/휴무는 프로젝트명만, 일반 프로젝트는 Job No.만 표시
const displayText = isVacation ? projectName : jobNo;
tableRows.push(`
<tr class="${rowClass}">
${isFirstWorkType ? `<td class="project-name" rowspan="${rowspan}">${displayText}</td>` : ''}
<td class="work-content">${workTypeName}</td>
<td class="man-days">${manDays}</td>
<td class="load-rate">${loadRate}%</td>
<td class="labor-cost">₩${laborCost.toLocaleString()}</td>
</tr>
`);
});
// 프로젝트별 합계 행 추가 (연차/휴무는 합계 행 제외)
if (project.project_id !== 'vacation') {
const projectLoadRate = grandTotalHours > 0 ? ((projectTotalHours / grandTotalHours) * 100).toFixed(2) : '0.00';
tableRows.push(`
<tr class="project-subtotal">
<td class="work-content"><strong>합계</strong></td>
<td class="man-days"><strong>${Math.round(projectTotalManDays * 100) / 100}</strong></td>
<td class="load-rate"><strong>${projectLoadRate}%</strong></td>
<td class="labor-cost"><strong>₩${projectTotalLaborCost.toLocaleString()}</strong></td>
</tr>
`);
}
}
grandTotalManDays += projectTotalManDays;
grandTotalLaborCost += projectTotalLaborCost;
});
tbody.innerHTML = tableRows.join('');
// 전체 합계 행 업데이트
document.getElementById('totalManDays').textContent = `${Math.round(grandTotalManDays * 100) / 100} 공수`;
document.getElementById('totalLaborCost').textContent = `${grandTotalLaborCost.toLocaleString()}`;
if (tfoot) {
tfoot.style.display = 'table-footer-group';
}
}
// 분석 모드 전환
function switchAnalysisMode(mode) {
// 탭 활성화 상태 변경