diff --git a/frontend/src/pages/mobile/Schedule.jsx b/frontend/src/pages/mobile/Schedule.jsx index 571162b..0c53d8a 100644 --- a/frontend/src/pages/mobile/Schedule.jsx +++ b/frontend/src/pages/mobile/Schedule.jsx @@ -13,6 +13,15 @@ function MobileSchedule() { const [isSearchMode, setIsSearchMode] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [showCalendar, setShowCalendar] = useState(false); + const [calendarViewDate, setCalendarViewDate] = useState(new Date()); // 달력 뷰 날짜 + const [calendarShowYearMonth, setCalendarShowYearMonth] = useState(false); // 달력 년월 선택 모드 + + // 달력 월 변경 함수 + const changeCalendarMonth = (delta) => { + const newDate = new Date(calendarViewDate); + newDate.setMonth(newDate.getMonth() + delta); + setCalendarViewDate(newDate); + }; const SEARCH_LIMIT = 10; const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' }); @@ -53,11 +62,14 @@ function MobileSchedule() { } }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]); - // 일정 및 카테고리 로드 + // 일정 및 카테고리 로드 (월이 변경될 때만 실행) + const viewMonth = `${selectedDate.getFullYear()}-${selectedDate.getMonth()}`; + useEffect(() => { const year = selectedDate.getFullYear(); const month = selectedDate.getMonth() + 1; + setLoading(true); Promise.all([ fetch(`/api/schedules?year=${year}&month=${month}`).then(res => res.json()), fetch('/api/schedules/categories').then(res => res.json()) @@ -66,7 +78,7 @@ function MobileSchedule() { setCategories(categoriesData); setLoading(false); }).catch(console.error); - }, [selectedDate]); + }, [viewMonth]); // 월 변경 const changeMonth = (delta) => { @@ -75,6 +87,23 @@ function MobileSchedule() { setSelectedDate(newDate); }; + // 캘린더가 열릴 때 배경 스크롤 방지 + useEffect(() => { + const preventScroll = (e) => e.preventDefault(); + + if (showCalendar) { + document.body.style.overflow = 'hidden'; + document.addEventListener('touchmove', preventScroll, { passive: false }); + } else { + document.body.style.overflow = ''; + document.removeEventListener('touchmove', preventScroll); + } + return () => { + document.body.style.overflow = ''; + document.removeEventListener('touchmove', preventScroll); + }; + }, [showCalendar]); + // 카테고리 색상 const getCategoryColor = (categoryId) => { const category = categories.find(c => c.id === categoryId); @@ -92,6 +121,63 @@ function MobileSchedule() { return Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0])); }, [schedules]); + // 해당 달의 모든 날짜 배열 + const daysInMonth = useMemo(() => { + const year = selectedDate.getFullYear(); + const month = selectedDate.getMonth(); + const lastDay = new Date(year, month + 1, 0).getDate(); + const days = []; + for (let d = 1; d <= lastDay; d++) { + days.push(new Date(year, month, d)); + } + return days; + }, [selectedDate]); + + // 선택된 날짜의 일정 + const selectedDateSchedules = useMemo(() => { + // KST 기준 날짜 문자열 생성 + const year = selectedDate.getFullYear(); + const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); + const day = String(selectedDate.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + // API 응답의 date는 ISO 형식이므로 T 이전 부분만 비교 + return schedules.filter(s => s.date.split('T')[0] === dateStr); + }, [schedules, selectedDate]); + + // 요일 이름 + const getDayName = (date) => { + return ['일', '월', '화', '수', '목', '금', '토'][date.getDay()]; + }; + + // 오늘 여부 + const isToday = (date) => { + const today = new Date(); + return date.getDate() === today.getDate() && + date.getMonth() === today.getMonth() && + date.getFullYear() === today.getFullYear(); + }; + + // 선택된 날짜 여부 + const isSelected = (date) => { + return date.getDate() === selectedDate.getDate() && + date.getMonth() === selectedDate.getMonth() && + date.getFullYear() === selectedDate.getFullYear(); + }; + + // 날짜 선택 컨테이너 ref + const dateScrollRef = useRef(null); + + // 선택된 날짜로 자동 스크롤 + useEffect(() => { + if (dateScrollRef.current) { + const selectedDay = selectedDate.getDate(); + const buttons = dateScrollRef.current.querySelectorAll('button'); + if (buttons[selectedDay - 1]) { + buttons[selectedDay - 1].scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }); + } + } + }, [selectedDate]); + return (
{/* 헤더 */} @@ -122,55 +208,188 @@ function MobileSchedule() {
) : ( -
-
- - -
- - {selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월 - -
- - -
+
+ {showCalendar ? ( + // 달력 열렸을 때: 년월은 absolute로 가운데 고정, 드롭다운은 바로 옆에 + <> +
+ + +
+ + {/* 년월 텍스트: absolute로 정확히 가운데 고정, 클릭하면 드롭다운 토글 */} + + + {/* 드롭다운 버튼: 년월 텍스트 바로 옆에 위치하도록 가운데 배치 */} + + +
+ + +
+ + ) : ( + // 달력 닫혔을 때: 기존 UI + <> +
+ + +
+ + {selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월 + +
+ + +
+ + )} +
+ )} + + {/* 가로 스크롤 날짜 선택기 */} + {!isSearchMode && ( +
+ {daysInMonth.map((date) => { + const dayOfWeek = date.getDay(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}`; + const hasSchedule = schedules.some(s => s.date.split('T')[0] === dateStr); + + return ( + + ); + })}
)} - - {/* 달력 팝업 */} - - {showCalendar && !isSearchMode && ( - - { - setSelectedDate(date); - setShowCalendar(false); - }} - /> - - )} -
+ {/* 달력 팝업 - fixed로 위에 띄우기 */} + + {showCalendar && !isSearchMode && ( + + { + setSelectedDate(date); + setShowCalendar(false); + }} + /> + + )} + + + {/* 캘린더 배경 오버레이 */} + + {showCalendar && !isSearchMode && ( + setShowCalendar(false)} + className="fixed inset-0 bg-black/40 z-40" + style={{ top: 0 }} + /> + )} + + {/* 컨텐츠 */}
{isSearchMode && searchTerm ? ( @@ -208,50 +427,22 @@ function MobileSchedule() {
- ) : groupedSchedules.length === 0 ? ( + ) : selectedDateSchedules.length === 0 ? (
- 이번 달 일정이 없습니다 + {selectedDate.getMonth() + 1}월 {selectedDate.getDate()}일 일정이 없습니다
) : ( - // 깔끔한 날짜별 일정 -
- {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) => ( - - ))} -
-
- ); - })} + // 선택된 날짜의 일정 +
+ {selectedDateSchedules.map((schedule, index) => ( + + ))}
)}
@@ -395,8 +586,28 @@ function TimelineScheduleCard({ schedule, categoryColor, categories, delay = 0 } } // 달력 선택기 컴포넌트 -function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelectDate }) { - const [viewDate, setViewDate] = useState(new Date(selectedDate)); +function CalendarPicker({ + selectedDate, + schedules = [], + categories = [], + onSelectDate, + hideHeader = false, // 헤더 숨김 여부 + externalViewDate, // 외부에서 제어하는 viewDate + onViewDateChange, // viewDate 변경 콜백 + externalShowYearMonth, // 외부에서 제어하는 년월 선택 모드 + onShowYearMonthChange // 년월 선택 모드 변경 콜백 +}) { + const [internalViewDate, setInternalViewDate] = useState(new Date(selectedDate)); + + // 외부 viewDate가 있으면 사용, 없으면 내부 상태 사용 + const viewDate = externalViewDate || internalViewDate; + const setViewDate = (date) => { + if (onViewDateChange) { + onViewDateChange(date); + } else { + setInternalViewDate(date); + } + }; // 터치 스와이프 핸들링 const touchStartX = useRef(0); @@ -480,8 +691,17 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec date.getFullYear() === today.getFullYear(); }; - // 년월 선택 모드 - const [showYearMonth, setShowYearMonth] = useState(false); + // 년월 선택 모드 - 외부에서 제어 가능 + const [internalShowYearMonth, setInternalShowYearMonth] = useState(false); + const showYearMonth = externalShowYearMonth !== undefined ? externalShowYearMonth : internalShowYearMonth; + const setShowYearMonth = (value) => { + if (onShowYearMonthChange) { + onShowYearMonthChange(value); + } else { + setInternalShowYearMonth(value); + } + }; + const [yearRangeStart, setYearRangeStart] = useState(Math.floor(year / 12) * 12); const yearRange = Array.from({ length: 12 }, (_, i) => yearRangeStart + i); @@ -659,16 +879,6 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec ))}
- - {/* 취소 버튼 */} -
- -
) : ( - {/* 달력 헤더 */} -
- - - -
+ {/* 달력 헤더 - hideHeader일 때 숨김 */} + {!hideHeader && ( +
+ + + +
+ )} {/* 달력 (터치 스와이프 지원) */} {renderMonth(currentMonthDays)}