From b5118f2dea18f5c315acdb86a7536e23bd00a4de Mon Sep 17 00:00:00 2001 From: caadiq Date: Fri, 6 Feb 2026 18:04:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B4=87=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=EB=B3=84=20=EB=8B=A4=EB=A5=B8=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Meilisearch: 리스트형 (BotListItem) - 한 줄에 모든 정보 - YouTube: 미니 카드형 (BotMiniCard) - 호버시 액션 버튼 - X: 테이블형 (BotTableRow) - 표 형식으로 정보 비교 Co-Authored-By: Claude Opus 4.5 --- .../src/components/pc/admin/bot/BotCard.jsx | 348 +++++++++++------- frontend/src/components/pc/admin/bot/index.js | 2 +- .../pages/pc/admin/schedules/ScheduleBots.jsx | 53 ++- 3 files changed, 274 insertions(+), 129 deletions(-) diff --git a/frontend/src/components/pc/admin/bot/BotCard.jsx b/frontend/src/components/pc/admin/bot/BotCard.jsx index 199bb9a..784dc0b 100644 --- a/frontend/src/components/pc/admin/bot/BotCard.jsx +++ b/frontend/src/components/pc/admin/bot/BotCard.jsx @@ -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 }) => ( - + - + - + - - - + + + ); /** - * @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 ( - {/* 상단: 이름 + 상태 */} -
-
-
-

{bot.name}

-

- {bot.last_check_at ? formatTime(bot.last_check_at) : '대기 중'} -

-
- - - {statusInfo.text} - -
+ {/* 상태 표시 */} +
- {/* 통계 */} -
-
-
{bot.schedules_added || 0}
-
총 추가
-
-
-
-
0 ? 'text-green-600' : 'text-gray-400'}`}> - +{bot.last_added_count || 0} -
-
최근
-
-
-
-
{formatInterval(bot.check_interval)}
-
간격
-
+ {/* 이름 */} +
+

{bot.name}

+
+ + {/* 통계 */} +
+
+ {bot.schedules_added || 0} + 추가 +
+
+ {bot.last_check_at ? formatTime(bot.last_check_at) : '-'}
- {/* 오류 메시지 */} - {bot.status === 'error' && bot.error_message && ( -
- {bot.error_message} -
- )} - - {/* 액션 버튼 */} -
+ {/* 버튼 */} +
); }); +/** + * 미니 카드형 봇 (YouTube용) - 컴팩트한 카드 + */ +export const BotMiniCard = memo(function BotMiniCard({ + bot, + index, + isInitialLoad, + syncing, + statusInfo, + onSync, + onToggle, + onAnimationComplete, + formatTime, + formatInterval, +}) { + return ( + + {/* 메인 영역 */} +
+
+

{bot.name}

+ + + {statusInfo.text} + +
+ + {/* 간단 통계 */} +
+ {bot.schedules_added || 0} + + 최근 0 ? 'text-green-600' : 'text-gray-400'}>+{bot.last_added_count || 0} + + {formatInterval(bot.check_interval)} +
+ + {/* 마지막 업데이트 */} +

+ {bot.last_check_at ? formatTime(bot.last_check_at) : '대기 중'} +

+
+ + {/* 오류 메시지 */} + {bot.status === 'error' && bot.error_message && ( +
+ {bot.error_message} +
+ )} + + {/* 호버시 나타나는 액션 버튼 */} +
+ + +
+
+ ); +}); + +/** + * 테이블 행 봇 (X용) + */ +export const BotTableRow = memo(function BotTableRow({ + bot, + index, + isInitialLoad, + syncing, + statusInfo, + onSync, + onToggle, + onAnimationComplete, + formatTime, + formatInterval, +}) { + return ( + + +
+ + {bot.name} +
+ + + + {statusInfo.text} + + + {bot.schedules_added || 0} + + 0 ? 'text-green-600 font-medium' : 'text-gray-400'}> + +{bot.last_added_count || 0} + + + {formatInterval(bot.check_interval)} + + {bot.last_check_at ? formatTime(bot.last_check_at) : '-'} + + +
+ + +
+ +
+ ); +}); + +/** + * 테이블 래퍼 (X용) + */ +export const BotTable = ({ children }) => ( +
+ + + + + + + + + + + + + {children} +
이름상태총 추가최근간격마지막 업데이트액션
+
+); + +// 기본 카드 (호환성 유지) +const BotCard = BotMiniCard; + export default BotCard; diff --git a/frontend/src/components/pc/admin/bot/index.js b/frontend/src/components/pc/admin/bot/index.js index e95a011..64c22aa 100644 --- a/frontend/src/components/pc/admin/bot/index.js +++ b/frontend/src/components/pc/admin/bot/index.js @@ -1 +1 @@ -export { default as BotCard, XIcon, MeilisearchIcon } from './BotCard'; +export { default as BotCard, XIcon, MeilisearchIcon, BotListItem, BotMiniCard, BotTableRow, BotTable } from './BotCard'; diff --git a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx index fb9cdd0..49956f3 100644 --- a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx +++ b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx @@ -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() {
- {/* 봇 카드 목록 */} + {/* 봇 목록 - 타입별 다른 스타일 */} {sectionBots.length === 0 ? (
@@ -388,10 +388,53 @@ function ScheduleBots() {

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

)}
- ) : ( -
+ ) : type === 'meilisearch' ? ( + /* Meilisearch: 리스트형 */ +
{sectionBots.map((bot, index) => ( - + isInitialLoad && index === sectionBots.length - 1 && setIsInitialLoad(false) + } + formatTime={formatTime} + formatInterval={formatInterval} + /> + ))} +
+ ) : type === 'x' ? ( + /* X: 테이블형 */ + + {sectionBots.map((bot, index) => ( + + isInitialLoad && index === sectionBots.length - 1 && setIsInitialLoad(false) + } + formatTime={formatTime} + formatInterval={formatInterval} + /> + ))} + + ) : ( + /* YouTube: 미니 카드형 */ +
+ {sectionBots.map((bot, index) => ( +