Files
Groupware/Project/Web/wwwroot/react/Kuntae.jsx
ChiKyun Kim 6bd4f84192 feat(service): Console_SendMail을 Windows 서비스로 변환
- 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>
2025-09-11 09:08:40 +09:00

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>
);
}