feat(mobile-schedule): 카테고리 필터 추가 + 리스트 카드 플랫화
- 일정 리스트 위에 카테고리 필터 칩 추가 (해당 달 전체 카테고리) - 스크롤 방향 감지 자동 숨김 (내리면 숨고 올리면 보임), 상단 여백 일관 - 카테고리 선택 시 일정 목록 + 날짜 점 필터링 (공개 PC와 store 공유) - 일정 리스트 카드: 그림자 제거, 1.5px 테두리로 플랫하게 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
381461c25e
commit
9b2e4e190d
2 changed files with 96 additions and 7 deletions
|
|
@ -27,8 +27,8 @@ const ScheduleListCard = memo(function ScheduleListCard({
|
|||
onClick={onClick}
|
||||
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="flex items-center gap-1.5 mb-2">
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ function MobileSchedule() {
|
|||
const {
|
||||
selectedDate: storedSelectedDate,
|
||||
setSelectedDate: setStoredSelectedDate,
|
||||
selectedCategories,
|
||||
setSelectedCategories,
|
||||
toggleCategory,
|
||||
} = useScheduleStore();
|
||||
|
||||
// 선택된 날짜 (store에 없으면 오늘 날짜)
|
||||
|
|
@ -343,15 +346,58 @@ function MobileSchedule() {
|
|||
return days;
|
||||
}, [selectedDate]);
|
||||
|
||||
// 선택된 날짜의 일정 (생일 우선)
|
||||
// 선택된 날짜의 일정 (생일 우선) — 카테고리 필터 반영
|
||||
const selectedDateSchedules = useMemo(() => {
|
||||
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}`;
|
||||
// 백엔드에서 이미 정렬된 상태로 전달됨 (특수 일정 우선)
|
||||
return schedules.filter((s) => s.date.split('T')[0] === dateStr);
|
||||
}, [schedules, selectedDate]);
|
||||
return schedules.filter((s) => {
|
||||
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()];
|
||||
|
|
@ -588,7 +634,7 @@ function MobileSchedule() {
|
|||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const dateStr = `${year}-${month}-${day}`;
|
||||
|
||||
const daySchedules = schedules
|
||||
const daySchedules = dotSchedules
|
||||
.filter((s) => s.date?.split('T')[0] === dateStr)
|
||||
.slice(0, 3);
|
||||
|
||||
|
|
@ -645,6 +691,7 @@ function MobileSchedule() {
|
|||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* 달력 팝업 */}
|
||||
|
|
@ -778,7 +825,47 @@ function MobileSchedule() {
|
|||
</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="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
|
|
@ -830,6 +917,8 @@ function MobileSchedule() {
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue