feat: 봇 관리 섹션별 다른 디자인 적용
- Meilisearch: 리스트형 (BotListItem) - 한 줄에 모든 정보 - YouTube: 미니 카드형 (BotMiniCard) - 호버시 액션 버튼 - X: 테이블형 (BotTableRow) - 표 형식으로 정보 비교 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dbfee503d5
commit
b5118f2dea
3 changed files with 274 additions and 129 deletions
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
import { memo } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Youtube, Play, Square, RefreshCw, Download } from 'lucide-react';
|
||||
import { Play, Square, RefreshCw, Download } from 'lucide-react';
|
||||
|
||||
// X 아이콘 컴포넌트
|
||||
export const XIcon = ({ size = 20, fill = 'currentColor' }) => (
|
||||
|
|
@ -16,72 +16,29 @@ export const XIcon = ({ size = 20, fill = 'currentColor' }) => (
|
|||
export const MeilisearchIcon = ({ size = 20 }) => (
|
||||
<svg width={size} height={size} viewBox="0 108.4 512 295.2">
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="meili-a"
|
||||
x1="488.157"
|
||||
x2="-21.055"
|
||||
y1="469.917"
|
||||
y2="179.001"
|
||||
gradientTransform="matrix(1 0 0 -1 0 514)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<linearGradient id="meili-a" x1="488.157" x2="-21.055" y1="469.917" y2="179.001" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="#ff5caa" />
|
||||
<stop offset="1" stopColor="#ff4e62" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="meili-b"
|
||||
x1="522.305"
|
||||
x2="13.094"
|
||||
y1="410.144"
|
||||
y2="119.228"
|
||||
gradientTransform="matrix(1 0 0 -1 0 514)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<linearGradient id="meili-b" x1="522.305" x2="13.094" y1="410.144" y2="119.228" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="#ff5caa" />
|
||||
<stop offset="1" stopColor="#ff4e62" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="meili-c"
|
||||
x1="556.456"
|
||||
x2="47.244"
|
||||
y1="350.368"
|
||||
y2="59.452"
|
||||
gradientTransform="matrix(1 0 0 -1 0 514)"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<linearGradient id="meili-c" x1="556.456" x2="47.244" y1="350.368" y2="59.452" gradientTransform="matrix(1 0 0 -1 0 514)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stopColor="#ff5caa" />
|
||||
<stop offset="1" stopColor="#ff4e62" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
d="m0 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z"
|
||||
fill="url(#meili-a)"
|
||||
/>
|
||||
<path
|
||||
d="m138.8 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z"
|
||||
fill="url(#meili-b)"
|
||||
/>
|
||||
<path
|
||||
d="m277.6 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z"
|
||||
fill="url(#meili-c)"
|
||||
/>
|
||||
<path d="m0 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z" fill="url(#meili-a)" />
|
||||
<path d="m138.8 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z" fill="url(#meili-b)" />
|
||||
<path d="m277.6 403.6 94.6-239.3c13.3-33.7 46.2-55.9 82.8-55.9h57l-94.6 239.3c-13.3 33.7-46.2 55.9-82.8 55.9z" fill="url(#meili-c)" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {Object} props.bot - 봇 데이터
|
||||
* @param {number} props.index - 인덱스 (애니메이션용)
|
||||
* @param {boolean} props.isInitialLoad - 첫 로드 여부
|
||||
* @param {string|null} props.syncing - 동기화 중인 봇 ID
|
||||
* @param {Object} props.statusInfo - 상태 정보 (text, color, bg, dot)
|
||||
* @param {Function} props.onSync - 동기화 핸들러
|
||||
* @param {Function} props.onToggle - 토글 핸들러
|
||||
* @param {Function} props.onAnimationComplete - 애니메이션 완료 핸들러
|
||||
* @param {Function} props.formatTime - 시간 포맷 함수
|
||||
* @param {Function} props.formatInterval - 간격 포맷 함수
|
||||
* 리스트형 봇 (Meilisearch용) - 한 줄에 모든 정보
|
||||
*/
|
||||
const BotCard = memo(function BotCard({
|
||||
export const BotListItem = memo(function BotListItem({
|
||||
bot,
|
||||
index,
|
||||
isInitialLoad,
|
||||
|
|
@ -95,101 +52,246 @@ const BotCard = memo(function BotCard({
|
|||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={isInitialLoad ? { opacity: 0, y: 10 } : false}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={isInitialLoad ? { opacity: 0, x: -10 } : false}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={isInitialLoad ? { delay: index * 0.05, duration: 0.2 } : { duration: 0.15 }}
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
className="group relative bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden hover:shadow-md transition-all"
|
||||
className="flex items-center gap-4 p-4 bg-white border border-gray-200 rounded-xl hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
{/* 상단: 이름 + 상태 */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-gray-900 truncate">{bot.name}</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5">
|
||||
{bot.last_check_at ? formatTime(bot.last_check_at) : '대기 중'}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`flex-shrink-0 flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-full ${statusInfo.bg} ${statusInfo.color}`}
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot} ${bot.status === 'running' ? 'animate-pulse' : ''}`}
|
||||
/>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
</div>
|
||||
{/* 상태 표시 */}
|
||||
<div className={`w-2 h-2 rounded-full ${statusInfo.dot} ${bot.status === 'running' ? 'animate-pulse' : ''}`} />
|
||||
|
||||
{/* 통계 */}
|
||||
<div className="flex items-center gap-3 mt-3 p-2.5 bg-gray-50 rounded-lg text-sm">
|
||||
<div className="flex-1 text-center">
|
||||
<div className="font-semibold text-gray-900">{bot.schedules_added || 0}</div>
|
||||
<div className="text-xs text-gray-400">총 추가</div>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-gray-200" />
|
||||
<div className="flex-1 text-center">
|
||||
<div className={`font-semibold ${bot.last_added_count > 0 ? 'text-green-600' : 'text-gray-400'}`}>
|
||||
+{bot.last_added_count || 0}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">최근</div>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-gray-200" />
|
||||
<div className="flex-1 text-center">
|
||||
<div className="font-semibold text-gray-900">{formatInterval(bot.check_interval)}</div>
|
||||
<div className="text-xs text-gray-400">간격</div>
|
||||
</div>
|
||||
{/* 이름 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-medium text-gray-900 truncate">{bot.name}</h3>
|
||||
</div>
|
||||
|
||||
{/* 통계 */}
|
||||
<div className="hidden sm:flex items-center gap-6 text-sm text-gray-500">
|
||||
<div className="text-center">
|
||||
<span className="font-semibold text-gray-900">{bot.schedules_added || 0}</span>
|
||||
<span className="ml-1">추가</span>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<span className="text-xs">{bot.last_check_at ? formatTime(bot.last_check_at) : '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 오류 메시지 */}
|
||||
{bot.status === 'error' && bot.error_message && (
|
||||
<div className="px-4 py-2 bg-red-50 text-red-600 text-xs border-t border-red-100">
|
||||
{bot.error_message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex border-t border-gray-100">
|
||||
{/* 버튼 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onSync(bot.id)}
|
||||
disabled={syncing === bot.id}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-4 py-2.5 text-sm font-medium text-gray-600 hover:bg-gray-50 transition-colors disabled:opacity-50 border-r border-gray-100"
|
||||
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors disabled:opacity-50"
|
||||
title="전체 동기화"
|
||||
>
|
||||
{syncing === bot.id ? (
|
||||
<>
|
||||
<RefreshCw size={14} className="animate-spin" />
|
||||
<span>동기화 중</span>
|
||||
</>
|
||||
<RefreshCw size={18} className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Download size={14} />
|
||||
<span>전체 동기화</span>
|
||||
</>
|
||||
<Download size={18} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onToggle(bot.id, bot.status, bot.name)}
|
||||
className={`flex items-center justify-center gap-1.5 px-4 py-2.5 text-sm font-medium transition-colors ${
|
||||
className={`p-2 rounded-lg transition-colors ${
|
||||
bot.status === 'running'
|
||||
? 'text-gray-600 hover:bg-gray-50'
|
||||
: 'text-green-600 hover:bg-green-50'
|
||||
? 'text-gray-500 hover:text-red-600 hover:bg-red-50'
|
||||
: 'text-gray-500 hover:text-green-600 hover:bg-green-50'
|
||||
}`}
|
||||
title={bot.status === 'running' ? '정지' : '시작'}
|
||||
>
|
||||
{bot.status === 'running' ? (
|
||||
<>
|
||||
<Square size={14} />
|
||||
<span>정지</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={14} />
|
||||
<span>시작</span>
|
||||
</>
|
||||
)}
|
||||
{bot.status === 'running' ? <Square size={18} /> : <Play size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 미니 카드형 봇 (YouTube용) - 컴팩트한 카드
|
||||
*/
|
||||
export const BotMiniCard = memo(function BotMiniCard({
|
||||
bot,
|
||||
index,
|
||||
isInitialLoad,
|
||||
syncing,
|
||||
statusInfo,
|
||||
onSync,
|
||||
onToggle,
|
||||
onAnimationComplete,
|
||||
formatTime,
|
||||
formatInterval,
|
||||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={isInitialLoad ? { opacity: 0, scale: 0.95 } : false}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={isInitialLoad ? { delay: index * 0.05, duration: 0.2 } : { duration: 0.15 }}
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
className="group relative bg-white border border-gray-200 rounded-xl overflow-hidden hover:shadow-md transition-all"
|
||||
>
|
||||
{/* 메인 영역 */}
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-gray-900 truncate flex-1">{bot.name}</h3>
|
||||
<span
|
||||
className={`ml-2 flex items-center gap-1.5 px-2 py-0.5 text-xs font-medium rounded-full ${statusInfo.bg} ${statusInfo.color}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot} ${bot.status === 'running' ? 'animate-pulse' : ''}`} />
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 간단 통계 */}
|
||||
<div className="mt-2 flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>총 <strong className="text-gray-900">{bot.schedules_added || 0}</strong></span>
|
||||
<span>•</span>
|
||||
<span>최근 <strong className={bot.last_added_count > 0 ? 'text-green-600' : 'text-gray-400'}>+{bot.last_added_count || 0}</strong></span>
|
||||
<span>•</span>
|
||||
<span>{formatInterval(bot.check_interval)}</span>
|
||||
</div>
|
||||
|
||||
{/* 마지막 업데이트 */}
|
||||
<p className="mt-1 text-xs text-gray-400">
|
||||
{bot.last_check_at ? formatTime(bot.last_check_at) : '대기 중'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 오류 메시지 */}
|
||||
{bot.status === 'error' && bot.error_message && (
|
||||
<div className="px-4 py-2 bg-red-50 text-red-600 text-xs">
|
||||
{bot.error_message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 호버시 나타나는 액션 버튼 */}
|
||||
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-3">
|
||||
<button
|
||||
onClick={() => onSync(bot.id)}
|
||||
disabled={syncing === bot.id}
|
||||
className="flex items-center gap-1.5 px-3 py-2 bg-white text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-100 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{syncing === bot.id ? (
|
||||
<RefreshCw size={14} className="animate-spin" />
|
||||
) : (
|
||||
<Download size={14} />
|
||||
)}
|
||||
동기화
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onToggle(bot.id, bot.status, bot.name)}
|
||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
bot.status === 'running'
|
||||
? 'bg-red-500 text-white hover:bg-red-600'
|
||||
: 'bg-green-500 text-white hover:bg-green-600'
|
||||
}`}
|
||||
>
|
||||
{bot.status === 'running' ? <Square size={14} /> : <Play size={14} />}
|
||||
{bot.status === 'running' ? '정지' : '시작'}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 테이블 행 봇 (X용)
|
||||
*/
|
||||
export const BotTableRow = memo(function BotTableRow({
|
||||
bot,
|
||||
index,
|
||||
isInitialLoad,
|
||||
syncing,
|
||||
statusInfo,
|
||||
onSync,
|
||||
onToggle,
|
||||
onAnimationComplete,
|
||||
formatTime,
|
||||
formatInterval,
|
||||
}) {
|
||||
return (
|
||||
<motion.tr
|
||||
initial={isInitialLoad ? { opacity: 0 } : false}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={isInitialLoad ? { delay: index * 0.05, duration: 0.2 } : { duration: 0.15 }}
|
||||
onAnimationComplete={onAnimationComplete}
|
||||
className="border-b border-gray-100 last:border-0 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${statusInfo.dot} ${bot.status === 'running' ? 'animate-pulse' : ''}`} />
|
||||
<span className="font-medium text-gray-900">{bot.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${statusInfo.bg} ${statusInfo.color}`}>
|
||||
{statusInfo.text}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-900 font-medium">{bot.schedules_added || 0}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<span className={bot.last_added_count > 0 ? 'text-green-600 font-medium' : 'text-gray-400'}>
|
||||
+{bot.last_added_count || 0}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500">{formatInterval(bot.check_interval)}</td>
|
||||
<td className="px-4 py-3 text-xs text-gray-400">
|
||||
{bot.last_check_at ? formatTime(bot.last_check_at) : '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => onSync(bot.id)}
|
||||
disabled={syncing === bot.id}
|
||||
className="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors disabled:opacity-50"
|
||||
title="전체 동기화"
|
||||
>
|
||||
{syncing === bot.id ? (
|
||||
<RefreshCw size={16} className="animate-spin" />
|
||||
) : (
|
||||
<Download size={16} />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onToggle(bot.id, bot.status, bot.name)}
|
||||
className={`p-1.5 rounded transition-colors ${
|
||||
bot.status === 'running'
|
||||
? 'text-gray-400 hover:text-red-600 hover:bg-red-50'
|
||||
: 'text-gray-400 hover:text-green-600 hover:bg-green-50'
|
||||
}`}
|
||||
title={bot.status === 'running' ? '정지' : '시작'}
|
||||
>
|
||||
{bot.status === 'running' ? <Square size={16} /> : <Play size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</motion.tr>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 테이블 래퍼 (X용)
|
||||
*/
|
||||
export const BotTable = ({ children }) => (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-4 py-3">이름</th>
|
||||
<th className="px-4 py-3">상태</th>
|
||||
<th className="px-4 py-3">총 추가</th>
|
||||
<th className="px-4 py-3">최근</th>
|
||||
<th className="px-4 py-3">간격</th>
|
||||
<th className="px-4 py-3">마지막 업데이트</th>
|
||||
<th className="px-4 py-3">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{children}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 기본 카드 (호환성 유지)
|
||||
const BotCard = BotMiniCard;
|
||||
|
||||
export default BotCard;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default as BotCard, XIcon, MeilisearchIcon } from './BotCard';
|
||||
export { default as BotCard, XIcon, MeilisearchIcon, BotListItem, BotMiniCard, BotTableRow, BotTable } from './BotCard';
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Database, Youtube } from 'lucide-react';
|
||||
import { Toast, Tooltip, AnimatedNumber } from '@/components/common';
|
||||
import { AdminLayout, BotCard, XIcon } from '@/components/pc/admin';
|
||||
import { AdminLayout, BotCard, XIcon, BotListItem, BotMiniCard, BotTableRow, BotTable } from '@/components/pc/admin';
|
||||
import { useAdminAuth } from '@/hooks/pc/admin';
|
||||
import { useToast } from '@/hooks/common';
|
||||
import * as botsApi from '@/api/admin/bots';
|
||||
|
|
@ -379,7 +379,7 @@ function ScheduleBots() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* 봇 카드 목록 */}
|
||||
{/* 봇 목록 - 타입별 다른 스타일 */}
|
||||
{sectionBots.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-400">
|
||||
<Bot size={36} className="mx-auto mb-3 opacity-30" />
|
||||
|
|
@ -388,10 +388,53 @@ function ScheduleBots() {
|
|||
<p className="text-xs mt-1">위의 버튼을 클릭하여 봇을 추가하세요</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
) : type === 'meilisearch' ? (
|
||||
/* Meilisearch: 리스트형 */
|
||||
<div className="p-4 space-y-2">
|
||||
{sectionBots.map((bot, index) => (
|
||||
<BotCard
|
||||
<BotListItem
|
||||
key={bot.id}
|
||||
bot={bot}
|
||||
index={index}
|
||||
isInitialLoad={isInitialLoad}
|
||||
syncing={syncing}
|
||||
statusInfo={getStatusInfo(bot.status)}
|
||||
onSync={handleSyncAllVideos}
|
||||
onToggle={toggleBot}
|
||||
onAnimationComplete={() =>
|
||||
isInitialLoad && index === sectionBots.length - 1 && setIsInitialLoad(false)
|
||||
}
|
||||
formatTime={formatTime}
|
||||
formatInterval={formatInterval}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : type === 'x' ? (
|
||||
/* X: 테이블형 */
|
||||
<BotTable>
|
||||
{sectionBots.map((bot, index) => (
|
||||
<BotTableRow
|
||||
key={bot.id}
|
||||
bot={bot}
|
||||
index={index}
|
||||
isInitialLoad={isInitialLoad}
|
||||
syncing={syncing}
|
||||
statusInfo={getStatusInfo(bot.status)}
|
||||
onSync={handleSyncAllVideos}
|
||||
onToggle={toggleBot}
|
||||
onAnimationComplete={() =>
|
||||
isInitialLoad && index === sectionBots.length - 1 && setIsInitialLoad(false)
|
||||
}
|
||||
formatTime={formatTime}
|
||||
formatInterval={formatInterval}
|
||||
/>
|
||||
))}
|
||||
</BotTable>
|
||||
) : (
|
||||
/* YouTube: 미니 카드형 */
|
||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{sectionBots.map((bot, index) => (
|
||||
<BotMiniCard
|
||||
key={bot.id}
|
||||
bot={bot}
|
||||
index={index}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue