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, CheckCircle, XCircle, RefreshCw } from 'lucide-react'; import { Toast, Tooltip, AnimatedNumber } from '@/components/common'; import { AdminLayout, BotCard } 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 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}

)}
{/* 봇 목록 */}

봇 목록

{loading ? (
) : bots.length === 0 ? (

등록된 봇이 없습니다

위의 버튼을 클릭하여 봇을 추가하세요

) : (
{bots.map((bot, index) => ( isInitialLoad && index === bots.length - 1 && setIsInitialLoad(false) } formatTime={formatTime} formatInterval={formatInterval} /> ))}
)}
); } export default ScheduleBots;