- MailService.cs 추가: ServiceBase 상속받는 Windows 서비스 클래스 - Program.cs 수정: 서비스/콘솔 모드 지원, 설치/제거 기능 추가 - 프로젝트 설정: System.ServiceProcess 참조 추가 - 배치 파일 추가: 서비스 설치/제거/콘솔실행 스크립트 주요 기능: - Windows 서비스로 백그라운드 실행 - 명령행 인수로 모드 선택 (-install, -uninstall, -console) - EventLog를 통한 서비스 로깅 - 안전한 서비스 시작/중지 처리 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
339 lines
17 KiB
JavaScript
339 lines
17 KiB
JavaScript
const { useState, useEffect } = React;
|
|
|
|
function Kuntae() {
|
|
// 상태 관리
|
|
const [kuntaeData, setKuntaeData] = useState([]);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [filters, setFilters] = useState({
|
|
startDate: '',
|
|
endDate: ''
|
|
});
|
|
|
|
// 통계 데이터
|
|
const [statistics, setStatistics] = useState({
|
|
totalDays: 0,
|
|
approvedDays: 0,
|
|
pendingDays: 0,
|
|
rejectedDays: 0
|
|
});
|
|
|
|
// 컴포넌트 마운트시 초기화
|
|
useEffect(() => {
|
|
initializeFilters();
|
|
}, []);
|
|
|
|
// 초기 필터 설정 (이번 달)
|
|
const initializeFilters = () => {
|
|
const now = new Date();
|
|
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
|
|
setFilters({
|
|
startDate: firstDay.toISOString().split('T')[0],
|
|
endDate: lastDay.toISOString().split('T')[0]
|
|
});
|
|
|
|
// 초기 데이터 로드
|
|
loadKuntaeData({
|
|
startDate: firstDay.toISOString().split('T')[0],
|
|
endDate: lastDay.toISOString().split('T')[0]
|
|
});
|
|
};
|
|
|
|
// 근태 데이터 로드
|
|
const loadKuntaeData = async (searchFilters = filters) => {
|
|
setIsLoading(true);
|
|
try {
|
|
let url = '/Kuntae/GetData';
|
|
const params = new URLSearchParams();
|
|
if (searchFilters.startDate) params.append('startDate', searchFilters.startDate);
|
|
if (searchFilters.endDate) params.append('endDate', searchFilters.endDate);
|
|
|
|
if (params.toString()) {
|
|
url += '?' + params.toString();
|
|
}
|
|
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}: 데이터를 불러오는데 실패했습니다.`);
|
|
}
|
|
|
|
const responseData = await response.json();
|
|
|
|
if (Array.isArray(responseData)) {
|
|
setKuntaeData(responseData);
|
|
updateStatistics(responseData);
|
|
} else if (responseData.error) {
|
|
throw new Error(responseData.error);
|
|
} else {
|
|
setKuntaeData([]);
|
|
updateStatistics([]);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading kuntae data:', error);
|
|
setKuntaeData([]);
|
|
updateStatistics([]);
|
|
showNotification('데이터를 불러오는데 실패했습니다: ' + error.message, 'error');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// 통계 업데이트
|
|
const updateStatistics = (data) => {
|
|
const totalDays = data.length;
|
|
const approvedDays = data.filter(item => item.status === '승인').length;
|
|
const pendingDays = data.filter(item => item.status === '대기').length;
|
|
const rejectedDays = data.filter(item => item.status === '반려').length;
|
|
|
|
setStatistics({
|
|
totalDays,
|
|
approvedDays,
|
|
pendingDays,
|
|
rejectedDays
|
|
});
|
|
};
|
|
|
|
// 필터 변경 핸들러
|
|
const handleFilterChange = (field, value) => {
|
|
setFilters(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
// 검색 실행
|
|
const handleSearch = () => {
|
|
loadKuntaeData();
|
|
};
|
|
|
|
// 유틸리티 함수들
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '-';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('ko-KR');
|
|
};
|
|
|
|
const getStatusColor = (status) => {
|
|
switch (status) {
|
|
case '승인': return 'bg-green-100 text-green-800';
|
|
case '대기': return 'bg-yellow-100 text-yellow-800';
|
|
case '반려': return 'bg-red-100 text-red-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 = `
|
|
<div class="flex items-center">
|
|
<span class="ml-2">${message}</span>
|
|
</div>
|
|
`;
|
|
|
|
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);
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto px-4 py-8">
|
|
{/* 개발중 경고 메시지 */}
|
|
<div className="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
|
</svg>
|
|
<div>
|
|
<p className="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
|
<p className="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 검색 및 필터 섹션 */}
|
|
<div className="glass-effect rounded-lg p-6 mb-6 animate-slide-up">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-white/80 mb-2">시작일</label>
|
|
<input
|
|
type="date"
|
|
value={filters.startDate}
|
|
onChange={(e) => handleFilterChange('startDate', e.target.value)}
|
|
className="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-white/80 mb-2">종료일</label>
|
|
<input
|
|
type="date"
|
|
value={filters.endDate}
|
|
onChange={(e) => handleFilterChange('endDate', e.target.value)}
|
|
className="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
<div className="flex items-end">
|
|
<button
|
|
onClick={handleSearch}
|
|
disabled={isLoading}
|
|
className="w-full glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center justify-center disabled:opacity-50"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<div className="loading inline-block w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></div>
|
|
로딩중...
|
|
</>
|
|
) : (
|
|
'조회'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 통계 카드 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6 animate-slide-up">
|
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-primary-500/20 rounded-lg">
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-white/80">총 신청건</p>
|
|
<p className="text-2xl font-bold text-white">{statistics.totalDays}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-success-500/20 rounded-lg">
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-white/80">승인</p>
|
|
<p className="text-2xl font-bold text-green-300">{statistics.approvedDays}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-warning-500/20 rounded-lg">
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-white/80">대기</p>
|
|
<p className="text-2xl font-bold text-yellow-300">{statistics.pendingDays}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
|
<div className="flex items-center">
|
|
<div className="p-3 bg-red-500/20 rounded-lg">
|
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</div>
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-white/80">반려</p>
|
|
<p className="text-2xl font-bold text-red-300">{statistics.rejectedDays}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 데이터 테이블 */}
|
|
<div className="glass-effect rounded-lg overflow-hidden animate-slide-up">
|
|
<div className="table-container custom-scrollbar" style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
|
<table className="min-w-full divide-y divide-white/20">
|
|
<thead className="bg-white/10 sticky top-0">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">신청일</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">시작일</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">종료일</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">휴가종류</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">일수</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">상태</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사유</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/10">
|
|
{isLoading ? (
|
|
<tr>
|
|
<td colSpan="7" className="p-8 text-center">
|
|
<div className="inline-flex items-center">
|
|
<div className="loading inline-block w-5 h-5 border-3 border-white/30 border-t-white rounded-full animate-spin mr-3"></div>
|
|
<span className="text-white/80">데이터를 불러오는 중...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
) : kuntaeData.length === 0 ? (
|
|
<tr>
|
|
<td colSpan="7" className="p-8 text-center">
|
|
<svg className="w-12 h-12 text-white/60 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>
|
|
<p className="text-white/70">근태 데이터가 없습니다.</p>
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
kuntaeData.map((item, index) => (
|
|
<tr key={item.idx || index} className="hover:bg-white/10 transition-colors">
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
|
{formatDate(item.requestDate)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
|
{formatDate(item.startDate)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
|
{formatDate(item.endDate)}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
|
{item.type || '-'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
|
{item.days || '0'}일
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}`}>
|
|
{item.status || '-'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm text-white">
|
|
<div className="max-w-xs truncate" title={item.reason || ''}>
|
|
{item.reason || '-'}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |