/**
* X 봇 추가/수정 다이얼로그
*/
import { useState, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { motion, AnimatePresence } from 'framer-motion';
import { Twitter, Search, X, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
import { getXBot, createXBot, updateXBot, lookupXProfile } from '@/api/admin/bots';
// 동기화 간격 옵션
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시간' },
];
/**
* 커스텀 드롭다운 컴포넌트 (Portal 사용)
*/
function Dropdown({ value, options, onChange, placeholder = '선택', className = '' }) {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
const buttonRef = useRef(null);
const menuRef = useRef(null);
useEffect(() => {
const handleClickOutside = (event) => {
if (
buttonRef.current &&
!buttonRef.current.contains(event.target) &&
menuRef.current &&
!menuRef.current.contains(event.target)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen]);
useEffect(() => {
if (isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 4,
left: rect.left,
width: rect.width,
});
}
}, [isOpen]);
const selectedOption = options.find((opt) => opt.value === value);
return (
{createPortal(
{isOpen && (
{options.map((opt) => (
))}
)}
,
document.body
)}
);
}
function XBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
const queryClient = useQueryClient();
const isEdit = !!botId;
// 폼 상태
const [username, setUsername] = useState('');
const [profileInfo, setProfileInfo] = useState(null);
const [lookupLoading, setLookupLoading] = useState(false);
const [interval, setInterval] = useState(1);
const [submitting, setSubmitting] = useState(false);
// 고급 설정
const [showAdvanced, setShowAdvanced] = useState(false);
const [textFilters, setTextFilters] = useState([]);
const [filterInput, setFilterInput] = useState('');
// X 봇 상세 조회 (수정 모드)
const { data: bot, isLoading: botLoading } = useQuery({
queryKey: ['admin', 'x-bot', botId],
queryFn: () => getXBot(botId),
enabled: isOpen && !!botId,
staleTime: 0,
});
// 다이얼로그 열릴 때 데이터 설정
useEffect(() => {
if (!isOpen) return;
if (bot) {
// 수정 모드
setUsername(bot.username || '');
setProfileInfo({
username: bot.username,
displayName: bot.display_name,
avatarUrl: bot.avatar_url,
});
setInterval(bot.cron_interval || 1);
setTextFilters(bot.text_filters || []);
setShowAdvanced((bot.text_filters && bot.text_filters.length > 0) || false);
} else if (!botId) {
// 추가 모드
setUsername('');
setProfileInfo(null);
setInterval(1);
setTextFilters([]);
setFilterInput('');
setShowAdvanced(false);
}
}, [isOpen, bot, botId]);
// 프로필 조회
const handleLookup = async () => {
if (!username.trim()) return;
setLookupLoading(true);
try {
const data = await lookupXProfile(username);
setProfileInfo({
username: data.username,
displayName: data.displayName,
avatarUrl: data.avatarUrl,
});
} catch (error) {
console.error('프로필 조회 실패:', error);
alert(error.message || '프로필을 찾을 수 없습니다.');
} finally {
setLookupLoading(false);
}
};
// 제출
const handleSubmit = async (e) => {
e.preventDefault();
if (!profileInfo) return;
setSubmitting(true);
try {
const data = {
username: profileInfo.username,
display_name: profileInfo.displayName,
avatar_url: profileInfo.avatarUrl,
text_filters: textFilters.length > 0 ? textFilters : null,
cron_interval: interval,
};
if (isEdit) {
await updateXBot(botId, data);
} else {
await createXBot(data);
}
// 캐시 무효화
queryClient.invalidateQueries({ queryKey: ['admin', 'bots'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'x-bot'] });
onSuccess?.();
onClose();
} catch (error) {
console.error('봇 저장 실패:', error);
alert(error.message || '봇 저장에 실패했습니다.');
} finally {
setSubmitting(false);
}
};
return createPortal(
{isOpen && (
e.stopPropagation()}
>
{/* 헤더 */}
{isEdit ? 'X 봇 수정' : 'X 봇 추가'}
{/* 본문 */}
{botLoading ? (
) : (
)}
{/* 푸터 */}
)}
,
document.body
);
}
export default XBotDialog;