Skip to content

Commit

Permalink
Merge pull request #263 from boostcampwm-2024/feature-FE-#248-Main_st…
Browse files Browse the repository at this point in the history
…eam_fix

[FIX] 메인 페이지 네트워크 요청 최적화
  • Loading branch information
spearStr authored Dec 2, 2024
2 parents 1cf5300 + f742ffa commit b01b96e
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 205 deletions.
2 changes: 1 addition & 1 deletion frontend/src/apis/queries/main/useFetchMainLive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export const useMainLive = () => {
return useSuspenseQuery<MainLive[], Error>({
queryKey: ['mainLive'],
queryFn: fetchMainLive,
refetchOnWindowFocus: false,
refetchOnWindowFocus: false
});
};
81 changes: 21 additions & 60 deletions frontend/src/components/main/LiveVideoCard.tsx
Original file line number Diff line number Diff line change
@@ -1,85 +1,45 @@
import { useNavigate } from 'react-router-dom';
import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';

import sampleProfile from '@assets/sample_profile.png';
import ShowInfoBadge from '@common/ShowInfoBadge';
import { ASSETS } from '@constants/assets';
import { RecentLive } from '@type/live';
import { LiveBadge, LiveViewCountBadge } from './ThumbnailBadge';
import usePlayer from '@hooks/usePlayer';
import { useVideoPreview } from '@hooks/useVideoPreview';

interface LiveVideoCardProps {
videoData: RecentLive;
}

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

const { concurrentUserCount, category, channel, tags, defaultThumbnailImageUrl, liveId, liveImageUrl, liveTitle, streamUrl } =
videoData;

const videoRef = usePlayer(streamUrl);

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 handleLiveClick = () => {
navigate(`/live/${liveId}`);
};
const {
concurrentUserCount,
category,
channel,
tags,
defaultThumbnailImageUrl,
liveId,
liveImageUrl,
liveTitle,
streamUrl
} = videoData;

const handleMouseEnter = () => {
setIsHovered(true);
};
const { isHovered, isVideoLoaded, videoRef, handleMouseEnter, handleMouseLeave } = useVideoPreview(streamUrl);

const handleMouseLeave = () => {
setIsHovered(false);
const handleLiveClick = () => {
navigate(`/live/${liveId}`);
};

return (
<VideoCardContainer>
<ThumbnailContainer
ref={thumbnailRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onClick={handleLiveClick}
>
<VideoBox $isVisible={isHovered}>
<video ref={videoRef} muted playsInline />
<ThumbnailContainer onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleLiveClick}>
<VideoBox $isVisible={isHovered && isVideoLoaded}>
<video ref={videoRef} muted playsInline preload="none" />
</VideoBox>
<VideoCardThumbnail $isVideoVisible={isHovered}>
<VideoCardThumbnail $isVideoVisible={isHovered && isVideoLoaded}>
<VideoCardImage src={defaultThumbnailImageUrl ?? liveImageUrl} />
</VideoCardThumbnail>
<VideoCardDescription>
Expand Down Expand Up @@ -132,7 +92,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 Down Expand Up @@ -203,6 +163,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
52 changes: 19 additions & 33 deletions frontend/src/components/main/MiniPlayerItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { memo, useCallback, useMemo } from 'react';
import styled from 'styled-components';
import { MainLive } from '@type/live';

Expand All @@ -9,39 +8,26 @@ interface MiniPlayerItemProps {
isSelected: boolean;
}

const MiniPlayerItem = memo<MiniPlayerItemProps>(
({ item, onMouseEnter, index, isSelected }) => {
const handleMouseEnter = useCallback(() => {
onMouseEnter(index);
}, [onMouseEnter, index]);
const MiniPlayerItem = ({ item, onMouseEnter, index, isSelected }: MiniPlayerItemProps) => {
const thumbnailUrl = item.defaultThumbnailImageUrl ?? item.liveImageUrl;

const thumbnailUrl = useMemo(
() => item.defaultThumbnailImageUrl ?? item.liveImageUrl,
[item.defaultThumbnailImageUrl, item.liveImageUrl]
);

return (
<MiniPlayerItemStyled role="none" onMouseEnter={handleMouseEnter}>
<Thumbnail role="tab" aria-selected={isSelected}>
<ThumbnailWrapper $backgroundUrl={thumbnailUrl} />
</Thumbnail>
<TooltipContent>
<Title>{item.liveTitle}</Title>
<StreamerName>{item.channel.channelName}</StreamerName>
</TooltipContent>
</MiniPlayerItemStyled>
);
},
(prevProps, nextProps) => {
return (
prevProps.isSelected === nextProps.isSelected &&
prevProps.item.liveId === nextProps.item.liveId &&
prevProps.index === nextProps.index
);
}
);

MiniPlayerItem.displayName = 'MiniPlayerItem';
return (
<MiniPlayerItemStyled
role="none"
onMouseEnter={() => {
onMouseEnter(index);
}}
>
<Thumbnail role="tab" aria-selected={isSelected}>
<ThumbnailWrapper $backgroundUrl={thumbnailUrl} />
</Thumbnail>
<TooltipContent>
<Title>{item.liveTitle}</Title>
<StreamerName>{item.channel.channelName}</StreamerName>
</TooltipContent>
</MiniPlayerItemStyled>
);
};

export default MiniPlayerItem;

Expand Down
63 changes: 8 additions & 55 deletions frontend/src/components/main/RecommendLive.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,20 @@
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import styled from 'styled-components';

import AnimatedProfileSection from './AnimatedProfileSection';
import AnimatedLiveHeader from './AnimatedLiveHeader';
import RecommendList from './RecommendList';
import sampleProfile from '@assets/sample_profile.png';
import { RECOMMEND_LIVE } from '@constants/recommendLive';
import useRotatingPlayer from '@hooks/useRotatePlayer';
import { useMainLive } from '@queries/main/useFetchMainLive';
import useMainLiveRotation from '@hooks/useMainLiveRotation';

import AnimatedProfileSection from './AnimatedProfileSection';
import AnimatedLiveHeader from './AnimatedLiveHeader';
import RecommendList from './RecommendList';

const RecommendLive = () => {
const navigate = useNavigate();
const { videoRef, initPlayer } = useRotatingPlayer();
const { data: mainLiveData } = useMainLive();
const [currentUrlIndex, setCurrentUrlIndex] = useState(0);
const recommendListRef = useRef<HTMLDivElement>(null);
const [isTransitioning, setIsTransitioning] = useState(false);
const prevUrlIndexRef = useRef(currentUrlIndex);
const isInitialMount = useRef(true);

useEffect(() => {
if (!mainLiveData) return;
if (!mainLiveData[currentUrlIndex]) return;

const handleTransition = async () => {
const videoUrl = mainLiveData[currentUrlIndex].streamUrl;

if (isInitialMount.current) {
initPlayer(videoUrl);
setTimeout(() => {
isInitialMount.current = false;
}, 100);
return;
}

if (prevUrlIndexRef.current !== currentUrlIndex) {
setIsTransitioning(true);
await new Promise((resolve) => setTimeout(resolve, 200));

initPlayer(videoUrl);
prevUrlIndexRef.current = currentUrlIndex;

setTimeout(() => {
setIsTransitioning(false);
}, 100);
}
};

handleTransition();
}, [mainLiveData, currentUrlIndex, initPlayer]);

const onSelect = useCallback((index: number) => {
setCurrentUrlIndex(index);
}, []);

const currentLiveData = useMemo(() => mainLiveData?.[currentUrlIndex], [mainLiveData, currentUrlIndex]);

const { data: mainLiveData } = useMainLive();
const { currentLiveData, isTransitioning, videoRef, onSelect } = useMainLiveRotation(mainLiveData);
const { liveId, liveTitle, concurrentUserCount, channel, category } = currentLiveData;

return (
Expand All @@ -68,12 +26,7 @@ const RecommendLive = () => {
<AnimatedLiveHeader concurrentUserCount={concurrentUserCount} liveTitle={liveTitle} />
<RecommendLiveInformation>
<AnimatedProfileSection channel={channel} category={category} profileImage={sampleProfile} />
<RecommendList
ref={recommendListRef}
mainLiveData={mainLiveData}
onSelect={onSelect}
currentLiveId={liveId}
/>
<RecommendList mainLiveData={mainLiveData} onSelect={onSelect} currentLiveId={liveId} />
</RecommendLiveInformation>
</RecommendLiveWrapper>
</RecommendLiveContainer>
Expand Down
Loading

0 comments on commit b01b96e

Please sign in to comment.