import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { motion, AnimatePresence } from 'framer-motion'; import { Home, ChevronRight, Bot, Play, Square, Youtube, CheckCircle, XCircle, RefreshCw, Download, } from 'lucide-react'; import { Toast, Tooltip } from '@/components/common'; import { AdminLayout } from '@/components/pc/admin'; import { useAdminAuth } from '@/hooks/pc/admin'; import { useToast } from '@/hooks/common'; import * as botsApi from '@/api/admin/bots'; // 애니메이션 variants const containerVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.1 }, }, }; const itemVariants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.4, ease: 'easeOut' }, }, }; // 슬롯머신 스타일 롤링 숫자 컴포넌트 function AnimatedNumber({ value, className = '' }) { const chars = String(value).split(''); return ( {chars.map((char, i) => ( {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => ( {n} ))} ))} ); } // X 아이콘 컴포넌트 const XIcon = ({ size = 20, fill = 'currentColor' }) => ( ); // Meilisearch 아이콘 컴포넌트 const MeilisearchIcon = ({ size = 20 }) => ( ); function ScheduleBots() { const queryClient = useQueryClient(); const { user, isAuthenticated } = useAdminAuth(); const { toast, setToast } = useToast(); const [isInitialLoad, setIsInitialLoad] = useState(true); // 첫 로드 여부 (애니메이션용) const [syncing, setSyncing] = useState(null); // 동기화 중인 봇 ID const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태 // 봇 목록 조회 const { data: bots = [], isLoading: loading, isError, refetch: fetchBots, } = useQuery({ queryKey: ['admin', 'bots'], queryFn: botsApi.getBots, enabled: isAuthenticated, staleTime: 30000, }); // 할당량 경고 상태 조회 const { data: quotaData } = useQuery({ queryKey: ['admin', 'bots', 'quota'], queryFn: botsApi.getQuotaWarning, enabled: isAuthenticated, staleTime: 60000, }); // 에러 처리 useEffect(() => { if (isError) { setToast({ type: 'error', message: '봇 목록을 불러올 수 없습니다.' }); } }, [isError, setToast]); // 할당량 경고 상태 업데이트 useEffect(() => { if (quotaData?.active) { setQuotaWarning(quotaData); } }, [quotaData]); // 할당량 경고 해제 const handleDismissQuotaWarning = async () => { try { await botsApi.dismissQuotaWarning(); setQuotaWarning(null); } catch (error) { console.error('할당량 경고 해제 오류:', error); } }; // 봇 시작/정지 토글 const toggleBot = async (botId, currentStatus, botName) => { try { const action = currentStatus === 'running' ? 'stop' : 'start'; if (action === 'start') { await botsApi.startBot(botId); } else { await botsApi.stopBot(botId); } // 캐시 업데이트 (전체 목록 새로고침 대신) queryClient.setQueryData(['admin', 'bots'], (prev) => prev?.map((bot) => bot.id === botId ? { ...bot, status: action === 'start' ? 'running' : 'stopped' } : bot ) ); setToast({ type: 'success', message: action === 'start' ? `${botName} 봇이 시작되었습니다.` : `${botName} 봇이 정지되었습니다.`, }); } catch (error) { console.error('봇 토글 오류:', error); setToast({ type: 'error', message: error.message || '작업 중 오류가 발생했습니다.' }); } }; // 전체 동기화 const handleSyncAllVideos = async (botId) => { setSyncing(botId); try { const data = await botsApi.syncAllVideos(botId); setToast({ type: 'success', message: `${data.addedCount}개 일정이 추가되었습니다. (전체 ${data.total}개)`, }); fetchBots(); } catch (error) { console.error('전체 동기화 오류:', error); setToast({ type: 'error', message: error.message || '동기화 중 오류가 발생했습니다.' }); fetchBots(); } finally { setSyncing(null); } }; // 상태 아이콘 및 색상 const getStatusInfo = (status) => { switch (status) { case 'running': return { icon: , text: '실행 중', color: 'text-green-500', bg: 'bg-green-50', dot: 'bg-green-500', }; case 'stopped': return { icon: , text: '정지됨', color: 'text-gray-400', bg: 'bg-gray-50', dot: 'bg-gray-400', }; case 'error': return { icon: , text: '오류', color: 'text-red-500', bg: 'bg-red-50', dot: 'bg-red-500', }; default: return { icon: null, text: '알 수 없음', color: 'text-gray-400', bg: 'bg-gray-50', dot: 'bg-gray-400', }; } }; // 시간 포맷 (UTC → KST 변환) const formatTime = (dateString) => { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleString('ko-KR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; // 간격 포맷 (분 → 분/시간/일) const formatInterval = (minutes) => { if (!minutes) return '-'; if (minutes >= 1440) { const days = Math.floor(minutes / 1440); return `${days}일`; } else if (minutes >= 60) { const hours = Math.floor(minutes / 60); return `${hours}시간`; } return `${minutes}분`; }; // 통계 계산 const runningCount = bots.filter((b) => b.status === 'running').length; const stoppedCount = bots.filter((b) => b.status === 'stopped').length; const errorCount = bots.filter((b) => b.status === 'error').length; return ( setToast(null)} /> {/* 메인 콘텐츠 */} {/* 브레드크럼 */} 일정 관리 봇 관리 {/* 타이틀 */} 봇 관리 일정 자동화 봇을 관리합니다 {/* 봇 통계 */} 전체 봇 실행 중 정지됨 오류 {/* API 할당량 경고 배너 */} {quotaWarning && ( YouTube API 할당량 경고 {quotaWarning.message} 닫기 )} {/* 봇 목록 */} 봇 목록 { setIsInitialLoad(true); fetchBots(); }} disabled={loading} className="p-2 hover:bg-gray-100 rounded-lg transition-colors text-gray-500 hover:text-gray-700 disabled:opacity-50" > {loading ? ( ) : bots.length === 0 ? ( 등록된 봇이 없습니다 위의 버튼을 클릭하여 봇을 추가하세요 ) : ( {bots.map((bot, index) => { const statusInfo = getStatusInfo(bot.status); return ( isInitialLoad && index === bots.length - 1 && setIsInitialLoad(false) } className="relative bg-gradient-to-br from-gray-50 to-white rounded-xl border border-gray-200 overflow-hidden hover:shadow-md transition-all" > {/* 상단 헤더 */} {bot.type === 'x' ? ( ) : bot.type === 'meilisearch' ? ( ) : ( )} {bot.name} {bot.last_check_at ? `${formatTime(bot.last_check_at)}에 업데이트됨` : '아직 업데이트 없음'} {statusInfo.text} {/* 통계 정보 */} {bot.type === 'meilisearch' ? ( <> {bot.schedules_added || 0} 동기화 수 {bot.last_added_count ? `${(bot.last_added_count / 1000 || 0).toFixed(1)}초` : '-'} 소요 시간 > ) : ( <> {bot.schedules_added} 총 추가 0 ? 'text-green-500' : 'text-gray-400'}`} > +{bot.last_added_count || 0} 마지막 > )} {formatInterval(bot.check_interval)} 업데이트 간격 {/* 오류 메시지 */} {bot.status === 'error' && bot.error_message && ( {bot.error_message} )} {/* 액션 버튼 */} handleSyncAllVideos(bot.id)} disabled={syncing === bot.id} className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-500 text-white rounded-lg font-medium transition-colors hover:bg-blue-600 disabled:opacity-50" > {syncing === bot.id ? ( <> 동기화 중... > ) : ( <> 전체 동기화 > )} toggleBot(bot.id, bot.status, bot.name)} className={`flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg font-medium transition-colors ${ bot.status === 'running' ? 'bg-gray-100 text-gray-600 hover:bg-gray-200' : 'bg-green-500 text-white hover:bg-green-600' }`} > {bot.status === 'running' ? ( <> 정지 > ) : ( <> 시작 > )} ); })} )} ); } export default ScheduleBots;
일정 자동화 봇을 관리합니다
{quotaWarning.message}
등록된 봇이 없습니다
위의 버튼을 클릭하여 봇을 추가하세요
{bot.last_check_at ? `${formatTime(bot.last_check_at)}에 업데이트됨` : '아직 업데이트 없음'}