diff --git a/frontend/src/components/main/LiveVideoCard.tsx b/frontend/src/components/main/LiveVideoCard.tsx index f7529cca..77a3b8cb 100644 --- a/frontend/src/components/main/LiveVideoCard.tsx +++ b/frontend/src/components/main/LiveVideoCard.tsx @@ -7,7 +7,7 @@ import ShowInfoBadge from '@common/ShowInfoBadge'; import { ASSETS } from '@constants/assets'; import { RecentLive } from '@type/live'; import { LiveBadge, LiveViewCountBadge } from './ThumbnailBadge'; -import usePreviewPlayer from '@hooks/usePreviewPlayer'; +import { useVideoPreview } from '@hooks/useVideoPreview'; interface LiveVideoCardProps { videoData: RecentLive; @@ -15,11 +15,6 @@ interface LiveVideoCardProps { const LiveVideoCard = ({ videoData }: LiveVideoCardProps) => { const navigate = useNavigate(); - const [isHovered, setIsHovered] = useState(false); - const [isVideoLoaded, setIsVideoLoaded] = useState(false); - const hoverTimeoutRef = useRef(null); - const thumbnailRef = useRef(null); - const [videoRef, playerController] = usePreviewPlayer(); const { concurrentUserCount, @@ -33,64 +28,15 @@ const LiveVideoCard = ({ videoData }: LiveVideoCardProps) => { streamUrl } = videoData; - useEffect(() => { - const video = videoRef.current; - if (!video) return; - - const handleLoadedData = () => { - setIsVideoLoaded(true); - }; - - video.addEventListener('loadeddata', handleLoadedData); - - return () => { - video.removeEventListener('loadeddata', handleLoadedData); - playerController.reset(); - }; - }, []); - - useEffect(() => { - const clearHoverTimeout = () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; - } - }; - - if (isHovered) { - clearHoverTimeout(); - hoverTimeoutRef.current = setTimeout(() => { - playerController.loadSource(streamUrl); - playerController.play(); - }, 400); - } else { - clearHoverTimeout(); - playerController.reset(); - } - - return clearHoverTimeout; - }, [isHovered, isVideoLoaded, streamUrl]); + const { isHovered, isVideoLoaded, videoRef, handleMouseEnter, handleMouseLeave } = useVideoPreview(streamUrl); const handleLiveClick = () => { navigate(`/live/${liveId}`); }; - const handleMouseEnter = () => { - setIsHovered(true); - }; - - const handleMouseLeave = () => { - setIsHovered(false); - }; - return ( - + diff --git a/frontend/src/components/main/ReplayVideoCard.tsx b/frontend/src/components/main/ReplayVideoCard.tsx index 2a4d2231..73734ee9 100644 --- a/frontend/src/components/main/ReplayVideoCard.tsx +++ b/frontend/src/components/main/ReplayVideoCard.tsx @@ -6,8 +6,8 @@ import { ReplayBadge, ReplayViewCountBadge } from './ThumbnailBadge'; import sampleProfile from '@assets/sample_profile.png'; import ShowInfoBadge from '@common/ShowInfoBadge'; import { ASSETS } from '@constants/assets'; -import usePlayer from '@hooks/usePlayer'; import { ReplayStream } from '@type/replay'; +import { useVideoPreview } from '@hooks/useVideoPreview'; interface ReplayVideoCardProps { videoData: ReplayStream; @@ -15,44 +15,10 @@ interface ReplayVideoCardProps { const ReplayVideoCard = ({ videoData }: ReplayVideoCardProps) => { const navigate = useNavigate(); - const [isHovered, setIsHovered] = useState(false); - const hoverTimeoutRef = useRef(null); const { category, channel, tags, thumbnailImageUrl, livePr, replayUrl, videoTitle, videoId } = videoData; - const videoRef = usePlayer(replayUrl); - - useEffect(() => { - const video = videoRef.current; - if (!video) return; - - const resetVideo = () => { - video.pause(); - video.currentTime = 0; - }; - - const playVideo = () => { - video.currentTime = 0; - video.play(); - }; - - const clearHoverTimeout = () => { - if (hoverTimeoutRef.current) { - clearTimeout(hoverTimeoutRef.current); - hoverTimeoutRef.current = null; - } - }; - - if (isHovered) { - hoverTimeoutRef.current = setTimeout(playVideo, 500); - return; - } - - clearHoverTimeout(); - resetVideo(); - - return clearHoverTimeout; - }, [isHovered]); + const { isHovered, isVideoLoaded, videoRef, handleMouseEnter, handleMouseLeave } = useVideoPreview(replayUrl); const handleReplayClick = () => { navigate(`/replay/${videoId}`); @@ -60,27 +26,25 @@ const ReplayVideoCard = ({ videoData }: ReplayVideoCardProps) => { return ( - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - + + - + + + - + - + profile - + {videoTitle} {channel.channelName} @@ -99,18 +63,17 @@ const ReplayVideoCard = ({ videoData }: ReplayVideoCardProps) => { export default ReplayVideoCard; const VideoCardContainer = styled.div` + position: relative; word-wrap: break-word; word-break: break-all; `; -const VideoCardThumbnail = styled.div` - background: #21242a url(${ASSETS.IMAGES.THUMBNAIL.DEFAULT}) no-repeat center center / cover; - overflow: hidden; - border-radius: 12px; - display: block; - padding-top: 56.25%; +const ThumbnailContainer = styled.div` position: relative; cursor: pointer; + padding-top: 56.25%; + border-radius: 12px; + overflow: hidden; `; const VideoBox = styled.div<{ $isVisible: boolean }>` @@ -120,7 +83,7 @@ const VideoBox = styled.div<{ $isVisible: boolean }>` width: 100%; height: 100%; opacity: ${(props) => (props.$isVisible ? 1 : 0)}; - transition: opacity 0.3s ease-in-out; + transition: opacity 0.3s ease-in-out 0.6s; z-index: 1; video { @@ -133,10 +96,19 @@ const VideoBox = styled.div<{ $isVisible: boolean }>` } `; -const VideoCardImage = styled.img` +const VideoCardThumbnail = styled.div<{ $isVideoVisible: boolean }>` position: absolute; top: 0; left: 0; + width: 100%; + height: 100%; + background: #21242a url(${ASSETS.IMAGES.THUMBNAIL.DEFAULT}) no-repeat center center / cover; + opacity: ${(props) => (props.$isVideoVisible ? 0 : 1)}; + transition: opacity 0.3s ease-in-out; + z-index: 2; +`; + +const VideoCardImage = styled.img` width: 100%; height: 100%; object-fit: cover; @@ -148,6 +120,7 @@ const VideoCardDescription = styled.div` left: 10px; display: flex; gap: 4px; + z-index: 3; `; const VideoCardWrapper = styled.div` @@ -160,9 +133,9 @@ const VideoCardProfile = styled.div` margin-right: 10px; background: ${({ theme }) => theme.tokenColors['surface-alt']} no-repeat 50% / cover; border-radius: 50%; - margin-top: 5px; display: block; overflow: hidden; + margin-top: 5px; width: 40px; height: 40px; @@ -181,6 +154,7 @@ const VideoCardArea = styled.div` ${({ theme }) => theme.tokenTypographys['display-bold16']} color: ${({ theme }) => theme.tokenColors['text-strong']}; margin-bottom: 8px; + cursor: pointer; } .video_card_name { ${({ theme }) => theme.tokenTypographys['display-medium14']} diff --git a/frontend/src/hooks/useVideoPreview/index.ts b/frontend/src/hooks/useVideoPreview/index.ts new file mode 100644 index 00000000..5adc4ff0 --- /dev/null +++ b/frontend/src/hooks/useVideoPreview/index.ts @@ -0,0 +1,71 @@ +import usePreviewPlayer from '@hooks/useVideoPreview/usePreviewPlayer'; +import { useEffect, useRef, useState } from 'react'; + +interface UseVideoPreviewReturn { + isHovered: boolean; + isVideoLoaded: boolean; + videoRef: React.RefObject; + handleMouseEnter: () => void; + handleMouseLeave: () => void; +} + +export const useVideoPreview = (url: string, hoverDelay: number = 400): UseVideoPreviewReturn => { + const [isHovered, setIsHovered] = useState(false); + const [isVideoLoaded, setIsVideoLoaded] = useState(false); + const hoverTimeoutRef = useRef(null); + const [videoRef, playerController] = usePreviewPlayer(); + + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + const handleLoadedData = () => { + setIsVideoLoaded(true); + }; + + video.addEventListener('loadeddata', handleLoadedData); + + return () => { + video.removeEventListener('loadeddata', handleLoadedData); + playerController.reset(); + }; + }, [playerController]); + + useEffect(() => { + const clearHoverTimeout = () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + hoverTimeoutRef.current = null; + } + }; + + if (isHovered) { + clearHoverTimeout(); + hoverTimeoutRef.current = setTimeout(() => { + playerController.loadSource(url); + playerController.play(); + }, hoverDelay); + } else { + clearHoverTimeout(); + playerController.reset(); + } + + return clearHoverTimeout; + }, [isHovered, isVideoLoaded, url, hoverDelay, playerController]); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + + return { + isHovered, + isVideoLoaded, + videoRef, + handleMouseEnter, + handleMouseLeave + }; +}; diff --git a/frontend/src/hooks/usePreviewPlayer.ts b/frontend/src/hooks/useVideoPreview/usePreviewPlayer.ts similarity index 100% rename from frontend/src/hooks/usePreviewPlayer.ts rename to frontend/src/hooks/useVideoPreview/usePreviewPlayer.ts