import { useState, useEffect } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { LogOut, Home, ChevronRight, Bot, Play, Square, Youtube, Calendar, Clock, CheckCircle, XCircle, RefreshCw, Download } from 'lucide-react'; import Toast from '../../../components/Toast'; import Tooltip from '../../../components/Tooltip'; import * as botsApi from '../../../api/admin/bots'; function AdminScheduleBots() { const navigate = useNavigate(); const [user, setUser] = useState(null); const [toast, setToast] = useState(null); const [bots, setBots] = useState([]); const [loading, setLoading] = useState(true); const [isInitialLoad, setIsInitialLoad] = useState(true); // 첫 로드 여부 (애니메이션용) const [syncing, setSyncing] = useState(null); // 동기화 중인 봇 ID const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태 // Toast 자동 숨김 useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); useEffect(() => { const token = localStorage.getItem('adminToken'); const userData = localStorage.getItem('adminUser'); if (!token || !userData) { navigate('/admin'); return; } setUser(JSON.parse(userData)); fetchBots(); fetchQuotaWarning(); }, [navigate]); // 봇 목록 조회 const fetchBots = async () => { setLoading(true); try { const data = await botsApi.getBots(); setBots(data); } catch (error) { console.error('봇 목록 조회 오류:', error); setToast({ type: 'error', message: '봇 목록을 불러올 수 없습니다.' }); } finally { setLoading(false); } }; // 할당량 경고 상태 조회 const fetchQuotaWarning = async () => { try { const data = await botsApi.getQuotaWarning(); if (data.active) { setQuotaWarning(data); } } catch (error) { console.error('할당량 경고 조회 오류:', error); } }; // 할당량 경고 해제 const handleDismissQuotaWarning = async () => { try { await botsApi.dismissQuotaWarning(); setQuotaWarning(null); } catch (error) { console.error('할당량 경고 해제 오류:', error); } }; const handleLogout = () => { localStorage.removeItem('adminToken'); localStorage.removeItem('adminUser'); navigate('/admin'); }; // 봇 시작/정지 토글 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); } // 로컬 상태만 업데이트 (전체 목록 새로고침 대신) setBots(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', }; } }; // 시간 포맷 (DB에 KST로 저장되어 있으므로 그대로 표시) const formatTime = (dateString) => { if (!dateString) return '-'; // DB의 KST 시간을 UTC로 재해석하지 않도록 Z 접미사 제거 const cleanDateString = dateString.replace('Z', '').replace('T', ' '); const date = new Date(cleanDateString); 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}분`; }; return (
setToast(null)} /> {/* 헤더 */}
fromis_9 Admin
안녕하세요, {user?.username}
{/* 메인 콘텐츠 */}
{/* 브레드크럼 */}
일정 관리 봇 관리
{/* 타이틀 */}

봇 관리

일정 자동화 봇을 관리합니다

{/* 봇 통계 */}
전체 봇
{bots.length}
실행 중
{bots.filter(b => b.status === 'running').length}
정지됨
{bots.filter(b => b.status === 'stopped').length}
오류
{bots.filter(b => b.status === 'error').length}
{/* API 할당량 경고 배너 */} {quotaWarning && (

YouTube API 할당량 경고

{quotaWarning.message}

)} {/* 봇 목록 */}

봇 목록

{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.name}

{bot.last_check_at ? `${formatTime(bot.last_check_at)}에 업데이트됨` : '아직 업데이트 없음'}

{statusInfo.text}
{/* 통계 정보 */}
{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}
)} {/* 액션 버튼 */}
); })}
)}
); } export default AdminScheduleBots;