diff --git a/client/src/features/CasperShowCase/CasperCards.tsx b/client/src/features/CasperShowCase/CasperCards.tsx index 58d8707b..54226d79 100644 --- a/client/src/features/CasperShowCase/CasperCards.tsx +++ b/client/src/features/CasperShowCase/CasperCards.tsx @@ -1,13 +1,14 @@ import { useMemo } from "react"; import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper"; -import { CasperCardType, TransitionCasperCards } from "./TransitionCasperCards"; +import type { CasperCardType } from "@/types/casper"; +import { TransitionCasperCards } from "./TransitionCasperCards"; interface CasperCardsProps { cardList: CasperCardType[]; } export function CasperCards({ cardList }: CasperCardsProps) { - const cardLength = cardList.length; + const cardLength = 20; const cardLengthHalf = Math.floor(cardLength / 2); const visibleCardCount = useMemo(() => { const width = window.innerWidth; @@ -22,7 +23,7 @@ export function CasperCards({ cardList }: CasperCardsProps) { const itemWidth = CASPER_CARD_SIZE[CASPER_SIZE_OPTION.SM].CARD_WIDTH; const gap = 40; - const totalWidth = (itemWidth + gap) * topCardList.length; + const totalWidth = (itemWidth + gap) * visibleCardCount; const isEndTopCard = (latestX: number) => { return latestX <= -totalWidth; diff --git a/client/src/features/CasperShowCase/TransitionCasperCardItem.tsx b/client/src/features/CasperShowCase/TransitionCasperCardItem.tsx new file mode 100644 index 00000000..8d605bf2 --- /dev/null +++ b/client/src/features/CasperShowCase/TransitionCasperCardItem.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper"; +import useLazyLoading from "@/hooks/useLazyLoading"; +import type { CasperCardType } from "@/types/casper"; +import { CasperFlipCard } from "../CasperCustom/CasperFlipCard"; + +interface TransitionCasperCardItemProps { + cardItem: CasperCardType; + id: string; + stopAnimation?: () => void; + startAnimation?: () => void; +} + +export function TransitionCasperCardItem({ + cardItem, + id, + stopAnimation, + startAnimation, +}: TransitionCasperCardItemProps) { + const [isFlipped, setIsFlipped] = useState(false); + const { isInView, cardRef } = useLazyLoading(); + + const handleMouseEnter = () => { + stopAnimation && stopAnimation(); + setIsFlipped(true); + }; + + const handleMouseLeave = () => { + startAnimation && startAnimation(); + setIsFlipped(false); + }; + + return ( +
  • + {isInView && ( + + )} +
  • + ); +} diff --git a/client/src/features/CasperShowCase/TransitionCasperCards.tsx b/client/src/features/CasperShowCase/TransitionCasperCards.tsx index a441b0c7..ba69cb2c 100644 --- a/client/src/features/CasperShowCase/TransitionCasperCards.tsx +++ b/client/src/features/CasperShowCase/TransitionCasperCards.tsx @@ -1,17 +1,10 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { AnimatePresence, motion, useAnimation } from "framer-motion"; +import { AnimatePresence, type ResolvedValues, motion, useAnimation } from "framer-motion"; import { CASPER_CARD_SIZE, CASPER_SIZE_OPTION } from "@/constants/CasperCustom/casper"; import { CARD_TRANSITION } from "@/constants/CasperShowCase/showCase"; -import useLazyLoading from "@/hooks/useLazyLoading"; -import { SelectedCasperIdxType } from "@/types/casperCustom"; -import { CasperFlipCard } from "../CasperCustom/CasperFlipCard"; +import { CasperCardType } from "@/types/casper"; +import { TransitionCasperCardItem } from "./TransitionCasperCardItem"; -export interface CasperCardType { - id: number; - casperName: string; - expectations: string; - selectedCasperIdx: SelectedCasperIdxType; -} interface TransitionCasperCardsProps { cardList: CasperCardType[]; initialX: number; @@ -36,11 +29,13 @@ export function TransitionCasperCards({ return Math.ceil(width / cardWidth); }, []); const isAnimated = visibleCardCount <= cardList.length; + const expandedCardList = useMemo(() => [...cardList, ...cardList], [cardList]); const containerRef = useRef(null); const transitionControls = useAnimation(); const [x, setX] = useState(initialX); + const [visibleCardListIdx, setVisibleCardListIdx] = useState(0); const startAnimation = (x: number) => { transitionControls.start({ @@ -58,52 +53,40 @@ export function TransitionCasperCards({ } }; - useEffect(() => { - startAnimation(x); - }, [transitionControls, totalWidth]); - - const expandedCardList = useMemo(() => { + const visibleCardList = useMemo(() => { if (isAnimated) { - return [...cardList, ...cardList.slice(0, visibleCardCount)]; + return [ + ...expandedCardList.slice( + visibleCardListIdx, + visibleCardListIdx + visibleCardCount + ), + ...expandedCardList.slice( + visibleCardListIdx + visibleCardCount, + visibleCardListIdx + visibleCardCount * 2 + ), + ]; } return cardList; - }, [cardList]); + }, [cardList, visibleCardCount, visibleCardListIdx]); - const renderCardItem = (cardItem: CasperCardType, id: string) => { - const [isFlipped, setIsFlipped] = useState(false); - const { isInView, cardRef } = useLazyLoading(); + useEffect(() => { + startAnimation(x); + }, [transitionControls, totalWidth]); - const handleMouseEnter = () => { - stopAnimation(); - setIsFlipped(true); - }; + const handleUpdateAnimation = (latest: ResolvedValues) => { + if (isEndCard(parseInt(String(latest.x)))) { + startAnimation(initialX); - const handleMouseLeave = () => { - startAnimation(x); - setIsFlipped(false); - }; + let nextIdx = visibleCardListIdx + visibleCardCount; - return ( -
  • - {isInView && ( - - )} -
  • - ); + // 만약 nextIdx가 cardList의 길이를 초과하면 0으로 초기화하거나 초과분을 조정합니다. + if (nextIdx + visibleCardCount >= cardList.length) { + nextIdx = (nextIdx + visibleCardCount) % cardList.length; + } + + setVisibleCardListIdx(nextIdx); + } }; return ( @@ -114,17 +97,27 @@ export function TransitionCasperCards({ className="flex" animate={transitionControls} style={{ gap: `${gap}px` }} - onUpdate={(latest) => { - if (isEndCard(parseInt(String(latest.x)))) { - startAnimation(initialX); - } - }} + onUpdate={handleUpdateAnimation} > - {expandedCardList.map((card, idx) => renderCardItem(card, `${card.id}-${idx}`))} + {visibleCardList.map((card, idx) => ( + startAnimation(x)} + /> + ))} ) : (
      - {expandedCardList.map((card, idx) => renderCardItem(card, `${card.id}-${idx}`))} + {visibleCardList.map((card, idx) => ( + + ))}
    )} diff --git a/client/src/types/casper.ts b/client/src/types/casper.ts new file mode 100644 index 00000000..7a358979 --- /dev/null +++ b/client/src/types/casper.ts @@ -0,0 +1,8 @@ +import { SelectedCasperIdxType } from "./casperCustom"; + +export interface CasperCardType { + id: number; + casperName: string; + expectations: string; + selectedCasperIdx: SelectedCasperIdxType; +}