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:
@@ -391,14 +391,24 @@ class WorkAnalysisController {
|
|||||||
const testResults = await db.query(testQuery, [start, end]);
|
const testResults = await db.query(testQuery, [start, end]);
|
||||||
console.log('📊 데이터 확인:', testResults[0]);
|
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 = `
|
const query = `
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(p.project_id, 0) as project_id,
|
COALESCE(p.project_id, dwr.project_id) as project_id,
|
||||||
COALESCE(p.project_name, 'Unknown Project') as project_name,
|
COALESCE(p.project_name, CONCAT('프로젝트 ', dwr.project_id)) as project_name,
|
||||||
COALESCE(p.job_no, 'N/A') as job_no,
|
COALESCE(p.job_no, 'N/A') as job_no,
|
||||||
dwr.work_type_id,
|
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,
|
SUM(dwr.work_hours) as total_hours,
|
||||||
@@ -421,18 +431,22 @@ class WorkAnalysisController {
|
|||||||
) as error_rate_percent
|
) as error_rate_percent
|
||||||
|
|
||||||
FROM daily_work_reports dwr
|
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 ?
|
WHERE dwr.report_date BETWEEN ? AND ?
|
||||||
GROUP BY p.project_id, p.project_name, p.job_no, 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, dwr.work_type_id
|
ORDER BY p.project_name, wt.name
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const results = await db.query(query, [start, end]);
|
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 = {};
|
const groupedData = {};
|
||||||
|
|
||||||
results.forEach(row => {
|
results[0].forEach(row => {
|
||||||
const projectKey = `${row.project_id}_${row.project_name}`;
|
const projectKey = `${row.project_id}_${row.project_name}`;
|
||||||
|
|
||||||
if (!groupedData[projectKey]) {
|
if (!groupedData[projectKey]) {
|
||||||
@@ -476,7 +490,7 @@ class WorkAnalysisController {
|
|||||||
// 전체 요약 통계
|
// 전체 요약 통계
|
||||||
const totalStats = {
|
const totalStats = {
|
||||||
total_projects: Object.keys(groupedData).length,
|
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_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_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)
|
grand_error_hours: Object.values(groupedData).reduce((sum, p) => sum + p.total_error_hours, 0)
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ class WorkAnalysis {
|
|||||||
w.worker_name,
|
w.worker_name,
|
||||||
dwr.project_id,
|
dwr.project_id,
|
||||||
p.project_name,
|
p.project_name,
|
||||||
|
p.job_no,
|
||||||
dwr.work_type_id,
|
dwr.work_type_id,
|
||||||
wt.name as work_type_name,
|
wt.name as work_type_name,
|
||||||
dwr.work_status_id,
|
dwr.work_status_id,
|
||||||
@@ -202,7 +203,7 @@ class WorkAnalysis {
|
|||||||
dwr.created_at
|
dwr.created_at
|
||||||
FROM daily_work_reports dwr
|
FROM daily_work_reports dwr
|
||||||
LEFT JOIN workers w ON dwr.worker_id = w.worker_id
|
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_types wt ON dwr.work_type_id = wt.id
|
||||||
LEFT JOIN work_status_types wst ON dwr.work_status_id = wst.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
|
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}`,
|
worker_name: row.worker_name || `작업자 ${row.worker_id}`,
|
||||||
project_id: row.project_id,
|
project_id: row.project_id,
|
||||||
project_name: row.project_name || `프로젝트 ${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_id: row.work_type_id,
|
||||||
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
work_type_name: row.work_type_name || `작업유형 ${row.work_type_id}`,
|
||||||
work_status_id: row.work_status_id,
|
work_status_id: row.work_status_id,
|
||||||
|
|||||||
@@ -609,6 +609,152 @@ body {
|
|||||||
color: var(--gray-600);
|
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 {
|
.work-report-table .input-hours {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--success);
|
color: var(--success);
|
||||||
@@ -720,6 +866,66 @@ body {
|
|||||||
transform: none;
|
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 {
|
.chart-title {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -986,6 +1192,16 @@ body {
|
|||||||
font-size: 0.85rem;
|
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 {
|
.result-card {
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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=21">
|
<link rel="stylesheet" href="/css/work-analysis.css?v=33">
|
||||||
<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>
|
||||||
@@ -83,14 +83,26 @@
|
|||||||
<p class="loading-text">분석 중입니다...</p>
|
<p class="loading-text">분석 중입니다...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 빈 상태 -->
|
<!-- 분석 탭 네비게이션 -->
|
||||||
<div id="emptyState" class="empty-state">
|
<div id="analysisTabNavigation" class="tab-navigation" style="display: none;">
|
||||||
<div class="empty-icon">📊</div>
|
<div class="tab-buttons">
|
||||||
<h3 class="empty-title">분석을 시작해보세요</h3>
|
<button class="tab-button active" data-tab="work-status">
|
||||||
<p class="empty-description">
|
<span class="icon">📈</span>
|
||||||
기간을 설정하고 분석 실행 버튼을 클릭하여<br>
|
기간별 작업 현황
|
||||||
상세한 작업 현황 분석을 확인할 수 있습니다.
|
</button>
|
||||||
</p>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 결과 카드 그리드 -->
|
<!-- 결과 카드 그리드 -->
|
||||||
@@ -172,20 +184,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 차트 영역 -->
|
<!-- 탭 컨텐츠 영역 -->
|
||||||
<div id="chartsContainer" style="display: none;">
|
<div id="tabContentContainer" style="display: none;">
|
||||||
<!-- 기간별 작업 현황 테이블 -->
|
<!-- 기간별 작업 현황 탭 -->
|
||||||
<div class="chart-container table-type">
|
<div id="work-status-tab" class="tab-content active">
|
||||||
<div class="chart-header">
|
<div class="chart-container table-type">
|
||||||
<h3 class="chart-title">
|
<div class="chart-header">
|
||||||
<span class="icon">📈</span>
|
<h3 class="chart-title">
|
||||||
기간별 작업 현황
|
<span class="icon">📈</span>
|
||||||
</h3>
|
기간별 작업 현황
|
||||||
<button class="chart-analyze-btn" onclick="analyzeWorkStatus()" disabled>
|
</h3>
|
||||||
<span class="icon">🔍</span>
|
<button class="chart-analyze-btn" onclick="analyzeWorkStatus()" disabled>
|
||||||
분석 실행
|
<span class="icon">🔍</span>
|
||||||
</button>
|
분석 실행
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="work-report-table" id="workReportTable">
|
<table class="work-report-table" id="workReportTable">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -217,51 +230,85 @@
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 프로젝트별 작업 분포 차트 -->
|
<!-- 프로젝트별 작업 분포 탭 -->
|
||||||
<div class="chart-container chart-type">
|
<div id="project-distribution-tab" class="tab-content">
|
||||||
<div class="chart-header">
|
<div class="chart-container table-type">
|
||||||
<h3 class="chart-title">
|
<div class="chart-header">
|
||||||
<span class="icon">🥧</span>
|
<h3 class="chart-title">
|
||||||
프로젝트별 작업 분포
|
<span class="icon">📋</span>
|
||||||
</h3>
|
프로젝트별 작업 분포
|
||||||
<button class="chart-analyze-btn" onclick="analyzeProjectDistribution()" disabled>
|
</h3>
|
||||||
<span class="icon">🔍</span>
|
<button class="chart-analyze-btn" onclick="analyzeProjectDistribution()" disabled>
|
||||||
분석 실행
|
<span class="icon">🔍</span>
|
||||||
</button>
|
분석 실행
|
||||||
|
</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>
|
</div>
|
||||||
<canvas id="projectDistributionChart" class="chart-canvas"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 작업자별 성과 차트 -->
|
<!-- 작업자별 성과 탭 -->
|
||||||
<div class="chart-container chart-type">
|
<div id="worker-performance-tab" class="tab-content">
|
||||||
<div class="chart-header">
|
<div class="chart-container chart-type">
|
||||||
<h3 class="chart-title">
|
<div class="chart-header">
|
||||||
<span class="icon">👤</span>
|
<h3 class="chart-title">
|
||||||
작업자별 성과
|
<span class="icon">👤</span>
|
||||||
</h3>
|
작업자별 성과
|
||||||
<button class="chart-analyze-btn" onclick="analyzeWorkerPerformance()" disabled>
|
</h3>
|
||||||
<span class="icon">🔍</span>
|
<button class="chart-analyze-btn" onclick="analyzeWorkerPerformance()" disabled>
|
||||||
분석 실행
|
<span class="icon">🔍</span>
|
||||||
</button>
|
분석 실행
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<canvas id="workerPerformanceChart" class="chart-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="workerPerformanceChart" class="chart-canvas"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 오류 분석 차트 -->
|
<!-- 오류 분석 탭 -->
|
||||||
<div class="chart-container chart-type">
|
<div id="error-analysis-tab" class="tab-content">
|
||||||
<div class="chart-header">
|
<div class="chart-container chart-type">
|
||||||
<h3 class="chart-title">
|
<div class="chart-header">
|
||||||
<span class="icon">⚠️</span>
|
<h3 class="chart-title">
|
||||||
오류 유형별 분석
|
<span class="icon">⚠️</span>
|
||||||
</h3>
|
오류 유형별 분석
|
||||||
<button class="chart-analyze-btn" onclick="analyzeErrorTypes()" disabled>
|
</h3>
|
||||||
<span class="icon">🔍</span>
|
<button class="chart-analyze-btn" onclick="analyzeErrorTypes()" disabled>
|
||||||
분석 실행
|
<span class="icon">🔍</span>
|
||||||
</button>
|
분석 실행
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<canvas id="errorAnalysisChart" class="chart-canvas"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="errorAnalysisChart" class="chart-canvas"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -406,12 +453,47 @@
|
|||||||
btn.disabled = false;
|
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}`);
|
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() {
|
async function analyzeWorkStatus() {
|
||||||
if (!isAnalysisEnabled) {
|
if (!isAnalysisEnabled) {
|
||||||
@@ -437,16 +519,31 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🥧 프로젝트별 작업 분포 분석 시작');
|
console.log('📋 프로젝트별 작업 분포 분석 시작');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
start: confirmedStartDate,
|
start: confirmedStartDate,
|
||||||
end: confirmedEndDate
|
end: confirmedEndDate,
|
||||||
|
limit: 2000
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await apiCall(`/work-analysis/project-stats?${params}`, 'GET');
|
// 기간별 작업 현황 API를 사용해서 프로젝트별 데이터 가져오기
|
||||||
renderProjectDistributionChart(response.data);
|
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('✅ 프로젝트별 작업 분포 분석 완료');
|
console.log('✅ 프로젝트별 작업 분포 분석 완료');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 프로젝트별 작업 분포 분석 오류:', error);
|
console.error('❌ 프로젝트별 작업 분포 분석 오류:', error);
|
||||||
@@ -499,7 +596,430 @@
|
|||||||
alert('오류 유형별 분석에 실패했습니다.');
|
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) {
|
function switchAnalysisMode(mode) {
|
||||||
// 탭 활성화 상태 변경
|
// 탭 활성화 상태 변경
|
||||||
|
|||||||
Reference in New Issue
Block a user