import { useState, useEffect, useRef, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Clock, ChevronLeft, ChevronRight, ChevronDown, Tag, Search, ArrowLeft } from 'lucide-react'; function Schedule() { const navigate = useNavigate(); // KST 기준 오늘 날짜 (YYYY-MM-DD) const getTodayKST = () => { const now = new Date(); const kstOffset = 9 * 60 * 60 * 1000; // 9시간 const kstDate = new Date(now.getTime() + kstOffset); return kstDate.toISOString().split('T')[0]; }; const [currentDate, setCurrentDate] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST 기준 오늘 const [showYearMonthPicker, setShowYearMonthPicker] = useState(false); const [viewMode, setViewMode] = useState('yearMonth'); const [slideDirection, setSlideDirection] = useState(0); const pickerRef = useRef(null); // 데이터 상태 const [schedules, setSchedules] = useState([]); const [categories, setCategories] = useState([]); const [selectedCategories, setSelectedCategories] = useState([]); const [loading, setLoading] = useState(true); // 카테고리 필터 툴팁 const [showCategoryTooltip, setShowCategoryTooltip] = useState(false); const categoryRef = useRef(null); // 검색 상태 const [isSearchMode, setIsSearchMode] = useState(false); const [searchInput, setSearchInput] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); // 데이터 로드 useEffect(() => { fetchSchedules(); fetchCategories(); }, []); const fetchSchedules = async () => { try { const response = await fetch('/api/schedules'); if (response.ok) { const data = await response.json(); setSchedules(data); } } catch (error) { console.error('일정 로드 오류:', error); } finally { setLoading(false); } }; const fetchCategories = async () => { try { const response = await fetch('/api/schedule-categories'); if (response.ok) { const data = await response.json(); setCategories(data); } } catch (error) { console.error('카테고리 로드 오류:', error); } }; // 검색 함수 (API 호출) const searchSchedules = async (term) => { if (!term.trim()) { setSearchResults([]); return; } setSearchLoading(true); try { const response = await fetch(`/api/admin/schedules?search=${encodeURIComponent(term)}`); if (response.ok) { const data = await response.json(); setSearchResults(data); } } catch (error) { console.error('검색 오류:', error); } finally { setSearchLoading(false); } }; // 외부 클릭시 팝업 닫기 useEffect(() => { const handleClickOutside = (event) => { if (pickerRef.current && !pickerRef.current.contains(event.target)) { setShowYearMonthPicker(false); setViewMode('yearMonth'); } if (categoryRef.current && !categoryRef.current.contains(event.target)) { setShowCategoryTooltip(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // 달력 관련 함수 const getDaysInMonth = (year, month) => new Date(year, month + 1, 0).getDate(); const getFirstDayOfMonth = (year, month) => new Date(year, month, 1).getDay(); const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const daysInMonth = getDaysInMonth(year, month); const firstDay = getFirstDayOfMonth(year, month); const days = ['일', '월', '화', '수', '목', '금', '토']; // 스케줄이 있는 날짜 목록 (ISO 형식에서 YYYY-MM-DD 추출) const scheduleDates = schedules.map(s => s.date ? s.date.split('T')[0] : ''); // 해당 날짜의 첫 번째 일정 카테고리 색상 const getScheduleColor = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const schedule = schedules.find(s => (s.date ? s.date.split('T')[0] : '') === dateStr); if (!schedule) return null; const cat = categories.find(c => c.id === schedule.category_id); return cat?.color || '#4A7C59'; }; const hasSchedule = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; return scheduleDates.includes(dateStr); }; const prevMonth = () => { setSlideDirection(-1); setCurrentDate(new Date(year, month - 1, 1)); setSelectedDate(null); // 월 변경 시 초기화 }; const nextMonth = () => { setSlideDirection(1); setCurrentDate(new Date(year, month + 1, 1)); setSelectedDate(null); // 월 변경 시 초기화 }; const selectDate = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; setSelectedDate(selectedDate === dateStr ? null : dateStr); }; const selectYear = (newYear) => { setCurrentDate(new Date(newYear, month, 1)); setViewMode('months'); }; const selectMonth = (newMonth) => { setCurrentDate(new Date(year, newMonth, 1)); setShowYearMonthPicker(false); setViewMode('yearMonth'); }; // 카테고리 토글 const toggleCategory = (categoryId) => { setSelectedCategories(prev => prev.includes(categoryId) ? prev.filter(id => id !== categoryId) : [...prev, categoryId] ); }; // 필터링된 스케줄 (useMemo로 성능 최적화, 시간순 정렬) const currentYearMonth = `${year}-${String(month + 1).padStart(2, '0')}`; const filteredSchedules = useMemo(() => { // 검색 모드일 때 if (isSearchMode) { // 검색 전엔 빈 목록, 검색 후엔 API 결과 if (!searchTerm) return []; return searchResults.sort((a, b) => { const dateA = a.date ? a.date.split('T')[0] : ''; const dateB = b.date ? b.date.split('T')[0] : ''; if (dateA !== dateB) return dateA.localeCompare(dateB); const timeA = a.time || '00:00:00'; const timeB = b.time || '00:00:00'; return timeA.localeCompare(timeB); }); } // 일반 모드: 기존 필터링 return schedules .filter(s => { const scheduleDate = s.date ? s.date.split('T')[0] : ''; const matchesDate = selectedDate ? scheduleDate === selectedDate : scheduleDate.startsWith(currentYearMonth); const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(s.category_id); return matchesDate && matchesCategory; }) .sort((a, b) => { const dateA = a.date ? a.date.split('T')[0] : ''; const dateB = b.date ? b.date.split('T')[0] : ''; if (dateA !== dateB) return dateA.localeCompare(dateB); const timeA = a.time || '00:00:00'; const timeB = b.time || '00:00:00'; return timeA.localeCompare(timeB); }); }, [schedules, selectedDate, currentYearMonth, selectedCategories, isSearchMode, searchTerm, searchResults]); const formatDate = (dateStr) => { const date = new Date(dateStr); const dayNames = ['일', '월', '화', '수', '목', '금', '토']; return { month: date.getMonth() + 1, day: date.getDate(), weekday: dayNames[date.getDay()], }; }; // 일정 클릭 핸들러 const handleScheduleClick = (schedule) => { // 설명이 없고 URL만 있으면 바로 링크 열기 if (!schedule.description && schedule.source_url) { window.open(schedule.source_url, '_blank'); } else { // 상세 페이지로 이동 (추후 구현) navigate(`/schedule/${schedule.id}`); } }; // 년도 범위 const startYear = Math.floor(year / 10) * 10 - 1; const yearRange = Array.from({ length: 12 }, (_, i) => startYear + i); const isCurrentYear = (y) => new Date().getFullYear() === y; const isCurrentMonth = (m) => { const today = new Date(); return today.getFullYear() === year && today.getMonth() === m; }; const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; const prevYearRange = () => setCurrentDate(new Date(year - 10, month, 1)); const nextYearRange = () => setCurrentDate(new Date(year + 10, month, 1)); // 선택된 카테고리 이름 const getSelectedCategoryNames = () => { if (selectedCategories.length === 0) return '전체'; const names = selectedCategories.map(id => { const cat = categories.find(c => c.id === id); return cat?.name || ''; }).filter(Boolean); if (names.length <= 2) return names.join(', '); return `${names.slice(0, 2).join(', ')} 외 ${names.length - 2}개`; }; // 카테고리 색상 가져오기 const getCategoryColor = (categoryId) => { const cat = categories.find(c => c.id === categoryId); return cat?.color || '#808080'; }; const getCategoryName = (categoryId) => { const cat = categories.find(c => c.id === categoryId); return cat?.name || ''; }; return (