- 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>
669 lines
38 KiB
JavaScript
669 lines
38 KiB
JavaScript
// DashboardApp.jsx - React Dashboard Component for GroupWare
|
|
const { useState, useEffect, useRef } = React;
|
|
|
|
function DashboardApp() {
|
|
// 상태 관리
|
|
const [dashboardData, setDashboardData] = useState({
|
|
presentCount: 0,
|
|
leaveCount: 0,
|
|
leaveRequestCount: 0,
|
|
purchaseCountNR: 0,
|
|
purchaseCountCR: 0
|
|
});
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [lastUpdated, setLastUpdated] = useState('');
|
|
const [modals, setModals] = useState({
|
|
presentUsers: false,
|
|
holidayUsers: false,
|
|
holidayRequest: false,
|
|
purchaseNR: false,
|
|
purchaseCR: false
|
|
});
|
|
|
|
const [modalData, setModalData] = useState({
|
|
presentUsers: [],
|
|
holidayUsers: [],
|
|
holidayRequests: [],
|
|
purchaseNR: [],
|
|
purchaseCR: []
|
|
});
|
|
|
|
const [todoList, setTodoList] = useState([]);
|
|
|
|
// 모달 제어 함수
|
|
const showModal = (modalName) => {
|
|
setModals(prev => ({ ...prev, [modalName]: true }));
|
|
loadModalData(modalName);
|
|
};
|
|
|
|
const hideModal = (modalName) => {
|
|
setModals(prev => ({ ...prev, [modalName]: false }));
|
|
};
|
|
|
|
// Dashboard 데이터 로드
|
|
const loadDashboardData = async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
// 실제 DashBoardController API 호출
|
|
const [
|
|
currentUserResponse,
|
|
leaveCountResponse,
|
|
holyRequestResponse,
|
|
purchaseWaitResponse
|
|
] = await Promise.all([
|
|
fetch('http://127.0.0.1:7979/DashBoard/GetCurrentUserCount'),
|
|
fetch('http://127.0.0.1:7979/DashBoard/TodayCountH'),
|
|
fetch('http://127.0.0.1:7979/DashBoard/GetHolydayRequestCount'),
|
|
fetch('http://127.0.0.1:7979/DashBoard/GetPurchaseWaitCount')
|
|
]);
|
|
|
|
// 현재 근무자 수 (JSON 응답)
|
|
const currentUserData = await currentUserResponse.json();
|
|
const presentCount = currentUserData.Count || 0;
|
|
|
|
// 휴가자 수 (텍스트 응답)
|
|
const leaveCountText = await leaveCountResponse.text();
|
|
const leaveCount = parseInt(leaveCountText.replace(/"/g, ''), 10) || 0;
|
|
|
|
// 휴가 요청 수 (JSON 응답)
|
|
const holyRequestData = await holyRequestResponse.json();
|
|
const leaveRequestCount = holyRequestData.HOLY || 0;
|
|
|
|
// 구매 대기 수 (JSON 응답)
|
|
const purchaseWaitData = await purchaseWaitResponse.json();
|
|
const purchaseCountNR = purchaseWaitData.NR || 0;
|
|
const purchaseCountCR = purchaseWaitData.CR || 0;
|
|
|
|
setDashboardData({
|
|
presentCount,
|
|
leaveCount,
|
|
leaveRequestCount,
|
|
purchaseCountNR,
|
|
purchaseCountCR
|
|
});
|
|
|
|
setLastUpdated(new Date().toLocaleString('ko-KR'));
|
|
} catch (error) {
|
|
console.error('대시보드 데이터 로드 실패:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
// 모달 데이터 로드
|
|
const loadModalData = async (modalName) => {
|
|
try {
|
|
let endpoint = '';
|
|
switch (modalName) {
|
|
case 'presentUsers':
|
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetPresentUserList';
|
|
break;
|
|
case 'holidayUsers':
|
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetholyUser';
|
|
break;
|
|
case 'holidayRequest':
|
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetholyRequestUser';
|
|
break;
|
|
case 'purchaseNR':
|
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetPurchaseNRList';
|
|
break;
|
|
case 'purchaseCR':
|
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetPurchaseCRList';
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(endpoint);
|
|
const data = await response.json();
|
|
|
|
setModalData(prev => ({
|
|
...prev,
|
|
[modalName]: Array.isArray(data) ? data : []
|
|
}));
|
|
} catch (error) {
|
|
console.error(`모달 데이터 로드 실패 (${modalName}):`, error);
|
|
setModalData(prev => ({
|
|
...prev,
|
|
[modalName]: []
|
|
}));
|
|
}
|
|
};
|
|
|
|
// Todo 목록 로드
|
|
const loadTodoList = async () => {
|
|
try {
|
|
const response = await fetch('http://127.0.0.1:7979/Todo/GetUrgentTodos');
|
|
const data = await response.json();
|
|
|
|
if (data.Success && data.Data) {
|
|
setTodoList(data.Data);
|
|
} else {
|
|
setTodoList([]);
|
|
}
|
|
} catch (error) {
|
|
console.error('Todo 목록 로드 실패:', error);
|
|
setTodoList([]);
|
|
}
|
|
};
|
|
|
|
// 자동 새로고침 (30초마다)
|
|
useEffect(() => {
|
|
loadDashboardData();
|
|
loadModalData('holidayUsers'); // 휴가자 목록 자동 로드
|
|
loadTodoList(); // Todo 목록 자동 로드
|
|
|
|
const interval = setInterval(() => {
|
|
loadDashboardData();
|
|
loadModalData('holidayUsers'); // 30초마다 휴가자 목록도 새로고침
|
|
loadTodoList(); // 30초마다 Todo 목록도 새로고침
|
|
}, 30000); // 30초
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
// 통계 카드 컴포넌트
|
|
const StatCard = ({ title, count, icon, color, onClick, isClickable = false }) => {
|
|
return (
|
|
<div
|
|
className={`glass-effect rounded-2xl p-6 card-hover animate-slide-up ${isClickable ? 'cursor-pointer' : ''}`}
|
|
onClick={onClick}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<p className="text-white/70 text-sm font-medium">{title}</p>
|
|
<p className="text-3xl font-bold text-white">
|
|
{isLoading ? (
|
|
<div className="animate-pulse bg-white/20 h-8 w-16 rounded"></div>
|
|
) : count}
|
|
</p>
|
|
</div>
|
|
<div className={`w-12 h-12 ${color}/20 rounded-full flex items-center justify-center`}>
|
|
{icon}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// 테이블 모달 컴포넌트
|
|
const TableModal = ({ isOpen, onClose, title, headers, data, renderRow, maxWidth = 'max-w-4xl' }) => {
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
|
<div className="flex items-center justify-center min-h-screen p-4">
|
|
<div className={`glass-effect rounded-2xl w-full ${maxWidth} max-h-[80vh] overflow-hidden animate-slide-up`}>
|
|
{/* 모달 헤더 */}
|
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
{title}
|
|
</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-white/70 hover:text-white transition-colors"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* 모달 내용 */}
|
|
<div className="overflow-x-auto max-h-[60vh] custom-scrollbar">
|
|
<table className="w-full">
|
|
<thead className="bg-white/10 sticky top-0">
|
|
<tr>
|
|
{headers.map((header, index) => (
|
|
<th key={index} className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">
|
|
{header}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/10">
|
|
{data.length > 0 ? (
|
|
data.map((item, index) => renderRow(item, index))
|
|
) : (
|
|
<tr>
|
|
<td colSpan={headers.length} className="px-6 py-8 text-center text-white/50">
|
|
데이터가 없습니다.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* 모달 푸터 */}
|
|
<div className="px-6 py-4 border-t border-white/10 flex justify-between items-center">
|
|
<p className="text-white/70 text-sm">
|
|
총 <span className="font-medium">{data.length}</span>건
|
|
</p>
|
|
<button
|
|
onClick={onClose}
|
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
|
>
|
|
닫기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
|
<div className="container mx-auto px-4 py-8">
|
|
{/* 헤더 */}
|
|
<div className="text-center mb-8 animate-fade-in">
|
|
<h1 className="text-4xl font-bold mb-4">근태현황 대시보드</h1>
|
|
<p className="text-white/70">실시간 근태 및 업무 현황을 확인하세요</p>
|
|
{lastUpdated && (
|
|
<p className="text-white/50 text-sm mt-2">마지막 업데이트: {lastUpdated}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* 새로고침 버튼 */}
|
|
<div className="flex justify-end mb-6">
|
|
<button
|
|
onClick={loadDashboardData}
|
|
disabled={isLoading}
|
|
className="glass-effect rounded-lg px-4 py-2 text-white hover:bg-white/10 transition-all duration-300 disabled:opacity-50"
|
|
>
|
|
{isLoading ? (
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2 inline-block"></div>
|
|
) : (
|
|
<svg className="w-4 h-4 mr-2 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
</svg>
|
|
)}
|
|
새로고침
|
|
</button>
|
|
</div>
|
|
|
|
{/* 통계 카드 */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
|
<StatCard
|
|
title="출근(대상)"
|
|
count={dashboardData.presentCount}
|
|
isClickable={true}
|
|
onClick={() => showModal('presentUsers')}
|
|
color="bg-success-500"
|
|
icon={
|
|
<svg className="w-6 h-6 text-success-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
}
|
|
/>
|
|
|
|
<StatCard
|
|
title="휴가"
|
|
count={dashboardData.leaveCount}
|
|
isClickable={true}
|
|
onClick={() => showModal('holidayUsers')}
|
|
color="bg-warning-500"
|
|
icon={
|
|
<svg className="w-6 h-6 text-warning-400" 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>
|
|
}
|
|
/>
|
|
|
|
<StatCard
|
|
title="휴가요청"
|
|
count={dashboardData.leaveRequestCount}
|
|
isClickable={true}
|
|
onClick={() => showModal('holidayRequest')}
|
|
color="bg-primary-500"
|
|
icon={
|
|
<svg className="w-6 h-6 text-primary-400" 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>
|
|
}
|
|
/>
|
|
|
|
<StatCard
|
|
title="구매요청(NR)"
|
|
count={dashboardData.purchaseCountNR}
|
|
isClickable={true}
|
|
onClick={() => showModal('purchaseNR')}
|
|
color="bg-danger-500"
|
|
icon={
|
|
<svg className="w-6 h-6 text-danger-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
|
</svg>
|
|
}
|
|
/>
|
|
|
|
<StatCard
|
|
title="구매요청(CR)"
|
|
count={dashboardData.purchaseCountCR}
|
|
isClickable={true}
|
|
onClick={() => showModal('purchaseCR')}
|
|
color="bg-purple-500"
|
|
icon={
|
|
<svg className="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
|
</svg>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* 2칸 레이아웃: 좌측 휴가현황, 우측 할일 */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-slide-up">
|
|
{/* 좌측: 휴가/기타 현황 */}
|
|
<div className="glass-effect rounded-2xl overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-white/10">
|
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
|
</svg>
|
|
휴가/기타 현황
|
|
</h2>
|
|
</div>
|
|
<div className="overflow-x-auto max-h-[460px] custom-scrollbar">
|
|
<table className="w-full">
|
|
<thead className="bg-white/10 sticky top-0">
|
|
<tr>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">이름</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">형태</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">종류</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">기간</th>
|
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">사유</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/10">
|
|
{modalData.holidayUsers.map((user, index) => {
|
|
// 형태에 따른 색상 결정
|
|
const typeColorClass = (user.type === '휴가') ? 'bg-green-500/20 text-green-300' : 'bg-warning-500/20 text-warning-300';
|
|
|
|
// 종류에 따른 색상 결정
|
|
let cateColorClass = 'bg-warning-500/20 text-warning-300'; // 기본값
|
|
if (user.cate === '휴가') {
|
|
cateColorClass = 'bg-warning-500/20 text-warning-300'; // 노란색 계열
|
|
} else if (user.cate === '파견') {
|
|
cateColorClass = 'bg-purple-500/20 text-purple-300'; // 보라색 계열
|
|
} else {
|
|
cateColorClass = 'bg-warning-500/20 text-warning-300'; // 기타는 주황색 계열
|
|
}
|
|
|
|
// 기간 표시 형식 개선
|
|
let periodText = '';
|
|
if (user.sdate && user.edate) {
|
|
if (user.sdate === user.edate) {
|
|
periodText = user.sdate;
|
|
} else {
|
|
periodText = `${user.sdate}~${user.edate}`;
|
|
}
|
|
} else {
|
|
periodText = '-';
|
|
}
|
|
|
|
return (
|
|
<tr key={index} className="hover:bg-white/5">
|
|
<td className="px-4 py-3 text-white text-sm font-medium">{user.name || '이름 없음'}</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${typeColorClass}`}>
|
|
{user.type || 'N/A'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${cateColorClass}`}>
|
|
{user.cate || '종류 없음'}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-3 text-white text-sm">{periodText}</td>
|
|
<td className="px-4 py-3 text-white/70 text-sm">{user.title || '사유 없음'}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
{modalData.holidayUsers.length === 0 && (
|
|
<tr>
|
|
<td colSpan="5" className="px-4 py-8 text-center text-white/50">
|
|
현재 휴가자가 없습니다
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 우측: 할일 */}
|
|
<div className="glass-effect rounded-2xl overflow-hidden">
|
|
<div className="px-6 py-4 border-b border-white/10">
|
|
<h2 className="text-xl font-semibold text-white flex items-center justify-between">
|
|
<span className="flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
|
</svg>
|
|
할일
|
|
</span>
|
|
<div className="flex items-center space-x-2">
|
|
<button
|
|
className="text-xs bg-primary-500/20 hover:bg-primary-500/30 text-primary-300 hover:text-primary-200 px-3 py-1 rounded-full transition-colors flex items-center"
|
|
onClick={() => alert('할일 추가 기능은 준비 중입니다.')}
|
|
>
|
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
|
</svg>
|
|
할일추가
|
|
</button>
|
|
<button
|
|
className="text-xs bg-white/20 hover:bg-white/30 px-3 py-1 rounded-full transition-colors"
|
|
onClick={() => window.location.href = '/react/todo'}
|
|
>
|
|
전체보기
|
|
</button>
|
|
</div>
|
|
</h2>
|
|
</div>
|
|
<div className="p-4">
|
|
<div className="space-y-3 max-h-[384px] overflow-y-auto custom-scrollbar">
|
|
{todoList.length > 0 ? (
|
|
todoList.map((todo, index) => {
|
|
const flagIcon = todo.flag ? '📌 ' : '';
|
|
|
|
// 상태별 클래스
|
|
const getTodoStatusClass = (status) => {
|
|
switch(status) {
|
|
case '0': return 'bg-gray-500/20 text-gray-300';
|
|
case '1': return 'bg-primary-500/20 text-primary-300';
|
|
case '2': return 'bg-danger-500/20 text-danger-300';
|
|
case '3': return 'bg-warning-500/20 text-warning-300';
|
|
case '5': return 'bg-success-500/20 text-success-300';
|
|
default: return 'bg-white/10 text-white/50';
|
|
}
|
|
};
|
|
|
|
const getTodoStatusText = (status) => {
|
|
switch(status) {
|
|
case '0': return '대기';
|
|
case '1': return '진행';
|
|
case '2': return '취소';
|
|
case '3': return '보류';
|
|
case '5': return '완료';
|
|
default: return '대기';
|
|
}
|
|
};
|
|
|
|
const getTodoSeqnoClass = (seqno) => {
|
|
switch(seqno) {
|
|
case '0': return 'bg-gray-500/20 text-gray-300';
|
|
case '1': return 'bg-success-500/20 text-success-300';
|
|
case '2': return 'bg-warning-500/20 text-warning-300';
|
|
case '3': return 'bg-danger-500/20 text-danger-300';
|
|
default: return 'bg-white/10 text-white/50';
|
|
}
|
|
};
|
|
|
|
const getTodoSeqnoText = (seqno) => {
|
|
switch(seqno) {
|
|
case '0': return '낮음';
|
|
case '1': return '보통';
|
|
case '2': return '높음';
|
|
case '3': return '긴급';
|
|
default: return '보통';
|
|
}
|
|
};
|
|
|
|
const statusClass = getTodoStatusClass(todo.status);
|
|
const statusText = getTodoStatusText(todo.status);
|
|
const seqnoClass = getTodoSeqnoClass(todo.seqno);
|
|
const seqnoText = getTodoSeqnoText(todo.seqno);
|
|
|
|
const expireText = todo.expire ? new Date(todo.expire).toLocaleDateString('ko-KR') : '';
|
|
const isExpired = todo.expire && new Date(todo.expire) < new Date();
|
|
const expireClass = isExpired ? 'text-danger-400' : 'text-white/60';
|
|
|
|
// 만료일이 지난 경우 배경을 적색계통으로 강조
|
|
const expiredBgClass = isExpired ? 'bg-danger-600/30 border-danger-400/40 hover:bg-danger-600/40' : 'bg-white/10 hover:bg-white/15 border-white/20';
|
|
|
|
return (
|
|
<div key={index} className={`${expiredBgClass} backdrop-blur-sm rounded-lg p-3 transition-colors cursor-pointer border`}
|
|
onClick={() => alert('Todo 상세보기 기능은 준비 중입니다.')}>
|
|
<div className="flex items-start justify-between mb-2">
|
|
<div className="flex items-center gap-1">
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${statusClass}`}>
|
|
{statusText}
|
|
</span>
|
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${seqnoClass}`}>
|
|
{seqnoText}
|
|
</span>
|
|
</div>
|
|
{expireText && (
|
|
<span className={`text-xs ${expireClass}`}>
|
|
{expireText}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-white text-sm font-medium mb-1">
|
|
{flagIcon}{todo.title || '제목 없음'}
|
|
</p>
|
|
{todo.description && (
|
|
<p className="text-white/70 text-xs mb-2 line-clamp-2">
|
|
{todo.description}
|
|
</p>
|
|
)}
|
|
{todo.request && (
|
|
<p className="text-white/50 text-xs mt-2">요청자: {todo.request}</p>
|
|
)}
|
|
</div>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="text-center text-white/50 py-8">
|
|
<svg className="w-8 h-8 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
|
</svg>
|
|
급한 할일이 없습니다
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 출근 대상자 모달 */}
|
|
<TableModal
|
|
isOpen={modals.presentUsers}
|
|
onClose={() => hideModal('presentUsers')}
|
|
title="금일 출근 대상자 목록"
|
|
headers={['사번', '이름', '공정', '직급', '상태', '이메일']}
|
|
data={modalData.presentUsers}
|
|
renderRow={(user, index) => (
|
|
<tr key={index} className="hover:bg-white/5">
|
|
<td className="px-6 py-4 text-white text-sm">{user.id || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{user.name || '이름 없음'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{user.gname || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{user.level || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-success-400 text-sm">출근</td>
|
|
<td className="px-6 py-4 text-white/70 text-sm">{user.email || 'N/A'}</td>
|
|
</tr>
|
|
)}
|
|
/>
|
|
|
|
{/* 휴가 요청 모달 */}
|
|
<TableModal
|
|
isOpen={modals.holidayRequest}
|
|
onClose={() => hideModal('holidayRequest')}
|
|
title="휴가 신청 목록"
|
|
maxWidth="max-w-6xl"
|
|
headers={['사번', '이름', '항목', '일자', '요청일', '요청시간', '비고']}
|
|
data={modalData.holidayRequests}
|
|
renderRow={(request, index) => (
|
|
<tr key={index} className="hover:bg-white/5">
|
|
<td className="px-6 py-4 text-white text-sm">{request.uid || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{request.name || '이름 없음'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{request.cate || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">
|
|
{request.sdate && request.edate ? `${request.sdate} ~ ${request.edate}` : 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 text-white text-sm">{request.holydays || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{request.holytimes || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white/70 text-sm">{request.HolyReason || request.remark || 'N/A'}</td>
|
|
</tr>
|
|
)}
|
|
/>
|
|
|
|
{/* 구매요청 NR 모달 */}
|
|
<TableModal
|
|
isOpen={modals.purchaseNR}
|
|
onClose={() => hideModal('purchaseNR')}
|
|
title="구매요청(NR) 목록"
|
|
maxWidth="max-w-7xl"
|
|
headers={['요청일', '공정', '품목', '규격', '단위', '수량', '단가', '금액']}
|
|
data={modalData.purchaseNR}
|
|
renderRow={(item, index) => (
|
|
<tr key={index} className="hover:bg-white/5">
|
|
<td className="px-6 py-4 text-white text-sm">{item.pdate || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.process || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumname || '품목 없음'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumscale || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumunit || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumqtyreq || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">
|
|
{item.pumprice ? `₩${Number(item.pumprice).toLocaleString()}` : 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 text-danger-400 text-sm font-medium">
|
|
{item.pumamt ? `₩${Number(item.pumamt).toLocaleString()}` : 'N/A'}
|
|
</td>
|
|
</tr>
|
|
)}
|
|
/>
|
|
|
|
{/* 구매요청 CR 모달 */}
|
|
<TableModal
|
|
isOpen={modals.purchaseCR}
|
|
onClose={() => hideModal('purchaseCR')}
|
|
title="구매요청(CR) 목록"
|
|
maxWidth="max-w-7xl"
|
|
headers={['요청일', '공정', '품목', '규격', '단위', '수량', '단가', '금액']}
|
|
data={modalData.purchaseCR}
|
|
renderRow={(item, index) => (
|
|
<tr key={index} className="hover:bg-white/5">
|
|
<td className="px-6 py-4 text-white text-sm">{item.pdate || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.process || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumname || '품목 없음'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumscale || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumunit || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">{item.pumqtyreq || 'N/A'}</td>
|
|
<td className="px-6 py-4 text-white text-sm">
|
|
{item.pumprice ? `₩${Number(item.pumprice).toLocaleString()}` : 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 text-purple-400 text-sm font-medium">
|
|
{item.pumamt ? `₩${Number(item.pumamt).toLocaleString()}` : 'N/A'}
|
|
</td>
|
|
</tr>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |