Files
TK-FB-Project/fastapi-bridge/static/js/daily-report-viewer 복사본.js

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;