refactor: BotCard 디자인 개선

- 타입별 아이콘 제거 (섹션 헤더에서 표시)
- 통계 정보 가로 배열로 컴팩트하게 변경
- 버튼을 하단에 플랫하게 배치
- 전체적으로 미니멀하고 깔끔한 스타일로 변경

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-02-06 18:01:15 +09:00
parent 68027f0654
commit b3357e0663

View file

@ -95,68 +95,49 @@ const BotCard = memo(function BotCard({
}) {
return (
<motion.div
initial={isInitialLoad ? { opacity: 0, scale: 0.95 } : false}
animate={{ opacity: 1, scale: 1 }}
transition={isInitialLoad ? { delay: index * 0.05 } : { duration: 0.15 }}
initial={isInitialLoad ? { opacity: 0, y: 10 } : false}
animate={{ opacity: 1, y: 0 }}
transition={isInitialLoad ? { delay: index * 0.05, duration: 0.2 } : { duration: 0.15 }}
onAnimationComplete={onAnimationComplete}
className="relative bg-gradient-to-br from-gray-50 to-white rounded-xl border border-gray-200 overflow-hidden hover:shadow-md transition-all"
className="group relative bg-white rounded-xl border border-gray-100 overflow-hidden hover:border-gray-200 hover:shadow-sm transition-all"
>
{/* 상단 헤더 */}
<div className="flex items-center justify-between p-4 border-b border-gray-100">
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-lg flex items-center justify-center ${
bot.type === 'x'
? 'bg-black'
: bot.type === 'meilisearch'
? 'bg-[#ddf1fd]'
: 'bg-red-50'
}`}
>
{bot.type === 'x' ? (
<XIcon size={20} fill="white" />
) : bot.type === 'meilisearch' ? (
<MeilisearchIcon size={20} />
) : (
<Youtube size={20} className="text-red-500" />
)}
</div>
<div>
<h3 className="font-bold text-gray-900">{bot.name}</h3>
<p className="text-xs text-gray-400">
{bot.last_check_at
? `${formatTime(bot.last_check_at)}에 업데이트됨`
: '아직 업데이트 없음'}
{/* 상단: 이름 + 상태 */}
<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>
</div>
<span
className={`flex items-center gap-1.5 px-2.5 py-1 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' : ''}`}
></span>
{statusInfo.text}
</span>
</div>
{/* 통계 정보 */}
<div className="grid grid-cols-3 divide-x divide-gray-100 bg-gray-50/50">
<div className="p-3 text-center">
<div className="text-lg font-bold text-gray-900">{bot.schedules_added || 0}</div>
<div className="text-xs text-gray-400"> 추가</div>
</div>
<div className="p-3 text-center">
<div
className={`text-lg font-bold ${bot.last_added_count > 0 ? 'text-green-500' : 'text-gray-400'}`}
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}`}
>
+{bot.last_added_count || 0}
</div>
<div className="text-xs text-gray-400">마지막</div>
<span
className={`w-1.5 h-1.5 rounded-full ${statusInfo.dot} ${bot.status === 'running' ? 'animate-pulse' : ''}`}
/>
{statusInfo.text}
</span>
</div>
<div className="p-3 text-center">
<div className="text-lg font-bold text-gray-900">{formatInterval(bot.check_interval)}</div>
<div className="text-xs text-gray-400">업데이트 간격</div>
{/* 통계 */}
<div className="flex items-center gap-4 text-sm">
<div className="flex items-center gap-1.5">
<span className="text-gray-400">추가</span>
<span className="font-semibold text-gray-900">{bot.schedules_added || 0}</span>
</div>
<div className="w-px h-3 bg-gray-200" />
<div className="flex items-center gap-1.5">
<span className="text-gray-400">최근</span>
<span className={`font-semibold ${bot.last_added_count > 0 ? 'text-green-600' : 'text-gray-400'}`}>
+{bot.last_added_count || 0}
</span>
</div>
<div className="w-px h-3 bg-gray-200" />
<div className="flex items-center gap-1.5">
<span className="text-gray-400">간격</span>
<span className="font-semibold text-gray-900">{formatInterval(bot.check_interval)}</span>
</div>
</div>
</div>
@ -168,46 +149,44 @@ const BotCard = memo(function BotCard({
)}
{/* 액션 버튼 */}
<div className="p-4 border-t border-gray-100">
<div className="flex gap-2">
<button
onClick={() => onSync(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 ? (
<>
<RefreshCw size={16} className="animate-spin" />
<span>동기화 ...</span>
</>
) : (
<>
<Download size={16} />
<span>전체 동기화</span>
</>
)}
</button>
<button
onClick={() => onToggle(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' ? (
<>
<Square size={16} />
<span>정지</span>
</>
) : (
<>
<Play size={16} />
<span>시작</span>
</>
)}
</button>
</div>
<div className="flex border-t border-gray-100">
<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"
>
{syncing === bot.id ? (
<>
<RefreshCw size={14} className="animate-spin" />
<span>동기화 </span>
</>
) : (
<>
<Download size={14} />
<span>전체 동기화</span>
</>
)}
</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 ${
bot.status === 'running'
? 'text-gray-600 hover:bg-gray-50'
: 'text-green-600 hover:bg-green-50'
}`}
>
{bot.status === 'running' ? (
<>
<Square size={14} />
<span>정지</span>
</>
) : (
<>
<Play size={14} />
<span>시작</span>
</>
)}
</button>
</div>
</motion.div>
);