2026-01-13 14:38:59 +09:00
|
|
|
import { motion } from 'framer-motion';
|
|
|
|
|
import { useState, useMemo, useRef, useEffect } from 'react';
|
2026-01-12 15:46:34 +09:00
|
|
|
import { useQuery } from '@tanstack/react-query';
|
2026-01-13 14:38:59 +09:00
|
|
|
import { Instagram, Calendar } from 'lucide-react';
|
|
|
|
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
|
|
|
|
import 'swiper/css';
|
2026-01-10 00:02:42 +09:00
|
|
|
import { getMembers } from '../../../api/public/members';
|
2026-01-07 10:10:12 +09:00
|
|
|
|
2026-01-13 14:38:59 +09:00
|
|
|
// 모바일 멤버 페이지 - 카드 스와이프 스타일
|
2026-01-07 10:10:12 +09:00
|
|
|
function MobileMembers() {
|
2026-01-13 14:38:59 +09:00
|
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
|
|
|
const swiperRef = useRef(null);
|
|
|
|
|
const indicatorRef = useRef(null);
|
2026-01-07 10:10:12 +09:00
|
|
|
|
2026-01-12 15:46:34 +09:00
|
|
|
// useQuery로 멤버 데이터 로드
|
|
|
|
|
const { data: allMembers = [] } = useQuery({
|
|
|
|
|
queryKey: ['members'],
|
|
|
|
|
queryFn: getMembers,
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-13 14:38:59 +09:00
|
|
|
// useMemo로 현재/전 멤버 정렬 (현재 멤버 먼저, 전 멤버 나중)
|
|
|
|
|
const members = useMemo(() => {
|
|
|
|
|
return [...allMembers].sort((a, b) => {
|
|
|
|
|
if (a.is_former !== b.is_former) {
|
|
|
|
|
return a.is_former ? 1 : -1;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
});
|
|
|
|
|
}, [allMembers]);
|
2026-01-07 10:10:12 +09:00
|
|
|
|
2026-01-12 12:47:59 +09:00
|
|
|
// 나이 계산
|
|
|
|
|
const calculateAge = (birthDate) => {
|
|
|
|
|
if (!birthDate) return null;
|
|
|
|
|
const birth = new Date(birthDate);
|
|
|
|
|
const today = new Date();
|
|
|
|
|
let age = today.getFullYear() - birth.getFullYear();
|
|
|
|
|
const monthDiff = today.getMonth() - birth.getMonth();
|
|
|
|
|
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
|
|
|
|
age--;
|
|
|
|
|
}
|
|
|
|
|
return age;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 14:38:59 +09:00
|
|
|
// 인디케이터 자동 스크롤
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (indicatorRef.current && members.length > 0) {
|
|
|
|
|
const container = indicatorRef.current;
|
|
|
|
|
const itemWidth = 64; // 52px 썸네일 + 12px 간격
|
|
|
|
|
const containerWidth = container.offsetWidth;
|
|
|
|
|
const paddingLeft = 16; // px-4
|
|
|
|
|
const targetScroll = paddingLeft + (currentIndex * itemWidth) + 26 - (containerWidth / 2);
|
|
|
|
|
|
|
|
|
|
container.scrollTo({
|
|
|
|
|
left: Math.max(0, targetScroll),
|
|
|
|
|
behavior: 'smooth'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, [currentIndex, members.length]);
|
|
|
|
|
|
|
|
|
|
// 인디케이터 클릭 핸들러
|
|
|
|
|
const handleIndicatorClick = (index) => {
|
|
|
|
|
if (swiperRef.current) {
|
|
|
|
|
swiperRef.current.slideTo(index);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (members.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
|
|
|
|
|
<p className="text-gray-400">멤버 정보가 없습니다</p>
|
2026-01-07 10:10:12 +09:00
|
|
|
</div>
|
2026-01-13 14:38:59 +09:00
|
|
|
);
|
|
|
|
|
}
|
2026-01-07 10:10:12 +09:00
|
|
|
|
|
|
|
|
return (
|
2026-01-13 14:38:59 +09:00
|
|
|
<div className="flex flex-col h-[calc(100dvh-120px)] overflow-hidden overscroll-none touch-none">
|
|
|
|
|
{/* 상단 썸네일 인디케이터 */}
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: -20 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ duration: 0.4, ease: 'easeOut' }}
|
|
|
|
|
className="bg-white shadow-sm"
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
ref={indicatorRef}
|
|
|
|
|
className="flex gap-3 px-4 py-4 overflow-x-auto scrollbar-hide"
|
|
|
|
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
|
|
|
|
>
|
|
|
|
|
{members.map((member, index) => {
|
|
|
|
|
const isSelected = index === currentIndex;
|
|
|
|
|
const isFormer = member.is_former;
|
2026-01-07 10:10:12 +09:00
|
|
|
|
2026-01-13 14:38:59 +09:00
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={member.id}
|
|
|
|
|
onClick={() => handleIndicatorClick(index)}
|
|
|
|
|
className={`flex-shrink-0 w-[52px] h-[52px] rounded-full p-[2px] transition-all duration-200
|
|
|
|
|
${isSelected
|
|
|
|
|
? 'ring-[2.5px] ring-primary shadow-[0_0_8px_rgba(var(--primary-rgb),0.35)]'
|
|
|
|
|
: 'ring-[1.5px] ring-gray-300'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<div className={`w-full h-full rounded-full overflow-hidden bg-gray-200
|
|
|
|
|
${isFormer ? 'grayscale' : ''}`}
|
|
|
|
|
>
|
|
|
|
|
{member.image_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={member.image_url}
|
|
|
|
|
alt={member.name}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="w-full h-full flex items-center justify-center bg-gray-300 text-white font-bold">
|
|
|
|
|
{member.name[0]}
|
2026-01-12 12:47:59 +09:00
|
|
|
</div>
|
2026-01-13 14:38:59 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
|
|
|
|
{/* 메인 카드 영역 */}
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: 40 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ duration: 0.5, delay: 0.2, ease: 'easeOut' }}
|
|
|
|
|
className="flex-1 overflow-visible"
|
|
|
|
|
>
|
|
|
|
|
<Swiper
|
|
|
|
|
onSwiper={(swiper) => { swiperRef.current = swiper; }}
|
|
|
|
|
onSlideChange={(swiper) => setCurrentIndex(swiper.activeIndex)}
|
|
|
|
|
slidesPerView={1.12}
|
|
|
|
|
centeredSlides={true}
|
|
|
|
|
spaceBetween={0}
|
|
|
|
|
className="h-full !overflow-visible [&>.swiper-wrapper]:!overflow-visible"
|
|
|
|
|
style={{ padding: '8px 0' }}
|
|
|
|
|
>
|
|
|
|
|
{members.map((member, index) => {
|
|
|
|
|
const isFormer = member.is_former;
|
|
|
|
|
const age = calculateAge(member.birth_date);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SwiperSlide key={member.id} className="!flex items-center justify-center">
|
|
|
|
|
{({ isActive }) => (
|
|
|
|
|
<div
|
|
|
|
|
className={`relative w-full h-full max-h-[calc(100%-16px)] rounded-3xl overflow-hidden shadow-xl
|
|
|
|
|
transition-transform duration-300
|
|
|
|
|
${isActive ? 'scale-100' : 'scale-[0.92]'}`}
|
|
|
|
|
>
|
|
|
|
|
{/* 배경 이미지 */}
|
|
|
|
|
{member.image_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={member.image_url}
|
|
|
|
|
alt={member.name}
|
|
|
|
|
className={`absolute inset-0 w-full h-full object-cover
|
|
|
|
|
${isFormer ? 'grayscale' : ''}`}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="absolute inset-0 bg-gradient-to-br from-gray-300 to-gray-400" />
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 하단 그라데이션 오버레이 */}
|
|
|
|
|
<div className="absolute inset-x-0 bottom-0 h-[220px] bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
|
|
|
|
|
|
|
|
|
|
{/* 전 멤버 라벨 */}
|
|
|
|
|
{isFormer && (
|
|
|
|
|
<div className="absolute top-4 right-4 px-3 py-1.5 bg-black/60 rounded-full">
|
|
|
|
|
<span className="text-white/70 text-xs font-medium">전 멤버</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 멤버 정보 */}
|
|
|
|
|
<div className="absolute inset-x-0 bottom-0 p-6">
|
|
|
|
|
{/* 이름 */}
|
|
|
|
|
<h2 className="text-[32px] font-bold text-white drop-shadow-lg">
|
|
|
|
|
{member.name}
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
{/* 포지션 */}
|
|
|
|
|
{member.position && (
|
|
|
|
|
<p className="mt-2 text-base text-white/90 font-medium">
|
|
|
|
|
{member.position}
|
|
|
|
|
</p>
|
2026-01-12 12:47:59 +09:00
|
|
|
)}
|
2026-01-13 14:38:59 +09:00
|
|
|
|
|
|
|
|
{/* 생일 정보 */}
|
|
|
|
|
{member.birth_date && (
|
|
|
|
|
<div className="flex items-center gap-1.5 mt-3 text-white/80">
|
|
|
|
|
<Calendar size={16} className="text-white/70" />
|
|
|
|
|
<span className="text-sm">
|
|
|
|
|
{member.birth_date?.slice(0, 10).replaceAll('-', '.')}
|
2026-01-12 12:47:59 +09:00
|
|
|
</span>
|
2026-01-13 14:38:59 +09:00
|
|
|
{age && (
|
|
|
|
|
<span className="ml-2 px-2 py-0.5 bg-white/20 rounded-lg text-xs text-white font-medium">
|
|
|
|
|
{age}세
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-01-12 12:47:59 +09:00
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-13 14:38:59 +09:00
|
|
|
|
|
|
|
|
{/* 인스타그램 버튼 */}
|
|
|
|
|
{!isFormer && member.instagram && (
|
|
|
|
|
<a
|
|
|
|
|
href={member.instagram}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline-flex items-center gap-2 mt-4 px-4 py-2.5
|
|
|
|
|
bg-gradient-to-r from-[#833AB4] via-[#E1306C] to-[#F77737]
|
|
|
|
|
rounded-full shadow-lg shadow-[#E1306C]/40
|
|
|
|
|
active:scale-95 transition-transform"
|
|
|
|
|
>
|
|
|
|
|
<Instagram size={18} className="text-white" />
|
|
|
|
|
<span className="text-white text-sm font-semibold">Instagram</span>
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
2026-01-12 12:47:59 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-13 14:38:59 +09:00
|
|
|
)}
|
|
|
|
|
</SwiperSlide>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Swiper>
|
|
|
|
|
</motion.div>
|
2026-01-07 10:10:12 +09:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default MobileMembers;
|