feat(mobile-schedule): 카테고리 필터 추가 + 리스트 카드 플랫화

- 일정 리스트 위에 카테고리 필터 칩 추가 (해당 달 전체 카테고리)
- 스크롤 방향 감지 자동 숨김 (내리면 숨고 올리면 보임), 상단 여백 일관
- 카테고리 선택 시 일정 목록 + 날짜 점 필터링 (공개 PC와 store 공유)
- 일정 리스트 카드: 그림자 제거, 1.5px 테두리로 플랫하게

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-06-01 14:10:16 +09:00
parent 381461c25e
commit 9b2e4e190d
2 changed files with 96 additions and 7 deletions

View file

@ -27,8 +27,8 @@ const ScheduleListCard = memo(function ScheduleListCard({
onClick={onClick} onClick={onClick}
className={`cursor-pointer ${className}`} className={`cursor-pointer ${className}`}
> >
{/* 카드 본체 */} {/* 카드 본체 (플랫 테두리) */}
<div className="relative bg-white rounded-md shadow-[0_2px_12px_rgba(0,0,0,0.06)] border border-gray-100/50 overflow-hidden active:bg-gray-50 transition-colors"> <div className="relative bg-white rounded-xl border-[1.5px] border-gray-100 overflow-hidden active:bg-gray-50 transition-colors">
<div className="p-4"> <div className="p-4">
{/* 시간 및 카테고리 뱃지 */} {/* 시간 및 카테고리 뱃지 */}
<div className="flex items-center gap-1.5 mb-2"> <div className="flex items-center gap-1.5 mb-2">

View file

@ -30,6 +30,9 @@ function MobileSchedule() {
const { const {
selectedDate: storedSelectedDate, selectedDate: storedSelectedDate,
setSelectedDate: setStoredSelectedDate, setSelectedDate: setStoredSelectedDate,
selectedCategories,
setSelectedCategories,
toggleCategory,
} = useScheduleStore(); } = useScheduleStore();
// (store ) // (store )
@ -343,15 +346,58 @@ function MobileSchedule() {
return days; return days;
}, [selectedDate]); }, [selectedDate]);
// ( ) // ( )
const selectedDateSchedules = useMemo(() => { const selectedDateSchedules = useMemo(() => {
const year = selectedDate.getFullYear(); const year = selectedDate.getFullYear();
const month = String(selectedDate.getMonth() + 1).padStart(2, '0'); const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
const day = String(selectedDate.getDate()).padStart(2, '0'); const day = String(selectedDate.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`; const dateStr = `${year}-${month}-${day}`;
// ( ) // ( )
return schedules.filter((s) => s.date.split('T')[0] === dateStr); return schedules.filter((s) => {
}, [schedules, selectedDate]); if (s.date.split('T')[0] !== dateStr) return false;
return selectedCategories.length === 0 || selectedCategories.includes(s.category_id);
});
}, [schedules, selectedDate, selectedCategories]);
// ( , calendarSchedules )
const monthCategories = useMemo(() => {
const map = new Map();
schedules.forEach((s) => {
if (!s.category_id) return;
const existing = map.get(s.category_id);
if (existing) existing.count += 1;
else map.set(s.category_id, { id: s.category_id, name: s.category_name, color: s.category_color, count: 1 });
});
return Array.from(map.values()).sort((a, b) => {
if (a.name === '기타') return 1;
if (b.name === '기타') return -1;
return b.count - a.count;
});
}, [schedules]);
// ( , )
const dotSchedules = useMemo(() => {
if (selectedCategories.length === 0) return schedules;
return schedules.filter((s) => selectedCategories.includes(s.category_id));
}, [schedules, selectedCategories]);
// ( : , )
const [chipsVisible, setChipsVisible] = useState(true);
const lastScrollYRef = useRef(0);
useEffect(() => {
const container = document.querySelector('.mobile-content');
if (!container) return;
const onScroll = () => {
const y = container.scrollTop;
const last = lastScrollYRef.current;
if (y < 16) setChipsVisible(true);
else if (y > last + 6) setChipsVisible(false);
else if (y < last - 6) setChipsVisible(true);
lastScrollYRef.current = y;
};
container.addEventListener('scroll', onScroll, { passive: true });
return () => container.removeEventListener('scroll', onScroll);
}, []);
// //
const getDayName = (date) => ['일', '월', '화', '수', '목', '금', '토'][date.getDay()]; const getDayName = (date) => ['일', '월', '화', '수', '목', '금', '토'][date.getDay()];
@ -588,7 +634,7 @@ function MobileSchedule() {
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`; const dateStr = `${year}-${month}-${day}`;
const daySchedules = schedules const daySchedules = dotSchedules
.filter((s) => s.date?.split('T')[0] === dateStr) .filter((s) => s.date?.split('T')[0] === dateStr)
.slice(0, 3); .slice(0, 3);
@ -645,6 +691,7 @@ function MobileSchedule() {
})} })}
</div> </div>
)} )}
</div> </div>
{/* 달력 팝업 */} {/* 달력 팝업 */}
@ -778,7 +825,47 @@ function MobileSchedule() {
</div> </div>
</> </>
) )
) : loading ? ( ) : (
<>
{/* 카테고리 필터 칩 (스크롤 방향 감지 자동 숨김) */}
{monthCategories.length > 0 && (
<div
className="sticky top-0 z-20 bg-white -mx-4 px-4 -mt-4 pt-4 pb-3 transition-transform duration-300 ease-out"
style={{ transform: chipsVisible ? 'translateY(0)' : 'translateY(-130%)' }}
>
<div
className="flex overflow-x-auto scrollbar-hide gap-1.5"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<button
onClick={() => setSelectedCategories([])}
className={`flex-shrink-0 px-3 py-1.5 rounded-full text-xs font-semibold transition-colors ${
selectedCategories.length === 0 ? 'bg-primary text-white' : 'bg-gray-100 text-gray-500'
}`}
>
전체
</button>
{monthCategories.map((cat) => {
const active = selectedCategories.includes(cat.id);
return (
<button
key={cat.id}
onClick={() => toggleCategory(cat.id)}
className={`flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold transition-colors ${
active ? 'text-white' : 'bg-gray-100 text-gray-600'
}`}
style={active ? { backgroundColor: cat.color } : undefined}
>
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: active ? '#fff' : cat.color }} />
{cat.name}
</button>
);
})}
</div>
</div>
)}
{loading ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<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>
@ -831,6 +918,8 @@ function MobileSchedule() {
})} })}
</div> </div>
)} )}
</>
)}
</div> </div>
</div> </div>