diff --git a/frontend/src/components/common/BirthdayCelebrationDialog.jsx b/frontend/src/components/common/BirthdayCelebrationDialog.jsx new file mode 100644 index 0000000..78644ae --- /dev/null +++ b/frontend/src/components/common/BirthdayCelebrationDialog.jsx @@ -0,0 +1,113 @@ +import { memo, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X } from 'lucide-react'; + +/** + * 생일 축하 다이얼로그 + * @param {boolean} isOpen - 다이얼로그 표시 여부 + * @param {function} onClose - 닫기 핸들러 + * @param {string} title - 제목 (예: HAPPY Jiwon DAY) + * @param {string} memberImage - 멤버 이미지 URL + * @param {string} date - 생일 날짜 (YYYY-MM-DD) + */ +const BirthdayCelebrationDialog = memo(function BirthdayCelebrationDialog({ + isOpen, + onClose, + title = '', + memberImage = '', + date = '', +}) { + // ESC 키로 닫기 + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === 'Escape') onClose(); + }; + if (isOpen) { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + } + }, [isOpen, onClose]); + + const dateObj = date ? new Date(date) : new Date(); + const month = dateObj.getMonth() + 1; + const day = dateObj.getDate(); + + return ( + + {isOpen && ( + + {/* 배경 오버레이 */} +
+ + {/* 다이얼로그 */} + e.stopPropagation()} + className="relative w-full max-w-md overflow-hidden rounded-3xl bg-gradient-to-br from-pink-400 via-purple-400 to-indigo-400 shadow-2xl" + > + {/* 닫기 버튼 */} + + + {/* 배경 장식 */} +
+
🎉
+
🎂
+
🎁
+
🎉
+
+
+
+
+
+ + {/* 컨텐츠 */} +
+ {/* 멤버 사진 */} +
+ {memberImage ? ( + {title} + ) : ( + 🎂 + )} +
+ + {/* 텍스트 */} +

+ {title} +

+

+ 🎂 {month}월 {day}일 +

+
+ + + )} + + ); +}); + +export default BirthdayCelebrationDialog; diff --git a/frontend/src/components/common/index.js b/frontend/src/components/common/index.js index 2344b66..e98d766 100644 --- a/frontend/src/components/common/index.js +++ b/frontend/src/components/common/index.js @@ -9,3 +9,4 @@ export { default as LightboxIndicator } from './LightboxIndicator'; export { default as AnimatedNumber } from './AnimatedNumber'; export { default as Fromis9Logo } from './Fromis9Logo'; export { default as DebutCelebrationDialog } from './DebutCelebrationDialog'; +export { default as BirthdayCelebrationDialog } from './BirthdayCelebrationDialog'; diff --git a/frontend/src/pages/mobile/schedule/Schedule.jsx b/frontend/src/pages/mobile/schedule/Schedule.jsx index a602287..3db0419 100644 --- a/frontend/src/pages/mobile/schedule/Schedule.jsx +++ b/frontend/src/pages/mobile/schedule/Schedule.jsx @@ -17,7 +17,7 @@ import { BirthdayCard as MobileBirthdayCard, DebutCard as MobileDebutCard, } from '@/components/mobile'; -import { DebutCelebrationDialog } from '@/components/common'; +import { DebutCelebrationDialog, BirthdayCelebrationDialog } from '@/components/common'; import { fireBirthdayConfetti, fireDebutConfetti } from '@/utils'; /** @@ -55,6 +55,8 @@ function MobileSchedule() { 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 = () => { @@ -194,8 +196,19 @@ function MobileSchedule() { }); 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); @@ -813,6 +826,14 @@ function MobileSchedule() { isDebut={debutDialogInfo.isDebut} anniversaryYear={debutDialogInfo.anniversaryYear} /> + {/* 생일 축하 다이얼로그 */} + setShowBirthdayDialog(false)} + title={birthdayInfo.title} + memberImage={birthdayInfo.memberImage} + date={birthdayInfo.date} + /> ); } diff --git a/frontend/src/pages/pc/public/schedule/Schedule.jsx b/frontend/src/pages/pc/public/schedule/Schedule.jsx index 20c49be..c3774f8 100644 --- a/frontend/src/pages/pc/public/schedule/Schedule.jsx +++ b/frontend/src/pages/pc/public/schedule/Schedule.jsx @@ -13,7 +13,7 @@ import { BirthdayCard, DebutCard, } from '@/components/pc/public'; -import { DebutCelebrationDialog } from '@/components/common'; +import { DebutCelebrationDialog, BirthdayCelebrationDialog } from '@/components/common'; import { fireBirthdayConfetti, fireDebutConfetti } from '@/utils'; import { getSchedules, searchSchedules } from '@/api'; import { useScheduleStore } from '@/stores'; @@ -57,6 +57,8 @@ function PCSchedule() { const [showCategoryTooltip, setShowCategoryTooltip] = 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 year = currentDate.getFullYear(); const month = currentDate.getMonth(); @@ -131,8 +133,15 @@ function PCSchedule() { if (localStorage.getItem(confettiKey)) return; const hasBirthdayToday = schedules.some((s) => s.is_birthday && s.date === today); if (hasBirthdayToday) { + const birthdaySchedule = schedules.find((s) => s.is_birthday && s.date === 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); @@ -618,6 +627,13 @@ function PCSchedule() { isDebut={debutDialogInfo.isDebut} anniversaryYear={debutDialogInfo.anniversaryYear} /> + setShowBirthdayDialog(false)} + title={birthdayInfo.title} + memberImage={birthdayInfo.memberImage} + date={birthdayInfo.date} + />
); }