Files
Groupware/Project/Web/wwwroot/Jobreport/index.html
backuppc 479a736b80 ..
2025-07-14 16:30:03 +09:00

863 lines
42 KiB
HTML

<!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, '&quot;')})">
${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, '&quot;')})">
<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, '&quot;')})">
<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>