From 5a68ced13b5aa8fd06021ae5a7d0ef8fcad8a4de Mon Sep 17 00:00:00 2001 From: hyungi Date: Mon, 28 Jul 2025 12:32:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EB=B7=B0=EC=96=B4=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=A0=84=EC=B2=B4=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 765줄의 daily-report-viewer.js를 API, UI, Export, Controller의 4개 모듈로 분리 - 관심사 분리를 통해 코드의 가독성, 테스트 용이성, 유지보수성을 극적으로 향상 - 프로젝트 전반의 코드 일관성 확보 및 레거시 로직 제거 --- web-ui/js/daily-report-viewer.js | 814 +++--------------------------- web-ui/js/report-viewer-api.js | 91 ++++ web-ui/js/report-viewer-export.js | 72 +++ web-ui/js/report-viewer-ui.js | 144 ++++++ 4 files changed, 372 insertions(+), 749 deletions(-) create mode 100644 web-ui/js/report-viewer-api.js create mode 100644 web-ui/js/report-viewer-export.js create mode 100644 web-ui/js/report-viewer-ui.js diff --git a/web-ui/js/daily-report-viewer.js b/web-ui/js/daily-report-viewer.js index abbda90..c201d3e 100644 --- a/web-ui/js/daily-report-viewer.js +++ b/web-ui/js/daily-report-viewer.js @@ -1,765 +1,81 @@ -// daily-report-viewer.js - 통합 API 설정 적용 버전 +// /js/daily-report-viewer.js -// ================================================================= -// 🌐 통합 API 설정 import -// ================================================================= -import { API, getAuthHeaders, apiCall } from '/js/api-config.js'; +import { fetchReportData } from './report-viewer-api.js'; +import { renderReport, processReportData, showLoading, showError } from './report-viewer-ui.js'; +import { exportToExcel, printReport } from './report-viewer-export.js'; +import { getUser } from './auth.js'; -// ================================================================= -// 🌐 전역 변수 및 기본 설정 -// ================================================================= -let currentReportData = null; -let workTypes = []; -let workStatusTypes = []; -let errorTypes = []; +// 전역 상태: 현재 화면에 표시된 데이터 +let currentProcessedData = null; -// ================================================================= -// 🔧 유틸리티 함수들 (입력 페이지와 동일) -// ================================================================= +/** + * 날짜를 기준으로 보고서를 검색하고 화면에 렌더링합니다. + */ +async function searchReports() { + const dateInput = document.getElementById('reportDate'); + const selectedDate = dateInput.value; -// 현재 로그인한 사용자 정보 가져오기 -function getCurrentUser() { - try { - const token = localStorage.getItem('token'); - if (!token) return null; - - const payloadBase64 = token.split('.')[1]; - if (payloadBase64) { - const payload = JSON.parse(atob(payloadBase64)); - console.log('토큰에서 추출한 사용자 정보:', payload); - return payload; - } - } catch (error) { - console.log('토큰에서 사용자 정보 추출 실패:', error); - } - - try { - 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; + if (!selectedDate) { + showError('날짜를 선택해주세요.'); + return; + } + + showLoading(true); + currentProcessedData = null; // 새 검색이 시작되면 이전 데이터 초기화 + + try { + const rawData = await fetchReportData(selectedDate); + currentProcessedData = processReportData(rawData, selectedDate); + renderReport(currentProcessedData); + } catch (error) { + showError(error.message); + renderReport(null); // 에러 발생 시 데이터 없는 화면 표시 + } finally { + showLoading(false); + } } -// 한국 시간 기준 오늘 날짜 가져기기 -function getKoreaToday() { - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; +/** + * 페이지의 모든 이벤트 리스너를 설정합니다. + */ +function setupEventListeners() { + document.getElementById('searchBtn')?.addEventListener('click', searchReports); + document.getElementById('todayBtn')?.addEventListener('click', () => { + 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: '권한 없음' }; - } - - const accessLevel = user.access_level.toLowerCase(); - - // 🎯 권한 레벨 정의 (더 유연하게) - 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('로그인이 필요합니다.'); +/** + * 페이지가 처음 로드될 때 실행되는 초기화 함수 + */ +function initializePage() { + // auth.js를 사용하여 인증 상태 확인 + const user = getUser(); + if (!user) { + showError('로그인이 필요합니다. 2초 후 로그인 페이지로 이동합니다.'); setTimeout(() => window.location.href = '/index.html', 2000); return; } - - try { - showMessage('시스템을 초기화하는 중...', 'loading'); - - // 기본 설정 - setupEventListeners(); - setTodayDate(); - - // 마스터 데이터 로드 - await loadMasterData(); - - // 권한 표시 - displayUserPermission(permission); - - hideMessage(); - console.log('✅ 초기화 완료!'); - - } catch (error) { - console.error('❌ 초기화 실패:', error); - showError(`초기화 오류: ${error.message}`); - } -}); -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); -} + setupEventListeners(); -function setTodayDate() { - const today = getKoreaToday(); + // 페이지 로드 시 오늘 날짜로 자동 검색 const dateInput = document.getElementById('reportDate'); - - if (dateInput) { - dateInput.value = today; - searchReports(); - } + dateInput.value = new Date().toISOString().split('T')[0]; + searchReports(); } -// 권한 표시 함수 (더 상세하게) -function displayUserPermission(permission) { - // 권한 정보를 UI에 표시 - const headerElement = document.querySelector('h1'); - if (headerElement) { - headerElement.innerHTML += ` (${permission.description})`; - } - - 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 = ` -
👤 ${worker.worker_name || '미지정'}
-
총 ${worker.total_hours || 0}시간
- `; - - 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 = ` -
${entry.project_name || '프로젝트 미지정'}
-
${entry.work_hours || 0}시간
- `; - - 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 = ` - ${label}: - ${value} - `; - 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 = `
${message}
`; - - 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 = ` -
- 📭 -

${selectedDate} 작업보고서가 없습니다

-
-

💡 ${helpMessage}

-
    -
  • 다른 날짜를 선택해보세요 (예: ${getKoreaToday()})
  • -
  • 📝 작업보고서 입력 페이지에서 데이터를 먼저 입력해보세요
  • -
  • 입력 후 잠시 기다린 다음 다시 시도해보세요
  • -
-

- -

-
-
- `; - 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('📋 페이지 종료'); -}); \ No newline at end of file +// DOM이 로드되면 페이지 초기화를 시작합니다. +document.addEventListener('DOMContentLoaded', initializePage); \ No newline at end of file diff --git a/web-ui/js/report-viewer-api.js b/web-ui/js/report-viewer-api.js new file mode 100644 index 0000000..fe3963c --- /dev/null +++ b/web-ui/js/report-viewer-api.js @@ -0,0 +1,91 @@ +// /js/report-viewer-api.js +import { apiGet } from './api-helper.js'; +import { getUser } from './auth.js'; + +/** + * 보고서 조회를 위한 마스터 데이터를 로드합니다. (작업 유형, 상태 등) + * 실패 시 기본값을 반환할 수 있도록 개별적으로 처리합니다. + * @returns {Promise} - 각 마스터 데이터 배열을 포함하는 객체 + */ +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} - 작업 보고서 데이터 배열 + */ +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('서버에서 데이터를 가져오는 데 실패했습니다.'); + } +} \ No newline at end of file diff --git a/web-ui/js/report-viewer-export.js b/web-ui/js/report-viewer-export.js new file mode 100644 index 0000000..d6f8d67 --- /dev/null +++ b/web-ui/js/report-viewer-export.js @@ -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('인쇄 중 오류가 발생했습니다.'); + } +} \ No newline at end of file diff --git a/web-ui/js/report-viewer-ui.js b/web-ui/js/report-viewer-ui.js new file mode 100644 index 0000000..3669196 --- /dev/null +++ b/web-ui/js/report-viewer-ui.js @@ -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 = ` +
+
${entry.project_name || '프로젝트 미지정'}
+
${entry.work_hours || 0}시간
+
+
+
+ 작업 유형: + ${entry.work_type_name || '-'} +
+ ${entry.work_status_id === 2 ? ` +
+ 에러 유형: + ${entry.error_type_name || '에러'} +
` : ''} +
+ `; + 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 = ` +
+
👤 ${worker.worker_name}
+
총 ${worker.total_hours}시간
+
+ `; + 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'); +} \ No newline at end of file