import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Clock, Tag, Link2, ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; // 모바일 일정 페이지 function MobileSchedule() { const [selectedDate, setSelectedDate] = useState(new Date()); const [schedules, setSchedules] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [isSearchMode, setIsSearchMode] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [showCalendar, setShowCalendar] = useState(false); const SEARCH_LIMIT = 10; const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' }); // 검색 무한 스크롤 const { data: searchData, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading: searchLoading, } = useInfiniteQuery({ queryKey: ['mobileScheduleSearch', searchTerm], queryFn: async ({ pageParam = 0 }) => { const response = await fetch( `/api/schedules?search=${encodeURIComponent(searchTerm)}&offset=${pageParam}&limit=${SEARCH_LIMIT}` ); if (!response.ok) throw new Error('Search failed'); return response.json(); }, 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]); useEffect(() => { if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) { fetchNextPage(); } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]); // 일정 및 카테고리 로드 useEffect(() => { const year = selectedDate.getFullYear(); const month = selectedDate.getMonth() + 1; Promise.all([ fetch(`/api/schedules?year=${year}&month=${month}`).then(res => res.json()), fetch('/api/schedules/categories').then(res => res.json()) ]).then(([schedulesData, categoriesData]) => { setSchedules(schedulesData); setCategories(categoriesData); setLoading(false); }).catch(console.error); }, [selectedDate]); // 월 변경 const changeMonth = (delta) => { const newDate = new Date(selectedDate); newDate.setMonth(newDate.getMonth() + delta); setSelectedDate(newDate); }; // 카테고리 색상 const getCategoryColor = (categoryId) => { const category = categories.find(c => c.id === categoryId); return category?.color || '#6b7280'; }; // 날짜별 일정 그룹화 const groupedSchedules = useMemo(() => { const groups = {}; schedules.forEach(schedule => { const date = schedule.date; if (!groups[date]) groups[date] = []; groups[date].push(schedule); }); return Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0])); }, [schedules]); return (
{/* 헤더 */}
{isSearchMode ? (
setSearchTerm(e.target.value)} className="flex-1 bg-transparent outline-none text-sm" autoFocus /> {searchTerm && ( )}
) : (
{selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월
)} {/* 달력 팝업 */} {showCalendar && !isSearchMode && ( { setSelectedDate(date); setShowCalendar(false); }} /> )}
{/* 컨텐츠 */}
{isSearchMode && searchTerm ? ( // 검색 결과
{searchLoading ? (
) : searchResults.length === 0 ? (
검색 결과가 없습니다
) : ( <> {searchResults.map((schedule, index) => ( ))}
{isFetchingNextPage && (
)}
)}
) : loading ? (
) : groupedSchedules.length === 0 ? (
이번 달 일정이 없습니다
) : ( // 깔끔한 날짜별 일정
{groupedSchedules.map(([date, daySchedules], groupIndex) => { const dateObj = new Date(date); const month = dateObj.getMonth() + 1; const day = dateObj.getDate(); const weekday = ['일', '월', '화', '수', '목', '금', '토'][dateObj.getDay()]; const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6; return (
{/* 날짜 헤더 - 심플 스타일 */}
{day} {weekday}
{/* 일정 카드들 */}
{daySchedules.map((schedule, index) => ( ))}
); })}
)}
); } // 일정 카드 컴포넌트 (검색용) function ScheduleCard({ schedule, categoryColor, categories, delay = 0 }) { const categoryName = categories.find(c => c.id === schedule.category_id)?.name || '미분류'; const memberNames = schedule.member_names || schedule.members?.map(m => m.name).join(',') || ''; const memberList = memberNames.split(',').filter(name => name.trim()); return (

{schedule.title}

{schedule.time && ( {schedule.time.slice(0, 5)} )} {categoryName} {schedule.source_name && ( {schedule.source_name} )}
{memberList.length > 0 && (
{memberList.length >= 5 ? ( 프로미스나인 ) : ( memberList.map((name, i) => ( {name.trim()} )) )}
)}
); } // 타임라인용 일정 카드 컴포넌트 - 모던 디자인 function TimelineScheduleCard({ schedule, categoryColor, categories, delay = 0 }) { const categoryName = categories.find(c => c.id === schedule.category_id)?.name || '미분류'; const memberNames = schedule.member_names || schedule.members?.map(m => m.name).join(',') || ''; const memberList = memberNames.split(',').filter(name => name.trim()); return ( {/* 카드 본체 */}
{/* 시간 뱃지 */} {schedule.time && (
{schedule.time.slice(0, 5)}
{categoryName}
)} {/* 제목 */}

{schedule.title}

{/* 출처 */} {schedule.source_name && (
{schedule.source_name}
)} {/* 멤버 */} {memberList.length > 0 && (
{memberList.length >= 5 ? ( 프로미스나인 ) : ( memberList.map((name, i) => ( {name.trim()} )) )}
)}
); } // 달력 선택기 컴포넌트 function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelectDate }) { const [viewDate, setViewDate] = useState(new Date(selectedDate)); // 터치 스와이프 핸들링 const touchStartX = useRef(0); const touchEndX = useRef(0); // 날짜별 일정 존재 여부 및 카테고리 색상 const scheduleDates = useMemo(() => { const dateMap = {}; schedules.forEach(schedule => { const date = schedule.date; if (!dateMap[date]) { dateMap[date] = []; } const category = categories.find(c => c.id === schedule.category_id); dateMap[date].push(category?.color || '#6b7280'); }); return dateMap; }, [schedules, categories]); const getScheduleColors = (date) => { const dateStr = date.toISOString().split('T')[0]; const colors = scheduleDates[dateStr] || []; // 최대 3개까지만 표시 return [...new Set(colors)].slice(0, 3); }; const year = viewDate.getFullYear(); const month = viewDate.getMonth(); // 달력 데이터 생성 함수 const getCalendarDays = useCallback((y, m) => { const firstDay = new Date(y, m, 1); const lastDay = new Date(y, m + 1, 0); const startDay = firstDay.getDay(); const daysInMonth = lastDay.getDate(); const days = []; // 이전 달 날짜 const prevMonth = new Date(y, m, 0); for (let i = startDay - 1; i >= 0; i--) { days.push({ day: prevMonth.getDate() - i, isCurrentMonth: false, date: new Date(y, m - 1, prevMonth.getDate() - i) }); } // 현재 달 날짜 for (let i = 1; i <= daysInMonth; i++) { days.push({ day: i, isCurrentMonth: true, date: new Date(y, m, i) }); } // 다음 달 날짜 (현재 줄만 채우기) const remaining = (7 - (days.length % 7)) % 7; for (let i = 1; i <= remaining; i++) { days.push({ day: i, isCurrentMonth: false, date: new Date(y, m + 1, i) }); } return days; }, []); const changeMonth = useCallback((delta) => { const newDate = new Date(viewDate); newDate.setMonth(newDate.getMonth() + delta); setViewDate(newDate); }, [viewDate]); const isToday = (date) => { const today = new Date(); return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear(); }; // 년월 선택 모드 const [showYearMonth, setShowYearMonth] = useState(false); const [yearRangeStart, setYearRangeStart] = useState(Math.floor(year / 12) * 12); const yearRange = Array.from({ length: 12 }, (_, i) => yearRangeStart + i); // 배경 스크롤 막기 useEffect(() => { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, []); // 현재 달 캘린더 데이터 const currentMonthDays = useMemo(() => { return getCalendarDays(year, month); }, [year, month, getCalendarDays]); // 터치 핸들러 const handleTouchStart = (e) => { touchStartX.current = e.touches[0].clientX; }; const handleTouchMove = (e) => { touchEndX.current = e.touches[0].clientX; }; const handleTouchEnd = () => { const diff = touchStartX.current - touchEndX.current; const threshold = 50; if (Math.abs(diff) > threshold) { if (diff > 0) { changeMonth(1); } else { changeMonth(-1); } } touchStartX.current = 0; touchEndX.current = 0; }; // 월 렌더링 컴포넌트 const renderMonth = (days) => (
{/* 요일 헤더 */}
{['일', '월', '화', '수', '목', '금', '토'].map((day, i) => (
{day}
))}
{/* 날짜 그리드 */}
{days.map((item, index) => { const dayOfWeek = index % 7; const isSunday = dayOfWeek === 0; const isSaturday = dayOfWeek === 6; const scheduleColors = item.isCurrentMonth ? getScheduleColors(item.date) : []; return ( ); })}
); return (
{showYearMonth ? ( // 년월 선택 UI {/* 년도 범위 헤더 */}
{yearRangeStart} - {yearRangeStart + 11}
{/* 년도 선택 */}
년도
{yearRange.map(y => ( ))}
{/* 월 선택 */}
{Array.from({ length: 12 }, (_, i) => i + 1).map(m => ( ))}
{/* 취소 버튼 */}
) : ( {/* 달력 헤더 */}
{/* 달력 (터치 스와이프 지원) */} {renderMonth(currentMonthDays)} {/* 오늘 버튼 */}
)}
); } export default MobileSchedule;