diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3809265..09e4ac3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -91,7 +91,7 @@ function App() { } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/mobile/Layout.jsx b/frontend/src/components/mobile/Layout.jsx index 08e256a..08bf3a0 100644 --- a/frontend/src/components/mobile/Layout.jsx +++ b/frontend/src/components/mobile/Layout.jsx @@ -4,9 +4,9 @@ import { useEffect } from 'react'; import '../../mobile.css'; // 모바일 헤더 컴포넌트 -function MobileHeader({ title }) { +function MobileHeader({ title, noShadow = false }) { return ( -
+
{title ? ( {title} @@ -62,7 +62,7 @@ function MobileBottomNav() { // pageTitle: 헤더에 표시할 제목 (없으면 fromis_9) // hideHeader: true면 헤더 숨김 (일정 페이지처럼 자체 헤더가 있는 경우) // useCustomLayout: true면 자체 레이아웃 사용 (mobile-layout-container를 페이지에서 관리) -function MobileLayout({ children, pageTitle, hideHeader = false, useCustomLayout = false }) { +function MobileLayout({ children, pageTitle, hideHeader = false, useCustomLayout = false, noShadow = false }) { // 모바일 레이아웃 활성화 (body 스크롤 방지) useEffect(() => { document.documentElement.classList.add('mobile-layout'); @@ -83,7 +83,7 @@ function MobileLayout({ children, pageTitle, hideHeader = false, useCustomLayout return (
- {!hideHeader && } + {!hideHeader && }
{children}
diff --git a/frontend/src/pages/mobile/public/Members.jsx b/frontend/src/pages/mobile/public/Members.jsx index be9db69..cf4127e 100644 --- a/frontend/src/pages/mobile/public/Members.jsx +++ b/frontend/src/pages/mobile/public/Members.jsx @@ -1,12 +1,16 @@ -import { motion, AnimatePresence } from 'framer-motion'; -import { useState, useMemo } from 'react'; +import { motion } from 'framer-motion'; +import { useState, useMemo, useRef, useEffect } from 'react'; import { useQuery } from '@tanstack/react-query'; -import { Instagram, Calendar, X } from 'lucide-react'; +import { Instagram, Calendar } from 'lucide-react'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import 'swiper/css'; import { getMembers } from '../../../api/public/members'; -// 모바일 멤버 페이지 +// 모바일 멤버 페이지 - 카드 스와이프 스타일 function MobileMembers() { - const [selectedMember, setSelectedMember] = useState(null); + const [currentIndex, setCurrentIndex] = useState(0); + const swiperRef = useRef(null); + const indicatorRef = useRef(null); // useQuery로 멤버 데이터 로드 const { data: allMembers = [] } = useQuery({ @@ -14,10 +18,15 @@ function MobileMembers() { queryFn: getMembers, }); - // useMemo로 현재/전 멤버 분리 - const members = useMemo(() => allMembers.filter(m => !m.is_former), [allMembers]); - const formerMembers = useMemo(() => allMembers.filter(m => m.is_former), [allMembers]); - + // 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]); // 나이 계산 const calculateAge = (birthDate) => { @@ -32,176 +41,188 @@ function MobileMembers() { return age; }; - // 모달 닫기 - const closeModal = () => setSelectedMember(null); + // 인디케이터 자동 스크롤 + 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); - // 멤버 카드 렌더링 함수 - const renderMemberCard = (member, index, isFormer = false) => ( - setSelectedMember(member)} - className="cursor-pointer group" - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: index * 0.05, duration: 0.3 }} - whileTap={{ scale: 0.95 }} - > - {/* 카드 컨테이너 */} -
- {/* 이미지 영역 - 3:4 비율 */} -
- {member.image_url && ( - {member.name} - )} -
- - {/* 정보 영역 - 하단 그라데이션 오버레이 */} -
-

- {member.name} -

-
- - {/* 호버시 반짝이 효과 */} - {!isFormer && ( -
- )} + 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 ( +
+

멤버 정보가 없습니다

- - ); + ); + } return ( -
- {/* 현재 멤버 그리드 */} -
-
- {members.map((member, index) => renderMemberCard(member, index))} -
-
+
+ {/* 상단 썸네일 인디케이터 */} + +
+ {members.map((member, index) => { + const isSelected = index === currentIndex; + const isFormer = member.is_former; - {/* 전 멤버 */} - {formerMembers.length > 0 && ( -
-
-
-

전 멤버

-
-
-
- {formerMembers.map((member, index) => renderMemberCard(member, index, true))} -
-
- )} - - {/* 멤버 상세 모달 - 드래그로 닫기 가능 */} - - {selectedMember && ( - - { - if (info.offset.y > 100 || info.velocity.y > 300) { - closeModal(); - } - }} - className="bg-white w-full rounded-t-3xl overflow-hidden" - onClick={e => e.stopPropagation()} - > - {/* 드래그 핸들 */} -
-
-
- - {/* 헤더 */} -
-

멤버 정보

- -
- - {/* 모달 콘텐츠 */} -
-
- {/* 프로필 이미지 - 원본 비율 */} -
-
- {selectedMember.image_url && ( - {selectedMember.name} - )} + return ( + + ); + })} +
+ + + {/* 메인 카드 영역 */} + + { 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 ( + + {({ isActive }) => ( +
+ {/* 배경 이미지 */} + {member.image_url ? ( + {member.name} + ) : ( +
+ )} + + {/* 하단 그라데이션 오버레이 */} +
+ + {/* 전 멤버 라벨 */} + {isFormer && ( +
+ 전 멤버 +
+ )} + + {/* 멤버 정보 */} +
+ {/* 이름 */} +

+ {member.name} +

+ + {/* 포지션 */} + {member.position && ( +

+ {member.position} +

)} - - {selectedMember.birth_date && ( -
- - - {selectedMember.birth_date?.slice(0, 10).replaceAll('-', '.')} - {calculateAge(selectedMember.birth_date) && ( - - ({calculateAge(selectedMember.birth_date)}세) - - )} + + {/* 생일 정보 */} + {member.birth_date && ( +
+ + + {member.birth_date?.slice(0, 10).replaceAll('-', '.')} + {age && ( + + {age}세 + + )}
)} + + {/* 인스타그램 버튼 */} + {!isFormer && member.instagram && ( + + + Instagram + + )}
- - {/* 인스타그램 버튼 - 정보 영역 아래쪽 */} - {!selectedMember.is_former && selectedMember.instagram && ( - - - Instagram - - )}
-
-
- - - )} - + )} + + ); + })} + +
); }