일정 관리
fromis_9의 일정을 관리합니다
카테고리
등록된 일정이 없습니다
{schedule.title}
{schedule.description && ({schedule.description}
)} {/* 멤버 태그 */} {schedule.members && schedule.members.length > 0 && (import { useState, useEffect, useRef } from 'react'; import { useNavigate, Link } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { LogOut, Home, ChevronRight, Calendar, Plus, Edit2, Trash2, ChevronLeft, Search, ChevronDown, AlertTriangle, Bot, Tag, ArrowLeft } from 'lucide-react'; import Toast from '../../../components/Toast'; import Tooltip from '../../../components/Tooltip'; function AdminSchedule() { const navigate = useNavigate(); // KST 기준 오늘 날짜 (YYYY-MM-DD) const getTodayKST = () => { const now = new Date(); const kstOffset = 9 * 60 * 60 * 1000; const kstDate = new Date(now.getTime() + kstOffset); return kstDate.toISOString().split('T')[0]; }; const [loading, setLoading] = useState(false); const [user, setUser] = useState(null); const [toast, setToast] = useState(null); const [searchInput, setSearchInput] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [isSearchMode, setIsSearchMode] = useState(false); const [searchResults, setSearchResults] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const [selectedCategories, setSelectedCategories] = useState([]); const [selectedDate, setSelectedDate] = useState(getTodayKST()); // KST 기준 오늘 const [currentDate, setCurrentDate] = useState(new Date()); const [slideDirection, setSlideDirection] = useState(0); // 년월 선택 관련 (Schedule.jsx와 동일한 패턴) const [showYearMonthPicker, setShowYearMonthPicker] = useState(false); const [showCategoryTooltip, setShowCategoryTooltip] = useState(false); const [viewMode, setViewMode] = useState('yearMonth'); // 'yearMonth' | 'months' const pickerRef = useRef(null); const categoryTooltipRef = useRef(null); // 달력 관련 const year = currentDate.getFullYear(); const month = currentDate.getMonth(); const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const days = ['일', '월', '화', '수', '목', '금', '토']; const monthNames = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; // 년도 범위 (현재 년도 기준 10년 단위 - Schedule.jsx와 동일) 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 getDaysInMonth = (y, m) => new Date(y, m + 1, 0).getDate(); // 카테고리 목록 (API에서 로드) const [categories, setCategories] = useState([ { id: 'all', name: '전체', color: 'gray' } ]); // 일정 목록 (API에서 로드) const [schedules, setSchedules] = useState([]); // 카테고리 색상 맵핑 const colorMap = { blue: 'bg-blue-500', green: 'bg-green-500', purple: 'bg-purple-500', red: 'bg-red-500', pink: 'bg-pink-500', yellow: 'bg-yellow-500', orange: 'bg-orange-500', gray: 'bg-gray-500', }; // 색상 스타일 (기본 색상 또는 커스텀 HEX) const getColorStyle = (color) => { if (!color) return { className: 'bg-gray-500' }; if (color.startsWith('#')) { return { style: { backgroundColor: color } }; } return { className: colorMap[color] || 'bg-gray-500' }; }; // 카테고리별 색상 (배지용) const getCategoryColor = (color) => { const colors = { blue: 'bg-blue-100 text-blue-700', green: 'bg-green-100 text-green-700', purple: 'bg-purple-100 text-purple-700', red: 'bg-red-100 text-red-700', pink: 'bg-pink-100 text-pink-700', yellow: 'bg-yellow-100 text-yellow-700', orange: 'bg-orange-100 text-orange-700', gray: 'bg-gray-100 text-gray-700', }; if (color?.startsWith('#')) { return 'bg-gray-100 text-gray-700'; } return colors[color] || 'bg-gray-100 text-gray-700'; }; // 해당 날짜에 일정이 있는지 확인 const hasSchedule = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; return schedules.some(s => { const scheduleDate = new Date(s.date).toISOString().split('T')[0]; return scheduleDate === dateStr; }); }; // 해당 날짜의 첫 번째 일정 카테고리 색상 const getScheduleColor = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const schedule = schedules.find(s => { const scheduleDate = new Date(s.date).toISOString().split('T')[0]; return scheduleDate === dateStr; }); if (!schedule) return null; const cat = categories.find(c => c.id === schedule.category_id); return cat?.color || '#4A7C59'; }; // Toast 자동 숨김 useEffect(() => { if (toast) { const timer = setTimeout(() => setToast(null), 3000); return () => clearTimeout(timer); } }, [toast]); useEffect(() => { const token = localStorage.getItem('adminToken'); const userData = localStorage.getItem('adminUser'); if (!token || !userData) { navigate('/admin'); return; } setUser(JSON.parse(userData)); // 카테고리 로드 fetchCategories(); }, [navigate]); // 월이 변경될 때마다 일정 로드 useEffect(() => { fetchSchedules(); }, [year, month]); // 카테고리 로드 함수 const fetchCategories = async () => { try { const res = await fetch('/api/admin/schedule-categories'); const data = await res.json(); setCategories([ { id: 'all', name: '전체', color: 'gray' }, ...data ]); } catch (error) { console.error('카테고리 로드 오류:', error); } }; // 일정 로드 함수 const fetchSchedules = async () => { setLoading(true); try { const res = await fetch(`/api/admin/schedules?year=${year}&month=${month + 1}`); const data = await res.json(); setSchedules(data); } catch (error) { console.error('일정 로드 오류:', error); } finally { setLoading(false); } }; // 검색 함수 (API 호출) const searchSchedules = async (term) => { if (!term.trim()) { setSearchResults([]); return; } setSearchLoading(true); try { const res = await fetch(`/api/admin/schedules?search=${encodeURIComponent(term)}`); const data = await res.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 (categoryTooltipRef.current && !categoryTooltipRef.current.contains(event.target)) { setShowCategoryTooltip(false); } }; if (showYearMonthPicker || showCategoryTooltip) { document.addEventListener('mousedown', handleClickOutside); } return () => document.removeEventListener('mousedown', handleClickOutside); }, [showYearMonthPicker, showCategoryTooltip]); const handleLogout = () => { localStorage.removeItem('adminToken'); localStorage.removeItem('adminUser'); navigate('/admin'); }; // 월 이동 const prevMonth = () => { setSlideDirection(-1); setCurrentDate(new Date(year, month - 1, 1)); setSelectedDate(null); setSchedules([]); // 이전 달 데이터 즉시 초기화 }; const nextMonth = () => { setSlideDirection(1); setCurrentDate(new Date(year, month + 1, 1)); setSelectedDate(null); setSchedules([]); // 이전 달 데이터 즉시 초기화 }; // 년도 범위 이동 const prevYearRange = () => setCurrentDate(new Date(year - 10, month, 1)); const nextYearRange = () => setCurrentDate(new Date(year + 10, month, 1)); // 년도 선택 시 월 선택 모드로 전환 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 selectDate = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; setSelectedDate(selectedDate === dateStr ? null : dateStr); }; // 전체보기 const showAll = () => { setSelectedDate(null); }; // 삭제 관련 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [scheduleToDelete, setScheduleToDelete] = useState(null); const [deleting, setDeleting] = useState(false); // 삭제 확인 다이얼로그 열기 const openDeleteDialog = (schedule) => { setScheduleToDelete(schedule); setDeleteDialogOpen(true); }; // 일정 삭제 const handleDelete = async () => { if (!scheduleToDelete) return; setDeleting(true); try { const token = localStorage.getItem('adminToken'); const response = await fetch(`/api/admin/schedules/${scheduleToDelete.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}`, }, }); if (response.ok) { setToast({ type: 'success', message: '일정이 삭제되었습니다.' }); fetchSchedules(); // 일정 목록 새로고침 } else { const data = await response.json(); setToast({ type: 'error', message: data.error || '삭제 실패' }); } } catch (error) { console.error('삭제 오류:', error); setToast({ type: 'error', message: '삭제 중 오류가 발생했습니다.' }); } finally { setDeleting(false); setDeleteDialogOpen(false); setScheduleToDelete(null); } }; // 검색어 정규화 (대소문자, 띄어쓰기, 특수문자 무시) const normalizeForSearch = (str) => { return (str || '').toLowerCase().replace(/[\s\-_.,!?#@]/g, ''); }; // 일정 목록 (검색 모드일 때 searchResults, 일반 모드일 때 로컬 필터링) const filteredSchedules = isSearchMode ? (searchTerm ? searchResults : []) // 검색 모드: 검색 전엔 빈 목록, 검색 후엔 API 결과 : schedules.filter(schedule => { // 일반 모드: 로컬 필터링 const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(schedule.category_id); const scheduleDate = new Date(schedule.date).toISOString().split('T')[0]; const matchesDate = !selectedDate || scheduleDate === selectedDate; return matchesCategory && matchesDate; }); // 검색 모드일 때 카테고리별 검색 결과 카운트 계산 const getSearchCategoryCount = (categoryId) => { if (!isSearchMode || !searchTerm) { return schedules.filter(s => s.category_id === categoryId).length; } return searchResults.filter(s => s.category_id === categoryId).length; }; // 검색 모드일 때 전체 일정 수 const getTotalCount = () => { if (!isSearchMode || !searchTerm) { return schedules.length; } return searchResults.length; }; return (
다음 일정을 삭제하시겠습니까?
{scheduleToDelete?.title}
이 작업은 되돌릴 수 없습니다.
fromis_9의 일정을 관리합니다
등록된 일정이 없습니다
{schedule.description}
)} {/* 멤버 태그 */} {schedule.members && schedule.members.length > 0 && (