import { useState, useEffect, useRef, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar as CalendarIcon, List } from 'lucide-react'; import { useQuery, useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; import { MobileScheduleListCard, MobileScheduleSearchCard, MobileBirthdayCard, fireBirthdayConfetti, } from '@/components/schedule'; import { getSchedules, searchSchedules } from '@/api/schedules'; import { useScheduleStore } from '@/stores'; import { getTodayKST, dayjs, getCategoryInfo } from '@/utils'; const WEEKDAYS = ['일', '월', '화', '수', '목', '금', '토']; const MONTHS = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월']; const SEARCH_LIMIT = 20; const MIN_YEAR = 2017; /** * Mobile 스케줄 페이지 */ function MobileSchedule() { const navigate = useNavigate(); const scrollContainerRef = useRef(null); // 상태 관리 (zustand store) const { currentDate, setCurrentDate, selectedDate: storedSelectedDate, setSelectedDate: setStoredSelectedDate, selectedCategories, setSelectedCategories, isSearchMode, setIsSearchMode, searchInput, setSearchInput, searchTerm, setSearchTerm, } = useScheduleStore(); const selectedDate = storedSelectedDate === undefined ? getTodayKST() : storedSelectedDate; const setSelectedDate = setStoredSelectedDate; // 로컬 상태 const [viewMode, setViewMode] = useState('calendar'); // 'calendar' | 'list' const [showMonthPicker, setShowMonthPicker] = useState(false); const year = currentDate.getFullYear(); const month = currentDate.getMonth(); // 월별 일정 데이터 const { data: schedules = [], isLoading: loading } = useQuery({ queryKey: ['schedules', year, month + 1], queryFn: () => getSchedules(year, month + 1), }); // 검색 무한 스크롤 const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' }); const { data: searchData, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['scheduleSearch', searchTerm], queryFn: async ({ pageParam = 0 }) => { return searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT }); }, getNextPageParam: (lastPage) => { if (lastPage.hasMore) { return lastPage.offset + lastPage.schedules.length; } return undefined; }, enabled: !!searchTerm && isSearchMode, }); const searchResults = useMemo(() => { if (!searchData?.pages) return []; return searchData.pages.flatMap((page) => page.schedules); }, [searchData]); // 무한 스크롤 트리거 const prevInViewRef = useRef(false); useEffect(() => { if (inView && !prevInViewRef.current && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) { fetchNextPage(); } prevInViewRef.current = inView; }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]); // 오늘 생일 폭죽 useEffect(() => { if (loading || schedules.length === 0) return; const today = getTodayKST(); const confettiKey = `birthday-confetti-${today}`; if (localStorage.getItem(confettiKey)) return; const hasBirthdayToday = schedules.some((s) => s.is_birthday && s.date === today); if (hasBirthdayToday) { const timer = setTimeout(() => { fireBirthdayConfetti(); localStorage.setItem(confettiKey, 'true'); }, 500); return () => clearTimeout(timer); } }, [schedules, loading]); // 달력 계산 const getDaysInMonth = (y, m) => new Date(y, m + 1, 0).getDate(); const getFirstDayOfMonth = (y, m) => new Date(y, m, 1).getDay(); const daysInMonth = getDaysInMonth(year, month); const firstDay = getFirstDayOfMonth(year, month); // 일정 날짜별 맵 const scheduleDateMap = useMemo(() => { const map = new Map(); schedules.forEach((s) => { const dateStr = s.date; if (!map.has(dateStr)) { map.set(dateStr, []); } map.get(dateStr).push(s); }); return map; }, [schedules]); // 카테고리 추출 const categories = useMemo(() => { const categoryMap = new Map(); schedules.forEach((s) => { if (s.category_id && !categoryMap.has(s.category_id)) { categoryMap.set(s.category_id, { id: s.category_id, name: s.category_name, color: s.category_color, }); } }); return Array.from(categoryMap.values()); }, [schedules]); // 필터링된 스케줄 const filteredSchedules = useMemo(() => { if (isSearchMode && searchTerm) { if (selectedCategories.length === 0) return searchResults; return searchResults.filter((s) => selectedCategories.includes(s.category_id)); } return schedules .filter((s) => { const matchesDate = selectedDate ? s.date === selectedDate : true; const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(s.category_id); return matchesDate && matchesCategory; }) .sort((a, b) => { // 생일 우선 if (a.is_birthday && !b.is_birthday) return -1; if (!a.is_birthday && b.is_birthday) return 1; // 시간순 return (a.time || '00:00:00').localeCompare(b.time || '00:00:00'); }); }, [schedules, selectedDate, selectedCategories, isSearchMode, searchTerm, searchResults]); // 날짜별 그룹화 (리스트 모드용) const groupedSchedules = useMemo(() => { if (isSearchMode && searchTerm) { const groups = new Map(); searchResults.forEach((s) => { if (!groups.has(s.date)) { groups.set(s.date, []); } groups.get(s.date).push(s); }); return Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0])); } const groups = new Map(); schedules.forEach((s) => { if (selectedCategories.length > 0 && !selectedCategories.includes(s.category_id)) return; if (!groups.has(s.date)) { groups.set(s.date, []); } groups.get(s.date).push(s); }); return Array.from(groups.entries()).sort((a, b) => a[0].localeCompare(b[0])); }, [schedules, selectedCategories, isSearchMode, searchTerm, searchResults]); // 월 이동 const canGoPrevMonth = !(year === MIN_YEAR && month === 0); const prevMonth = () => { if (!canGoPrevMonth) return; const newDate = new Date(year, month - 1, 1); setCurrentDate(newDate); }; const nextMonth = () => { const newDate = new Date(year, month + 1, 1); setCurrentDate(newDate); }; // 날짜 선택 const selectDate = (day) => { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; setSelectedDate(dateStr); }; // 일정 클릭 const handleScheduleClick = (schedule) => { if (schedule.is_birthday) { const scheduleYear = new Date(schedule.date).getFullYear(); navigate(`/birthday/${encodeURIComponent(schedule.member_names)}/${scheduleYear}`); return; } if ([2, 3, 6].includes(schedule.category_id)) { navigate(`/schedule/${schedule.id}`); return; } if (!schedule.description && schedule.source?.url) { window.open(schedule.source.url, '_blank'); } else { navigate(`/schedule/${schedule.id}`); } }; // 검색 모드 종료 const exitSearchMode = () => { setIsSearchMode(false); setSearchInput(''); setSearchTerm(''); }; return (