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 { memo } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { Youtube, Play, Square, RefreshCw, Download } from 'lucide-react';
|
import { Play, Square, RefreshCw, Download } from 'lucide-react';
|
||||||
|
|
||||||
// X 아이콘 컴포넌트
|
// X 아이콘 컴포넌트
|
||||||
export const XIcon = ({ size = 20, fill = 'currentColor' }) => (
|
export const XIcon = ({ size = 20, fill = 'currentColor' }) => (
|
||||||
|
|
@ -16,72 +16,29 @@ export const XIcon = ({ size = 20, fill = 'currentColor' }) => (
|
||||||
export const MeilisearchIcon = ({ size = 20 }) => (
|
export const MeilisearchIcon = ({ size = 20 }) => (
|
||||||
<svg width={size} height={size} viewBox="0 108.4 512 295.2">
|
<svg width={size} height={size} viewBox="0 108.4 512 295.2">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<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">
|
||||||
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="0" stopColor="#ff5caa" />
|
||||||
<stop offset="1" stopColor="#ff4e62" />
|
<stop offset="1" stopColor="#ff4e62" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<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">
|
||||||
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="0" stopColor="#ff5caa" />
|
||||||
<stop offset="1" stopColor="#ff4e62" />
|
<stop offset="1" stopColor="#ff4e62" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<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">
|
||||||
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="0" stopColor="#ff5caa" />
|
||||||
<stop offset="1" stopColor="#ff4e62" />
|
<stop offset="1" stopColor="#ff4e62" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
<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)" />
|
||||||
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"
|
<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)" />
|
||||||
fill="url(#meili-a)"
|
<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="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>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} props
|
* 리스트형 봇 (Meilisearch용) - 한 줄에 모든 정보
|
||||||
* @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 - 간격 포맷 함수
|
|
||||||
*/
|
*/
|
||||||
const BotCard = memo(function BotCard({
|
export const BotListItem = memo(function BotListItem({
|
||||||
bot,
|
bot,
|
||||||
index,
|
index,
|
||||||
isInitialLoad,
|
isInitialLoad,
|
||||||
|
|
@ -95,101 +52,246 @@ const BotCard = memo(function BotCard({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={isInitialLoad ? { opacity: 0, y: 10 } : false}
|
initial={isInitialLoad ? { opacity: 0, x: -10 } : false}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={isInitialLoad ? { delay: index * 0.05, duration: 0.2 } : { duration: 0.15 }}
|
transition={isInitialLoad ? { delay: index * 0.05, duration: 0.2 } : { duration: 0.15 }}
|
||||||
onAnimationComplete={onAnimationComplete}
|
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={`w-2 h-2 rounded-full ${statusInfo.dot} ${bot.status === 'running' ? 'animate-pulse' : ''}`} />
|
||||||
<div className="flex items-start justify-between mb-3">
|
|
||||||
|
{/* 이름 */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-semibold text-gray-900 truncate">{bot.name}</h3>
|
<h3 className="font-medium 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>
|
||||||
|
|
||||||
{/* 통계 */}
|
{/* 통계 */}
|
||||||
<div className="flex items-center gap-3 mt-3 p-2.5 bg-gray-50 rounded-lg text-sm">
|
<div className="hidden sm:flex items-center gap-6 text-sm text-gray-500">
|
||||||
<div className="flex-1 text-center">
|
<div className="text-center">
|
||||||
<div className="font-semibold text-gray-900">{bot.schedules_added || 0}</div>
|
<span className="font-semibold text-gray-900">{bot.schedules_added || 0}</span>
|
||||||
<div className="text-xs text-gray-400">총 추가</div>
|
<span className="ml-1">추가</span>
|
||||||
</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>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-xs">{bot.last_check_at ? formatTime(bot.last_check_at) : '-'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 오류 메시지 */}
|
{/* 버튼 */}
|
||||||
{bot.status === 'error' && bot.error_message && (
|
<div className="flex items-center gap-2">
|
||||||
<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">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onSync(bot.id)}
|
onClick={() => onSync(bot.id)}
|
||||||
disabled={syncing === 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 ? (
|
{syncing === bot.id ? (
|
||||||
<>
|
<RefreshCw size={18} className="animate-spin" />
|
||||||
<RefreshCw size={14} className="animate-spin" />
|
|
||||||
<span>동기화 중</span>
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<Download size={18} />
|
||||||
<Download size={14} />
|
|
||||||
<span>전체 동기화</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onToggle(bot.id, bot.status, bot.name)}
|
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'
|
bot.status === 'running'
|
||||||
? 'text-gray-600 hover:bg-gray-50'
|
? 'text-gray-500 hover:text-red-600 hover:bg-red-50'
|
||||||
: 'text-green-600 hover:bg-green-50'
|
: 'text-gray-500 hover:text-green-600 hover:bg-green-50'
|
||||||
}`}
|
}`}
|
||||||
|
title={bot.status === 'running' ? '정지' : '시작'}
|
||||||
>
|
>
|
||||||
{bot.status === 'running' ? (
|
{bot.status === 'running' ? <Square size={18} /> : <Play size={18} />}
|
||||||
<>
|
|
||||||
<Square size={14} />
|
|
||||||
<span>정지</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play size={14} />
|
|
||||||
<span>시작</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.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;
|
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 { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Database, Youtube } from 'lucide-react';
|
import { Home, ChevronRight, Bot, CheckCircle, XCircle, RefreshCw, Plus, Database, Youtube } from 'lucide-react';
|
||||||
import { Toast, Tooltip, AnimatedNumber } from '@/components/common';
|
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 { useAdminAuth } from '@/hooks/pc/admin';
|
||||||
import { useToast } from '@/hooks/common';
|
import { useToast } from '@/hooks/common';
|
||||||
import * as botsApi from '@/api/admin/bots';
|
import * as botsApi from '@/api/admin/bots';
|
||||||
|
|
@ -379,7 +379,7 @@ function ScheduleBots() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 봇 카드 목록 */}
|
{/* 봇 목록 - 타입별 다른 스타일 */}
|
||||||
{sectionBots.length === 0 ? (
|
{sectionBots.length === 0 ? (
|
||||||
<div className="text-center py-12 text-gray-400">
|
<div className="text-center py-12 text-gray-400">
|
||||||
<Bot size={36} className="mx-auto mb-3 opacity-30" />
|
<Bot size={36} className="mx-auto mb-3 opacity-30" />
|
||||||
|
|
@ -388,10 +388,53 @@ function ScheduleBots() {
|
||||||
<p className="text-xs mt-1">위의 버튼을 클릭하여 봇을 추가하세요</p>
|
<p className="text-xs mt-1">위의 버튼을 클릭하여 봇을 추가하세요</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : type === 'meilisearch' ? (
|
||||||
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
|
/* Meilisearch: 리스트형 */
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
{sectionBots.map((bot, index) => (
|
{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}
|
key={bot.id}
|
||||||
bot={bot}
|
bot={bot}
|
||||||
index={index}
|
index={index}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue