모바일 일정: 달력 헤더 툴바 통합, 년월 드롭다운 개선
This commit is contained in:
parent
767cbcaf5f
commit
f2b0170cf8
1 changed files with 336 additions and 124 deletions
|
|
@ -13,6 +13,15 @@ function MobileSchedule() {
|
||||||
const [isSearchMode, setIsSearchMode] = useState(false);
|
const [isSearchMode, setIsSearchMode] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [showCalendar, setShowCalendar] = useState(false);
|
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 SEARCH_LIMIT = 10;
|
||||||
const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' });
|
const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' });
|
||||||
|
|
@ -53,11 +62,14 @@ function MobileSchedule() {
|
||||||
}
|
}
|
||||||
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]);
|
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]);
|
||||||
|
|
||||||
// 일정 및 카테고리 로드
|
// 일정 및 카테고리 로드 (월이 변경될 때만 실행)
|
||||||
|
const viewMonth = `${selectedDate.getFullYear()}-${selectedDate.getMonth()}`;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const year = selectedDate.getFullYear();
|
const year = selectedDate.getFullYear();
|
||||||
const month = selectedDate.getMonth() + 1;
|
const month = selectedDate.getMonth() + 1;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
fetch(`/api/schedules?year=${year}&month=${month}`).then(res => res.json()),
|
fetch(`/api/schedules?year=${year}&month=${month}`).then(res => res.json()),
|
||||||
fetch('/api/schedules/categories').then(res => res.json())
|
fetch('/api/schedules/categories').then(res => res.json())
|
||||||
|
|
@ -66,7 +78,7 @@ function MobileSchedule() {
|
||||||
setCategories(categoriesData);
|
setCategories(categoriesData);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}, [selectedDate]);
|
}, [viewMonth]);
|
||||||
|
|
||||||
// 월 변경
|
// 월 변경
|
||||||
const changeMonth = (delta) => {
|
const changeMonth = (delta) => {
|
||||||
|
|
@ -75,6 +87,23 @@ function MobileSchedule() {
|
||||||
setSelectedDate(newDate);
|
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 getCategoryColor = (categoryId) => {
|
||||||
const category = categories.find(c => c.id === 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]));
|
return Object.entries(groups).sort((a, b) => a[0].localeCompare(b[0]));
|
||||||
}, [schedules]);
|
}, [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 (
|
return (
|
||||||
<div className="pb-4">
|
<div className="pb-4">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
|
|
@ -122,55 +208,188 @@ function MobileSchedule() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-between px-4 py-3">
|
<div className="relative flex items-center justify-between px-4 py-3">
|
||||||
<div className="flex items-center gap-1">
|
{showCalendar ? (
|
||||||
<button
|
// 달력 열렸을 때: 년월은 absolute로 가운데 고정, 드롭다운은 바로 옆에
|
||||||
onClick={() => setShowCalendar(!showCalendar)}
|
<>
|
||||||
className="p-2 rounded-lg hover:bg-gray-100"
|
<div className="flex items-center gap-1">
|
||||||
>
|
<button
|
||||||
<Calendar size={20} className="text-primary" />
|
onClick={() => {
|
||||||
</button>
|
setShowCalendar(false);
|
||||||
<button onClick={() => changeMonth(-1)} className="p-2">
|
setCalendarShowYearMonth(false);
|
||||||
<ChevronLeft size={20} />
|
}}
|
||||||
</button>
|
className="p-2 rounded-lg hover:bg-gray-100"
|
||||||
</div>
|
>
|
||||||
<span className="font-bold">
|
<Calendar size={20} className="text-primary" />
|
||||||
{selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월
|
</button>
|
||||||
</span>
|
<button onClick={() => changeCalendarMonth(-1)} className="p-2">
|
||||||
<div className="flex items-center gap-1">
|
<ChevronLeft size={20} />
|
||||||
<button onClick={() => changeMonth(1)} className="p-2">
|
</button>
|
||||||
<ChevronRight size={20} />
|
</div>
|
||||||
</button>
|
|
||||||
<button onClick={() => setIsSearchMode(true)} className="p-2">
|
{/* 년월 텍스트: absolute로 정확히 가운데 고정, 클릭하면 드롭다운 토글 */}
|
||||||
<Search size={20} />
|
<button
|
||||||
</button>
|
onClick={() => setCalendarShowYearMonth(!calendarShowYearMonth)}
|
||||||
</div>
|
className={`absolute left-1/2 -translate-x-1/2 font-bold transition-colors ${
|
||||||
|
calendarShowYearMonth ? 'text-primary' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{calendarViewDate.getFullYear()}년 {calendarViewDate.getMonth() + 1}월
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 드롭다운 버튼: 년월 텍스트 바로 옆에 위치하도록 가운데 배치 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setCalendarShowYearMonth(!calendarShowYearMonth)}
|
||||||
|
className={`absolute transition-colors ${
|
||||||
|
calendarShowYearMonth ? 'text-primary' : ''
|
||||||
|
}`}
|
||||||
|
style={{ left: 'calc(50% + 52px)' }}
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={`transition-transform duration-200 ${calendarShowYearMonth ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button onClick={() => changeCalendarMonth(1)} className="p-2">
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setIsSearchMode(true)} className="p-2">
|
||||||
|
<Search size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// 달력 닫혔을 때: 기존 UI
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCalendarViewDate(selectedDate);
|
||||||
|
setShowCalendar(true);
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-lg hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
<Calendar size={20} className="text-gray-600" />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => changeMonth(-1)} className="p-2">
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span className="font-bold">
|
||||||
|
{selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button onClick={() => changeMonth(1)} className="p-2">
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setIsSearchMode(true)} className="p-2">
|
||||||
|
<Search size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 달력 팝업 */}
|
{/* 가로 스크롤 날짜 선택기 */}
|
||||||
<AnimatePresence>
|
{!isSearchMode && (
|
||||||
{showCalendar && !isSearchMode && (
|
<div
|
||||||
<motion.div
|
ref={dateScrollRef}
|
||||||
initial={{ height: 0, opacity: 0 }}
|
className="flex overflow-x-auto scrollbar-hide px-2 py-2 gap-1"
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||||
exit={{ height: 0, opacity: 0 }}
|
>
|
||||||
className="overflow-hidden border-t bg-white"
|
{daysInMonth.map((date) => {
|
||||||
>
|
const dayOfWeek = date.getDay();
|
||||||
<CalendarPicker
|
const year = date.getFullYear();
|
||||||
selectedDate={selectedDate}
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
schedules={schedules}
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
categories={categories}
|
const dateStr = `${year}-${month}-${day}`;
|
||||||
onSelectDate={(date) => {
|
const hasSchedule = schedules.some(s => s.date.split('T')[0] === dateStr);
|
||||||
setSelectedDate(date);
|
|
||||||
setShowCalendar(false);
|
return (
|
||||||
}}
|
<button
|
||||||
/>
|
key={date.getDate()}
|
||||||
</motion.div>
|
onClick={() => setSelectedDate(date)}
|
||||||
)}
|
className={`flex flex-col items-center min-w-[44px] py-2 px-1 rounded-xl transition-all ${
|
||||||
</AnimatePresence>
|
isSelected(date)
|
||||||
|
? 'bg-primary text-white'
|
||||||
|
: 'hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`text-[10px] font-medium ${
|
||||||
|
isSelected(date)
|
||||||
|
? 'text-white/80'
|
||||||
|
: dayOfWeek === 0
|
||||||
|
? 'text-red-400'
|
||||||
|
: dayOfWeek === 6
|
||||||
|
? 'text-blue-400'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}>
|
||||||
|
{getDayName(date)}
|
||||||
|
</span>
|
||||||
|
<span className={`text-sm font-semibold mt-0.5 ${
|
||||||
|
isSelected(date)
|
||||||
|
? 'text-white'
|
||||||
|
: isToday(date)
|
||||||
|
? 'text-primary'
|
||||||
|
: 'text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{date.getDate()}
|
||||||
|
</span>
|
||||||
|
{hasSchedule && !isSelected(date) && (
|
||||||
|
<div className="w-1 h-1 rounded-full bg-primary mt-1" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 달력 팝업 - fixed로 위에 띄우기 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showCalendar && !isSearchMode && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: 'auto', opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
className="fixed left-0 right-0 bg-white shadow-lg z-50 border-b overflow-hidden"
|
||||||
|
style={{ top: '56px' }}
|
||||||
|
>
|
||||||
|
<CalendarPicker
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
schedules={schedules}
|
||||||
|
categories={categories}
|
||||||
|
hideHeader={true}
|
||||||
|
externalViewDate={calendarViewDate}
|
||||||
|
onViewDateChange={setCalendarViewDate}
|
||||||
|
externalShowYearMonth={calendarShowYearMonth}
|
||||||
|
onShowYearMonthChange={setCalendarShowYearMonth}
|
||||||
|
onSelectDate={(date) => {
|
||||||
|
setSelectedDate(date);
|
||||||
|
setShowCalendar(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 캘린더 배경 오버레이 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showCalendar && !isSearchMode && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setShowCalendar(false)}
|
||||||
|
className="fixed inset-0 bg-black/40 z-40"
|
||||||
|
style={{ top: 0 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* 컨텐츠 */}
|
{/* 컨텐츠 */}
|
||||||
<div className="px-4 py-4">
|
<div className="px-4 py-4">
|
||||||
{isSearchMode && searchTerm ? (
|
{isSearchMode && searchTerm ? (
|
||||||
|
|
@ -208,50 +427,22 @@ function MobileSchedule() {
|
||||||
<div className="flex justify-center py-8">
|
<div className="flex justify-center py-8">
|
||||||
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : groupedSchedules.length === 0 ? (
|
) : selectedDateSchedules.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-400">
|
<div className="text-center py-8 text-gray-400">
|
||||||
이번 달 일정이 없습니다
|
{selectedDate.getMonth() + 1}월 {selectedDate.getDate()}일 일정이 없습니다
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 깔끔한 날짜별 일정
|
// 선택된 날짜의 일정
|
||||||
<div className="space-y-6">
|
<div className="space-y-3">
|
||||||
{groupedSchedules.map(([date, daySchedules], groupIndex) => {
|
{selectedDateSchedules.map((schedule, index) => (
|
||||||
const dateObj = new Date(date);
|
<TimelineScheduleCard
|
||||||
const month = dateObj.getMonth() + 1;
|
key={schedule.id}
|
||||||
const day = dateObj.getDate();
|
schedule={schedule}
|
||||||
const weekday = ['일', '월', '화', '수', '목', '금', '토'][dateObj.getDay()];
|
categoryColor={getCategoryColor(schedule.category_id)}
|
||||||
const isWeekend = dateObj.getDay() === 0 || dateObj.getDay() === 6;
|
categories={categories}
|
||||||
|
delay={index * 0.05}
|
||||||
return (
|
/>
|
||||||
<div key={date}>
|
))}
|
||||||
{/* 날짜 헤더 - 심플 스타일 */}
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
|
||||||
<div className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl font-bold ${
|
|
||||||
isWeekend
|
|
||||||
? 'bg-red-50 text-red-500'
|
|
||||||
: 'bg-primary/10 text-primary'
|
|
||||||
}`}>
|
|
||||||
<span className="text-lg">{day}</span>
|
|
||||||
<span className="text-xs opacity-70">{weekday}</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-px flex-1 bg-gray-200" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 일정 카드들 */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{daySchedules.map((schedule, index) => (
|
|
||||||
<TimelineScheduleCard
|
|
||||||
key={schedule.id}
|
|
||||||
schedule={schedule}
|
|
||||||
categoryColor={getCategoryColor(schedule.category_id)}
|
|
||||||
categories={categories}
|
|
||||||
delay={groupIndex * 0.05 + index * 0.02}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -395,8 +586,28 @@ function TimelineScheduleCard({ schedule, categoryColor, categories, delay = 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 달력 선택기 컴포넌트
|
// 달력 선택기 컴포넌트
|
||||||
function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelectDate }) {
|
function CalendarPicker({
|
||||||
const [viewDate, setViewDate] = useState(new Date(selectedDate));
|
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);
|
const touchStartX = useRef(0);
|
||||||
|
|
@ -480,8 +691,17 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec
|
||||||
date.getFullYear() === today.getFullYear();
|
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 [yearRangeStart, setYearRangeStart] = useState(Math.floor(year / 12) * 12);
|
||||||
const yearRange = Array.from({ length: 12 }, (_, i) => yearRangeStart + i);
|
const yearRange = Array.from({ length: 12 }, (_, i) => yearRangeStart + i);
|
||||||
|
|
||||||
|
|
@ -659,16 +879,6 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 취소 버튼 */}
|
|
||||||
<div className="mt-4 flex justify-center">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowYearMonth(false)}
|
|
||||||
className="text-xs text-gray-500 px-4 py-1.5"
|
|
||||||
>
|
|
||||||
취소
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -678,28 +888,30 @@ function CalendarPicker({ selectedDate, schedules = [], categories = [], onSelec
|
||||||
exit={{ opacity: 0, y: 10 }}
|
exit={{ opacity: 0, y: 10 }}
|
||||||
transition={{ duration: 0.15 }}
|
transition={{ duration: 0.15 }}
|
||||||
>
|
>
|
||||||
{/* 달력 헤더 */}
|
{/* 달력 헤더 - hideHeader일 때 숨김 */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
{!hideHeader && (
|
||||||
<button
|
<div className="flex items-center justify-between mb-4">
|
||||||
onClick={() => changeMonth(-1)}
|
<button
|
||||||
className="p-1"
|
onClick={() => changeMonth(-1)}
|
||||||
>
|
className="p-1"
|
||||||
<ChevronLeft size={18} />
|
>
|
||||||
</button>
|
<ChevronLeft size={18} />
|
||||||
<button
|
</button>
|
||||||
onClick={() => setShowYearMonth(true)}
|
<button
|
||||||
className="flex items-center gap-1 font-semibold text-sm hover:text-primary transition-colors"
|
onClick={() => setShowYearMonth(true)}
|
||||||
>
|
className="flex items-center gap-1 font-semibold text-sm hover:text-primary transition-colors"
|
||||||
{year}년 {month + 1}월
|
>
|
||||||
<ChevronDown size={16} />
|
{year}년 {month + 1}월
|
||||||
</button>
|
<ChevronDown size={16} />
|
||||||
<button
|
</button>
|
||||||
onClick={() => changeMonth(1)}
|
<button
|
||||||
className="p-1"
|
onClick={() => changeMonth(1)}
|
||||||
>
|
className="p-1"
|
||||||
<ChevronRight size={18} />
|
>
|
||||||
</button>
|
<ChevronRight size={18} />
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 달력 (터치 스와이프 지원) */}
|
{/* 달력 (터치 스와이프 지원) */}
|
||||||
{renderMonth(currentMonthDays)}
|
{renderMonth(currentMonthDays)}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue