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}
+
+
+ 🎂 {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}
+ />
);
}