diff --git a/frontend/src/pages/mobile/members/Members.jsx b/frontend/src/pages/mobile/members/Members.jsx
index a06b384..6e600eb 100644
--- a/frontend/src/pages/mobile/members/Members.jsx
+++ b/frontend/src/pages/mobile/members/Members.jsx
@@ -1,80 +1,143 @@
-import { motion, AnimatePresence } from 'framer-motion';
-import { useState, useMemo, useRef, useEffect } from 'react';
-import { Cake, X, Instagram } from 'lucide-react';
+import { useMemo, useRef, useEffect, forwardRef } from 'react';
+import { Cake, Instagram } from 'lucide-react';
import { useMembers } from '@/hooks';
-/**
- * 멤버 카드 컴포넌트 (외부 정의로 리렌더링 방지)
- */
-const MemberCard = ({ member, index, onClick, shouldAnimate }) => (
-
- {member.image_medium || member.image_url ? (
-
- ) : (
-
-
- {member.name[0]}
-
-
- )}
-
-
-);
+// 생일/나이 계산
+const formatBirth = (birthDate) => {
+ if (!birthDate) return null;
+ return birthDate.slice(0, 10).replaceAll('-', '.'); // YYYY.MM.DD
+};
+const calcAge = (birthDate) => {
+ if (!birthDate) return null;
+ const b = new Date(birthDate);
+ const t = new Date();
+ let age = t.getFullYear() - b.getFullYear();
+ const m = t.getMonth() - b.getMonth();
+ if (m < 0 || (m === 0 && t.getDate() < b.getDate())) age--;
+ return age;
+};
/**
- * Mobile 멤버 페이지 - 그리드 레이아웃
+ * 현재 멤버 화보 카드 (스티키 스택)
+ */
+const MemberHeroCard = forwardRef(function MemberHeroCard({ member }, ref) {
+ const birth = formatBirth(member.birth_date);
+ const age = calcAge(member.birth_date);
+
+ return (
+
+ {member.image_medium || member.image_url ? (
+

+ ) : (
+
+ {member.name[0]}
+
+ )}
+
+ {/* 하단 그라데이션 + 정보 */}
+
+
+ {member.name}
+
+ {birth && (
+
+
+ {birth}{age != null && ` · ${age}세`}
+
+ )}
+
+ {member.instagram && (
+
+
+ Instagram
+
+ )}
+
+
+ );
+});
+
+/**
+ * Mobile 멤버 페이지 - 스티키 스택 (덮이며 흐려짐)
*/
function MobileMembers() {
- const [selectedMember, setSelectedMember] = useState(null);
- const hasAnimated = useRef(false);
-
- // 멤버 데이터 로드
const { data: allMembers = [] } = useMembers();
+ const currentMembers = useMemo(() => allMembers.filter((m) => !m.is_former), [allMembers]);
- // 초기 애니메이션 완료 추적
+ const cardRefs = useRef([]);
+ const spacerRef = useRef(null);
useEffect(() => {
- if (allMembers.length > 0 && !hasAnimated.current) {
- const timer = setTimeout(() => {
- hasAnimated.current = true;
- }, allMembers.length * 50 + 300);
- return () => clearTimeout(timer);
- }
- }, [allMembers.length]);
+ const container = document.querySelector('.mobile-content');
+ if (!container) return;
- // 현재 멤버만 표시
- const currentMembers = useMemo(
- () => allMembers.filter((m) => !m.is_former),
- [allMembers]
- );
+ // 마지막 카드가 top-3에서 딱 멈추도록 하단 여유 공간 계산
+ const setSpacer = () => {
+ const cards = cardRefs.current.filter(Boolean);
+ if (!cards.length || !spacerRef.current) return;
+ const cardH = cards[cards.length - 1].getBoundingClientRect().height;
+ spacerRef.current.style.height = `${Math.max(0, container.clientHeight - cardH - 16)}px`;
+ };
- // 나이 계산
- 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;
- };
+ const onScroll = () => {
+ const cards = cardRefs.current.filter(Boolean);
+ if (cards.length === 0) return;
+ const rects = cards.map((c) => c.getBoundingClientRect());
+ for (let i = 0; i < cards.length; i++) {
+ let coverage = 0;
+ if (i < cards.length - 1) {
+ const cur = rects[i];
+ const next = rects[i + 1];
+ coverage = Math.min(1, Math.max(0, (cur.bottom - next.top) / cur.height));
+ }
+ cards[i].style.opacity = String(1 - coverage * 0.55);
+ cards[i].style.transform = `scale(${(1 - coverage * 0.05).toFixed(3)})`;
+ }
+ };
+
+ // 스크롤 멈추면, 올라오는 카드를 상단(top-4)에 덮이거나 되돌리도록 자석 스냅
+ let snapTimer;
+ const snapToNearest = () => {
+ const cards = cardRefs.current.filter(Boolean);
+ if (cards.length < 2) return;
+ const line = container.getBoundingClientRect().top + 16; // top-4
+ // 상단 라인 아래에서 올라오는 중인(=delta>0) 가장 가까운 카드
+ let riser = null;
+ let riserDelta = Infinity;
+ for (const c of cards) {
+ const d = c.getBoundingClientRect().top - line;
+ if (d > 1 && d < riserDelta) { riserDelta = d; riser = c; }
+ }
+ if (!riser) return;
+ const trigger = riser.getBoundingClientRect().height + 16; // rest(아래) → 덮임(상단) 이동량
+ // 절반 기준: 가까운 쪽(덮임 0 / 되돌림 trigger)으로 스냅
+ const target = riserDelta < trigger / 2 ? riserDelta : riserDelta - trigger;
+ if (Math.abs(target) > 1) {
+ container.scrollBy({ top: target, behavior: 'smooth' });
+ }
+ };
+
+ const handleScroll = () => {
+ onScroll();
+ clearTimeout(snapTimer);
+ snapTimer = setTimeout(snapToNearest, 90);
+ };
+
+ setSpacer();
+ onScroll();
+ container.addEventListener('scroll', handleScroll, { passive: true });
+ window.addEventListener('resize', setSpacer);
+ return () => {
+ container.removeEventListener('scroll', handleScroll);
+ window.removeEventListener('resize', setSpacer);
+ clearTimeout(snapTimer);
+ };
+ }, [currentMembers.length]);
if (allMembers.length === 0) {
return (
@@ -84,96 +147,19 @@ function MobileMembers() {
);
}
- const shouldAnimate = !hasAnimated.current;
-
return (
-
- {/* 현재 멤버 */}
-
- {currentMembers.map((member, index) => (
-
+
+ {currentMembers.map((member, i) => (
+ setSelectedMember(member)}
- shouldAnimate={shouldAnimate}
+ ref={(el) => { cardRefs.current[i] = el; }}
/>
))}
-
- {/* 선택된 멤버 모달 */}
-
- {selectedMember && (
- setSelectedMember(null)}
- >
- e.stopPropagation()}
- >
- {/* 이미지 */}
-
- {selectedMember.image_thumb || selectedMember.image_url ? (
-

- ) : (
-
- {selectedMember.name[0]}
-
- )}
- {/* 닫기 버튼 */}
-
-
-
- {/* 정보 영역 */}
-
-
{selectedMember.name}
- {selectedMember.birth_date && (
-
-
-
- {selectedMember.birth_date?.slice(0, 10).replaceAll('-', '.')}
-
- {calculateAge(selectedMember.birth_date) && (
-
- ({calculateAge(selectedMember.birth_date)}세)
-
- )}
-
- )}
- {!selectedMember.is_former && selectedMember.instagram && (
-
-
- Instagram
-
- )}
-
-
-
- )}
-
+ {/* 마지막 카드가 top-3에서 멈추도록 동적 여유 공간 */}
+
);
}