208 lines
9.6 KiB
React
208 lines
9.6 KiB
React
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
||
|
|
import { useQuery } from '@tanstack/react-query';
|
||
|
|
import { motion } from 'framer-motion';
|
||
|
|
import { ArrowLeft, Calendar, MapPin, Clock } from 'lucide-react';
|
||
|
|
import { fetchApi } from '../../../api';
|
||
|
|
|
||
|
|
// 한글 이름 → 영어 이름 매핑
|
||
|
|
const memberEnglishName = {
|
||
|
|
'송하영': 'HAYOUNG',
|
||
|
|
'박지원': 'JIWON',
|
||
|
|
'이채영': 'CHAEYOUNG',
|
||
|
|
'이나경': 'NAKYUNG',
|
||
|
|
'백지헌': 'JIHEON',
|
||
|
|
'장규리': 'GYURI',
|
||
|
|
'이새롬': 'SAEROM',
|
||
|
|
'노지선': 'JISUN',
|
||
|
|
'이서연': 'SEOYEON',
|
||
|
|
};
|
||
|
|
|
||
|
|
function Birthday() {
|
||
|
|
const { memberName, year } = useParams();
|
||
|
|
const navigate = useNavigate();
|
||
|
|
|
||
|
|
// URL 디코딩
|
||
|
|
const decodedMemberName = decodeURIComponent(memberName || '');
|
||
|
|
const englishName = memberEnglishName[decodedMemberName];
|
||
|
|
|
||
|
|
// 멤버 정보 조회
|
||
|
|
const { data: member, isLoading: memberLoading, error } = useQuery({
|
||
|
|
queryKey: ['member', decodedMemberName],
|
||
|
|
queryFn: () => fetchApi(`/api/members/${encodeURIComponent(decodedMemberName)}`),
|
||
|
|
enabled: !!decodedMemberName,
|
||
|
|
});
|
||
|
|
|
||
|
|
// 해당 년도 생일카페 정보 조회 (나중에 구현)
|
||
|
|
// const { data: cafes } = useQuery({
|
||
|
|
// queryKey: ['birthdayCafes', decodedMemberName, year],
|
||
|
|
// queryFn: () => fetchApi(`/api/birthday-cafes?member=${encodeURIComponent(decodedMemberName)}&year=${year}`),
|
||
|
|
// });
|
||
|
|
|
||
|
|
if (!decodedMemberName || error) {
|
||
|
|
return (
|
||
|
|
<div className="min-h-[calc(100vh-64px)] flex items-center justify-center">
|
||
|
|
<div className="text-center">
|
||
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">멤버를 찾을 수 없습니다</h1>
|
||
|
|
<button
|
||
|
|
onClick={() => navigate('/schedule')}
|
||
|
|
className="text-primary hover:underline"
|
||
|
|
>
|
||
|
|
일정으로 돌아가기
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (memberLoading) {
|
||
|
|
return (
|
||
|
|
<div className="min-h-[calc(100vh-64px)] flex items-center justify-center">
|
||
|
|
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 생일 계산
|
||
|
|
const birthDate = member?.birth_date ? new Date(member.birth_date) : null;
|
||
|
|
const birthdayThisYear = birthDate ? new Date(parseInt(year), birthDate.getMonth(), birthDate.getDate()) : null;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-[calc(100vh-64px)] bg-gradient-to-b from-pink-50 to-purple-50">
|
||
|
|
<div className="max-w-4xl mx-auto px-6 py-8">
|
||
|
|
{/* 뒤로가기 */}
|
||
|
|
<button
|
||
|
|
onClick={() => navigate(-1)}
|
||
|
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors"
|
||
|
|
>
|
||
|
|
<ArrowLeft size={20} />
|
||
|
|
<span>뒤로가기</span>
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{/* 헤더 카드 */}
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 20 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
className="relative overflow-hidden bg-gradient-to-r from-pink-400 via-purple-400 to-indigo-400 rounded-3xl shadow-xl mb-8"
|
||
|
|
>
|
||
|
|
{/* 배경 장식 */}
|
||
|
|
<div className="absolute inset-0 overflow-hidden">
|
||
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
|
||
|
|
<div className="absolute -bottom-10 -left-10 w-48 h-48 bg-white/10 rounded-full" />
|
||
|
|
<div className="absolute top-1/3 right-1/4 w-20 h-20 bg-white/5 rounded-full" />
|
||
|
|
<div className="absolute top-6 right-12 text-4xl animate-pulse">✨</div>
|
||
|
|
<div className="absolute bottom-6 left-16 text-3xl animate-pulse delay-300">🎉</div>
|
||
|
|
<div className="absolute top-1/2 right-8 text-2xl animate-bounce">🎈</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="relative flex items-center p-8 gap-8">
|
||
|
|
{/* 멤버 사진 */}
|
||
|
|
{member?.image_url && (
|
||
|
|
<div className="flex-shrink-0">
|
||
|
|
<div className="w-32 h-32 rounded-full border-4 border-white/50 shadow-xl overflow-hidden bg-white">
|
||
|
|
<img
|
||
|
|
src={member.image_url}
|
||
|
|
alt={member.name}
|
||
|
|
className="w-full h-full object-cover"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* 내용 */}
|
||
|
|
<div className="flex-1 text-white">
|
||
|
|
<div className="flex items-center gap-3 mb-2">
|
||
|
|
<span className="text-5xl">🎂</span>
|
||
|
|
<h1 className="font-bold text-4xl tracking-wide">
|
||
|
|
HAPPY {englishName} DAY
|
||
|
|
</h1>
|
||
|
|
</div>
|
||
|
|
<p className="text-white/80 text-lg mt-2">
|
||
|
|
{year}년 {birthdayThisYear?.getMonth() + 1}월 {birthdayThisYear?.getDate()}일
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 년도 뱃지 */}
|
||
|
|
<div className="flex-shrink-0 bg-white/20 backdrop-blur-sm rounded-2xl px-6 py-4 text-center">
|
||
|
|
<div className="text-white/70 text-sm font-medium">YEAR</div>
|
||
|
|
<div className="text-white text-4xl font-bold">{year}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
{/* 생일카페 섹션 */}
|
||
|
|
<motion.div
|
||
|
|
initial={{ opacity: 0, y: 20 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ delay: 0.1 }}
|
||
|
|
className="bg-white rounded-2xl shadow-lg p-8"
|
||
|
|
>
|
||
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-6 flex items-center gap-2">
|
||
|
|
<span>☕</span>
|
||
|
|
생일카페
|
||
|
|
</h2>
|
||
|
|
|
||
|
|
{/* 준비 중 메시지 */}
|
||
|
|
<div className="text-center py-12">
|
||
|
|
<div className="text-6xl mb-4">🎁</div>
|
||
|
|
<p className="text-gray-500 text-lg">
|
||
|
|
{year}년 {decodedMemberName} 생일카페 정보가 준비 중입니다
|
||
|
|
</p>
|
||
|
|
<p className="text-gray-400 text-sm mt-2">
|
||
|
|
생일카페 정보가 등록되면 이곳에 표시됩니다
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 생일카페 목록 (나중에 구현) */}
|
||
|
|
{/* {cafes?.length > 0 ? (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{cafes.map((cafe) => (
|
||
|
|
<div key={cafe.id} className="border border-gray-200 rounded-xl p-6">
|
||
|
|
<h3 className="font-bold text-lg mb-3">{cafe.name}</h3>
|
||
|
|
<div className="space-y-2 text-gray-600">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Calendar size={16} />
|
||
|
|
<span>{cafe.start_date} ~ {cafe.end_date}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<Clock size={16} />
|
||
|
|
<span>{cafe.open_time} - {cafe.close_time}</span>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<MapPin size={16} />
|
||
|
|
<span>{cafe.location}</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
) : null} */}
|
||
|
|
</motion.div>
|
||
|
|
|
||
|
|
{/* 다른 년도 보기 (나중에 구현) */}
|
||
|
|
{/* <motion.div
|
||
|
|
initial={{ opacity: 0, y: 20 }}
|
||
|
|
animate={{ opacity: 1, y: 0 }}
|
||
|
|
transition={{ delay: 0.2 }}
|
||
|
|
className="mt-6 flex justify-center gap-2"
|
||
|
|
>
|
||
|
|
{[2023, 2024, 2025, 2026].map((y) => (
|
||
|
|
<button
|
||
|
|
key={y}
|
||
|
|
onClick={() => navigate(`/birthday/${encodeURIComponent(decodedMemberName)}/${y}`)}
|
||
|
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||
|
|
parseInt(year) === y
|
||
|
|
? 'bg-primary text-white'
|
||
|
|
: 'bg-white text-gray-600 hover:bg-gray-100'
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
{y}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</motion.div> */}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default Birthday;
|