diff --git a/src/app/_components/user/workation/WkResultInfo.tsx b/src/app/_components/user/workation/WkResultInfo.tsx index 0d715b43..e8785dfd 100644 --- a/src/app/_components/user/workation/WkResultInfo.tsx +++ b/src/app/_components/user/workation/WkResultInfo.tsx @@ -28,7 +28,7 @@ const WkResultInfo = ({ id }: { id: number }) => { return (
-
+
{data?.wktWinningUserInfos.map( diff --git a/src/app/_components/user/workation/WkSlider.tsx b/src/app/_components/user/workation/WkSlider.tsx index 0bea6ec6..f16b651f 100644 --- a/src/app/_components/user/workation/WkSlider.tsx +++ b/src/app/_components/user/workation/WkSlider.tsx @@ -8,32 +8,21 @@ const Slider = ({ id }: { id: number }) => { const [animate, setAnimate] = useState(false); const [hoveredAccount, setHoveredAccount] = useState(null); const [tooltipPosition, setTooltipPosition] = useState({ left: '0%' }); - const sliderRef = useRef(null); + const [visibleWinners, setVisibleWinners] = useState([]); + const sliderRef = useRef(null); const [hoveredIndex, setHoveredIndex] = useState(null); - const [visibleWinner, setVisibleWinner] = useState(null); const { data: sessionData } = useSession(); const userId = String(sessionData?.accountId); - const restartAnimation = () => { - setAnimate(false); - setTimeout(() => { - setAnimate(true); - setVisibleWinner(null); // Ensure winner is reset on animation restart - }, 10); - }; - - useEffect(() => { - setAnimate(true); - }, []); - const { data, isLoading, isError } = useGetWkSimulationQuery({ wktId: id }); - if (isLoading || isError || !data) return; - const totalRange = data?.raffleMemberIndexInfos.reduce((max, member) => { - return Math.max(max, member.raffleIndex); - }, 0); + const totalRange = + data?.raffleMemberIndexInfos?.reduce((max, member) => { + return Math.max(max, member.raffleIndex); + }, 0) ?? 0; const getSliderWidth = (index: number) => { + if (!data) return 0; if (index === 0) { return (data.raffleMemberIndexInfos[0].raffleIndex / totalRange) * 100; } @@ -44,13 +33,86 @@ const Slider = ({ id }: { id: number }) => { const calculateLeftPosition = (index: number) => { let left = 0; - /* eslint-disable no-plusplus */ + // eslint-disable-next-line no-plusplus for (let i = 0; i < index; i++) { left += getSliderWidth(i); } return left; }; + const sortedRafflePickedIndexInfos = [ + ...(data?.rafflePickedIndexInfos || []), + ].sort((a, b) => a.pickedIndex - b.pickedIndex); + + const pausePositions = sortedRafflePickedIndexInfos.map((winner) => { + return { + position: (winner.pickedIndex / totalRange) * 100, + }; + }); + + const totalDuration = 30; + const holdTime = 2; + const runningTime = totalDuration - holdTime * pausePositions.length; + + const generateKeyframes = () => { + const keyframes: { [key: string]: { left: string } } = { + '0%': { left: '0%' }, + }; + + let totalPauseTime = 0; + + pausePositions.forEach((positionObj) => { + const positionKey = `${(((positionObj.position * runningTime) / 100 + totalPauseTime) / totalDuration) * 100}%`; + keyframes[positionKey] = { left: `${positionObj.position}%` }; + + totalPauseTime += holdTime; + const holdKey = `${(((positionObj.position * runningTime) / 100 + totalPauseTime) / totalDuration) * 100}%`; + keyframes[holdKey] = { left: `${positionObj.position}%` }; + }); + + keyframes['100%'] = { left: '100%' }; + return keyframes; + }; + + const triggerWinnerVisibility = () => { + let totalPauseTime = 0; + + pausePositions.forEach((_, index) => { + const delay = + (pausePositions[index].position * runningTime) / 100 + + totalPauseTime * 1000; + + setTimeout( + () => { + setVisibleWinners((prev) => [...prev, index]); + }, + delay + holdTime * 1000, + ); + + totalPauseTime += holdTime; + }); + }; + + const restartAnimation = () => { + setAnimate(false); + setVisibleWinners([]); + setTimeout(() => { + setAnimate(true); + triggerWinnerVisibility(); + }, 10); + }; + + useEffect(() => { + if (data && !isLoading && !isError) { + setAnimate(false); + setVisibleWinners([]); + setTimeout(() => { + setAnimate(true); + triggerWinnerVisibility(); + }, 10); + } + }, [data, isLoading, isError]); + const handleMouseEnter = (accountId: string, index: number) => { setHoveredAccount(accountId); setTooltipPosition({ @@ -64,21 +126,20 @@ const Slider = ({ id }: { id: number }) => { setHoveredIndex(null); }; - const handleMouseMove = (event) => { + const handleMouseMove = (event: React.MouseEvent) => { + if (!data || !sliderRef.current) return; + const rect = sliderRef.current.getBoundingClientRect(); - const xPos = event.clientX - rect.left; // Calculate mouse position within the slider - const relativePosition = (xPos / rect.width) * 100; // Convert to relative percentage position + const xPos = event.clientX - rect.left; + const relativePosition = (xPos / rect.width) * 100; - // Find the closest account based on mouse position percentage - let closestAccount = null; + let closestAccount: string | null = null; let minDiff = Infinity; data.raffleMemberIndexInfos.forEach((member, index) => { - // Calculate the start and end percentage positions for this index const startPercent = index === 0 ? 0 : calculateLeftPosition(index); const endPercent = startPercent + getSliderWidth(index); - // Check if the current mouse position falls within this range if (relativePosition >= startPercent && relativePosition < endPercent) { const diff = Math.min( Math.abs(startPercent - relativePosition), @@ -97,45 +158,10 @@ const Slider = ({ id }: { id: number }) => { } }; - // pausePositions 계산 로직 변경 - const pausePositions = data.rafflePickedIndexInfos - .map((winner) => { - return { - position: (winner.pickedIndex / totalRange) * 100, - accountId: winner.accountId, - pickedIndex: winner.pickedIndex, - }; - }) - .sort((a, b) => a.pickedIndex - b.pickedIndex) // pickedIndex를 기준으로 오름차순 정렬 - .map((winner) => winner.position); // 최종적으로 포지션만 추출 - - // 동적으로 키프레임 생성 - const generateKeyframes = () => { - const keyframes = { - '0%': { left: '0%' }, - }; - - let totalPauseTime = 0; - const totalDuration = 15; // 전체 애니메이션 시간 (초) - const holdTime = 2; // 각 위치에서 멈출 시간 (초 단위) - const runningTime = totalDuration - holdTime * pausePositions.length; // 멈추지 않는 동안의 총 시간 - - pausePositions.forEach((position, i) => { - const positionKey = `${(((position * runningTime) / 100 + totalPauseTime) / totalDuration) * 100}%`; - keyframes[positionKey] = { left: `${position}%` }; - - totalPauseTime += holdTime; - const holdKey = `${(((position * runningTime) / 100 + totalPauseTime) / totalDuration) * 100}%`; - keyframes[holdKey] = { left: `${position}%` }; - }); - - keyframes['100%'] = { left: '100%' }; - return keyframes; - }; - - // 동적으로 생성된 키프레임을 스타일 태그에 주입 const customKeyframes = generateKeyframes(); + if (isLoading || isError || !data) return null; + return (
{data.rafflePickedIndexInfos[0] && ( @@ -152,7 +178,7 @@ const Slider = ({ id }: { id: number }) => { />
@@ -172,41 +198,58 @@ const Slider = ({ id }: { id: number }) => { `}
- {data?.raffleMemberIndexInfos.map((member, index) => ( -
handleMouseEnter(member.accountId, index)} - onMouseLeave={handleMouseLeave} - > - {member.accountId === userId && ( -
- {userId} -
- )} - {hoveredIndex === index && ( -
- )} -
- ))} - {data?.rafflePickedIndexInfos.map((winner, index) => ( + {data?.raffleMemberIndexInfos.map((member, index) => { + const isWinner = visibleWinners.includes( + sortedRafflePickedIndexInfos.findIndex( + (winner) => winner.accountId === member.accountId, + ), + ); + + return ( +
+ handleMouseEnter(member.accountId, index) + } + onMouseLeave={handleMouseLeave} + > + {member.accountId === userId && ( +
+ {userId} +
+ )} + {hoveredIndex === index && ( +
+ )} +
+ ); + })} + {sortedRafflePickedIndexInfos.map((winner, index) => (
{ left: `${(winner.pickedIndex / totalRange) * 100}%`, top: '-40px', transform: 'translateX(-50%)', + display: visibleWinners.includes(index) ? 'block' : 'none', }} > 당첨 +
+
+
))} {hoveredAccount && ( @@ -226,7 +273,7 @@ const Slider = ({ id }: { id: number }) => { left: tooltipPosition.left, }} > - ID {hoveredAccount} + {hoveredAccount}
)}