..
This commit is contained in:
		
							
								
								
									
										863
									
								
								Project/Web/wwwroot/Jobreport/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										863
									
								
								Project/Web/wwwroot/Jobreport/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,863 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="ko"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> | ||||
|     <meta http-equiv="Pragma" content="no-cache"> | ||||
|     <meta http-equiv="Expires" content="0"> | ||||
|     <title>업무일지</title> | ||||
|     <script src="https://cdn.tailwindcss.com"></script> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | ||||
|     <script src="https://unpkg.com/feather-icons"></script> | ||||
|     <script> | ||||
|         tailwind.config = { | ||||
|             theme: { | ||||
|                 extend: { | ||||
|                     colors: { | ||||
|                         primary: '#3B82F6', | ||||
|                         secondary: '#6B7280', | ||||
|                         success: '#10B981', | ||||
|                         danger: '#EF4444', | ||||
|                         warning: '#F59E0B' | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     </script> | ||||
| </head> | ||||
| <body class="bg-gray-50 min-h-screen"> | ||||
|     <!-- 헤더 --> | ||||
|     <header class="bg-white shadow-sm border-b"> | ||||
|         <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | ||||
|             <div class="flex justify-between items-center py-4"> | ||||
|                 <div class="flex items-center"> | ||||
|                     <i data-feather="file-text" class="w-8 h-8 text-primary mr-3"></i> | ||||
|                     <h1 class="text-2xl font-bold text-gray-900">업무일지</h1> | ||||
|                 </div> | ||||
|                 <div class="flex items-center space-x-4"> | ||||
|                     <button id="refreshBtn" class="bg-primary hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center"> | ||||
|                         <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i> | ||||
|                         새로고침 | ||||
|                     </button> | ||||
|                     <div class="text-sm text-gray-600"> | ||||
|                         <span id="currentDate"></span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </header> | ||||
|  | ||||
|     <!-- 메인 컨텐츠 --> | ||||
|     <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | ||||
|         <!-- 통계 카드 --> | ||||
|         <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8"> | ||||
|             <div class="bg-white rounded-lg shadow p-6"> | ||||
|                 <div class="flex items-center"> | ||||
|                     <div class="p-2 bg-blue-100 rounded-lg"> | ||||
|                         <i data-feather="calendar" class="w-6 h-6 text-primary"></i> | ||||
|                     </div> | ||||
|                     <div class="ml-4"> | ||||
|                         <p class="text-sm font-medium text-gray-600">총 업무일수</p> | ||||
|                         <p id="totalDays" class="text-2xl font-bold text-gray-900">0</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="bg-white rounded-lg shadow p-6"> | ||||
|                 <div class="flex items-center"> | ||||
|                     <div class="p-2 bg-green-100 rounded-lg"> | ||||
|                         <i data-feather="clock" class="w-6 h-6 text-success"></i> | ||||
|                     </div> | ||||
|                     <div class="ml-4"> | ||||
|                         <p class="text-sm font-medium text-gray-600">총 근무시간</p> | ||||
|                         <p id="totalHours" class="text-2xl font-bold text-gray-900">0h</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="bg-white rounded-lg shadow p-6"> | ||||
|                 <div class="flex items-center"> | ||||
|                     <div class="p-2 bg-orange-100 rounded-lg"> | ||||
|                         <i data-feather="zap" class="w-6 h-6 text-warning"></i> | ||||
|                     </div> | ||||
|                     <div class="ml-4"> | ||||
|                         <p class="text-sm font-medium text-gray-600">총 초과근무</p> | ||||
|                         <p id="totalOT" class="text-2xl font-bold text-gray-900">0h</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="bg-white rounded-lg shadow p-6"> | ||||
|                 <div class="flex items-center"> | ||||
|                     <div class="p-2 bg-purple-100 rounded-lg"> | ||||
|                         <i data-feather="folder" class="w-6 h-6 text-purple-600"></i> | ||||
|                     </div> | ||||
|                     <div class="ml-4"> | ||||
|                         <p class="text-sm font-medium text-gray-600">진행중 프로젝트</p> | ||||
|                         <p id="activeProjects" class="text-2xl font-bold text-gray-900">0</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- 필터 및 검색 --> | ||||
|         <div class="bg-white rounded-lg shadow mb-6"> | ||||
|             <div class="p-6 border-b border-gray-200"> | ||||
|                 <div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0"> | ||||
|                     <div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-4"> | ||||
|                         <div> | ||||
|                             <label class="block text-sm font-medium text-gray-700 mb-1">조회기간</label> | ||||
|                             <div class="flex space-x-2"> | ||||
|                                 <input type="date" id="startDate" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm"> | ||||
|                                 <span class="flex items-center text-gray-500">~</span> | ||||
|                                 <input type="date" id="endDate" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-sm"> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div> | ||||
|                             <label class="block text-sm font-medium text-gray-700 mb-1">상태</label> | ||||
|                             <select id="statusFilter" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"> | ||||
|                                 <option value="">전체</option> | ||||
|                                 <option value="진행중">진행중</option> | ||||
|                                 <option value="완료">완료</option> | ||||
|                                 <option value="대기">대기</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                         <div> | ||||
|                             <label class="block text-sm font-medium text-gray-700 mb-1">프로젝트</label> | ||||
|                             <select id="projectFilter" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"> | ||||
|                                 <option value="">전체</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                         <div> | ||||
|                             <label class="block text-sm font-medium text-gray-700 mb-1">검색</label> | ||||
|                             <input type="text" id="searchInput" placeholder="업무 내용 검색..." class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="flex space-x-2"> | ||||
|                         <button id="clearFilterBtn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-md flex items-center"> | ||||
|                             <i data-feather="x" class="w-4 h-4 mr-2"></i> | ||||
|                             필터 초기화 | ||||
|                         </button> | ||||
|                         <button id="exportBtn" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md flex items-center"> | ||||
|                             <i data-feather="download" class="w-4 h-4 mr-2"></i> | ||||
|                             엑셀 다운로드 | ||||
|                         </button> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- 데이터 테이블 --> | ||||
|         <div class="bg-white rounded-lg shadow overflow-hidden"> | ||||
|             <div class="overflow-x-auto"> | ||||
|                 <table class="min-w-full divide-y divide-gray-200"> | ||||
|                     <thead class="bg-gray-50"> | ||||
|                         <tr> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="pdate"> | ||||
|                                 날짜 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="status"> | ||||
|                                 상태 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="projectName"> | ||||
|                                 프로젝트명 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="requestpart"> | ||||
|                                 요청부서 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="package"> | ||||
|                                 패키지 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="type"> | ||||
|                                 타입 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="process"> | ||||
|                                 프로세스 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">업무내용</th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="hrs"> | ||||
|                                 근무시간 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100" data-sort="ot"> | ||||
|                                 초과근무 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i> | ||||
|                             </th> | ||||
|                             <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">초과근무 시간</th> | ||||
|                         </tr> | ||||
|                     </thead> | ||||
|                     <tbody id="jobTableBody" class="bg-white divide-y divide-gray-200"> | ||||
|                         <!-- 데이터가 여기에 동적으로 로드됩니다 --> | ||||
|                     </tbody> | ||||
|                 </table> | ||||
|             </div> | ||||
|             <!-- 로딩 상태 --> | ||||
|             <div id="loadingState" class="hidden p-8 text-center"> | ||||
|                 <div class="inline-flex items-center"> | ||||
|                     <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-primary" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | ||||
|                         <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | ||||
|                         <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | ||||
|                     </svg> | ||||
|                     <span class="text-gray-600">데이터를 불러오는 중...</span> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <!-- 빈 상태 --> | ||||
|             <div id="emptyState" class="hidden p-8 text-center"> | ||||
|                 <i data-feather="inbox" class="w-12 h-12 text-gray-400 mx-auto mb-4"></i> | ||||
|                 <p class="text-gray-500">업무일지 데이터가 없습니다.</p> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|  | ||||
|  | ||||
|         <!-- 페이지네이션 --> | ||||
|         <div id="pagination" class="mt-6 flex items-center justify-between"> | ||||
|             <div class="flex items-center space-x-2"> | ||||
|                 <span class="text-sm text-gray-700">페이지당 행 수:</span> | ||||
|                 <select id="pageSize" class="border border-gray-300 rounded-md px-2 py-1 text-sm"> | ||||
|                     <option value="10">10</option> | ||||
|                     <option value="25" selected>25</option> | ||||
|                     <option value="50">50</option> | ||||
|                     <option value="100">100</option> | ||||
|                 </select> | ||||
|             </div> | ||||
|             <div class="flex items-center space-x-2"> | ||||
|                 <button id="prevPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed"> | ||||
|                     이전 | ||||
|                 </button> | ||||
|                 <span id="pageInfo" class="text-sm text-gray-700">1 / 1</span> | ||||
|                 <button id="nextPage" class="px-3 py-1 border border-gray-300 rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed"> | ||||
|                     다음 | ||||
|                 </button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </main> | ||||
|  | ||||
|     <!-- 상세 모달 --> | ||||
|     <div id="detailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50"> | ||||
|         <div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white"> | ||||
|             <div class="mt-3"> | ||||
|                 <div class="flex items-center justify-between mb-4"> | ||||
|                     <h3 class="text-lg font-medium text-gray-900">업무 상세정보</h3> | ||||
|                     <button id="closeModal" class="text-gray-400 hover:text-gray-600"> | ||||
|                         <i data-feather="x" class="w-6 h-6"></i> | ||||
|                     </button> | ||||
|                 </div> | ||||
|                 <div id="modalContent" class="space-y-4"> | ||||
|                     <!-- 모달 내용이 여기에 동적으로 로드됩니다 --> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <script> | ||||
|         // 전역 변수 | ||||
|         let jobData = []; | ||||
|         let filteredData = []; | ||||
|         let currentPage = 1; | ||||
|         let pageSize = 25; | ||||
|         let sortColumn = 'pdate'; | ||||
|         let sortDirection = 'desc'; | ||||
|  | ||||
|         // 초기화 | ||||
|         document.addEventListener('DOMContentLoaded', function() { | ||||
|             initializeApp(); | ||||
|             loadJobData(); | ||||
|         }); | ||||
|  | ||||
|         function initializeApp() { | ||||
|             // 현재 날짜 표시 | ||||
|             const now = new Date(); | ||||
|             document.getElementById('currentDate').textContent = now.toLocaleDateString('ko-KR', { | ||||
|                 year: 'numeric', | ||||
|                 month: 'long', | ||||
|                 day: 'numeric', | ||||
|                 weekday: 'long' | ||||
|             }); | ||||
|  | ||||
|             // 조회기간 기본값 설정 (이번 달) | ||||
|             const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1); | ||||
|             const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); | ||||
|              | ||||
|             document.getElementById('startDate').value = currentMonth.toISOString().split('T')[0]; | ||||
|             document.getElementById('endDate').value = lastDayOfMonth.toISOString().split('T')[0]; | ||||
|  | ||||
|             // 이벤트 리스너 등록 | ||||
|             document.getElementById('refreshBtn').addEventListener('click', loadJobData); | ||||
|             document.getElementById('startDate').addEventListener('change', loadJobData); | ||||
|             document.getElementById('endDate').addEventListener('change', loadJobData); | ||||
|             document.getElementById('statusFilter').addEventListener('change', filterData); | ||||
|             document.getElementById('projectFilter').addEventListener('change', filterData); | ||||
|             document.getElementById('searchInput').addEventListener('input', filterData); | ||||
|             document.getElementById('clearFilterBtn').addEventListener('click', clearFilters); | ||||
|             document.getElementById('pageSize').addEventListener('change', function() { | ||||
|                 pageSize = parseInt(this.value); | ||||
|                 currentPage = 1; | ||||
|                 renderTable(); | ||||
|             }); | ||||
|             document.getElementById('prevPage').addEventListener('click', function() { | ||||
|                 if (currentPage > 1) { | ||||
|                     currentPage--; | ||||
|                     renderTable(); | ||||
|                 } | ||||
|             }); | ||||
|             document.getElementById('nextPage').addEventListener('click', function() { | ||||
|                 const maxPage = Math.ceil(filteredData.length / pageSize); | ||||
|                 if (currentPage < maxPage) { | ||||
|                     currentPage++; | ||||
|                     renderTable(); | ||||
|                 } | ||||
|             }); | ||||
|             document.getElementById('exportBtn').addEventListener('click', exportToExcel); | ||||
|             document.getElementById('closeModal').addEventListener('click', closeModal); | ||||
|  | ||||
|             // 정렬 이벤트 리스너 | ||||
|             document.querySelectorAll('[data-sort]').forEach(th => { | ||||
|                 th.addEventListener('click', function() { | ||||
|                     const column = this.getAttribute('data-sort'); | ||||
|                     if (sortColumn === column) { | ||||
|                         sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; | ||||
|                     } else { | ||||
|                         sortColumn = column; | ||||
|                         sortDirection = 'asc'; | ||||
|                     } | ||||
|                     sortData(); | ||||
|                     renderTable(); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             // 모달 외부 클릭 시 닫기 | ||||
|             document.getElementById('detailModal').addEventListener('click', function(e) { | ||||
|                 if (e.target === this) { | ||||
|                     closeModal(); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             // Feather 아이콘 초기화 | ||||
|             feather.replace(); | ||||
|         } | ||||
|  | ||||
|         async function loadJobData() { | ||||
|             showLoading(true); | ||||
|             try { | ||||
|                 // 조회기간 파라미터 가져오기 | ||||
|                 const startDate = document.getElementById('startDate').value; | ||||
|                 const endDate = document.getElementById('endDate').value; | ||||
|                  | ||||
|                 // API URL 구성 | ||||
|                 let url = '/DashBoard/GetJobData'; | ||||
|                 const params = new URLSearchParams(); | ||||
|                 if (startDate) params.append('startDate', startDate); | ||||
|                 if (endDate) params.append('endDate', endDate); | ||||
|                  | ||||
|                 if (params.toString()) { | ||||
|                     url += '?' + params.toString(); | ||||
|                 } | ||||
|                  | ||||
|                 const response = await fetch(url); | ||||
|                 if (!response.ok) { | ||||
|                     throw new Error('데이터를 불러오는데 실패했습니다.'); | ||||
|                 } | ||||
|                 jobData = await response.json(); | ||||
|                 filteredData = [...jobData]; | ||||
|                  | ||||
|                 updateStatistics(); | ||||
|                 updateProjectFilter(); | ||||
|                 sortData(); | ||||
|                 renderTable(); | ||||
|                 // 확장된 행들 닫기 | ||||
|                 closeAllExpandedRows(); | ||||
|             } catch (error) { | ||||
|                 console.error('Error loading job data:', error); | ||||
|                 showError('데이터를 불러오는데 실패했습니다: ' + error.message); | ||||
|             } finally { | ||||
|                 showLoading(false); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function updateStatistics() { | ||||
|             const totalDays = new Set(jobData.map(item => item.pdate)).size; | ||||
|             const totalHours = jobData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0); | ||||
|             const totalOT = jobData.reduce((sum, item) => sum + (parseFloat(item.ot) || 0), 0); | ||||
|             const activeProjects = new Set(jobData.filter(item => item.status === '진행중').map(item => item.projectName)).size; | ||||
|  | ||||
|             document.getElementById('totalDays').textContent = totalDays; | ||||
|             document.getElementById('totalHours').textContent = totalHours.toFixed(1) + 'h'; | ||||
|             document.getElementById('totalOT').textContent = totalOT.toFixed(1) + 'h'; | ||||
|             document.getElementById('activeProjects').textContent = activeProjects; | ||||
|         } | ||||
|  | ||||
|         function updateProjectFilter() { | ||||
|             const projectSelect = document.getElementById('projectFilter'); | ||||
|             const projects = [...new Set(jobData.map(item => item.projectName).filter(Boolean))]; | ||||
|              | ||||
|             // 기존 옵션 제거 (전체 옵션 제외) | ||||
|             while (projectSelect.children.length > 1) { | ||||
|                 projectSelect.removeChild(projectSelect.lastChild); | ||||
|             } | ||||
|              | ||||
|             // 새 옵션 추가 | ||||
|             projects.forEach(project => { | ||||
|                 const option = document.createElement('option'); | ||||
|                 option.value = project; | ||||
|                 option.textContent = project; | ||||
|                 projectSelect.appendChild(option); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function filterData() { | ||||
|             const statusFilter = document.getElementById('statusFilter').value; | ||||
|             const projectFilter = document.getElementById('projectFilter').value; | ||||
|             const searchTerm = document.getElementById('searchInput').value.toLowerCase(); | ||||
|  | ||||
|             filteredData = jobData.filter(item => { | ||||
|                 const statusMatch = !statusFilter || item.status === statusFilter; | ||||
|                 const projectMatch = !projectFilter || item.projectName === projectFilter; | ||||
|                 const searchMatch = !searchTerm ||  | ||||
|                     (item.description && item.description.toLowerCase().includes(searchTerm)) || | ||||
|                     (item.projectName && item.projectName.toLowerCase().includes(searchTerm)) || | ||||
|                     (item.requestpart && item.requestpart.toLowerCase().includes(searchTerm)); | ||||
|  | ||||
|                 return statusMatch && projectMatch && searchMatch; | ||||
|             }); | ||||
|  | ||||
|             currentPage = 1; | ||||
|             sortData(); | ||||
|             renderTable(); | ||||
|         } | ||||
|  | ||||
|         function sortData() { | ||||
|             filteredData.sort((a, b) => { | ||||
|                 let aVal = a[sortColumn]; | ||||
|                 let bVal = b[sortColumn]; | ||||
|  | ||||
|                 // 날짜 정렬 | ||||
|                 if (sortColumn === 'pdate') { | ||||
|                     aVal = new Date(aVal); | ||||
|                     bVal = new Date(bVal); | ||||
|                 } | ||||
|                 // 숫자 정렬 | ||||
|                 else if (['hrs', 'ot'].includes(sortColumn)) { | ||||
|                     aVal = parseFloat(aVal) || 0; | ||||
|                     bVal = parseFloat(bVal) || 0; | ||||
|                 } | ||||
|                 // 문자열 정렬 | ||||
|                 else { | ||||
|                     aVal = (aVal || '').toString().toLowerCase(); | ||||
|                     bVal = (bVal || '').toString().toLowerCase(); | ||||
|                 } | ||||
|  | ||||
|                 if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; | ||||
|                 if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; | ||||
|                 return 0; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function renderTable() { | ||||
|             const tbody = document.getElementById('jobTableBody'); | ||||
|             const startIndex = (currentPage - 1) * pageSize; | ||||
|             const endIndex = startIndex + pageSize; | ||||
|             const pageData = filteredData.slice(startIndex, endIndex); | ||||
|  | ||||
|             if (pageData.length === 0) { | ||||
|                 showEmptyState(); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             hideEmptyState(); | ||||
|             tbody.innerHTML = ''; | ||||
|  | ||||
|             pageData.forEach((item, index) => { | ||||
|                 const row = document.createElement('tr'); | ||||
|                 row.className = 'hover:bg-gray-50 cursor-pointer'; | ||||
|                 row.setAttribute('data-item-id', item.idx || index); | ||||
|                 row.addEventListener('click', () => toggleRowDetail(item, row)); | ||||
|  | ||||
|                 const statusColor = getStatusColor(item.status); | ||||
|                 const otTime = item.otStart && item.otEnd ?  | ||||
|                     `${item.otStart} ~ ${item.otEnd}` : '-'; | ||||
|  | ||||
|                 row.innerHTML = ` | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         <div class="flex items-center"> | ||||
|                             <i data-feather="chevron-right" class="w-4 h-4 mr-2 text-gray-400 expand-icon"></i> | ||||
|                             ${formatDate(item.pdate)} | ||||
|                         </div> | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap"> | ||||
|                         <span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${statusColor}"> | ||||
|                             ${item.status || '-'} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.projectName || '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.requestpart || '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.package || '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.type || '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.process || '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 text-sm text-gray-900"> | ||||
|                         <div class="max-w-xs truncate cursor-pointer hover:text-primary hover:underline"  | ||||
|                              title="${item.description || ''}"  | ||||
|                              onclick="event.stopPropagation(); showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})"> | ||||
|                             ${item.description || '-'} | ||||
|                         </div> | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'} | ||||
|                     </td> | ||||
|                     <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900"> | ||||
|                         ${otTime} | ||||
|                     </td> | ||||
|                 `; | ||||
|  | ||||
|                 tbody.appendChild(row); | ||||
|                  | ||||
|                 // 확장 행 추가 (숨겨진 상태) | ||||
|                 const expandRow = document.createElement('tr'); | ||||
|                 expandRow.className = 'hidden expand-row'; | ||||
|                 expandRow.setAttribute('data-parent-id', item.idx || index); | ||||
|                 expandRow.innerHTML = ` | ||||
|                     <td colspan="11" class="px-6 py-4 bg-gray-50 border-t border-gray-200"> | ||||
|                         <div> | ||||
|                             <label class="block text-sm font-medium text-gray-700 mb-2">업무내용</label> | ||||
|                             <div class="bg-white rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors border"  | ||||
|                                  onclick="showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})"> | ||||
|                                 <p class="text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p> | ||||
|                                 <div class="mt-2 text-xs text-gray-500 flex items-center"> | ||||
|                                     <i data-feather="maximize-2" class="w-4 h-4 mr-1"></i> | ||||
|                                     클릭하여 전체 내용 보기 | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </td> | ||||
|                 `; | ||||
|  | ||||
|                 tbody.appendChild(expandRow); | ||||
|             }); | ||||
|  | ||||
|             updatePagination(); | ||||
|             feather.replace(); | ||||
|         } | ||||
|  | ||||
|         function getStatusColor(status) { | ||||
|             switch (status) { | ||||
|                 case '진행 중': return 'bg-blue-100 text-blue-800'; | ||||
|                 case '진행 완료': return 'bg-green-100 text-green-800'; | ||||
|                 case '대기': return 'bg-yellow-100 text-yellow-800'; | ||||
|                 default: return 'bg-gray-100 text-gray-800'; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function formatDate(dateString) { | ||||
|             if (!dateString) return '-'; | ||||
|             const date = new Date(dateString); | ||||
|             return date.toLocaleDateString('ko-KR'); | ||||
|         } | ||||
|  | ||||
|         function updatePagination() { | ||||
|             const maxPage = Math.ceil(filteredData.length / pageSize); | ||||
|             const startItem = (currentPage - 1) * pageSize + 1; | ||||
|             const endItem = Math.min(currentPage * pageSize, filteredData.length); | ||||
|  | ||||
|             document.getElementById('pageInfo').textContent = `${currentPage} / ${maxPage}`; | ||||
|             document.getElementById('prevPage').disabled = currentPage <= 1; | ||||
|             document.getElementById('nextPage').disabled = currentPage >= maxPage; | ||||
|         } | ||||
|  | ||||
|         function showDetailModal(item) { | ||||
|             // 문자열로 전달된 경우 JSON 파싱 | ||||
|             if (typeof item === 'string') { | ||||
|                 try { | ||||
|                     item = JSON.parse(item); | ||||
|                 } catch (e) { | ||||
|                     console.error('Error parsing item:', e); | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             const modal = document.getElementById('detailModal'); | ||||
|             const content = document.getElementById('modalContent'); | ||||
|  | ||||
|             content.innerHTML = ` | ||||
|                 <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">날짜</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">상태</label> | ||||
|                         <p class="mt-1"> | ||||
|                             <span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}"> | ||||
|                                 ${item.status || '-'} | ||||
|                             </span> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">프로젝트명</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.projectName || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">요청부서</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">패키지</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">타입</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">프로세스</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">근무시간</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">초과근무</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">초과근무 시간</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.otStart && item.otEnd ? `${item.otStart} ~ ${item.otEnd}` : '-'}</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="mt-4"> | ||||
|                     <label class="block text-sm font-medium text-gray-700">업무내용</label> | ||||
|                     <p class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p> | ||||
|                 </div> | ||||
|             `; | ||||
|  | ||||
|             modal.classList.remove('hidden'); | ||||
|             feather.replace(); | ||||
|         } | ||||
|  | ||||
|         function closeModal() { | ||||
|             document.getElementById('detailModal').classList.add('hidden'); | ||||
|         } | ||||
|  | ||||
|         function showSelectedJobDetail(item) { | ||||
|             const detailContainer = document.getElementById('selectedJobDetail'); | ||||
|             const contentContainer = document.getElementById('selectedJobContent'); | ||||
|              | ||||
|             // 기존 선택된 행의 스타일 제거 | ||||
|             document.querySelectorAll('#jobTableBody tr').forEach(row => { | ||||
|                 row.classList.remove('bg-blue-50', 'border-l-4', 'border-blue-500'); | ||||
|             }); | ||||
|              | ||||
|             // 현재 행에 선택 스타일 적용 | ||||
|             const currentRow = event.target.closest('tr'); | ||||
|             if (currentRow) { | ||||
|                 currentRow.classList.add('bg-blue-50', 'border-l-4', 'border-blue-500'); | ||||
|             } | ||||
|              | ||||
|             contentContainer.innerHTML = ` | ||||
|                 <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">날짜</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">상태</label> | ||||
|                         <p class="mt-1"> | ||||
|                             <span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}"> | ||||
|                                 ${item.status || '-'} | ||||
|                             </span> | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">프로젝트명</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.projectName || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">요청부서</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">패키지</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">타입</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">프로세스</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">근무시간</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">초과근무</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}</p> | ||||
|                     </div> | ||||
|                     <div> | ||||
|                         <label class="block text-sm font-medium text-gray-700">초과근무 시간</label> | ||||
|                         <p class="mt-1 text-sm text-gray-900">${item.otStart && item.otEnd ? `${item.otStart} ~ ${item.otEnd}` : '-'}</p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div> | ||||
|                     <label class="block text-sm font-medium text-gray-700 mb-2">업무내용</label> | ||||
|                     <div class="bg-gray-50 rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors"  | ||||
|                          onclick="showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})"> | ||||
|                         <p class="text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p> | ||||
|                         <div class="mt-2 text-xs text-gray-500 flex items-center"> | ||||
|                             <i data-feather="maximize-2" class="w-4 h-4 mr-1"></i> | ||||
|                             클릭하여 전체 내용 보기 | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             `; | ||||
|              | ||||
|             detailContainer.classList.remove('hidden'); | ||||
|             feather.replace(); | ||||
|              | ||||
|             // 부드러운 스크롤로 상세 내용으로 이동 | ||||
|             detailContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | ||||
|         } | ||||
|  | ||||
|         function toggleRowDetail(item, row) { | ||||
|             const itemId = item.idx || row.getAttribute('data-item-id'); | ||||
|             const expandRow = document.querySelector(`tr[data-parent-id="${itemId}"]`); | ||||
|             const expandIcon = row.querySelector('.expand-icon'); | ||||
|              | ||||
|             if (expandRow.classList.contains('hidden')) { | ||||
|                 // 확장 | ||||
|                 expandRow.classList.remove('hidden'); | ||||
|                 expandIcon.setAttribute('data-feather', 'chevron-down'); | ||||
|                 row.classList.add('bg-blue-50'); | ||||
|             } else { | ||||
|                 // 축소 | ||||
|                 expandRow.classList.add('hidden'); | ||||
|                 expandIcon.setAttribute('data-feather', 'chevron-right'); | ||||
|                 row.classList.remove('bg-blue-50'); | ||||
|             } | ||||
|              | ||||
|             // 아이콘 업데이트 | ||||
|             feather.replace(); | ||||
|         } | ||||
|  | ||||
|         function closeAllExpandedRows() { | ||||
|             document.querySelectorAll('.expand-row').forEach(row => { | ||||
|                 row.classList.add('hidden'); | ||||
|             }); | ||||
|              | ||||
|             document.querySelectorAll('#jobTableBody tr').forEach(row => { | ||||
|                 if (!row.classList.contains('expand-row')) { | ||||
|                     row.classList.remove('bg-blue-50'); | ||||
|                     const icon = row.querySelector('.expand-icon'); | ||||
|                     if (icon) { | ||||
|                         icon.setAttribute('data-feather', 'chevron-right'); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|              | ||||
|             feather.replace(); | ||||
|         } | ||||
|  | ||||
|         function showLoading(show) { | ||||
|             const loadingState = document.getElementById('loadingState'); | ||||
|             const tableBody = document.getElementById('jobTableBody'); | ||||
|              | ||||
|             if (show) { | ||||
|                 loadingState.classList.remove('hidden'); | ||||
|                 tableBody.style.display = 'none'; | ||||
|             } else { | ||||
|                 loadingState.classList.add('hidden'); | ||||
|                 tableBody.style.display = 'table-row-group'; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function showEmptyState() { | ||||
|             document.getElementById('emptyState').classList.remove('hidden'); | ||||
|             document.getElementById('jobTableBody').style.display = 'none'; | ||||
|         } | ||||
|  | ||||
|         function hideEmptyState() { | ||||
|             document.getElementById('emptyState').classList.add('hidden'); | ||||
|             document.getElementById('jobTableBody').style.display = 'table-row-group'; | ||||
|         } | ||||
|  | ||||
|         function showError(message) { | ||||
|             // 간단한 에러 알림 (실제 구현에서는 더 나은 알림 시스템 사용) | ||||
|             alert(message); | ||||
|         } | ||||
|  | ||||
|         function clearFilters() { | ||||
|             // 조회기간을 이번 달로 초기화 | ||||
|             const now = new Date(); | ||||
|             const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1); | ||||
|             const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); | ||||
|              | ||||
|             document.getElementById('startDate').value = currentMonth.toISOString().split('T')[0]; | ||||
|             document.getElementById('endDate').value = lastDayOfMonth.toISOString().split('T')[0]; | ||||
|              | ||||
|             // 다른 필터들 초기화 | ||||
|             document.getElementById('statusFilter').value = ''; | ||||
|             document.getElementById('projectFilter').value = ''; | ||||
|             document.getElementById('searchInput').value = ''; | ||||
|              | ||||
|             // 서버에서 새로운 데이터 가져오기 | ||||
|             loadJobData(); | ||||
|             // 확장된 행들 닫기 | ||||
|             closeAllExpandedRows(); | ||||
|         } | ||||
|  | ||||
|         function exportToExcel() { | ||||
|             if (filteredData.length === 0) { | ||||
|                 alert('내보낼 데이터가 없습니다.'); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // 조회기간 정보 가져오기 | ||||
|             const startDate = document.getElementById('startDate').value; | ||||
|             const endDate = document.getElementById('endDate').value; | ||||
|             const periodText = startDate && endDate ? `_${startDate}_${endDate}` : ''; | ||||
|  | ||||
|             // CSV 형식으로 데이터 변환 | ||||
|             const headers = ['날짜', '상태', '프로젝트명', '요청부서', '패키지', '타입', '프로세스', '업무내용', '근무시간', '초과근무', '초과근무시작', '초과근무종료']; | ||||
|             const csvContent = [ | ||||
|                 headers.join(','), | ||||
|                 ...filteredData.map(item => [ | ||||
|                     formatDate(item.pdate), | ||||
|                     item.status || '', | ||||
|                     item.projectName || '', | ||||
|                     item.requestpart || '', | ||||
|                     item.package || '', | ||||
|                     item.type || '', | ||||
|                     item.process || '', | ||||
|                     `"${(item.description || '').replace(/"/g, '""')}"`, | ||||
|                     item.hrs || '', | ||||
|                     item.ot || '', | ||||
|                     item.otStart || '', | ||||
|                     item.otEnd || '' | ||||
|                 ].join(',')) | ||||
|             ].join('\n'); | ||||
|  | ||||
|             // 파일 다운로드 | ||||
|             const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); | ||||
|             const link = document.createElement('a'); | ||||
|             const url = URL.createObjectURL(blob); | ||||
|             link.setAttribute('href', url); | ||||
|             link.setAttribute('download', `업무일지${periodText}_${new Date().toISOString().split('T')[0]}.csv`); | ||||
|             link.style.visibility = 'hidden'; | ||||
|             document.body.appendChild(link); | ||||
|             link.click(); | ||||
|             document.body.removeChild(link); | ||||
|         } | ||||
|     </script> | ||||
| </body> | ||||
| </html>  | ||||
		Reference in New Issue
	
	Block a user
	 backuppc
					backuppc