feat(schedule): 카테고리 필터 개선 및 달력 오늘 버튼 추가

- 카테고리 카운트를 선택 날짜와 무관하게 해당 달 전체 기준으로 변경
- 카테고리 섹션이 길어지면 카드 내부 스크롤 (평소엔 콘텐츠 크기 유지)
- 달력 하단에 오늘 날짜로 이동하는 버튼 추가
- 달력 하단 여백 24px → 20px

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-05-20 22:47:13 +09:00
parent 51e8f17cc1
commit 9e3bff9762
3 changed files with 40 additions and 20 deletions

View file

@ -1,6 +1,6 @@
import { useState, useRef, useEffect, useMemo } from 'react'; import { useState, useRef, useEffect, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react'; import { ChevronLeft, ChevronRight, ChevronDown, CalendarDays } from 'lucide-react';
import { getTodayKST, dayjs } from '@/utils'; import { getTodayKST, dayjs } from '@/utils';
import { MIN_YEAR, WEEKDAYS, MONTH_NAMES } from '@/constants'; import { MIN_YEAR, WEEKDAYS, MONTH_NAMES } from '@/constants';
@ -93,6 +93,17 @@ function Calendar({
} }
}; };
//
const goToToday = () => {
const today = new Date();
const isFuture =
today.getFullYear() > year ||
(today.getFullYear() === year && today.getMonth() > month);
setSlideDirection(isFuture ? 1 : -1);
onDateChange(today);
onSelectDate(getTodayKST());
};
const selectYear = (newYear) => { const selectYear = (newYear) => {
onDateChange(new Date(newYear, month, 1)); onDateChange(new Date(newYear, month, 1));
}; };
@ -133,7 +144,7 @@ function Calendar({
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className={disabled ? 'pointer-events-none' : ''} className={disabled ? 'pointer-events-none' : ''}
> >
<div className="bg-white rounded-2xl shadow-sm pt-8 px-8 pb-6 relative" ref={pickerRef}> <div className="bg-white rounded-2xl shadow-sm pt-8 px-8 pb-5 relative" ref={pickerRef}>
{/* 헤더 */} {/* 헤더 */}
<div className="flex items-center justify-between mb-8"> <div className="flex items-center justify-between mb-8">
<button <button
@ -318,12 +329,20 @@ function Calendar({
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
{/* 범례 */} {/* 범례 + 오늘 버튼 */}
<div className="mt-6 pt-4 border-t border-gray-100 flex items-center text-sm"> <div className="mt-6 pt-4 border-t border-gray-100 flex items-center justify-between text-sm">
<div className="flex items-center gap-1.5 text-gray-500"> <div className="flex items-center gap-1.5 text-gray-500">
<span className="w-2 h-2 rounded-full bg-primary flex-shrink-0" /> <span className="w-2 h-2 rounded-full bg-primary flex-shrink-0" />
<span className="leading-none">일정 있음</span> <span className="leading-none">일정 있음</span>
</div> </div>
<button
aria-label="오늘 날짜로 이동"
onClick={goToToday}
className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-primary font-medium hover:bg-primary/10 transition-colors"
>
<CalendarDays size={14} aria-hidden="true" />
오늘
</button>
</div> </div>
</div> </div>
</motion.div> </motion.div>

View file

@ -39,10 +39,10 @@ function CategoryFilter({
<motion.div <motion.div
animate={{ opacity: disabled ? 0.4 : 1 }} animate={{ opacity: disabled ? 0.4 : 1 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
className={`bg-white rounded-2xl shadow-sm p-6 ${disabled ? 'pointer-events-none' : ''}`} className={`bg-white rounded-2xl shadow-sm p-6 flex flex-col min-h-0 ${disabled ? 'pointer-events-none' : ''}`}
> >
<h3 className="font-bold text-gray-900 mb-4">카테고리</h3> <h3 className="font-bold text-gray-900 mb-4 flex-shrink-0">카테고리</h3>
<div className="space-y-1"> <div className="space-y-1 flex-1 min-h-0 overflow-y-auto -mr-3 pr-3">
{/* 전체 */} {/* 전체 */}
<button <button
onClick={onClear} onClick={onClear}

View file

@ -206,14 +206,13 @@ function PCSchedule() {
return Array.from(categoryMap.values()); return Array.from(categoryMap.values());
}, [schedules]); }, [schedules]);
// // ( )
const categoryCounts = useMemo(() => { const categoryCounts = useMemo(() => {
const source = isSearchMode && searchTerm ? searchResults : schedules; const source = isSearchMode && searchTerm ? searchResults : schedules;
const counts = new Map(); const counts = new Map();
let total = 0; let total = 0;
source.forEach((s) => { source.forEach((s) => {
if (!(isSearchMode && searchTerm) && selectedDate && s.date !== selectedDate) return;
const catId = s.category_id; const catId = s.category_id;
if (catId) { if (catId) {
counts.set(catId, (counts.get(catId) || 0) + 1); counts.set(catId, (counts.get(catId) || 0) + 1);
@ -222,7 +221,7 @@ function PCSchedule() {
}); });
counts.set('total', total); counts.set('total', total);
return counts; return counts;
}, [schedules, searchResults, isSearchMode, searchTerm, selectedDate]); }, [schedules, searchResults, isSearchMode, searchTerm]);
// / // /
const getCategoryColor = useCallback( const getCategoryColor = useCallback(
@ -345,17 +344,19 @@ function PCSchedule() {
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, duration: 0.4 }} transition={{ delay: 0.3, duration: 0.4 }}
className="space-y-6" className="flex flex-col gap-6 min-h-0"
> >
<Calendar <div className="flex-shrink-0">
currentDate={currentDate} <Calendar
onDateChange={setCurrentDate} currentDate={currentDate}
selectedDate={selectedDate} onDateChange={setCurrentDate}
onSelectDate={setSelectedDate} selectedDate={selectedDate}
schedules={calendarSchedules} onSelectDate={setSelectedDate}
getCategoryColor={getCategoryColor} schedules={calendarSchedules}
disabled={isSearchMode} getCategoryColor={getCategoryColor}
/> disabled={isSearchMode}
/>
</div>
<CategoryFilter <CategoryFilter
categories={categories} categories={categories}
selectedCategories={selectedCategories} selectedCategories={selectedCategories}