feat: 생일 페이지 추가 (PC/Mobile)
- 멤버 생일 상세 페이지 구현 - 그라데이션 헤더 카드 (멤버 사진, 생일 정보) - 생일카페 섹션 (준비 중 플레이스홀더) - breadcrumb 네비게이션 (HAPPY OOO DAY 그라데이션 텍스트) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
76e0c2ee72
commit
dd0e508117
2 changed files with 307 additions and 0 deletions
155
frontend-temp/src/pages/schedule/MobileBirthday.jsx
Normal file
155
frontend-temp/src/pages/schedule/MobileBirthday.jsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
import { fetchApi } from '@/api/client';
|
||||
|
||||
// 한글 이름 → 영어 이름 매핑
|
||||
const memberEnglishName = {
|
||||
송하영: 'HAYOUNG',
|
||||
박지원: 'JIWON',
|
||||
이채영: 'CHAEYOUNG',
|
||||
이나경: 'NAKYUNG',
|
||||
백지헌: 'JIHEON',
|
||||
장규리: 'GYURI',
|
||||
이새롬: 'SAEROM',
|
||||
노지선: 'JISUN',
|
||||
이서연: 'SEOYEON',
|
||||
};
|
||||
|
||||
/**
|
||||
* Mobile 생일 페이지
|
||||
*/
|
||||
function MobileBirthday() {
|
||||
const { memberName, year } = useParams();
|
||||
|
||||
// URL 디코딩
|
||||
const decodedMemberName = decodeURIComponent(memberName || '');
|
||||
const englishName = memberEnglishName[decodedMemberName];
|
||||
|
||||
// 멤버 정보 조회
|
||||
const {
|
||||
data: member,
|
||||
isLoading: memberLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['member', decodedMemberName],
|
||||
queryFn: () => fetchApi(`/members/${encodeURIComponent(decodedMemberName)}`),
|
||||
enabled: !!decodedMemberName,
|
||||
});
|
||||
|
||||
if (!decodedMemberName || error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-6">
|
||||
<div className="text-center">
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-2">멤버를 찾을 수 없습니다</h1>
|
||||
<Link to="/schedule" className="text-primary">
|
||||
일정으로 돌아가기
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (memberLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<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-screen bg-gradient-to-b from-pink-50 to-purple-50">
|
||||
{/* 헤더 */}
|
||||
<div className="sticky top-0 bg-white/80 backdrop-blur-sm border-b border-gray-100 z-10">
|
||||
<div className="flex items-center h-14 px-4">
|
||||
<Link to="/schedule" className="p-2 -ml-2 rounded-lg active:bg-gray-100">
|
||||
<ChevronLeft size={24} />
|
||||
</Link>
|
||||
<div className="flex-1 text-center">
|
||||
<span className="text-sm font-medium bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 bg-clip-text text-transparent">
|
||||
HAPPY {englishName} DAY
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 py-6">
|
||||
{/* 헤더 카드 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="relative overflow-hidden bg-gradient-to-br from-pink-400 via-purple-400 to-indigo-400 rounded-2xl shadow-xl mb-6"
|
||||
>
|
||||
{/* 배경 장식 */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-8 -right-8 w-24 h-24 bg-white/10 rounded-full" />
|
||||
<div className="absolute -bottom-8 -left-8 w-32 h-32 bg-white/10 rounded-full" />
|
||||
<div className="absolute top-4 right-6 text-2xl">✨</div>
|
||||
<div className="absolute bottom-4 left-10 text-xl">🎉</div>
|
||||
<div className="absolute top-1/2 right-4 text-lg">🎈</div>
|
||||
</div>
|
||||
|
||||
<div className="relative p-5">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
{/* 멤버 사진 */}
|
||||
{member?.image_url && (
|
||||
<div className="w-20 h-20 rounded-full border-3 border-white/50 shadow-lg overflow-hidden bg-white flex-shrink-0">
|
||||
<img src={member.image_url} alt={member.name} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="flex-1 text-white">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-3xl">🎂</span>
|
||||
<h1 className="font-bold text-xl tracking-wide">HAPPY {englishName} DAY</h1>
|
||||
</div>
|
||||
<p className="text-white/80 text-sm">
|
||||
{year}년 {birthdayThisYear?.getMonth() + 1}월 {birthdayThisYear?.getDate()}일
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 년도 뱃지 */}
|
||||
<div className="bg-white/20 backdrop-blur-sm rounded-xl px-4 py-2 inline-flex items-center gap-2">
|
||||
<span className="text-white/70 text-xs font-medium">YEAR</span>
|
||||
<span className="text-white text-xl font-bold">{year}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 생일카페 섹션 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="bg-white rounded-2xl shadow-md p-5"
|
||||
>
|
||||
<h2 className="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
|
||||
<span>☕</span>
|
||||
생일카페
|
||||
</h2>
|
||||
|
||||
{/* 준비 중 메시지 */}
|
||||
<div className="text-center py-8">
|
||||
<div className="text-5xl mb-3">🎁</div>
|
||||
<p className="text-gray-500 text-sm">
|
||||
{year}년 {decodedMemberName} 생일카페 정보가 준비 중입니다
|
||||
</p>
|
||||
<p className="text-gray-400 text-xs mt-1">생일카페 정보가 등록되면 이곳에 표시됩니다</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MobileBirthday;
|
||||
152
frontend-temp/src/pages/schedule/PCBirthday.jsx
Normal file
152
frontend-temp/src/pages/schedule/PCBirthday.jsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { useParams, Link } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { fetchApi } from '@/api/client';
|
||||
|
||||
// 한글 이름 → 영어 이름 매핑
|
||||
const memberEnglishName = {
|
||||
송하영: 'HAYOUNG',
|
||||
박지원: 'JIWON',
|
||||
이채영: 'CHAEYOUNG',
|
||||
이나경: 'NAKYUNG',
|
||||
백지헌: 'JIHEON',
|
||||
장규리: 'GYURI',
|
||||
이새롬: 'SAEROM',
|
||||
노지선: 'JISUN',
|
||||
이서연: 'SEOYEON',
|
||||
};
|
||||
|
||||
/**
|
||||
* PC 생일 페이지
|
||||
*/
|
||||
function PCBirthday() {
|
||||
const { memberName, year } = useParams();
|
||||
|
||||
// URL 디코딩
|
||||
const decodedMemberName = decodeURIComponent(memberName || '');
|
||||
const englishName = memberEnglishName[decodedMemberName];
|
||||
|
||||
// 멤버 정보 조회
|
||||
const {
|
||||
data: member,
|
||||
isLoading: memberLoading,
|
||||
error,
|
||||
} = useQuery({
|
||||
queryKey: ['member', decodedMemberName],
|
||||
queryFn: () => fetchApi(`/members/${encodeURIComponent(decodedMemberName)}`),
|
||||
enabled: !!decodedMemberName,
|
||||
});
|
||||
|
||||
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>
|
||||
<Link to="/schedule" className="text-primary hover:underline">
|
||||
일정으로 돌아가기
|
||||
</Link>
|
||||
</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">
|
||||
{/* 네비게이션 */}
|
||||
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
|
||||
<Link to="/schedule" className="hover:text-primary transition-colors">
|
||||
일정
|
||||
</Link>
|
||||
<ChevronRight size={14} />
|
||||
<span className="font-medium bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 bg-clip-text text-transparent">
|
||||
HAPPY {englishName} DAY
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 헤더 카드 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
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">✨</div>
|
||||
<div className="absolute bottom-6 left-16 text-3xl">🎉</div>
|
||||
<div className="absolute top-1/2 right-8 text-2xl">🎈</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: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
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>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PCBirthday;
|
||||
Loading…
Add table
Reference in a new issue