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)}