/**
* YouTube 봇 추가/수정 다이얼로그
*/
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 { Youtube, Search, X, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
import { getMembers } from '@/api/public/members';
import { getYouTubeBot, createYouTubeBot, updateYouTubeBot } 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시간' },
];
// 요일 옵션
const DAY_OPTIONS = [
{ value: 0, label: '일요일' },
{ value: 1, label: '월요일' },
{ value: 2, label: '화요일' },
{ value: 3, label: '수요일' },
{ value: 4, label: '목요일' },
{ value: 5, label: '금요일' },
{ value: 6, label: '토요일' },
];
// 시간 옵션 (00:00 ~ 23:00)
const TIME_OPTIONS = Array.from({ length: 24 }, (_, i) => ({
value: `${String(i).padStart(2, '0')}:00`,
label: `${String(i).padStart(2, '0')}:00`,
}));
/**
* 커스텀 드롭다운 컴포넌트 (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 MultiSelect({ values = [], 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 selectedOptions = options.filter((opt) => values.includes(opt.value));
const displayText = selectedOptions.length > 0
? selectedOptions.map((o) => o.label).join(', ')
: placeholder;
const toggleValue = (val) => {
if (values.includes(val)) {
onChange(values.filter((v) => v !== val));
} else {
onChange([...values, val]);
}
};
return (
{createPortal(
{isOpen && (
{options.map((opt) => (
))}
)}
,
document.body
)}
);
}
function YouTubeBotDialog({ isOpen, onClose, botId = null, onSuccess }) {
const queryClient = useQueryClient();
const isEdit = !!botId;
// 폼 상태
const [handle, setHandle] = useState('');
const [channelInfo, setChannelInfo] = useState(null);
const [lookupLoading, setLookupLoading] = useState(false);
const [interval, setInterval] = useState(2);
const [submitting, setSubmitting] = useState(false);
// 예정 일정 설정
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 [titleFilters, setTitleFilters] = useState([]);
const [filterInput, setFilterInput] = useState('');
const [defaultMemberIds, setDefaultMemberIds] = useState([]);
const [extractMembers, setExtractMembers] = useState(false);
// 멤버 목록 (탈퇴 멤버 제외)
const [members, setMembers] = useState([]);
// YouTube 봇 상세 조회 (수정 모드)
const { data: bot, isLoading: botLoading } = useQuery({
queryKey: ['admin', 'youtube-bot', botId],
queryFn: () => getYouTubeBot(botId),
enabled: isOpen && !!botId,
staleTime: 0, // 항상 fresh 데이터 가져오기
});
// 멤버 목록 로드
useEffect(() => {
if (isOpen) {
getMembers()
.then((data) => setMembers(data.filter((m) => !m.is_former)))
.catch(console.error);
}
}, [isOpen]);
// 다이얼로그 열릴 때 데이터 설정 (수정/추가 모드)
useEffect(() => {
if (!isOpen) {
return; // 닫혀있으면 아무것도 안 함
}
if (bot) {
// 수정 모드: 기존 데이터 로드
setHandle(bot.channel_handle || '');
setChannelInfo({
channelId: bot.channel_id,
title: bot.channel_name,
bannerUrl: bot.banner_url,
});
setInterval(bot.cron_interval || 2);
console.log('bot.auto_schedule_config:', bot.auto_schedule_config);
console.log('typeof:', typeof bot.auto_schedule_config);
const config = bot.auto_schedule_config
? (typeof bot.auto_schedule_config === 'string'
? JSON.parse(bot.auto_schedule_config)
: bot.auto_schedule_config)
: null;
console.log('parsed config:', config);
// config가 존재하고 dayOfWeek가 정의되어 있으면 활성화
if (config && config.dayOfWeek !== undefined) {
setAutoScheduleEnabled(true);
setScheduleDayOfWeek(config.dayOfWeek);
setScheduleTime(config.time?.slice(0, 5) || '18:00');
setTitleTemplate(config.titleTemplate || '{channelName} {episode}화');
setDeadlineDayOfWeek(config.deadlineDayOfWeek ?? 5);
} else {
setAutoScheduleEnabled(false);
setScheduleDayOfWeek(4);
setScheduleTime('18:00');
setTitleTemplate('{channelName} {episode}화');
setDeadlineDayOfWeek(5);
}
setTitleFilters(bot.title_filters || []);
setDefaultMemberIds(bot.default_member_ids || []);
setExtractMembers(bot.extract_members_from_desc || false);
// 고급 설정이 있으면 펼침
if ((bot.title_filters && bot.title_filters.length > 0) ||
(bot.default_member_ids && bot.default_member_ids.length > 0) ||
bot.extract_members_from_desc) {
setShowAdvanced(true);
} else {
setShowAdvanced(false);
}
} else if (!botId) {
// 추가 모드: 초기값으로 리셋
setHandle('');
setChannelInfo(null);
setInterval(2);
setAutoScheduleEnabled(false);
setScheduleDayOfWeek(4);
setScheduleTime('18:00');
setTitleTemplate('{channelName} {episode}화');
setDeadlineDayOfWeek(5);
setShowAdvanced(false);
setTitleFilters([]);
setFilterInput('');
setDefaultMemberIds([]);
setExtractMembers(false);
}
}, [isOpen, bot, botId]);
// 채널 조회
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 = async (e) => {
e.preventDefault();
if (!channelInfo) return;
setSubmitting(true);
try {
const data = {
channel_handle: handle || null,
channel_name: channelInfo.title,
cron_interval: interval,
title_filters: titleFilters.length > 0 ? titleFilters : null,
default_member_ids: defaultMemberIds.length > 0 ? defaultMemberIds : null,
extract_members_from_desc: extractMembers,
auto_schedule_config: autoScheduleEnabled
? {
dayOfWeek: scheduleDayOfWeek,
time: `${scheduleTime}:00`,
titleTemplate,
deadlineDayOfWeek,
}
: null,
};
if (isEdit) {
await updateYouTubeBot(botId, data);
} else {
data.channel_id = channelInfo.channelId;
await createYouTubeBot(data);
}
// 캐시 무효화
queryClient.invalidateQueries({ queryKey: ['admin', 'bots'] });
queryClient.invalidateQueries({ queryKey: ['admin', 'youtube-bot'] });
onSuccess?.();
onClose();
} catch (error) {
console.error('봇 저장 실패:', error);
alert(error.message || '봇 저장에 실패했습니다.');
} finally {
setSubmitting(false);
}
};
return createPortal(
{isOpen && (
e.stopPropagation()}
>
{/* 헤더 */}
{isEdit ? 'YouTube 봇 수정' : 'YouTube 봇 추가'}
{/* 본문 */}
{botLoading ? (
) : (
)}
{/* 푸터 */}
)}
,
document.body
);
}
export default YouTubeBotDialog;