diff --git a/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx b/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx new file mode 100644 index 0000000..b64cb34 --- /dev/null +++ b/frontend/src/components/pc/admin/bot/YouTubeBotDialog.jsx @@ -0,0 +1,388 @@ +/** + * YouTube 봇 추가/수정 다이얼로그 + */ +import { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Youtube, Search, X, ChevronDown, ChevronUp } from 'lucide-react'; + +// 동기화 간격 옵션 +const INTERVAL_OPTIONS = [ + { value: 1, label: '1분' }, + { value: 2, label: '2분' }, + { value: 5, label: '5분' }, + { value: 10, label: '10분' }, + { value: 30, label: '30분' }, + { value: 60, label: '1시간' }, +]; + +// 요일 옵션 +const DAY_OPTIONS = [ + { value: 0, label: '일요일' }, + { value: 1, label: '월요일' }, + { value: 2, label: '화요일' }, + { value: 3, label: '수요일' }, + { value: 4, label: '목요일' }, + { value: 5, label: '금요일' }, + { value: 6, label: '토요일' }, +]; + +function YouTubeBotDialog({ isOpen, onClose, bot = null, onSubmit }) { + const isEdit = !!bot; + + // 폼 상태 + const [handle, setHandle] = useState(''); + const [channelInfo, setChannelInfo] = useState(null); + const [lookupLoading, setLookupLoading] = useState(false); + const [interval, setInterval] = useState(2); + + // 예정 일정 설정 + const [autoScheduleEnabled, setAutoScheduleEnabled] = useState(false); + const [scheduleDayOfWeek, setScheduleDayOfWeek] = useState(4); + const [scheduleTime, setScheduleTime] = useState('18:00'); + const [titleTemplate, setTitleTemplate] = useState('{channelName} {episode}화'); + const [deadlineDayOfWeek, setDeadlineDayOfWeek] = useState(5); + + // 고급 설정 + const [showAdvanced, setShowAdvanced] = useState(false); + const [titleFilter, setTitleFilter] = useState(''); + const [extractMembers, setExtractMembers] = useState(false); + + // 수정 모드일 때 기존 데이터 로드 + useEffect(() => { + if (bot) { + setHandle(bot.channel_handle || ''); + setChannelInfo({ + channelId: bot.channel_id, + title: bot.channel_name, + }); + setInterval(bot.cron_interval || 2); + + if (bot.auto_schedule_config) { + const config = typeof bot.auto_schedule_config === 'string' + ? JSON.parse(bot.auto_schedule_config) + : bot.auto_schedule_config; + setAutoScheduleEnabled(true); + setScheduleDayOfWeek(config.dayOfWeek ?? 4); + setScheduleTime(config.time?.slice(0, 5) || '18:00'); + setTitleTemplate(config.titleTemplate || '{channelName} {episode}화'); + setDeadlineDayOfWeek(config.deadlineDayOfWeek ?? 5); + } + + setTitleFilter(bot.title_filter || ''); + setExtractMembers(bot.extract_members_from_desc || false); + } + }, [bot]); + + // 다이얼로그 닫힐 때 초기화 + useEffect(() => { + if (!isOpen) { + setHandle(''); + setChannelInfo(null); + setInterval(2); + setAutoScheduleEnabled(false); + setScheduleDayOfWeek(4); + setScheduleTime('18:00'); + setTitleTemplate('{channelName} {episode}화'); + setDeadlineDayOfWeek(5); + setShowAdvanced(false); + setTitleFilter(''); + setExtractMembers(false); + } + }, [isOpen]); + + // 채널 조회 + const handleLookup = async () => { + if (!handle.trim()) return; + setLookupLoading(true); + // TODO: API 호출 + setTimeout(() => { + setChannelInfo({ + channelId: 'UC_EXAMPLE_ID', + title: '예시 채널명', + thumbnailUrl: null, + }); + setLookupLoading(false); + }, 1000); + }; + + // 제출 + const handleSubmit = (e) => { + e.preventDefault(); + // TODO: onSubmit 호출 + onClose(); + }; + + return createPortal( + + {isOpen && ( + + e.stopPropagation()} + > + {/* 헤더 */} +
+
+
+ +
+

+ {isEdit ? 'YouTube 봇 수정' : 'YouTube 봇 추가'} +

+
+ +
+ + {/* 본문 */} +
+ {/* 채널 핸들 */} +
+ +
+
+ @ + setHandle(e.target.value)} + placeholder="studiofromis_9" + disabled={isEdit} + className="w-full pl-8 pr-4 py-2.5 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500 disabled:bg-gray-50 disabled:text-gray-500" + /> +
+ {!isEdit && ( + + )} +
+ {/* 채널 정보 표시 */} + {channelInfo && ( +
+
+ +
+
+

{channelInfo.title}

+

{channelInfo.channelId}

+
+
+ )} +
+ + {/* 동기화 간격 */} +
+ + +
+ + {/* 예정 일정 자동 생성 */} +
+
setAutoScheduleEnabled(!autoScheduleEnabled)} + > +
+

예정 일정 자동 생성

+

매주 특정 요일에 임시 일정을 미리 생성합니다

+
+
+
+
+
+ + {autoScheduleEnabled && ( +
+ {/* 요일 & 시간 */} +
+
+ + +
+
+ + setScheduleTime(e.target.value)} + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500" + /> +
+
+ + {/* 제목 템플릿 */} +
+ + setTitleTemplate(e.target.value)} + placeholder="{channelName} {episode}화" + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500" + /> +

+ {'{channelName}'}: 채널명, {'{episode}'}: 회차 번호 +

+
+ + {/* 마감 요일 */} +
+ + +

+ 이 요일까지 영상이 없으면 예정 일정을 삭제합니다 +

+
+
+ )} +
+ + {/* 고급 설정 */} +
+ + + {showAdvanced && ( +
+ {/* 제목 필터 */} +
+ + setTitleFilter(e.target.value)} + placeholder="특정 키워드가 포함된 영상만 추가" + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-500/20 focus:border-red-500" + /> +
+ + {/* 멤버 추출 */} +
setExtractMembers(!extractMembers)} + > +
+

설명에서 멤버 추출

+

영상 설명에서 멤버 이름을 찾아 자동 연결

+
+
+
+
+
+
+ )} +
+ + + {/* 푸터 */} +
+ + +
+ + + )} + , + document.body + ); +} + +export default YouTubeBotDialog; diff --git a/frontend/src/components/pc/admin/bot/index.js b/frontend/src/components/pc/admin/bot/index.js index 64c22aa..0d93be3 100644 --- a/frontend/src/components/pc/admin/bot/index.js +++ b/frontend/src/components/pc/admin/bot/index.js @@ -1 +1,2 @@ export { default as BotCard, XIcon, MeilisearchIcon, BotListItem, BotMiniCard, BotTableRow, BotTable } from './BotCard'; +export { default as YouTubeBotDialog } from './YouTubeBotDialog'; diff --git a/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx b/frontend/src/pages/pc/admin/schedules/ScheduleBots.jsx index 975fbf7..d05a261 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, Youtube } from 'lucide-react'; import { Toast, Tooltip, AnimatedNumber } from '@/components/common'; -import { AdminLayout, XIcon, MeilisearchIcon, BotTableRow, BotTable } from '@/components/pc/admin'; +import { AdminLayout, XIcon, MeilisearchIcon, BotTableRow, BotTable, YouTubeBotDialog } from '@/components/pc/admin'; import { useAdminAuth } from '@/hooks/pc/admin'; import { useToast } from '@/hooks/common'; import * as botsApi from '@/api/admin/bots'; @@ -60,6 +60,8 @@ function ScheduleBots() { const [isInitialLoad, setIsInitialLoad] = useState(true); // 첫 로드 여부 (애니메이션용) const [syncing, setSyncing] = useState(null); // 동기화 중인 봇 ID const [quotaWarning, setQuotaWarning] = useState(null); // 할당량 경고 상태 + const [botDialogOpen, setBotDialogOpen] = useState(false); // 봇 추가/수정 다이얼로그 + const [editingBot, setEditingBot] = useState(null); // 수정 중인 봇 // 봇 목록 조회 const { @@ -235,6 +237,18 @@ function ScheduleBots() { return ( setToast(null)} /> + { + setBotDialogOpen(false); + setEditingBot(null); + }} + bot={editingBot} + onSubmit={(data) => { + // TODO: API 호출 + console.log('submit', data); + }} + /> {/* 메인 콘텐츠 */} {section.canAdd && (