const { useState, useEffect } = React; function JobReport() { // 상태 관리 const [jobData, setJobData] = useState([]); const [filteredData, setFilteredData] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(25); const [sortColumn, setSortColumn] = useState('pdate'); const [sortDirection, setSortDirection] = useState('desc'); const [isLoading, setIsLoading] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [editData, setEditData] = useState({}); const [isEditMode, setIsEditMode] = useState(false); // 필터 상태 const [filters, setFilters] = useState({ startDate: '', endDate: '', status: '', type: '', user: '', project: '', search: '' }); // 통계 데이터 const [statistics, setStatistics] = useState({ totalDays: 0, todayHours: 0, todayProgress: 0, totalOT: 0, activeProjects: 0 }); // 컴포넌트 마운트시 초기화 useEffect(() => { initializeFilters(); loadJobData(); loadUserList(); }, []); // 필터 변경 시 데이터 필터링 useEffect(() => { filterData(); }, [jobData, filters]); // 초기 필터 설정 (오늘부터 -2주) const initializeFilters = () => { const now = new Date(); const today = now.toISOString().split('T')[0]; const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0]; setFilters(prev => ({ ...prev, startDate: twoWeeksAgo, endDate: today })); }; // 업무일지 데이터 로드 const loadJobData = async () => { setIsLoading(true); try { let url = '/Jobreport/GetJobData'; const params = new URLSearchParams(); if (filters.startDate) params.append('startDate', filters.startDate); if (filters.endDate) params.append('endDate', filters.endDate); if (filters.user) params.append('user', filters.user); if (params.toString()) { url += '?' + params.toString(); } const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: 데이터를 불러오는데 실패했습니다.`); } const responseText = await response.text(); const responseData = JSON.parse(responseText); if (Array.isArray(responseData)) { setJobData(responseData); } else if (responseData.error) { throw new Error(responseData.error); } else { setJobData([]); } } catch (error) { console.error('Error loading job data:', error); setJobData([]); showNotification('데이터를 불러오는데 실패했습니다: ' + error.message, 'error'); } finally { setIsLoading(false); } }; // 사용자 목록 로드 const loadUserList = async () => { try { const response = await fetch('/Jobreport/GetUsers'); if (response.ok) { const data = await response.json(); // 사용자 데이터를 상태로 저장 (필요시) } } catch (error) { console.error('사용자 목록 로드 중 오류:', error); } }; // 통계 업데이트 useEffect(() => { updateStatistics(); }, [jobData]); const updateStatistics = () => { const totalDays = new Set(jobData.map(item => item.pdate)).size; 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; const now = new Date(); const today = now.toISOString().split('T')[0]; const todayData = jobData.filter(item => { if (!item.pdate) return false; const itemDate = item.pdate.toString(); if (itemDate.length >= 10) { return itemDate.substring(0, 10) === today; } return false; }); let todayHours = 0; if (todayData.length > 0) { todayHours = todayData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0); } const todayProgress = (todayHours / 8) * 100; setStatistics({ totalDays, todayHours, todayProgress, totalOT, activeProjects }); }; // 데이터 필터링 const filterData = () => { let filtered = jobData.filter(item => { const statusMatch = !filters.status || item.status === filters.status; const typeMatch = !filters.type || item.type === filters.type; const projectMatch = !filters.project || item.projectName === filters.project; const searchMatch = !filters.search || (item.description && item.description.toLowerCase().includes(filters.search.toLowerCase())) || (item.projectName && item.projectName.toLowerCase().includes(filters.search.toLowerCase())) || (item.requestpart && item.requestpart.toLowerCase().includes(filters.search.toLowerCase())); return statusMatch && typeMatch && projectMatch && searchMatch; }); // 정렬 filtered.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; }); setFilteredData(filtered); setCurrentPage(1); }; // 필터 변경 핸들러 const handleFilterChange = (field, value) => { setFilters(prev => ({ ...prev, [field]: value })); // 날짜 필터 변경시 데이터 다시 로드 if (field === 'startDate' || field === 'endDate' || field === 'user') { setTimeout(() => loadJobData(), 100); } }; // 필터 초기화 const clearFilters = () => { const now = new Date(); const today = now.toISOString().split('T')[0]; const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0]; setFilters({ startDate: twoWeeksAgo, endDate: today, status: '', type: '', user: '', project: '', search: '' }); setTimeout(() => loadJobData(), 100); }; // 정렬 처리 const handleSort = (column) => { if (sortColumn === column) { setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); } else { setSortColumn(column); setSortDirection('asc'); } }; // 추가 모달 표시 const showAddJobModal = () => { const today = new Date().toISOString().split('T')[0]; setIsEditMode(false); setEditData({ idx: '', pdate: today, status: '진행 중', projectName: '', requestpart: '', type: '', hrs: '8', ot: '', otStart: '', otEnd: '', description: '' }); setShowEditModal(true); }; // 편집 모달 표시 const showEditJobModal = async (item) => { try { // 상세 정보 로드 const response = await fetch(`/Jobreport/GetJobDetail?id=${item.idx}`); if (response.ok) { const fullItem = await response.json(); item = fullItem.error ? item : fullItem; } } catch (error) { console.warn('Failed to load full details, using truncated data:', error); } setIsEditMode(true); setEditData({ idx: item.idx || '', pdate: item.pdate || '', status: item.status || '', projectName: item.projectName || '', requestpart: item.requestpart || '', type: item.type || '', hrs: item.hrs || '', ot: item.ot || '', otStart: item.otStart || '', otEnd: item.otEnd || '', description: item.description || '' }); setShowEditModal(true); }; // 저장 처리 const handleSave = async (event) => { event.preventDefault(); const formData = new URLSearchParams(); Object.keys(editData).forEach(key => { if (key !== 'idx' || editData.idx) { formData.append(key, editData[key]); } }); try { const url = isEditMode ? '/Jobreport/Edit' : '/Jobreport/Add'; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: formData }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${isEditMode ? '수정' : '추가'}에 실패했습니다.`); } setShowEditModal(false); await loadJobData(); showNotification(`업무일지가 성공적으로 ${isEditMode ? '수정' : '추가'}되었습니다.`, 'success'); } catch (error) { console.error('Error saving job:', error); showNotification(`업무일지 ${isEditMode ? '수정' : '추가'} 중 오류가 발생했습니다: ` + error.message, 'error'); } }; // 삭제 처리 const handleDelete = async () => { if (!editData.idx) { showNotification('삭제할 수 없는 항목입니다.', 'warning'); return; } if (!window.confirm('정말로 이 업무일지를 삭제하시겠습니까?')) { return; } try { const response = await fetch(`/Jobreport/Delete/${editData.idx}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('삭제에 실패했습니다.'); } const result = await response.json(); if (result.success) { setShowEditModal(false); await loadJobData(); showNotification('업무일지가 성공적으로 삭제되었습니다.', 'success'); } else { throw new Error(result.message || '삭제에 실패했습니다.'); } } catch (error) { console.error('Error deleting job:', error); showNotification('업무일지 삭제 중 오류가 발생했습니다: ' + error.message, 'error'); } }; // 엑셀 내보내기 const exportToExcel = () => { if (filteredData.length === 0) { showNotification('내보낼 데이터가 없습니다.', 'warning'); return; } const periodText = filters.startDate && filters.endDate ? `_${filters.startDate}_${filters.endDate}` : ''; const headers = ['날짜', '상태', '프로젝트명', '요청부서', '타입', '업무내용', '근무시간', '초과근무']; const csvContent = [ headers.join(','), ...filteredData.map(item => [ formatDate(item.pdate), item.status || '', item.projectName || '', item.requestpart || '', item.type || '', `"${(item.description || '').replace(/"/g, '""')}"`, item.hrs || '', item.ot || '' ].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); }; // 유틸리티 함수들 const formatDate = (dateString) => { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleDateString('ko-KR'); }; const 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'; } }; // 알림 표시 함수 const showNotification = (message, type = 'info') => { const colors = { info: 'bg-blue-500/90 backdrop-blur-sm', success: 'bg-green-500/90 backdrop-blur-sm', warning: 'bg-yellow-500/90 backdrop-blur-sm', error: 'bg-red-500/90 backdrop-blur-sm' }; const notification = document.createElement('div'); notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`; notification.innerHTML = `
${message}
`; notification.style.transform = 'translateX(100%)'; notification.style.opacity = '0'; document.body.appendChild(notification); setTimeout(() => { notification.style.transform = 'translateX(0)'; notification.style.opacity = '1'; }, 10); setTimeout(() => { notification.style.transform = 'translateX(100%)'; notification.style.opacity = '0'; setTimeout(() => notification.remove(), 300); }, 3000); }; // 페이지네이션 const maxPage = Math.ceil(filteredData.length / pageSize); const startIndex = (currentPage - 1) * pageSize; const endIndex = startIndex + pageSize; const pageData = filteredData.slice(startIndex, endIndex); // 프로젝트 목록 업데이트 const uniqueProjects = [...new Set(jobData.map(item => item.projectName).filter(Boolean))]; return (
{/* 개발중 경고 메시지 */}

🚧 개발중인 기능입니다

일부 기능이 정상적으로 동작하지 않을 수 있습니다.

{/* 통계 카드 */}

총 업무일수

{statistics.totalDays}

오늘 근무시간

{statistics.todayHours.toFixed(1)}h

(목표 8시간의 {statistics.todayProgress.toFixed(0)}%)

총 초과근무

{statistics.totalOT.toFixed(1)}h

진행중 프로젝트

{statistics.activeProjects}

{/* 필터 및 검색 */}
{/* 좌측: 필터 컨트롤 */}
handleFilterChange('startDate', e.target.value)} className="flex-1 bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs" /> ~ handleFilterChange('endDate', e.target.value)} className="flex-1 bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs" />
handleFilterChange('search', e.target.value)} placeholder="업무 내용 검색..." className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs" />
{/* 우측: 액션 버튼들 */}
{/* 데이터 테이블 */}
{isLoading ? ( ) : pageData.length === 0 ? ( ) : ( pageData.map((item, index) => { const hrs = parseFloat(item.hrs) || 0; const ot = parseFloat(item.ot) || 0; let workTimeDisplay = ''; if (hrs > 0) { workTimeDisplay = hrs.toFixed(1); if (ot > 0) { workTimeDisplay += '+' + ot.toFixed(1); } } else { workTimeDisplay = '-'; } return ( showEditJobModal(item)} > ); }) )}
handleSort('pdate')} > 날짜 handleSort('status')} > 상태 handleSort('hrs')} > 근무시간 handleSort('projectName')} > 프로젝트명 업무내용 handleSort('requestpart')} > 요청부서 handleSort('type')} > 타입
데이터를 불러오는 중...

업무일지 데이터가 없습니다.

{formatDate(item.pdate)} {item.status || '-'} {workTimeDisplay}
{item.projectName || '-'}
{item.description || '-'}
{item.requestpart || '-'} {item.type || '-'}
{/* 페이지네이션 */}
페이지당 행 수:
{currentPage} / {maxPage}
{/* 편집 모달 */} {showEditModal && (
{/* 모달 헤더 */}

업무일지 {isEditMode ? '편집' : '추가'}

{/* 모달 내용 */}
{/* 좌측: 기본 정보 */}
setEditData(prev => ({...prev, pdate: e.target.value}))} required className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />
setEditData(prev => ({...prev, requestpart: e.target.value}))} className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />
setEditData(prev => ({...prev, hrs: e.target.value}))} required className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />
setEditData(prev => ({...prev, ot: e.target.value}))} className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />
setEditData(prev => ({...prev, otStart: e.target.value}))} className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />
setEditData(prev => ({...prev, otEnd: e.target.value}))} className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />
{/* 우측: 프로젝트명과 업무내용 */}
setEditData(prev => ({...prev, projectName: e.target.value}))} required className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" />