Skip to content

Commit

Permalink
fix: 프리뷰 플레이어 커스텀 훅 구현 및 적용
Browse files Browse the repository at this point in the history
  • Loading branch information
jsk3342 committed Dec 2, 2024
1 parent 644152e commit a0146e2
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 112 deletions.
60 changes: 3 additions & 57 deletions frontend/src/components/main/LiveVideoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@ 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;
}

const LiveVideoCard = ({ videoData }: LiveVideoCardProps) => {
const navigate = useNavigate();
const [isHovered, setIsHovered] = useState(false);
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const thumbnailRef = useRef<HTMLDivElement>(null);
const [videoRef, playerController] = usePreviewPlayer();

const {
concurrentUserCount,
Expand All @@ -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 (
<VideoCardContainer>
<ThumbnailContainer
ref={thumbnailRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleLiveClick}
>
<ThumbnailContainer onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleLiveClick}>
<VideoBox $isVisible={isHovered && isVideoLoaded}>
<video ref={videoRef} muted playsInline preload="none" />
</VideoBox>
Expand Down
84 changes: 29 additions & 55 deletions frontend/src/components/main/ReplayVideoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,81 +6,45 @@ 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;
}

const ReplayVideoCard = ({ videoData }: ReplayVideoCardProps) => {
const navigate = useNavigate();
const [isHovered, setIsHovered] = useState(false);
const hoverTimeoutRef = useRef<NodeJS.Timeout | null>(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}`);
};

return (
<VideoCardContainer>
<VideoCardThumbnail
onClick={handleReplayClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<VideoBox $isVisible={isHovered}>
<ThumbnailContainer onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleReplayClick}>
<VideoBox $isVisible={isHovered && isVideoLoaded}>
<video ref={videoRef} muted playsInline />
</VideoBox>
<VideoCardImage src={thumbnailImageUrl} />
<VideoCardThumbnail $isVideoVisible={isHovered && isVideoLoaded}>
<VideoCardImage src={thumbnailImageUrl} />
</VideoCardThumbnail>
<VideoCardDescription>
<ReplayBadge />
<ReplayViewCountBadge count={livePr} />
</VideoCardDescription>
</VideoCardThumbnail>
</ThumbnailContainer>

<VideoCardWrapper>
<VideoCardProfile>
<img src={sampleProfile} />
<img src={sampleProfile} alt="profile" />
</VideoCardProfile>
<VideoCardArea>
<span className="video_card_title" style={{ cursor: 'pointer' }} onClick={handleReplayClick}>
<span className="video_card_title" onClick={handleReplayClick}>
{videoTitle}
</span>
<span className="video_card_name">{channel.channelName}</span>
Expand All @@ -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 }>`
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -148,6 +120,7 @@ const VideoCardDescription = styled.div`
left: 10px;
display: flex;
gap: 4px;
z-index: 3;
`;

const VideoCardWrapper = styled.div`
Expand All @@ -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;
Expand All @@ -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']}
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/hooks/useVideoPreview/index.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLVideoElement>;
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<NodeJS.Timeout | null>(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]);

Check warning on line 32 in frontend/src/hooks/useVideoPreview/index.ts

View workflow job for this annotation

GitHub Actions / build (frontend)

React Hook useEffect has a missing dependency: 'videoRef'. Either include it or remove the dependency array

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
};
};
File renamed without changes.

0 comments on commit a0146e2

Please sign in to comment.