1009 lines
34 KiB
JavaScript
1009 lines
34 KiB
JavaScript
// daily-report-viewer.js - 전체 데이터 조회 완전 버전
|
|
|
|
// =================================================================
|
|
// 🔧 API 설정 및 환경 구성
|
|
// =================================================================
|
|
class APIConfig {
|
|
constructor() {
|
|
// daily-work-report.js와 동일한 API 경로 사용
|
|
this.possibleBasePaths = [
|
|
'https://api.hyungi.net/api', // 기본 운영 서버
|
|
'/api', // 프록시를 통한 API
|
|
'/api/v1', // API 버전 1
|
|
'', // 루트 경로
|
|
'/backend/api', // 백엔드 직접 연결
|
|
'http://localhost:3005/api' // 로컬 개발 서버
|
|
];
|
|
|
|
this.currentBasePath = 'https://api.hyungi.net/api'; // 기본값
|
|
this.isInitialized = false;
|
|
|
|
// 엔드포인트 정의 (전체 조회용으로 확장)
|
|
this.endpoints = {
|
|
DAILY_REPORTS: '/daily-work-reports/date',
|
|
DAILY_REPORTS_QUERY: '/daily-work-reports',
|
|
WORK_TYPES: '/daily-work-reports/work-types',
|
|
WORK_STATUS_TYPES: '/daily-work-reports/work-status-types',
|
|
ERROR_TYPES: '/daily-work-reports/error-types'
|
|
};
|
|
}
|
|
|
|
// API 기본 경로 자동 감지
|
|
async detectBasePath() {
|
|
if (this.isInitialized && this.currentBasePath) {
|
|
console.log('🔄 이미 초기화된 경로 사용:', this.currentBasePath);
|
|
return this.currentBasePath;
|
|
}
|
|
|
|
console.log('🔍 API 기본 경로 자동 감지 시작...');
|
|
|
|
for (const basePath of this.possibleBasePaths) {
|
|
try {
|
|
console.log(`🧪 테스트 중: ${basePath}`);
|
|
|
|
const testUrl = `${basePath}${this.endpoints.WORK_TYPES}`;
|
|
const headers = this.getHeaders();
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 8000);
|
|
|
|
const response = await fetch(testUrl, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
signal: controller.signal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok || response.status === 401) {
|
|
this.currentBasePath = basePath;
|
|
this.isInitialized = true;
|
|
console.log(`✅ API 기본 경로 확정: ${basePath}`);
|
|
return basePath;
|
|
}
|
|
} catch (error) {
|
|
console.log(`❌ ${basePath} 연결 실패:`, error.message);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
this.currentBasePath = this.possibleBasePaths[0];
|
|
this.isInitialized = true;
|
|
console.log('🔧 기본 경로로 설정:', this.currentBasePath);
|
|
return this.currentBasePath;
|
|
}
|
|
|
|
// 토큰 가져오기 (만료 검사 포함)
|
|
getAuthToken() {
|
|
const tokens = {
|
|
token: localStorage.getItem('token'),
|
|
authToken: localStorage.getItem('authToken'),
|
|
jwt: localStorage.getItem('jwt')
|
|
};
|
|
|
|
const selectedToken = tokens.token || tokens.authToken || tokens.jwt || null;
|
|
|
|
if (selectedToken) {
|
|
// 토큰 만료 검사
|
|
if (this.isTokenExpired(selectedToken)) {
|
|
console.error('⏰ 토큰이 만료되었습니다.');
|
|
this.clearExpiredTokens();
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return selectedToken;
|
|
}
|
|
|
|
// JWT 토큰 만료 검사
|
|
isTokenExpired(token) {
|
|
try {
|
|
const parts = token.split('.');
|
|
if (parts.length !== 3) return true;
|
|
|
|
const payload = JSON.parse(atob(parts[1]));
|
|
if (payload.exp) {
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
return payload.exp < currentTime;
|
|
}
|
|
return false;
|
|
} catch (error) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 만료된 토큰 정리
|
|
clearExpiredTokens() {
|
|
localStorage.removeItem('token');
|
|
localStorage.removeItem('authToken');
|
|
localStorage.removeItem('jwt');
|
|
localStorage.removeItem('user');
|
|
}
|
|
|
|
// 인증 헤더 생성
|
|
getHeaders() {
|
|
const token = this.getAuthToken();
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json'
|
|
};
|
|
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
// 완전한 URL 생성
|
|
getFullUrl(endpoint) {
|
|
return `${this.currentBasePath}${endpoint}`;
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 🌐 향상된 API 클라이언트
|
|
// =================================================================
|
|
class APIClient {
|
|
constructor() {
|
|
this.config = new APIConfig();
|
|
this.retryCount = 3;
|
|
this.retryDelay = 1000;
|
|
}
|
|
|
|
// 초기화
|
|
async initialize() {
|
|
try {
|
|
await this.config.detectBasePath();
|
|
console.log('✅ API 클라이언트 초기화 완료');
|
|
} catch (error) {
|
|
console.error('❌ API 클라이언트 초기화 실패:', error);
|
|
throw new Error('API 서버에 연결할 수 없습니다.');
|
|
}
|
|
}
|
|
|
|
// API 호출
|
|
async get(endpoint) {
|
|
const url = this.config.getFullUrl(endpoint);
|
|
return this.fetchWithRetry(url, { method: 'GET' });
|
|
}
|
|
|
|
// 재시도 로직이 포함된 fetch
|
|
async fetchWithRetry(url, options = {}, attempt = 1) {
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers: {
|
|
...this.config.getHeaders(),
|
|
...options.headers
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
await this.handleErrorResponse(response);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data;
|
|
|
|
} catch (error) {
|
|
if (attempt < this.retryCount && this.shouldRetry(error)) {
|
|
await this.sleep(this.retryDelay);
|
|
return this.fetchWithRetry(url, options, attempt + 1);
|
|
}
|
|
throw this.createUserFriendlyError(error);
|
|
}
|
|
}
|
|
|
|
// 에러 응답 처리
|
|
async handleErrorResponse(response) {
|
|
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
let errorDetail = null;
|
|
|
|
try {
|
|
const errorData = await response.json();
|
|
errorMessage = errorData.error || errorData.message || errorMessage;
|
|
errorDetail = errorData.detail || null;
|
|
} catch (e) {
|
|
// JSON 파싱 실패는 무시
|
|
}
|
|
|
|
if (response.status === 403 && errorDetail === 'jwt expired') {
|
|
this.handleTokenExpired();
|
|
throw new Error('토큰이 만료되었습니다. 다시 로그인해주세요.');
|
|
}
|
|
|
|
if (response.status === 401) {
|
|
this.handleAuthError();
|
|
throw new Error('인증이 필요합니다. 다시 로그인해주세요.');
|
|
}
|
|
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
// 토큰 만료 처리
|
|
handleTokenExpired() {
|
|
console.warn('⏰ 토큰 만료 처리');
|
|
this.config.clearExpiredTokens();
|
|
setTimeout(() => {
|
|
alert('로그인 세션이 만료되었습니다.\n다시 로그인해주세요.');
|
|
window.location.href = '/index.html';
|
|
}, 1000);
|
|
}
|
|
|
|
// 인증 오류 처리
|
|
handleAuthError() {
|
|
console.warn('🔐 인증 오류 발생');
|
|
this.config.clearExpiredTokens();
|
|
setTimeout(() => {
|
|
if (confirm('인증이 만료되었습니다. 로그인 페이지로 이동하시겠습니까?')) {
|
|
window.location.href = '/index.html';
|
|
}
|
|
}, 2000);
|
|
}
|
|
|
|
// 재시도 여부 판단
|
|
shouldRetry(error) {
|
|
return error.name === 'TypeError' ||
|
|
error.message.includes('fetch') ||
|
|
error.message.includes('NetworkError');
|
|
}
|
|
|
|
// 사용자 친화적인 에러 메시지 생성
|
|
createUserFriendlyError(error) {
|
|
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
|
return new Error('서버에 연결할 수 없습니다. 네트워크 상태를 확인해주세요.');
|
|
}
|
|
return error;
|
|
}
|
|
|
|
// 지연 함수
|
|
sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 🌐 전역 변수 및 상태 관리
|
|
// =================================================================
|
|
let apiClient = null;
|
|
let currentReportData = null;
|
|
let workTypes = [];
|
|
let workStatusTypes = [];
|
|
let errorTypes = [];
|
|
|
|
// 캐시 관리
|
|
const dataCache = new Map();
|
|
const CACHE_DURATION = 5 * 60 * 1000; // 5분
|
|
|
|
function getCachedData(key) {
|
|
const cached = dataCache.get(key);
|
|
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
|
|
return cached.data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function setCachedData(key, data) {
|
|
dataCache.set(key, {
|
|
data: data,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
|
|
// =================================================================
|
|
// 🔧 유틸리티 함수들
|
|
// =================================================================
|
|
|
|
// 사용자 정보 가져오기
|
|
function getUserInfo() {
|
|
try {
|
|
const storedUser = localStorage.getItem('user');
|
|
if (storedUser) {
|
|
return JSON.parse(storedUser);
|
|
}
|
|
|
|
const token = localStorage.getItem('token');
|
|
if (token) {
|
|
const parts = token.split('.');
|
|
if (parts.length === 3) {
|
|
const payload = JSON.parse(atob(parts[1]));
|
|
return {
|
|
user_id: payload.user_id || payload.id,
|
|
username: payload.username,
|
|
access_level: payload.access_level,
|
|
name: payload.name
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
} catch (error) {
|
|
console.error('❌ 사용자 정보 파싱 오류:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 🚀 초기화 및 이벤트 설정
|
|
// =================================================================
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
console.log('🔥 ===== 일일보고서 뷰어 시작 =====');
|
|
console.log('📍 현재 페이지:', window.location.href);
|
|
|
|
// 사용자 정보 확인
|
|
const userInfo = getUserInfo();
|
|
console.log('👤 사용자 정보:', userInfo);
|
|
|
|
// 토큰 확인
|
|
const mainToken = localStorage.getItem('token');
|
|
if (!mainToken) {
|
|
console.error('❌ 토큰이 없습니다.');
|
|
alert('로그인이 필요합니다.');
|
|
setTimeout(() => window.location.href = '/index.html', 2000);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
showMessage('시스템을 초기화하는 중...', 'loading');
|
|
|
|
// API 클라이언트 초기화
|
|
apiClient = new APIClient();
|
|
await apiClient.initialize();
|
|
|
|
// 기본 설정
|
|
setupEventListeners();
|
|
setTodayDate();
|
|
|
|
// 마스터 데이터 로드
|
|
await loadMasterData();
|
|
|
|
hideMessage();
|
|
console.log('✅ 초기화 완료!');
|
|
|
|
} catch (error) {
|
|
console.error('❌ 초기화 실패:', error);
|
|
showError(`초기화 오류: ${error.message}`);
|
|
|
|
if (error.message.includes('인증') || error.message.includes('토큰')) {
|
|
setTimeout(() => window.location.href = '/index.html', 3000);
|
|
}
|
|
}
|
|
});
|
|
|
|
function 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 = new Date();
|
|
const dateStr = today.toISOString().split('T')[0];
|
|
const dateInput = document.getElementById('reportDate');
|
|
|
|
if (dateInput) {
|
|
dateInput.value = dateStr;
|
|
searchReports();
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 📊 마스터 데이터 로드
|
|
// =================================================================
|
|
async function loadMasterData() {
|
|
try {
|
|
console.log('📋 마스터 데이터 로딩...');
|
|
|
|
const [workTypesRes, workStatusRes, errorTypesRes] = await Promise.allSettled([
|
|
loadWorkTypes(),
|
|
loadWorkStatusTypes(),
|
|
loadErrorTypes()
|
|
]);
|
|
|
|
if (workTypesRes.status === 'fulfilled') {
|
|
workTypes = workTypesRes.value;
|
|
}
|
|
if (workStatusRes.status === 'fulfilled') {
|
|
workStatusTypes = workStatusRes.value;
|
|
}
|
|
if (errorTypesRes.status === 'fulfilled') {
|
|
errorTypes = errorTypesRes.value;
|
|
}
|
|
|
|
console.log('✅ 마스터 데이터 로드 완료', {
|
|
workTypes: workTypes.length,
|
|
workStatusTypes: workStatusTypes.length,
|
|
errorTypes: errorTypes.length
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error('❌ 마스터 데이터 로드 실패:', error);
|
|
}
|
|
}
|
|
|
|
async function loadWorkTypes() {
|
|
try {
|
|
return await apiClient.get(apiClient.config.endpoints.WORK_TYPES);
|
|
} catch (error) {
|
|
console.warn('⚠️ 작업 유형 API 실패, 기본값 사용');
|
|
return [
|
|
{id: 1, name: 'Base'},
|
|
{id: 2, name: 'Vessel'},
|
|
{id: 3, name: 'Piping'}
|
|
];
|
|
}
|
|
}
|
|
|
|
async function loadWorkStatusTypes() {
|
|
try {
|
|
return await apiClient.get(apiClient.config.endpoints.WORK_STATUS_TYPES);
|
|
} catch (error) {
|
|
console.warn('⚠️ 작업 상태 API 실패, 기본값 사용');
|
|
return [
|
|
{id: 1, name: '정규'},
|
|
{id: 2, name: '에러'}
|
|
];
|
|
}
|
|
}
|
|
|
|
async function loadErrorTypes() {
|
|
try {
|
|
return await apiClient.get(apiClient.config.endpoints.ERROR_TYPES);
|
|
} catch (error) {
|
|
console.warn('⚠️ 에러 유형 API 실패, 기본값 사용');
|
|
return [
|
|
{id: 1, name: '설계미스'},
|
|
{id: 2, name: '외주작업 불량'},
|
|
{id: 3, name: '입고지연'},
|
|
{id: 4, name: '작업 불량'}
|
|
];
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 🔍 검색 및 데이터 조회 (전체 데이터 조회 로직)
|
|
// =================================================================
|
|
async function searchReports() {
|
|
const selectedDate = document.getElementById('reportDate')?.value;
|
|
|
|
if (!selectedDate) {
|
|
showError('날짜를 선택해 주세요.');
|
|
return;
|
|
}
|
|
|
|
console.log(`\n🔍 ===== ${selectedDate} 전체 데이터 조회 시작 =====`);
|
|
|
|
// 🔍 먼저 데이터가 실제로 존재하는지 확인
|
|
console.log('📋 데이터 존재 여부 사전 확인...');
|
|
|
|
try {
|
|
// daily-work-report.js에서 사용하는 방식으로 본인 데이터 확인
|
|
const userInfo = getUserInfo();
|
|
const myDataUrl = apiClient.config.getFullUrl(`/daily-work-reports?date=${selectedDate}&created_by=${userInfo?.user_id}`);
|
|
|
|
console.log('🔍 본인 데이터 확인:', myDataUrl);
|
|
const myDataResponse = await fetch(myDataUrl, {
|
|
method: 'GET',
|
|
headers: apiClient.config.getHeaders()
|
|
});
|
|
|
|
if (myDataResponse.ok) {
|
|
const myData = await myDataResponse.json();
|
|
console.log(`📊 본인 입력 데이터: ${myData.length}개`);
|
|
|
|
if (myData.length === 0) {
|
|
console.warn('⚠️ 해당 날짜에 입력된 데이터가 전혀 없는 것 같습니다.');
|
|
console.log('💡 다른 날짜(6월 24일 등)를 시도해보세요.');
|
|
} else {
|
|
console.log('✅ 데이터 존재 확인됨, 전체 조회 시도 계속...');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('⚠️ 데이터 존재 확인 실패:', error.message);
|
|
}
|
|
|
|
try {
|
|
hideAllMessages();
|
|
showLoading(true);
|
|
|
|
const userInfo = getUserInfo();
|
|
console.log('👤 사용자 권한:', userInfo?.access_level);
|
|
|
|
// 🎯 Rate Limiting 대응: 단계별 API 시도 (한 번에 하나씩)
|
|
const priorityEndpoints = [
|
|
// 가장 유력한 후보들만 우선 시도
|
|
`/daily-work-reports/date/${selectedDate}`,
|
|
`/daily-work-reports?date=${selectedDate}`,
|
|
`/daily-work-reports?date=${selectedDate}&created_by=`,
|
|
`/daily-work-reports?date=${selectedDate}&created_by=all`,
|
|
`/daily-work-reports?date=${selectedDate}&admin=true`
|
|
];
|
|
|
|
console.log(`📡 Rate Limiting 대응: ${priorityEndpoints.length}개 우선 API 시도`);
|
|
|
|
let bestResult = null;
|
|
let bestEndpoint = null;
|
|
let maxDataCount = 0;
|
|
|
|
for (let i = 0; i < priorityEndpoints.length; i++) {
|
|
const endpoint = priorityEndpoints[i];
|
|
|
|
try {
|
|
console.log(`\n📡 우선 시도 ${i + 1}/${priorityEndpoints.length}: ${endpoint}`);
|
|
|
|
const fullUrl = apiClient.config.getFullUrl(endpoint);
|
|
const startTime = Date.now();
|
|
|
|
const response = await fetch(fullUrl, {
|
|
method: 'GET',
|
|
headers: apiClient.config.getHeaders()
|
|
});
|
|
|
|
const endTime = Date.now();
|
|
console.log(` ⏱️ 응답 시간: ${endTime - startTime}ms`);
|
|
console.log(` 📊 응답 상태: ${response.status} ${response.statusText}`);
|
|
|
|
if (response.status === 429) {
|
|
console.warn(` ⚠️ Rate Limit 발생 - 3초 대기 후 재시도`);
|
|
await this.sleep(3000); // 3초 대기
|
|
continue;
|
|
}
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const dataCount = Array.isArray(data) ? data.length : (data?.workers?.length || 0);
|
|
|
|
console.log(` ✅ 성공! 데이터 개수: ${dataCount}`);
|
|
|
|
if (dataCount > 0) {
|
|
console.log(` 🎯 데이터 발견! 더 이상 시도하지 않음`);
|
|
bestResult = data;
|
|
bestEndpoint = endpoint;
|
|
maxDataCount = dataCount;
|
|
break; // 데이터를 찾으면 즉시 중단
|
|
}
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.log(` ❌ 실패 (${response.status}): ${errorText.substring(0, 100)}`);
|
|
}
|
|
|
|
// API 호출 간 1초 대기 (Rate Limiting 방지)
|
|
console.log(` ⏳ 1초 대기 중...`);
|
|
await sleep(1000);
|
|
|
|
} catch (error) {
|
|
console.log(` ❌ 네트워크 오류: ${error.message}`);
|
|
await sleep(1000); // 에러 시에도 대기
|
|
}
|
|
}
|
|
|
|
// 유틸리티 함수: sleep
|
|
function sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
console.log(`\n🎯 최종 결과: ${bestEndpoint || '없음'}`);
|
|
console.log(`📊 최대 데이터 개수: ${maxDataCount}`);
|
|
|
|
if (bestResult && maxDataCount > 0) {
|
|
console.log(`✅ 전체 데이터 조회 성공!`);
|
|
|
|
// 데이터 구조 변환
|
|
const processedData = processRawData(bestResult, selectedDate);
|
|
currentReportData = processedData;
|
|
|
|
// 화면에 표시
|
|
displayReportData(processedData);
|
|
showExportSection(true);
|
|
|
|
} else {
|
|
console.log(`❌ 모든 API에서 데이터를 찾을 수 없습니다.`);
|
|
console.log(`💡 해결 방법:`);
|
|
console.log(` 1. 다른 날짜 시도 (예: 2025-06-24)`);
|
|
console.log(` 2. daily-work-report.html에서 데이터 먼저 입력`);
|
|
console.log(` 3. 백엔드 API에 전체 조회 기능 추가 요청`);
|
|
|
|
// 도움말 메시지와 함께 NoData 표시
|
|
showNoDataWithHelp(selectedDate);
|
|
showExportSection(false);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('❌ 조회 실패:', error);
|
|
showError(`데이터 조회 오류: ${error.message}`);
|
|
showExportSection(false);
|
|
} finally {
|
|
showLoading(false);
|
|
console.log('🔍 ===== 조회 완료 =====\n');
|
|
}
|
|
}
|
|
|
|
// 원시 데이터를 구조화된 형태로 변환
|
|
function processRawData(rawData, selectedDate) {
|
|
console.log('🔄 데이터 구조 변환 시작');
|
|
|
|
if (!Array.isArray(rawData)) {
|
|
console.log('📋 이미 구조화된 데이터');
|
|
return rawData;
|
|
}
|
|
|
|
// 작업자별로 그룹화
|
|
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
|
|
});
|
|
});
|
|
|
|
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 || '정상']
|
|
];
|
|
|
|
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') {
|
|
setTimeout(() => {
|
|
messageContainer.innerHTML = '';
|
|
}, 5000);
|
|
}
|
|
} else {
|
|
console.log(`📢 ${type.toUpperCase()}: ${message}`);
|
|
}
|
|
}
|
|
|
|
function hideMessage() {
|
|
const messageContainer = document.getElementById('message-container');
|
|
if (messageContainer) {
|
|
messageContainer.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
function showNoData() {
|
|
const noDataDiv = document.getElementById('noDataMessage');
|
|
if (noDataDiv) {
|
|
noDataDiv.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function showNoDataWithHelp(selectedDate) {
|
|
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>💡 해결 방법:</strong></p>
|
|
<ul style="text-align: left; margin: 10px 0;">
|
|
<li>다른 날짜를 선택해보세요 (예: 2025-06-24)</li>
|
|
<li><a href="/pages/common/daily-work-report.html" target="_blank" style="color: #3498db;">📝 작업보고서 입력 페이지</a>에서 데이터를 먼저 입력해보세요</li>
|
|
<li>입력된 데이터가 있다면 백엔드 API에 전체 조회 권한이 필요할 수 있습니다</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
|
|
].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('인쇄 중 오류가 발생했습니다.');
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 🔄 정리 및 디버그
|
|
// =================================================================
|
|
window.addEventListener('beforeunload', function() {
|
|
console.log('📋 페이지 종료 - 정리 작업 수행');
|
|
dataCache.clear();
|
|
});
|
|
|
|
// 개발 모드 디버깅
|
|
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
console.log('🐛 개발 모드 활성화');
|
|
|
|
window.DEBUG = {
|
|
apiClient,
|
|
currentReportData,
|
|
workTypes,
|
|
workStatusTypes,
|
|
errorTypes,
|
|
dataCache,
|
|
clearCache: () => dataCache.clear(),
|
|
searchReports: searchReports
|
|
};
|
|
}
|
|
|
|
// 전역 함수 노출
|
|
window.searchReports = searchReports;
|
|
window.exportToExcel = exportToExcel;
|
|
window.printReport = printReport; |