fromis_9/frontend/src/pages/mobile/schedule/Schedule.jsx
caadiq 9d18449d3a feat: 생일 축하 다이얼로그 추가 (PC/모바일)
생일 당일 접속 시 폭죽과 함께 멤버 사진, HAPPY OOO DAY 제목,
날짜를 보여주는 축하 다이얼로그 표시. 데뷔 다이얼로그와 동일하게
localStorage로 하루 1회만 표시.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 13:39:55 +09:00

841 lines
30 KiB
JavaScript

import { useState, useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import { ChevronLeft, ChevronRight, ChevronDown, Search, X, Calendar } from 'lucide-react';
import { useQuery, useInfiniteQuery } from '@tanstack/react-query';
import { useInView } from 'react-intersection-observer';
import { useVirtualizer } from '@tanstack/react-virtual';
import { getTodayKST, getCategoryInfo } from '@/utils';
import { getSchedules, searchSchedules } from '@/api';
import { useScheduleStore } from '@/stores';
import { MIN_YEAR, SEARCH_LIMIT } from '@/constants';
import {
Calendar as MobileCalendar,
ScheduleListCard as MobileScheduleListCard,
ScheduleSearchCard as MobileScheduleSearchCard,
BirthdayCard as MobileBirthdayCard,
DebutCard as MobileDebutCard,
} from '@/components/mobile';
import { DebutCelebrationDialog, BirthdayCelebrationDialog } from '@/components/common';
import { fireBirthdayConfetti, fireDebutConfetti } from '@/utils';
/**
* 모바일 일정 페이지
*/
function MobileSchedule() {
const navigate = useNavigate();
// zustand store에서 상태 가져오기
const {
selectedDate: storedSelectedDate,
setSelectedDate: setStoredSelectedDate,
} = useScheduleStore();
// 선택된 날짜 (store에 없으면 오늘 날짜)
const selectedDate = storedSelectedDate || new Date();
const setSelectedDate = (date) => setStoredSelectedDate(date);
const [isSearchMode, setIsSearchMode] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [searchTerm, setSearchTerm] = useState('');
const [showCalendar, setShowCalendar] = useState(false);
const [calendarViewDate, setCalendarViewDate] = useState(() => new Date(selectedDate));
const [calendarShowYearMonth, setCalendarShowYearMonth] = useState(false);
const contentRef = useRef(null);
const searchContainerRef = useRef(null);
const searchInputRef = useRef(null);
// 검색 추천 관련 상태
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
const [originalSearchQuery, setOriginalSearchQuery] = useState('');
const [suggestions, setSuggestions] = useState([]);
const [lastSearchTerm, setLastSearchTerm] = useState('');
const [showSuggestionsScreen, setShowSuggestionsScreen] = useState(false);
const [showDebutDialog, setShowDebutDialog] = useState(false);
const [debutDialogInfo, setDebutDialogInfo] = useState({ isDebut: false, anniversaryYear: 0 });
const [showBirthdayDialog, setShowBirthdayDialog] = useState(false);
const [birthdayInfo, setBirthdayInfo] = useState({ title: '', memberImage: '', date: '' });
// 검색 모드 진입/종료
const enterSearchMode = () => {
setIsSearchMode(true);
window.history.pushState({ searchMode: true }, '');
};
const exitSearchMode = () => {
setIsSearchMode(false);
setSearchInput('');
setOriginalSearchQuery('');
setSearchTerm('');
setLastSearchTerm('');
setShowSuggestions(false);
setShowSuggestionsScreen(false);
setSelectedSuggestionIndex(-1);
};
const hideSuggestionsScreen = () => {
setShowSuggestionsScreen(false);
setSearchInput(lastSearchTerm);
setOriginalSearchQuery(lastSearchTerm);
};
// 뒤로가기 버튼 처리
useEffect(() => {
const handlePopState = () => {
if (isSearchMode) {
if (showSuggestionsScreen && searchTerm) {
hideSuggestionsScreen();
window.history.pushState({ searchMode: true }, '');
} else {
exitSearchMode();
}
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [isSearchMode, showSuggestionsScreen, searchTerm, lastSearchTerm]);
// 달력 월 변경
const changeCalendarMonth = (delta) => {
const newDate = new Date(calendarViewDate);
newDate.setMonth(newDate.getMonth() + delta);
setCalendarViewDate(newDate);
};
const scrollContainerRef = useRef(null);
const { ref: loadMoreRef, inView } = useInView({ threshold: 0, rootMargin: '100px' });
// 검색 무한 스크롤
const {
data: searchData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading: searchLoading,
} = useInfiniteQuery({
queryKey: ['mobileScheduleSearch', searchTerm],
queryFn: async ({ pageParam = 0 }) => {
return searchSchedules(searchTerm, { offset: pageParam, limit: SEARCH_LIMIT });
},
getNextPageParam: (lastPage) => {
if (lastPage.hasMore) {
return lastPage.offset + lastPage.schedules.length;
}
return undefined;
},
enabled: !!searchTerm && isSearchMode,
});
const searchResults = useMemo(() => {
if (!searchData?.pages) return [];
return searchData.pages.flatMap((page) => page.schedules);
}, [searchData]);
// 가상 스크롤 설정
const virtualizer = useVirtualizer({
count: isSearchMode && searchTerm ? searchResults.length : 0,
getScrollElement: () => scrollContainerRef.current,
estimateSize: () => 100,
overscan: 5,
});
// 검색어 변경 시 스크롤 위치 초기화
useEffect(() => {
if (searchTerm && !showSuggestionsScreen) {
requestAnimationFrame(() => {
virtualizer.scrollToOffset(0);
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
}
});
}
}, [searchTerm, showSuggestionsScreen]);
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage && isSearchMode && searchTerm) {
fetchNextPage();
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage, isSearchMode, searchTerm]);
// 일정 데이터 로드
const viewYear = selectedDate.getFullYear();
const viewMonth = selectedDate.getMonth() + 1;
const { data: schedules = [], isLoading: loading } = useQuery({
queryKey: ['schedules', viewYear, viewMonth],
queryFn: () => getSchedules(viewYear, viewMonth),
});
// 달력 표시용 일정 데이터
const calendarYear = calendarViewDate.getFullYear();
const calendarMonth = calendarViewDate.getMonth() + 1;
const isSameMonth = viewYear === calendarYear && viewMonth === calendarMonth;
const { data: calendarSchedules = [] } = useQuery({
queryKey: ['schedules', calendarYear, calendarMonth],
queryFn: () => getSchedules(calendarYear, calendarMonth),
enabled: !isSameMonth,
});
// 생일 폭죽 효과
useEffect(() => {
if (loading || schedules.length === 0) return;
const today = getTodayKST();
const confettiKey = `birthday-confetti-${today}`;
if (localStorage.getItem(confettiKey)) return;
const hasBirthdayToday = schedules.some((s) => {
if (!s.is_birthday) return false;
const scheduleDate = s.date ? s.date.split('T')[0] : '';
return scheduleDate === today;
});
if (hasBirthdayToday) {
const birthdaySchedule = schedules.find((s) => {
if (!s.is_birthday) return false;
const scheduleDate = s.date ? s.date.split('T')[0] : '';
return scheduleDate === today;
});
const timer = setTimeout(() => {
fireBirthdayConfetti();
setBirthdayInfo({
title: birthdaySchedule?.title || '',
memberImage: birthdaySchedule?.member_image || '',
date: birthdaySchedule?.date || '',
});
setShowBirthdayDialog(true);
localStorage.setItem(confettiKey, 'true');
}, 500);
return () => clearTimeout(timer);
}
}, [schedules, loading]);
// 데뷔/주년 폭죽 효과 및 다이얼로그
useEffect(() => {
if (loading || schedules.length === 0) return;
const today = getTodayKST();
const confettiKey = `debut-confetti-${today}`;
if (localStorage.getItem(confettiKey)) return;
const debutSchedule = schedules.find((s) => {
if (!s.is_debut && !s.is_anniversary) return false;
const scheduleDate = s.date ? s.date.split('T')[0] : '';
return scheduleDate === today;
});
if (debutSchedule) {
const timer = setTimeout(() => {
fireDebutConfetti();
setDebutDialogInfo({
isDebut: debutSchedule.is_debut,
anniversaryYear: debutSchedule.anniversary_year || 0,
});
setShowDebutDialog(true);
localStorage.setItem(confettiKey, 'true');
}, 500);
return () => clearTimeout(timer);
}
}, [schedules, loading]);
// 2017년 1월 이전으로 이동 불가
const canGoPrevMonth = !(selectedDate.getFullYear() === MIN_YEAR && selectedDate.getMonth() === 0);
// 월 변경
const changeMonth = (delta) => {
if (delta < 0 && !canGoPrevMonth) return;
const newDate = new Date(selectedDate);
newDate.setMonth(newDate.getMonth() + delta);
const today = new Date();
if (newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()) {
newDate.setDate(today.getDate());
} else {
newDate.setDate(1);
}
setSelectedDate(newDate);
setCalendarViewDate(newDate);
};
// 날짜 변경 시 스크롤 초기화
useEffect(() => {
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
}, [selectedDate]);
// 캘린더 열릴 때 배경 스크롤 방지
useEffect(() => {
const preventScroll = (e) => e.preventDefault();
if (showCalendar) {
document.addEventListener('touchmove', preventScroll, { passive: false });
} else {
document.removeEventListener('touchmove', preventScroll);
}
return () => {
document.removeEventListener('touchmove', preventScroll);
};
}, [showCalendar]);
// 검색 추천 드롭다운 외부 클릭 감지
useEffect(() => {
const handleClickOutside = (event) => {
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target)) {
setShowSuggestions(false);
setSelectedSuggestionIndex(-1);
}
};
if (showSuggestions) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
};
}, [showSuggestions]);
// 검색어 자동완성 API 호출
useEffect(() => {
if (!originalSearchQuery || originalSearchQuery.trim().length === 0) {
setSuggestions([]);
return;
}
const timeoutId = setTimeout(async () => {
try {
const response = await fetch(
`/api/schedules/suggestions?q=${encodeURIComponent(originalSearchQuery)}&limit=10`
);
if (response.ok) {
const data = await response.json();
setSuggestions(data.suggestions || []);
}
} catch (error) {
console.error('추천 검색어 API 오류:', error);
setSuggestions([]);
}
}, 200);
return () => clearTimeout(timeoutId);
}, [originalSearchQuery]);
// 해당 달의 모든 날짜 배열
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(() => {
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]);
// 요일 이름
const getDayName = (date) => ['일', '월', '화', '수', '목', '금', '토'][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 (!isSearchMode && dateScrollRef.current) {
const selectedDay = selectedDate.getDate();
const buttons = dateScrollRef.current.querySelectorAll('button');
if (buttons[selectedDay - 1]) {
setTimeout(() => {
buttons[selectedDay - 1].scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'nearest',
});
}, 50);
}
}
}, [selectedDate, isSearchMode]);
// 검색 실행 핸들러
const handleSearch = (term) => {
if (term) {
setSearchInput(term);
setSearchTerm(term);
setLastSearchTerm(term);
setShowSuggestionsScreen(false);
}
setShowSuggestions(false);
setSelectedSuggestionIndex(-1);
searchInputRef.current?.blur();
};
return (
<>
{/* 툴바 (헤더 + 날짜 선택기) */}
<div className="mobile-toolbar-schedule shadow-sm z-50">
{isSearchMode ? (
<div className="flex items-center gap-3 px-4 py-3 relative" ref={searchContainerRef}>
<div className="flex-1 flex items-center gap-2 bg-gray-100 rounded-full px-4 py-2 min-w-0">
<Search size={18} className="text-gray-400 flex-shrink-0" />
<input
ref={searchInputRef}
type="text"
inputMode="search"
enterKeyHint="search"
placeholder="일정 검색..."
value={searchInput}
onChange={(e) => {
setSearchInput(e.target.value);
setOriginalSearchQuery(e.target.value);
setShowSuggestions(true);
setShowSuggestionsScreen(true);
setSelectedSuggestionIndex(-1);
}}
onFocus={() => {
setShowSuggestions(true);
setShowSuggestionsScreen(true);
}}
onKeyDown={(e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const newIndex =
selectedSuggestionIndex < suggestions.length - 1
? selectedSuggestionIndex + 1
: 0;
setSelectedSuggestionIndex(newIndex);
if (suggestions[newIndex]) {
setSearchInput(suggestions[newIndex]);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const newIndex =
selectedSuggestionIndex > 0
? selectedSuggestionIndex - 1
: suggestions.length - 1;
setSelectedSuggestionIndex(newIndex);
if (suggestions[newIndex]) {
setSearchInput(suggestions[newIndex]);
}
} else if (e.key === 'Enter') {
e.preventDefault();
const term =
selectedSuggestionIndex >= 0 && suggestions[selectedSuggestionIndex]
? suggestions[selectedSuggestionIndex]
: searchInput.trim();
handleSearch(term);
}
}}
className="flex-1 bg-transparent outline-none text-sm min-w-0 [&::-webkit-search-cancel-button]:hidden"
autoFocus={!searchTerm}
/>
{searchInput && (
<button
onClick={() => {
setSearchInput('');
setOriginalSearchQuery('');
setShowSuggestions(true);
setShowSuggestionsScreen(true);
setSelectedSuggestionIndex(-1);
}}
className="flex-shrink-0"
>
<X size={18} className="text-gray-400" />
</button>
)}
</div>
<button onClick={exitSearchMode} className="text-sm text-gray-500 flex-shrink-0">
취소
</button>
</div>
) : (
<div className="relative flex items-center justify-between px-4 py-3">
{showCalendar ? (
<>
<div className="flex items-center gap-1">
<button
onClick={() => {
setShowCalendar(false);
setCalendarShowYearMonth(false);
}}
className="p-2 rounded-lg hover:bg-gray-100"
>
<Calendar size={20} className="text-primary" />
</button>
<button onClick={() => changeCalendarMonth(-1)} className="p-2">
<ChevronLeft size={20} />
</button>
</div>
<button
onClick={() => setCalendarShowYearMonth(!calendarShowYearMonth)}
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={enterSearchMode} className="p-2">
<Search size={20} />
</button>
</div>
</>
) : (
<>
<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)}
disabled={!canGoPrevMonth}
className={`p-2 ${!canGoPrevMonth ? 'opacity-30' : ''}`}
>
<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={enterSearchMode} className="p-2">
<Search size={20} />
</button>
</div>
</>
)}
</div>
)}
{/* 가로 스크롤 날짜 선택기 */}
{!isSearchMode && (
<div
ref={dateScrollRef}
className="flex overflow-x-auto scrollbar-hide px-2 py-2 gap-1"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{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 daySchedules = schedules
.filter((s) => s.date?.split('T')[0] === dateStr)
.slice(0, 3);
return (
<button
key={date.getDate()}
onClick={() => {
setSelectedDate(date);
setCalendarViewDate(date);
}}
className={`flex flex-col items-center min-w-[44px] h-[64px] py-2 px-1 rounded-xl transition-all ${
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>
<div className="flex gap-0.5 mt-1 h-1.5">
{!isSelected(date) &&
daySchedules.map((schedule, i) => {
const categoryInfo = getCategoryInfo(schedule);
return (
<div
key={i}
className="w-1 h-1 rounded-full"
style={{ backgroundColor: categoryInfo.color }}
/>
);
})}
</div>
</button>
);
})}
</div>
)}
</div>
{/* 달력 팝업 */}
<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' }}
>
<MobileCalendar
selectedDate={selectedDate}
schedules={isSameMonth ? schedules : calendarSchedules}
hideHeader={true}
externalViewDate={calendarViewDate}
onViewDateChange={setCalendarViewDate}
externalShowYearMonth={calendarShowYearMonth}
onShowYearMonthChange={setCalendarShowYearMonth}
onSelectDate={(date) => {
setSelectedDate(date);
setCalendarViewDate(date);
setCalendarShowYearMonth(false);
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="mobile-content"
ref={isSearchMode && searchTerm && !showSuggestionsScreen ? scrollContainerRef : contentRef}
>
<div className={`px-4 pb-4 ${isSearchMode && showSuggestionsScreen ? 'pt-0' : 'pt-4'}`}>
{isSearchMode ? (
showSuggestionsScreen ? (
// 추천 검색어 화면
<div className="space-y-0">
{suggestions.length === 0 ? (
<div className="text-center py-8 text-gray-400">검색어를 입력하세요</div>
) : (
suggestions.map((suggestion, index) => (
<button
key={suggestion}
onClick={() => handleSearch(suggestion)}
className={`w-full px-0 py-3.5 text-left flex items-center gap-4 border-b border-gray-100 active:bg-gray-50 ${
index === selectedSuggestionIndex ? 'bg-primary/5' : ''
}`}
>
<Search size={18} className="text-gray-400 shrink-0" />
<span className="text-[15px] text-gray-700 flex-1">{suggestion}</span>
</button>
))
)}
</div>
) : !searchTerm ? (
<div className="text-center py-8 text-gray-400">검색어를 입력하세요</div>
) : searchLoading ? (
<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>
) : searchResults.length === 0 ? (
<div className="text-center py-8 text-gray-400">검색 결과가 없습니다</div>
) : (
<>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => {
const schedule = searchResults[virtualItem.index];
if (!schedule) return null;
return (
<div
key={virtualItem.key}
ref={virtualizer.measureElement}
data-index={virtualItem.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<div className={virtualItem.index < searchResults.length - 1 ? 'pb-3' : ''}>
<MobileScheduleSearchCard
schedule={schedule}
onClick={() => navigate(`/schedule/${schedule.id}`)}
/>
</div>
</div>
);
})}
</div>
<div ref={loadMoreRef} className="py-2">
{isFetchingNextPage && (
<div className="flex justify-center">
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
</>
)
) : loading ? (
<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>
) : selectedDateSchedules.length === 0 ? (
<div className="text-center py-8 text-gray-400">
{selectedDate.getMonth() + 1} {selectedDate.getDate()} 일정이 없습니다
</div>
) : (
<div className="space-y-3">
{selectedDateSchedules.map((schedule, index) => {
const isBirthday = schedule.is_birthday || String(schedule.id).startsWith('birthday-');
const isDebut = schedule.is_debut || schedule.is_anniversary;
if (isBirthday) {
return (
<MobileBirthdayCard
key={schedule.id}
schedule={schedule}
delay={index * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)}
/>
);
}
if (isDebut) {
return (
<MobileDebutCard
key={schedule.id}
schedule={schedule}
delay={index * 0.05}
/>
);
}
return (
<MobileScheduleListCard
key={schedule.id}
schedule={schedule}
delay={index * 0.05}
onClick={() => navigate(`/schedule/${schedule.id}`)}
/>
);
})}
</div>
)}
</div>
</div>
{/* 데뷔/주년 축하 다이얼로그 */}
<DebutCelebrationDialog
isOpen={showDebutDialog}
onClose={() => setShowDebutDialog(false)}
isDebut={debutDialogInfo.isDebut}
anniversaryYear={debutDialogInfo.anniversaryYear}
/>
{/* 생일 축하 다이얼로그 */}
<BirthdayCelebrationDialog
isOpen={showBirthdayDialog}
onClose={() => setShowBirthdayDialog(false)}
title={birthdayInfo.title}
memberImage={birthdayInfo.memberImage}
date={birthdayInfo.date}
/>
</>
);
}
export default MobileSchedule;