feat(frontend): 작업 보고서 뷰어 기능 전체 리팩토링
- 765줄의 daily-report-viewer.js를 API, UI, Export, Controller의 4개 모듈로 분리 - 관심사 분리를 통해 코드의 가독성, 테스트 용이성, 유지보수성을 극적으로 향상 - 프로젝트 전반의 코드 일관성 확보 및 레거시 로직 제거
This commit is contained in:
@@ -1,765 +1,81 @@
|
|||||||
// daily-report-viewer.js - 통합 API 설정 적용 버전
|
// /js/daily-report-viewer.js
|
||||||
|
|
||||||
// =================================================================
|
import { fetchReportData } from './report-viewer-api.js';
|
||||||
// 🌐 통합 API 설정 import
|
import { renderReport, processReportData, showLoading, showError } from './report-viewer-ui.js';
|
||||||
// =================================================================
|
import { exportToExcel, printReport } from './report-viewer-export.js';
|
||||||
import { API, getAuthHeaders, apiCall } from '/js/api-config.js';
|
import { getUser } from './auth.js';
|
||||||
|
|
||||||
// =================================================================
|
// 전역 상태: 현재 화면에 표시된 데이터
|
||||||
// 🌐 전역 변수 및 기본 설정
|
let currentProcessedData = null;
|
||||||
// =================================================================
|
|
||||||
let currentReportData = null;
|
|
||||||
let workTypes = [];
|
|
||||||
let workStatusTypes = [];
|
|
||||||
let errorTypes = [];
|
|
||||||
|
|
||||||
// =================================================================
|
/**
|
||||||
// 🔧 유틸리티 함수들 (입력 페이지와 동일)
|
* 날짜를 기준으로 보고서를 검색하고 화면에 렌더링합니다.
|
||||||
// =================================================================
|
*/
|
||||||
|
async function searchReports() {
|
||||||
|
const dateInput = document.getElementById('reportDate');
|
||||||
|
const selectedDate = dateInput.value;
|
||||||
|
|
||||||
// 현재 로그인한 사용자 정보 가져오기
|
if (!selectedDate) {
|
||||||
function getCurrentUser() {
|
showError('날짜를 선택해주세요.');
|
||||||
try {
|
return;
|
||||||
const token = localStorage.getItem('token');
|
}
|
||||||
if (!token) return null;
|
|
||||||
|
showLoading(true);
|
||||||
const payloadBase64 = token.split('.')[1];
|
currentProcessedData = null; // 새 검색이 시작되면 이전 데이터 초기화
|
||||||
if (payloadBase64) {
|
|
||||||
const payload = JSON.parse(atob(payloadBase64));
|
try {
|
||||||
console.log('토큰에서 추출한 사용자 정보:', payload);
|
const rawData = await fetchReportData(selectedDate);
|
||||||
return payload;
|
currentProcessedData = processReportData(rawData, selectedDate);
|
||||||
}
|
renderReport(currentProcessedData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('토큰에서 사용자 정보 추출 실패:', error);
|
showError(error.message);
|
||||||
}
|
renderReport(null); // 에러 발생 시 데이터 없는 화면 표시
|
||||||
|
} finally {
|
||||||
try {
|
showLoading(false);
|
||||||
const userInfo = localStorage.getItem('user') || localStorage.getItem('userInfo') || localStorage.getItem('currentUser');
|
}
|
||||||
if (userInfo) {
|
|
||||||
const parsed = JSON.parse(userInfo);
|
|
||||||
console.log('localStorage에서 가져온 사용자 정보:', parsed);
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('localStorage에서 사용자 정보 가져오기 실패:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 한국 시간 기준 오늘 날짜 가져기기
|
/**
|
||||||
function getKoreaToday() {
|
* 페이지의 모든 이벤트 리스너를 설정합니다.
|
||||||
const today = new Date();
|
*/
|
||||||
const year = today.getFullYear();
|
function setupEventListeners() {
|
||||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
document.getElementById('searchBtn')?.addEventListener('click', searchReports);
|
||||||
const day = String(today.getDate()).padStart(2, '0');
|
document.getElementById('todayBtn')?.addEventListener('click', () => {
|
||||||
return `${year}-${month}-${day}`;
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('reportDate').value = today;
|
||||||
|
searchReports();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('reportDate')?.addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') searchReports();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('exportExcelBtn')?.addEventListener('click', () => {
|
||||||
|
exportToExcel(currentProcessedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('printBtn')?.addEventListener('click', printReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 권한 확인 함수 (수정된 버전)
|
/**
|
||||||
function checkUserPermission(user) {
|
* 페이지가 처음 로드될 때 실행되는 초기화 함수
|
||||||
if (!user || !user.access_level) {
|
*/
|
||||||
return { level: 'none', canViewAll: false, description: '권한 없음' };
|
function initializePage() {
|
||||||
}
|
// auth.js를 사용하여 인증 상태 확인
|
||||||
|
const user = getUser();
|
||||||
const accessLevel = user.access_level.toLowerCase();
|
if (!user) {
|
||||||
|
showError('로그인이 필요합니다. 2초 후 로그인 페이지로 이동합니다.');
|
||||||
// 🎯 권한 레벨 정의 (더 유연하게)
|
|
||||||
if (accessLevel === 'system' || accessLevel === 'admin') {
|
|
||||||
return {
|
|
||||||
level: 'admin',
|
|
||||||
canViewAll: true,
|
|
||||||
description: '시스템/관리자 (전체 조회 시도 → 실패 시 본인 데이터)'
|
|
||||||
};
|
|
||||||
} else if (accessLevel === 'manager' || accessLevel === 'group_leader' || accessLevel === '그룹장') {
|
|
||||||
return {
|
|
||||||
level: 'manager',
|
|
||||||
canViewAll: false,
|
|
||||||
description: '그룹장 (본인 입력 데이터만)'
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
level: 'user',
|
|
||||||
canViewAll: false,
|
|
||||||
description: '일반 사용자 (본인 입력 데이터만)'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🚀 초기화 및 이벤트 설정
|
|
||||||
// =================================================================
|
|
||||||
document.addEventListener('DOMContentLoaded', async function() {
|
|
||||||
console.log('🔥 ===== 통합 API 설정 적용 일일보고서 뷰어 시작 =====');
|
|
||||||
|
|
||||||
// 사용자 정보 및 권한 확인
|
|
||||||
const userInfo = getCurrentUser();
|
|
||||||
const permission = checkUserPermission(userInfo);
|
|
||||||
|
|
||||||
console.log('👤 사용자 정보:', userInfo);
|
|
||||||
console.log('🔐 권한 정보:', permission);
|
|
||||||
|
|
||||||
// 토큰 확인
|
|
||||||
const mainToken = localStorage.getItem('token');
|
|
||||||
if (!mainToken) {
|
|
||||||
console.error('❌ 토큰이 없습니다.');
|
|
||||||
alert('로그인이 필요합니다.');
|
|
||||||
setTimeout(() => window.location.href = '/index.html', 2000);
|
setTimeout(() => window.location.href = '/index.html', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
showMessage('시스템을 초기화하는 중...', 'loading');
|
|
||||||
|
|
||||||
// 기본 설정
|
|
||||||
setupEventListeners();
|
|
||||||
setTodayDate();
|
|
||||||
|
|
||||||
// 마스터 데이터 로드
|
|
||||||
await loadMasterData();
|
|
||||||
|
|
||||||
// 권한 표시
|
|
||||||
displayUserPermission(permission);
|
|
||||||
|
|
||||||
hideMessage();
|
|
||||||
console.log('✅ 초기화 완료!');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 초기화 실패:', error);
|
|
||||||
showError(`초기화 오류: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function setupEventListeners() {
|
setupEventListeners();
|
||||||
document.getElementById('searchBtn')?.addEventListener('click', searchReports);
|
|
||||||
document.getElementById('todayBtn')?.addEventListener('click', setTodayDate);
|
|
||||||
document.getElementById('reportDate')?.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
searchReports();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.getElementById('exportExcelBtn')?.addEventListener('click', exportToExcel);
|
|
||||||
document.getElementById('printBtn')?.addEventListener('click', printReport);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTodayDate() {
|
// 페이지 로드 시 오늘 날짜로 자동 검색
|
||||||
const today = getKoreaToday();
|
|
||||||
const dateInput = document.getElementById('reportDate');
|
const dateInput = document.getElementById('reportDate');
|
||||||
|
dateInput.value = new Date().toISOString().split('T')[0];
|
||||||
if (dateInput) {
|
searchReports();
|
||||||
dateInput.value = today;
|
|
||||||
searchReports();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 권한 표시 함수 (더 상세하게)
|
// DOM이 로드되면 페이지 초기화를 시작합니다.
|
||||||
function displayUserPermission(permission) {
|
document.addEventListener('DOMContentLoaded', initializePage);
|
||||||
// 권한 정보를 UI에 표시
|
|
||||||
const headerElement = document.querySelector('h1');
|
|
||||||
if (headerElement) {
|
|
||||||
headerElement.innerHTML += ` <small style="color: #666; font-size: 0.6em;">(${permission.description})</small>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`🔐 현재 권한: ${permission.description}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 📊 마스터 데이터 로드 (통합 API 사용)
|
|
||||||
// =================================================================
|
|
||||||
async function loadMasterData() {
|
|
||||||
try {
|
|
||||||
console.log('📋 마스터 데이터 로딩...');
|
|
||||||
|
|
||||||
await loadWorkTypes();
|
|
||||||
await loadWorkStatusTypes();
|
|
||||||
await loadErrorTypes();
|
|
||||||
|
|
||||||
console.log('✅ 마스터 데이터 로드 완료');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 마스터 데이터 로드 실패:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadWorkTypes() {
|
|
||||||
try {
|
|
||||||
const data = await apiCall(`${API}/daily-work-reports/work-types`);
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
workTypes = data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('API 실패');
|
|
||||||
} catch (error) {
|
|
||||||
workTypes = [
|
|
||||||
{id: 1, name: 'Base'},
|
|
||||||
{id: 2, name: 'Vessel'},
|
|
||||||
{id: 3, name: 'Piping'}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadWorkStatusTypes() {
|
|
||||||
try {
|
|
||||||
const data = await apiCall(`${API}/daily-work-reports/work-status-types`);
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
workStatusTypes = data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('API 실패');
|
|
||||||
} catch (error) {
|
|
||||||
workStatusTypes = [
|
|
||||||
{id: 1, name: '정규'},
|
|
||||||
{id: 2, name: '에러'}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadErrorTypes() {
|
|
||||||
try {
|
|
||||||
const data = await apiCall(`${API}/daily-work-reports/error-types`);
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
errorTypes = data;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('API 실패');
|
|
||||||
} catch (error) {
|
|
||||||
errorTypes = [
|
|
||||||
{id: 1, name: '설계미스'},
|
|
||||||
{id: 2, name: '외주작업 불량'},
|
|
||||||
{id: 3, name: '입고지연'},
|
|
||||||
{id: 4, name: '작업 불량'}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🔍 스마트 권한별 데이터 조회 시스템 (통합 API 사용)
|
|
||||||
// =================================================================
|
|
||||||
async function searchReports() {
|
|
||||||
const selectedDate = document.getElementById('reportDate')?.value;
|
|
||||||
|
|
||||||
if (!selectedDate) {
|
|
||||||
showError('날짜를 선택해 주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n🔍 ===== ${selectedDate} 스마트 권한별 조회 시작 =====`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
hideAllMessages();
|
|
||||||
showLoading(true);
|
|
||||||
|
|
||||||
const currentUser = getCurrentUser();
|
|
||||||
const permission = checkUserPermission(currentUser);
|
|
||||||
|
|
||||||
console.log('🔐 권한 확인:', permission);
|
|
||||||
|
|
||||||
let data = [];
|
|
||||||
let queryMethod = '';
|
|
||||||
|
|
||||||
if (permission.canViewAll) {
|
|
||||||
// 🌍 관리자/시스템: 전체 데이터 조회 시도 → 실패 시 본인 데이터로 폴백
|
|
||||||
console.log('🌍 관리자 권한으로 전체 데이터 조회 시도');
|
|
||||||
data = await fetchAllDataWithFallback(selectedDate, currentUser);
|
|
||||||
queryMethod = '관리자 권한 (폴백 포함)';
|
|
||||||
} else {
|
|
||||||
// 🔒 일반 사용자/그룹장: 처음부터 본인 데이터만 조회
|
|
||||||
console.log('🔒 제한 권한으로 본인 데이터만 조회');
|
|
||||||
data = await fetchMyData(selectedDate, currentUser);
|
|
||||||
queryMethod = '제한 권한 (본인 데이터만)';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📊 최종 조회된 데이터: ${data.length}개`);
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
const processedData = processRawData(data, selectedDate);
|
|
||||||
currentReportData = processedData;
|
|
||||||
displayReportData(processedData);
|
|
||||||
showExportSection(true);
|
|
||||||
|
|
||||||
showMessage(`${queryMethod}으로 ${data.length}개 데이터를 표시했습니다.`, 'success');
|
|
||||||
} else {
|
|
||||||
const helpMessage = permission.canViewAll ?
|
|
||||||
'전체 조회 및 본인 데이터 조회 모두 실패했습니다.' :
|
|
||||||
'해당 날짜에 본인이 입력한 데이터가 없습니다.';
|
|
||||||
|
|
||||||
showNoDataWithHelp(selectedDate, helpMessage);
|
|
||||||
showExportSection(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 조회 오류:', error);
|
|
||||||
showError(`데이터 조회 오류: ${error.message}`);
|
|
||||||
showExportSection(false);
|
|
||||||
} finally {
|
|
||||||
showLoading(false);
|
|
||||||
console.log('🔍 ===== 조회 완료 =====\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전체 데이터 조회 + 본인 데이터 폴백 (시스템/관리자용) - 통합 API 사용
|
|
||||||
async function fetchAllDataWithFallback(selectedDate, currentUser) {
|
|
||||||
console.log('📡 전체 데이터 조회 시도 (폴백 지원)');
|
|
||||||
|
|
||||||
// 1단계: 전체 데이터 조회 시도
|
|
||||||
const allData = await fetchAllData(selectedDate);
|
|
||||||
if (allData.length > 0) {
|
|
||||||
console.log(`✅ 전체 데이터 조회 성공: ${allData.length}개`);
|
|
||||||
return allData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2단계: 전체 조회 실패 시 본인 데이터로 폴백
|
|
||||||
console.log('⚠️ 전체 조회 실패, 본인 데이터로 폴백');
|
|
||||||
const myData = await fetchMyData(selectedDate, currentUser);
|
|
||||||
if (myData.length > 0) {
|
|
||||||
console.log(`✅ 폴백 성공: 본인 데이터 ${myData.length}개`);
|
|
||||||
showMessage('⚠️ 전체 조회 권한이 없어 본인 입력 데이터만 표시합니다.', 'warning');
|
|
||||||
return myData;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('❌ 전체 조회 및 폴백 모두 실패');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전체 데이터 조회 (시스템/관리자용) - 통합 API 사용
|
|
||||||
async function fetchAllData(selectedDate) {
|
|
||||||
console.log('📡 전체 데이터 API 호출');
|
|
||||||
|
|
||||||
// 여러 방법으로 시도
|
|
||||||
const endpoints = [
|
|
||||||
`/daily-work-reports?date=${selectedDate}`,
|
|
||||||
`/daily-work-reports/date/${selectedDate}`
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const endpoint of endpoints) {
|
|
||||||
try {
|
|
||||||
console.log(`🔍 시도: ${API}${endpoint}`);
|
|
||||||
|
|
||||||
const rawData = await apiCall(`${API}${endpoint}`);
|
|
||||||
let data = Array.isArray(rawData) ? rawData : (rawData?.data || []);
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
console.log(`✅ 전체 조회 성공: ${data.length}개 데이터`);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ 오류: ${error.message}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('❌ 모든 전체 조회 방법 실패');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 본인 데이터 조회 (모든 사용자 공통) - 통합 API 사용
|
|
||||||
async function fetchMyData(selectedDate, currentUser) {
|
|
||||||
console.log('📡 본인 데이터 API 호출');
|
|
||||||
|
|
||||||
if (!currentUser?.user_id && !currentUser?.id) {
|
|
||||||
console.error('❌ 사용자 ID가 없습니다');
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = currentUser.user_id || currentUser.id;
|
|
||||||
|
|
||||||
console.log(`🔍 본인 데이터 URL: ${API}/daily-work-reports?date=${selectedDate}&created_by=${userId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rawData = await apiCall(`${API}/daily-work-reports?date=${selectedDate}&created_by=${userId}`);
|
|
||||||
let data = Array.isArray(rawData) ? rawData : (rawData?.data || []);
|
|
||||||
|
|
||||||
console.log(`✅ 본인 데이터: ${data.length}개`);
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 본인 데이터 조회 오류:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 원시 데이터를 구조화된 형태로 변환
|
|
||||||
function processRawData(rawData, selectedDate) {
|
|
||||||
console.log('🔄 데이터 구조 변환 시작');
|
|
||||||
|
|
||||||
if (!Array.isArray(rawData) || rawData.length === 0) {
|
|
||||||
return {
|
|
||||||
summary: {
|
|
||||||
date: selectedDate,
|
|
||||||
total_workers: 0,
|
|
||||||
total_hours: 0,
|
|
||||||
total_entries: 0,
|
|
||||||
error_count: 0
|
|
||||||
},
|
|
||||||
workers: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 작업자별로 그룹화
|
|
||||||
const workerGroups = {};
|
|
||||||
let totalHours = 0;
|
|
||||||
let errorCount = 0;
|
|
||||||
|
|
||||||
rawData.forEach(item => {
|
|
||||||
const workerName = item.worker_name || '미지정';
|
|
||||||
const workHours = parseFloat(item.work_hours || 0);
|
|
||||||
totalHours += workHours;
|
|
||||||
|
|
||||||
if (item.work_status_id === 2) {
|
|
||||||
errorCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!workerGroups[workerName]) {
|
|
||||||
workerGroups[workerName] = {
|
|
||||||
worker_name: workerName,
|
|
||||||
worker_id: item.worker_id,
|
|
||||||
total_hours: 0,
|
|
||||||
work_entries: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
workerGroups[workerName].total_hours += workHours;
|
|
||||||
workerGroups[workerName].work_entries.push({
|
|
||||||
project_name: item.project_name,
|
|
||||||
work_type_name: item.work_type_name,
|
|
||||||
work_status_name: item.work_status_name,
|
|
||||||
error_type_name: item.error_type_name,
|
|
||||||
work_hours: workHours,
|
|
||||||
work_status_id: item.work_status_id,
|
|
||||||
created_by_name: item.created_by_name || '입력자 미지정'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const processedData = {
|
|
||||||
summary: {
|
|
||||||
date: selectedDate,
|
|
||||||
total_workers: Object.keys(workerGroups).length,
|
|
||||||
total_hours: totalHours,
|
|
||||||
total_entries: rawData.length,
|
|
||||||
error_count: errorCount
|
|
||||||
},
|
|
||||||
workers: Object.values(workerGroups)
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('✅ 데이터 변환 완료:', {
|
|
||||||
작업자수: processedData.workers.length,
|
|
||||||
총항목수: rawData.length,
|
|
||||||
총시간: totalHours,
|
|
||||||
에러수: errorCount
|
|
||||||
});
|
|
||||||
|
|
||||||
return processedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🎨 UI 표시 함수들 (기존과 동일)
|
|
||||||
// =================================================================
|
|
||||||
function displayReportData(data) {
|
|
||||||
console.log('🎨 리포트 데이터 표시');
|
|
||||||
|
|
||||||
displaySummary(data.summary);
|
|
||||||
displayWorkersDetails(data.workers);
|
|
||||||
|
|
||||||
document.getElementById('reportSummary').style.display = 'block';
|
|
||||||
document.getElementById('workersReport').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
function displaySummary(summary) {
|
|
||||||
const elements = {
|
|
||||||
totalWorkers: summary?.total_workers || 0,
|
|
||||||
totalHours: `${summary?.total_hours || 0}시간`,
|
|
||||||
totalEntries: `${summary?.total_entries || 0}개`,
|
|
||||||
errorCount: `${summary?.error_count || 0}개`
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.entries(elements).forEach(([id, value]) => {
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
if (element) element.textContent = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 에러 카드 스타일링
|
|
||||||
const errorCard = document.querySelector('.summary-card.error-card');
|
|
||||||
if (errorCard) {
|
|
||||||
const hasErrors = (summary?.error_count || 0) > 0;
|
|
||||||
errorCard.style.borderLeftColor = hasErrors ? '#e74c3c' : '#28a745';
|
|
||||||
errorCard.style.backgroundColor = hasErrors ? '#fff5f5' : '#f8fff9';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayWorkersDetails(workers) {
|
|
||||||
const workersList = document.getElementById('workersList');
|
|
||||||
if (!workersList) return;
|
|
||||||
|
|
||||||
workersList.innerHTML = '';
|
|
||||||
|
|
||||||
workers.forEach(worker => {
|
|
||||||
const workerCard = createWorkerCard(worker);
|
|
||||||
workersList.appendChild(workerCard);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWorkerCard(worker) {
|
|
||||||
const workerDiv = document.createElement('div');
|
|
||||||
workerDiv.className = 'worker-card';
|
|
||||||
|
|
||||||
const workerHeader = document.createElement('div');
|
|
||||||
workerHeader.className = 'worker-header';
|
|
||||||
workerHeader.innerHTML = `
|
|
||||||
<div class="worker-name">👤 ${worker.worker_name || '미지정'}</div>
|
|
||||||
<div class="worker-total-hours">총 ${worker.total_hours || 0}시간</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const workEntries = document.createElement('div');
|
|
||||||
workEntries.className = 'work-entries';
|
|
||||||
|
|
||||||
if (worker.work_entries && Array.isArray(worker.work_entries)) {
|
|
||||||
worker.work_entries.forEach(entry => {
|
|
||||||
const entryDiv = createWorkEntryCard(entry);
|
|
||||||
workEntries.appendChild(entryDiv);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
workerDiv.appendChild(workerHeader);
|
|
||||||
workerDiv.appendChild(workEntries);
|
|
||||||
|
|
||||||
return workerDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createWorkEntryCard(entry) {
|
|
||||||
const entryDiv = document.createElement('div');
|
|
||||||
entryDiv.className = 'work-entry';
|
|
||||||
|
|
||||||
if (entry.work_status_id === 2) {
|
|
||||||
entryDiv.classList.add('error-entry');
|
|
||||||
}
|
|
||||||
|
|
||||||
const entryHeader = document.createElement('div');
|
|
||||||
entryHeader.className = 'entry-header';
|
|
||||||
entryHeader.innerHTML = `
|
|
||||||
<div class="project-name">${entry.project_name || '프로젝트 미지정'}</div>
|
|
||||||
<div class="work-hours">${entry.work_hours || 0}시간</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const entryDetails = document.createElement('div');
|
|
||||||
entryDetails.className = 'entry-details';
|
|
||||||
|
|
||||||
const details = [
|
|
||||||
['작업 유형', entry.work_type_name || '-'],
|
|
||||||
['작업 상태', entry.work_status_name || '정상'],
|
|
||||||
['입력자', entry.created_by_name || '미지정']
|
|
||||||
];
|
|
||||||
|
|
||||||
if (entry.work_status_id === 2 && entry.error_type_name) {
|
|
||||||
details.push(['에러 유형', entry.error_type_name, 'error-type']);
|
|
||||||
}
|
|
||||||
|
|
||||||
details.forEach(([label, value, valueClass]) => {
|
|
||||||
const detailRow = createDetailRow(label, value, valueClass);
|
|
||||||
entryDetails.appendChild(detailRow);
|
|
||||||
});
|
|
||||||
|
|
||||||
entryDiv.appendChild(entryHeader);
|
|
||||||
entryDiv.appendChild(entryDetails);
|
|
||||||
|
|
||||||
return entryDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDetailRow(label, value, valueClass = '') {
|
|
||||||
const detailDiv = document.createElement('div');
|
|
||||||
detailDiv.className = 'entry-detail';
|
|
||||||
detailDiv.innerHTML = `
|
|
||||||
<span class="detail-label">${label}:</span>
|
|
||||||
<span class="detail-value ${valueClass}">${value}</span>
|
|
||||||
`;
|
|
||||||
return detailDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🎭 UI 상태 관리
|
|
||||||
// =================================================================
|
|
||||||
function showLoading(show) {
|
|
||||||
const spinner = document.getElementById('loadingSpinner');
|
|
||||||
if (spinner) {
|
|
||||||
spinner.style.display = show ? 'flex' : 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
const errorDiv = document.getElementById('errorMessage');
|
|
||||||
if (errorDiv) {
|
|
||||||
const errorText = errorDiv.querySelector('.error-text');
|
|
||||||
if (errorText) errorText.textContent = message;
|
|
||||||
errorDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessage(message, type = 'info') {
|
|
||||||
const messageContainer = document.getElementById('message-container');
|
|
||||||
if (messageContainer) {
|
|
||||||
messageContainer.innerHTML = `<div class="message ${type}">${message}</div>`;
|
|
||||||
|
|
||||||
if (type === 'success' || type === 'info') {
|
|
||||||
setTimeout(() => {
|
|
||||||
messageContainer.innerHTML = '';
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`📢 ${type.toUpperCase()}: ${message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideMessage() {
|
|
||||||
const messageContainer = document.getElementById('message-container');
|
|
||||||
if (messageContainer) {
|
|
||||||
messageContainer.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showNoDataWithHelp(selectedDate, helpMessage = '해당 날짜에 데이터가 없습니다.') {
|
|
||||||
const noDataDiv = document.getElementById('noDataMessage');
|
|
||||||
if (noDataDiv) {
|
|
||||||
noDataDiv.innerHTML = `
|
|
||||||
<div class="no-data-content">
|
|
||||||
<span class="no-data-icon">📭</span>
|
|
||||||
<h3>${selectedDate} 작업보고서가 없습니다</h3>
|
|
||||||
<div class="help-section">
|
|
||||||
<p><strong>💡 ${helpMessage}</strong></p>
|
|
||||||
<ul style="text-align: left; margin: 10px 0;">
|
|
||||||
<li>다른 날짜를 선택해보세요 (예: ${getKoreaToday()})</li>
|
|
||||||
<li><a href="/pages/common/daily-work-report.html" target="_blank" style="color: #3498db;">📝 작업보고서 입력 페이지</a>에서 데이터를 먼저 입력해보세요</li>
|
|
||||||
<li>입력 후 잠시 기다린 다음 다시 시도해보세요</li>
|
|
||||||
</ul>
|
|
||||||
<p style="margin-top: 15px;">
|
|
||||||
<button onclick="window.location.reload()" style="padding: 8px 16px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
|
||||||
🔄 새로고침
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
noDataDiv.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showExportSection(show) {
|
|
||||||
const exportSection = document.getElementById('exportSection');
|
|
||||||
if (exportSection) {
|
|
||||||
exportSection.style.display = show ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideAllMessages() {
|
|
||||||
const elements = [
|
|
||||||
'errorMessage',
|
|
||||||
'noDataMessage',
|
|
||||||
'reportSummary',
|
|
||||||
'workersReport'
|
|
||||||
];
|
|
||||||
|
|
||||||
elements.forEach(id => {
|
|
||||||
const element = document.getElementById(id);
|
|
||||||
if (element) element.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 📤 내보내기 기능
|
|
||||||
// =================================================================
|
|
||||||
function exportToExcel() {
|
|
||||||
if (!currentReportData?.workers?.length) {
|
|
||||||
alert('내보낼 데이터가 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('📊 Excel 내보내기 시작');
|
|
||||||
|
|
||||||
try {
|
|
||||||
let csvContent = "\uFEFF작업자명,프로젝트명,작업유형,작업상태,에러유형,작업시간,입력자\n";
|
|
||||||
|
|
||||||
currentReportData.workers.forEach(worker => {
|
|
||||||
if (worker.work_entries && Array.isArray(worker.work_entries)) {
|
|
||||||
worker.work_entries.forEach(entry => {
|
|
||||||
const row = [
|
|
||||||
worker.worker_name || '',
|
|
||||||
entry.project_name || '',
|
|
||||||
entry.work_type_name || '',
|
|
||||||
entry.work_status_name || '',
|
|
||||||
entry.error_type_name || '',
|
|
||||||
entry.work_hours || 0,
|
|
||||||
entry.created_by_name || ''
|
|
||||||
].map(field => `"${String(field).replace(/"/g, '""')}"`).join(',');
|
|
||||||
|
|
||||||
csvContent += row + "\n";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
||||||
const link = document.createElement('a');
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
const fileName = `작업보고서_${currentReportData.summary?.date || '날짜미지정'}.csv`;
|
|
||||||
|
|
||||||
link.setAttribute('href', url);
|
|
||||||
link.setAttribute('download', fileName);
|
|
||||||
link.style.visibility = 'hidden';
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
console.log('✅ Excel 내보내기 완료');
|
|
||||||
showMessage('Excel 파일이 다운로드되었습니다.', 'success');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Excel 내보내기 실패:', error);
|
|
||||||
showError('Excel 내보내기 중 오류가 발생했습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function printReport() {
|
|
||||||
console.log('🖨️ 인쇄 시작');
|
|
||||||
|
|
||||||
if (!currentReportData?.workers?.length) {
|
|
||||||
alert('인쇄할 데이터가 없습니다.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
window.print();
|
|
||||||
console.log('✅ 인쇄 대화상자 표시');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 인쇄 실패:', error);
|
|
||||||
showError('인쇄 중 오류가 발생했습니다.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================================
|
|
||||||
// 🔄 전역 함수 및 디버깅
|
|
||||||
// =================================================================
|
|
||||||
|
|
||||||
// 개발 모드 디버깅
|
|
||||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
||||||
console.log('🐛 개발 모드 활성화');
|
|
||||||
|
|
||||||
window.DEBUG = {
|
|
||||||
currentReportData,
|
|
||||||
getCurrentUser,
|
|
||||||
checkUserPermission,
|
|
||||||
fetchAllData,
|
|
||||||
fetchMyData,
|
|
||||||
fetchAllDataWithFallback,
|
|
||||||
searchReports
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 전역 함수 노출
|
|
||||||
window.searchReports = searchReports;
|
|
||||||
window.exportToExcel = exportToExcel;
|
|
||||||
window.printReport = printReport;
|
|
||||||
|
|
||||||
// 페이지 정리
|
|
||||||
window.addEventListener('beforeunload', function() {
|
|
||||||
console.log('📋 페이지 종료');
|
|
||||||
});
|
|
||||||
91
web-ui/js/report-viewer-api.js
Normal file
91
web-ui/js/report-viewer-api.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// /js/report-viewer-api.js
|
||||||
|
import { apiGet } from './api-helper.js';
|
||||||
|
import { getUser } from './auth.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보고서 조회를 위한 마스터 데이터를 로드합니다. (작업 유형, 상태 등)
|
||||||
|
* 실패 시 기본값을 반환할 수 있도록 개별적으로 처리합니다.
|
||||||
|
* @returns {Promise<object>} - 각 마스터 데이터 배열을 포함하는 객체
|
||||||
|
*/
|
||||||
|
export async function loadMasterData() {
|
||||||
|
const masterData = {
|
||||||
|
workTypes: [],
|
||||||
|
workStatusTypes: [],
|
||||||
|
errorTypes: []
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
// Promise.allSettled를 사용해 일부 API가 실패해도 전체가 중단되지 않도록 함
|
||||||
|
const results = await Promise.allSettled([
|
||||||
|
apiGet('/daily-work-reports/work-types'),
|
||||||
|
apiGet('/daily-work-reports/work-status-types'),
|
||||||
|
apiGet('/daily-work-reports/error-types')
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (results[0].status === 'fulfilled') masterData.workTypes = results[0].value;
|
||||||
|
if (results[1].status === 'fulfilled') masterData.workStatusTypes = results[1].value;
|
||||||
|
if (results[2].status === 'fulfilled') masterData.errorTypes = results[2].value;
|
||||||
|
|
||||||
|
return masterData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('마스터 데이터 로딩 중 심각한 오류 발생:', error);
|
||||||
|
// 최소한의 기본값이라도 반환
|
||||||
|
return masterData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사용자의 권한을 확인하여 적절한 API 엔드포인트와 파라미터를 결정합니다.
|
||||||
|
* @param {string} selectedDate - 조회할 날짜
|
||||||
|
* @returns {string} - 호출할 API URL
|
||||||
|
*/
|
||||||
|
function getReportApiUrl(selectedDate) {
|
||||||
|
const user = getUser();
|
||||||
|
|
||||||
|
// 관리자(admin, system)는 모든 데이터를 조회
|
||||||
|
if (user && (user.role === 'admin' || user.role === 'system')) {
|
||||||
|
// 백엔드에서 GET /daily-work-reports?date=YYYY-MM-DD 요청 시
|
||||||
|
// 권한을 확인하고 모든 데이터를 내려준다고 가정
|
||||||
|
return `/daily-work-reports?date=${selectedDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 그 외 사용자(leader, user)는 본인이 생성한 데이터만 조회
|
||||||
|
// 백엔드에서 동일한 엔드포인트로 요청 시, 권한을 확인하고
|
||||||
|
// 본인 데이터만 필터링해서 내려준다고 가정
|
||||||
|
// (만약 엔드포인트가 다르다면 이 부분을 수정해야 함)
|
||||||
|
return `/daily-work-reports?date=${selectedDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 날짜의 작업 보고서 데이터를 서버에서 가져옵니다.
|
||||||
|
* @param {string} selectedDate - 조회할 날짜 (YYYY-MM-DD)
|
||||||
|
* @returns {Promise<Array>} - 작업 보고서 데이터 배열
|
||||||
|
*/
|
||||||
|
export async function fetchReportData(selectedDate) {
|
||||||
|
if (!selectedDate) {
|
||||||
|
throw new Error('조회할 날짜가 선택되지 않았습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiUrl = getReportApiUrl(selectedDate);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawData = await apiGet(apiUrl);
|
||||||
|
|
||||||
|
// 서버 응답이 { success: true, data: [...] } 형태일 경우와 [...] 형태일 경우 모두 처리
|
||||||
|
if (rawData && rawData.success && Array.isArray(rawData.data)) {
|
||||||
|
return rawData.data;
|
||||||
|
}
|
||||||
|
if (Array.isArray(rawData)) {
|
||||||
|
return rawData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 예상치 못한 형식의 응답
|
||||||
|
console.warn('예상치 못한 형식의 API 응답:', rawData);
|
||||||
|
return [];
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${selectedDate}의 작업 보고서 조회 실패:`, error);
|
||||||
|
throw new Error('서버에서 데이터를 가져오는 데 실패했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
72
web-ui/js/report-viewer-export.js
Normal file
72
web-ui/js/report-viewer-export.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// /js/report-viewer-export.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 주어진 데이터를 CSV 형식의 문자열로 변환합니다.
|
||||||
|
* @param {object} reportData - 요약 및 작업자별 데이터
|
||||||
|
* @returns {string} - CSV 형식의 문자열
|
||||||
|
*/
|
||||||
|
function convertToCsv(reportData) {
|
||||||
|
let csvContent = "\uFEFF"; // UTF-8 BOM
|
||||||
|
csvContent += "작업자명,프로젝트명,작업유형,작업상태,에러유형,작업시간,입력자\n";
|
||||||
|
|
||||||
|
reportData.workers.forEach(worker => {
|
||||||
|
worker.entries.forEach(entry => {
|
||||||
|
const row = [
|
||||||
|
worker.worker_name,
|
||||||
|
entry.project_name,
|
||||||
|
entry.work_type_name,
|
||||||
|
entry.work_status_name,
|
||||||
|
entry.error_type_name,
|
||||||
|
entry.work_hours,
|
||||||
|
entry.created_by_name
|
||||||
|
].map(field => `"${String(field || '').replace(/"/g, '""')}"`).join(',');
|
||||||
|
csvContent += row + "\n";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return csvContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가공된 보고서 데이터를 CSV 파일로 다운로드합니다.
|
||||||
|
* @param {object|null} reportData - UI에 표시된 가공된 데이터
|
||||||
|
*/
|
||||||
|
export function exportToExcel(reportData) {
|
||||||
|
if (!reportData || !reportData.workers || reportData.workers.length === 0) {
|
||||||
|
alert('내보낼 데이터가 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const csv = convertToCsv(reportData);
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const fileName = `작업보고서_${reportData.summary.date}.csv`;
|
||||||
|
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Excel 내보내기 실패:', error);
|
||||||
|
alert('Excel 파일을 생성하는 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 현재 페이지의 인쇄 기능을 호출합니다.
|
||||||
|
*/
|
||||||
|
export function printReport() {
|
||||||
|
try {
|
||||||
|
window.print();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('인쇄 실패:', error);
|
||||||
|
alert('인쇄 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
}
|
||||||
144
web-ui/js/report-viewer-ui.js
Normal file
144
web-ui/js/report-viewer-ui.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// /js/report-viewer-ui.js
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 데이터를 가공하여 UI에 표시하기 좋은 요약 형태로 변환합니다.
|
||||||
|
* @param {Array} rawData - 서버에서 받은 원시 데이터 배열
|
||||||
|
* @param {string} selectedDate - 선택된 날짜
|
||||||
|
* @returns {object} - 요약 정보와 작업자별로 그룹화된 데이터를 포함하는 객체
|
||||||
|
*/
|
||||||
|
export function processReportData(rawData, selectedDate) {
|
||||||
|
if (!Array.isArray(rawData) || rawData.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerGroups = {};
|
||||||
|
let totalHours = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
rawData.forEach(item => {
|
||||||
|
const workerName = item.worker_name || '미지정';
|
||||||
|
const workHours = parseFloat(item.work_hours || 0);
|
||||||
|
totalHours += workHours;
|
||||||
|
if (item.work_status_id === 2) errorCount++; // '에러' 상태 ID가 2라고 가정
|
||||||
|
|
||||||
|
if (!workerGroups[workerName]) {
|
||||||
|
workerGroups[workerName] = {
|
||||||
|
worker_name: workerName,
|
||||||
|
total_hours: 0,
|
||||||
|
entries: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
workerGroups[workerName].total_hours += workHours;
|
||||||
|
workerGroups[workerName].entries.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: {
|
||||||
|
date: selectedDate,
|
||||||
|
total_workers: Object.keys(workerGroups).length,
|
||||||
|
total_hours: totalHours,
|
||||||
|
total_entries: rawData.length,
|
||||||
|
error_count: errorCount
|
||||||
|
},
|
||||||
|
workers: Object.values(workerGroups)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function displaySummary(summary) {
|
||||||
|
const elements = {
|
||||||
|
totalWorkers: summary.total_workers,
|
||||||
|
totalHours: `${summary.total_hours}시간`,
|
||||||
|
totalEntries: `${summary.total_entries}개`,
|
||||||
|
errorCount: `${summary.error_count}개`
|
||||||
|
};
|
||||||
|
Object.entries(elements).forEach(([id, value]) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = value;
|
||||||
|
});
|
||||||
|
document.getElementById('reportSummary').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWorkEntryElement(entry) {
|
||||||
|
const entryDiv = document.createElement('div');
|
||||||
|
entryDiv.className = `work-entry ${entry.work_status_id === 2 ? 'error-entry' : ''}`;
|
||||||
|
entryDiv.innerHTML = `
|
||||||
|
<div class="entry-header">
|
||||||
|
<div class="project-name">${entry.project_name || '프로젝트 미지정'}</div>
|
||||||
|
<div class="work-hours">${entry.work_hours || 0}시간</div>
|
||||||
|
</div>
|
||||||
|
<div class="entry-details">
|
||||||
|
<div class="entry-detail">
|
||||||
|
<span class="detail-label">작업 유형:</span>
|
||||||
|
<span class="detail-value">${entry.work_type_name || '-'}</span>
|
||||||
|
</div>
|
||||||
|
${entry.work_status_id === 2 ? `
|
||||||
|
<div class="entry-detail">
|
||||||
|
<span class="detail-label">에러 유형:</span>
|
||||||
|
<span class="detail-value error-type">${entry.error_type_name || '에러'}</span>
|
||||||
|
</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return entryDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayWorkersDetails(workers) {
|
||||||
|
const workersListEl = document.getElementById('workersList');
|
||||||
|
workersListEl.innerHTML = '';
|
||||||
|
workers.forEach(worker => {
|
||||||
|
const workerCard = document.createElement('div');
|
||||||
|
workerCard.className = 'worker-card';
|
||||||
|
workerCard.innerHTML = `
|
||||||
|
<div class="worker-header">
|
||||||
|
<div class="worker-name">👤 ${worker.worker_name}</div>
|
||||||
|
<div class="worker-total-hours">총 ${worker.total_hours}시간</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
const entriesContainer = document.createElement('div');
|
||||||
|
entriesContainer.className = 'work-entries';
|
||||||
|
worker.entries.forEach(entry => entriesContainer.appendChild(createWorkEntryElement(entry)));
|
||||||
|
workerCard.appendChild(entriesContainer);
|
||||||
|
workersListEl.appendChild(workerCard);
|
||||||
|
});
|
||||||
|
document.getElementById('workersReport').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideElement = (id) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 가공된 데이터를 받아 화면 전체를 렌더링합니다.
|
||||||
|
* @param {object|null} processedData - 가공된 데이터 또는 데이터가 없을 경우 null
|
||||||
|
*/
|
||||||
|
export function renderReport(processedData) {
|
||||||
|
hideElement('loadingSpinner');
|
||||||
|
hideElement('errorMessage');
|
||||||
|
hideElement('noDataMessage');
|
||||||
|
hideElement('reportSummary');
|
||||||
|
hideElement('workersReport');
|
||||||
|
hideElement('exportSection');
|
||||||
|
|
||||||
|
if (!processedData) {
|
||||||
|
document.getElementById('noDataMessage').style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
displaySummary(processedData.summary);
|
||||||
|
displayWorkersDetails(processedData.workers);
|
||||||
|
document.getElementById('exportSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showLoading(isLoading) {
|
||||||
|
document.getElementById('loadingSpinner').style.display = isLoading ? 'flex' : 'none';
|
||||||
|
if(isLoading) {
|
||||||
|
hideElement('errorMessage');
|
||||||
|
hideElement('noDataMessage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function showError(message) {
|
||||||
|
const errorEl = document.getElementById('errorMessage');
|
||||||
|
errorEl.querySelector('.error-text').textContent = message;
|
||||||
|
errorEl.style.display = 'block';
|
||||||
|
hideElement('loadingSpinner');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user