From 0af3a3ea45613a48fd2a3c2b7391cc169ef4c530 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 16 Sep 2025 18:03:28 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20PlaylistInfo=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/discover/playlist/index.tsx | 77 ++---------------------- src/pages/mycd/playlist/index.tsx | 77 ++---------------------- src/widgets/playlist/PlaylistInfo.tsx | 84 +++++++++++++++++++++++++++ src/widgets/playlist/index.ts | 1 + 4 files changed, 95 insertions(+), 144 deletions(-) create mode 100644 src/widgets/playlist/PlaylistInfo.tsx diff --git a/src/pages/discover/playlist/index.tsx b/src/pages/discover/playlist/index.tsx index 08c3acec..1588d8e9 100644 --- a/src/pages/discover/playlist/index.tsx +++ b/src/pages/discover/playlist/index.tsx @@ -1,82 +1,13 @@ -import { useNavigate, useParams } from 'react-router-dom' +import { useParams } from 'react-router-dom' -import styled from 'styled-components' - -import { Cancel } from '@/assets/icons' import { usePlaylistDetail } from '@/entities/playlist' -import { getGenreLabel } from '@/shared/lib' -import { flexColCenter } from '@/shared/styles/mixins' -import { Error, Header, Link, Loading, SvgButton } from '@/shared/ui' -import { PlaylistHorizontal } from '@/widgets/playlist' +import { PlaylistInfo } from '@/widgets/playlist' const PlaylistInfoPage = () => { - const navigate = useNavigate() - const { id } = useParams<{ id: string }>() + const { data, isLoading, isError } = usePlaylistDetail(Number(id)) - const { data: playlistData, isLoading, isError } = usePlaylistDetail(Number(id)) - - if (isError || !playlistData) { - return ( - - - - ) - } - - if (isLoading) { - return ( - - - - ) - } - - return ( - -
플레이리스트} - right={ navigate(-1)} />} - /> - - - - {playlistData.songs && - playlistData.songs.map((track, index) => ( - - ))} - - - - ) + return } export default PlaylistInfoPage - -const Wrapper = styled.div` - ${flexColCenter} -` - -const Content = styled.section` - display: flex; - flex-direction: column; - width: 100%; - gap: 28px; -` -const TrackInfo = styled.div` - display: flex; - flex-direction: column; - gap: 20px; -` - -const NoDataWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 60dvh; -` diff --git a/src/pages/mycd/playlist/index.tsx b/src/pages/mycd/playlist/index.tsx index 0ef36dca..d778635a 100644 --- a/src/pages/mycd/playlist/index.tsx +++ b/src/pages/mycd/playlist/index.tsx @@ -1,79 +1,14 @@ -import { useLocation, useNavigate } from 'react-router-dom' +import { useLocation } from 'react-router-dom' -import styled from 'styled-components' - -import { Cancel } from '@/assets/icons' import { usePlaylistDetail } from '@/entities/playlist' -import { flexColCenter } from '@/shared/styles/mixins' -import { Header, Link, Loading, SvgButton, Error } from '@/shared/ui' -import { PlaylistHorizontal } from '@/widgets/playlist' +import PlaylistInfo from '@/widgets/playlist/PlaylistInfo' -const MyPlaylistInfoPage = () => { - const navigate = useNavigate() +const MyCdInfoPage = () => { const location = useLocation() const { playlistId } = location.state as { playlistId: number } + const { data, isLoading, isError } = usePlaylistDetail(Number(playlistId)) - const { data: playlistData, isLoading, isError } = usePlaylistDetail(Number(playlistId)) - - if (isError || !playlistData) { - return ( - - - - ) - } - - if (isLoading) { - return ( - - - - ) - } - - return ( - -
플레이리스트} - right={ navigate(-1)} />} - /> - - - - {playlistData.songs && - playlistData.songs.map((track, index) => ( - - ))} - - - - ) + return } -export default MyPlaylistInfoPage - -const Wrapper = styled.div` - ${flexColCenter} -` - -const Content = styled.section` - display: flex; - flex-direction: column; - gap: 28px; -` -const TrackInfo = styled.div` - display: flex; - flex-direction: column; - gap: 20px; -` - -const NoDataWrapper = styled.div` - display: flex; - justify-content: center; - align-items: center; - height: 60dvh; -` +export default MyCdInfoPage diff --git a/src/widgets/playlist/PlaylistInfo.tsx b/src/widgets/playlist/PlaylistInfo.tsx new file mode 100644 index 00000000..ac57db1b --- /dev/null +++ b/src/widgets/playlist/PlaylistInfo.tsx @@ -0,0 +1,84 @@ +import { useNavigate } from 'react-router-dom' + +import styled from 'styled-components' + +import { Cancel } from '@/assets/icons' +import type { PlaylistDetailResponse } from '@/entities/playlist' +import { getGenreLabel } from '@/shared/lib' +import { flexColCenter } from '@/shared/styles/mixins' +import { Error, Header, Link, Loading, SvgButton } from '@/shared/ui' +import { PlaylistHorizontal } from '@/widgets/playlist' + +interface PlaylistInfoProps { + playlistData?: PlaylistDetailResponse + isLoading: boolean + isError: boolean +} + +const PlaylistInfo = ({ playlistData, isLoading, isError }: PlaylistInfoProps) => { + const navigate = useNavigate() + + if (isError || !playlistData) { + return ( + + + + ) + } + + if (isLoading) { + return ( + + + + ) + } + + return ( + +
플레이리스트} + right={ navigate(-1)} />} + /> + + + + {playlistData.songs && + playlistData.songs.map((track, index) => ( + + ))} + + + + ) +} + +export default PlaylistInfo + +const Wrapper = styled.div` + ${flexColCenter} +` + +const Content = styled.section` + display: flex; + flex-direction: column; + width: 100%; + gap: 28px; +` +const TrackInfo = styled.div` + display: flex; + flex-direction: column; + gap: 20px; +` + +const NoDataWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 60dvh; +` diff --git a/src/widgets/playlist/index.ts b/src/widgets/playlist/index.ts index 653dea8d..061b59e5 100644 --- a/src/widgets/playlist/index.ts +++ b/src/widgets/playlist/index.ts @@ -6,3 +6,4 @@ export { default as ControlBar } from './ControlBar' export { default as PlaylistHorizontal } from './PlaylistHorizontal' export { default as PlaylistLayout } from './PlaylistLayout' export { default as YoutubePlayer } from './YoutubePlayer' +export { default as PlaylistInfo } from './PlaylistInfo' From e58de60ca1a1e162b62b03842daca55bfca7bbad Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 16 Sep 2025 18:08:10 +0900 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20Title=20width=EB=A5=BC=20100%?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Link.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/shared/ui/Link.tsx b/src/shared/ui/Link.tsx index 5ae5e7b8..90ab994b 100644 --- a/src/shared/ui/Link.tsx +++ b/src/shared/ui/Link.tsx @@ -46,6 +46,7 @@ const Thumbnail = styled.div` border-radius: 4px; display: flex; + flex-shrink: 0; justify-content: center; align-items: center; ` @@ -56,7 +57,7 @@ const Title = styled.p<{ $variant?: 'large' | 'small' }>` -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; - width: ${({ $variant }) => ($variant === 'large' ? '245px' : '128px')}; + width: 100%; color: ${({ theme }) => theme.COLOR['gray-50']}; ${({ $variant, theme }) => From 610304da6a2997c2d8ec1b3b86c238320e3f4dec Mon Sep 17 00:00:00 2001 From: gahyeon Date: Tue, 16 Sep 2025 18:49:40 +0900 Subject: [PATCH 3/7] =?UTF-8?q?chore:=20main=20=EB=B8=8C=EB=9E=9C=EC=B9=98?= =?UTF-8?q?=EB=A5=BC=20=ED=94=84=EB=A1=9C=EB=8D=95=EC=85=98=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=EB=A1=9C=20=EC=84=A4=EC=A0=95=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vercel.json b/vercel.json index 3a48e56b..88ab7c9f 100644 --- a/vercel.json +++ b/vercel.json @@ -1,3 +1,6 @@ { + "github": { + "productionBranch": "main" + }, "rewrites": [{ "source": "/(.*)", "destination": "/" }] } From 5d6988a8fc9452d135b2a4ccbd2be8ef82d0c4d6 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Wed, 17 Sep 2025 19:56:25 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20=ED=8A=B8=EB=9E=99=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=20=EB=A1=9C=EC=A7=81=EC=9D=84=20Provider=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=EB=A1=9C=20=ED=86=B5=ED=95=A9=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/providers/PlayerProvider.tsx | 75 ++++++++++++++++++------ src/pages/discover/index.tsx | 86 +++++++--------------------- src/pages/mycd/index.tsx | 62 ++++++-------------- 3 files changed, 94 insertions(+), 129 deletions(-) diff --git a/src/app/providers/PlayerProvider.tsx b/src/app/providers/PlayerProvider.tsx index 0f485afa..7ef96edd 100644 --- a/src/app/providers/PlayerProvider.tsx +++ b/src/app/providers/PlayerProvider.tsx @@ -1,4 +1,12 @@ -import { createContext, useState, useContext, type ReactNode } from 'react' +import { + createContext, + useState, + useContext, + useRef, + useEffect, + useCallback, + type ReactNode, +} from 'react' import type { PlaylistInfo } from '@/entities/playlist' @@ -13,6 +21,8 @@ type PlaylistContextType = { nextTrack: () => void prevTrack: () => void updateCurrentTime: (time: number) => void + playerRef: React.MutableRefObject + handlePlayerStateChange: (event: YT.OnStateChangeEvent) => void } interface PlaylistProviderProps { @@ -23,37 +33,66 @@ const PlaylistContext = createContext(undefined const PlaylistProvider = ({ children }: PlaylistProviderProps) => { const [currentPlaylist, setCurrentPlaylist] = useState(null) - const [currentTrackIndex, setCurrentTrackIndex] = useState(0) - const [isPlaying, setIsPlaying] = useState(false) - const [currentTime, setCurrentTime] = useState(0) + const [currentTrackIndex, setCurrentTrackIndex] = useState(0) + const [currentTime, setCurrentTime] = useState(0) + const [isPlaying, setIsPlaying] = useState(false) + + const playerRef = useRef(null) + + // 1초마다 재생 시간 자동 업데이트 + useEffect(() => { + const intervalId = setInterval(() => { + if (playerRef.current) setCurrentTime(playerRef.current.getCurrentTime()) + }, 1000) + return () => clearInterval(intervalId) + }, []) const setPlaylist = (playlist: PlaylistInfo, trackIndex?: number, time?: number) => { setCurrentPlaylist(playlist) if (trackIndex !== undefined) setCurrentTrackIndex(trackIndex) if (time !== undefined) setCurrentTime(time) setIsPlaying(true) + + if (playerRef.current) { + if (time !== undefined) playerRef.current.seekTo(time, true) + playerRef.current.playVideo() + } } - const play = () => setIsPlaying(true) - const pause = () => setIsPlaying(false) + const play = useCallback(() => { + if (playerRef.current) playerRef.current.playVideo() + setIsPlaying(true) + }, []) + + const pause = useCallback(() => { + if (playerRef.current) playerRef.current.pauseVideo() + setIsPlaying(false) + }, []) - const nextTrack = () => { + const nextTrack = useCallback(() => { if (currentPlaylist && currentTrackIndex < currentPlaylist.songs.length - 1) { - setCurrentTrackIndex((prevIndex) => prevIndex + 1) + setCurrentTrackIndex((prev) => prev + 1) setCurrentTime(0) + if (playerRef.current) playerRef.current.seekTo(0, true) } - } + }, [currentPlaylist, currentTrackIndex]) - const prevTrack = () => { + const prevTrack = useCallback(() => { if (currentTrackIndex > 0) { - setCurrentTrackIndex((prevIndex) => prevIndex - 1) + setCurrentTrackIndex((prev) => prev - 1) setCurrentTime(0) + if (playerRef.current) playerRef.current.seekTo(0, true) } - } + }, [currentTrackIndex]) - const updateCurrentTime = (time: number) => { - setCurrentTime(time) - } + const updateCurrentTime = (time: number) => setCurrentTime(time) + + const handlePlayerStateChange = useCallback( + (event: YT.OnStateChangeEvent) => { + if (event.data === window.YT.PlayerState.ENDED) nextTrack() + }, + [nextTrack] + ) const value = { currentPlaylist, @@ -66,6 +105,8 @@ const PlaylistProvider = ({ children }: PlaylistProviderProps) => { nextTrack, prevTrack, updateCurrentTime, + playerRef, + handlePlayerStateChange, } return {children} @@ -75,8 +116,6 @@ export default PlaylistProvider export const usePlaylist = () => { const context = useContext(PlaylistContext) - if (context === undefined) { - throw new Error('usePlaylist must be used within a PlaylistProvider') - } + if (!context) throw new Error('usePlaylist must be used within a PlaylistProvider') return context } diff --git a/src/pages/discover/index.tsx b/src/pages/discover/index.tsx index 42a5c150..089cf211 100644 --- a/src/pages/discover/index.tsx +++ b/src/pages/discover/index.tsx @@ -1,6 +1,5 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' -import type { YouTubeEvent } from 'react-youtube' import styled from 'styled-components' @@ -20,18 +19,19 @@ import { PlaylistLayout, YoutubePlayer } from '@/widgets/playlist' const DiscoverPage = () => { const { id: playlistId } = useParams() const { - setPlaylist, - isPlaying, currentPlaylist, currentTrackIndex, - nextTrack, - prevTrack, + currentTime, + isPlaying, + playerRef, + setPlaylist, play, pause, - currentTime, - updateCurrentTime, + nextTrack, + prevTrack, + handlePlayerStateChange, } = usePlaylist() - const playerRef = useRef(null) + const [showCoachmark, setShowCoachmark] = useState(false) const [isMuted, setIsMuted] = useState(null) @@ -96,8 +96,6 @@ const DiscoverPage = () => { return [playlistAsInfo, ...shufflePlaylists] }, [playlistAsInfo, shufflePlaylists]) - console.log(playlistsData) - const videoId = currentPlaylist ? getVideoId(currentPlaylist.songs[currentTrackIndex]?.youtubeUrl) : null @@ -109,11 +107,11 @@ const DiscoverPage = () => { if (!currentPlaylist && playlistsData.length > 0 && isReady) { const initialPlaylist = playlistsData.find((p) => p.playlistId === Number(playlistId)) || playlistsData[0] - setPlaylist(initialPlaylist, 0, 0) } - }, [playlistsData, currentPlaylist, playlistId, setPlaylist]) + }, [playlistsData, currentPlaylist, playlistId, setPlaylist, isReady]) + // 재생, 확인, 조회수 refetch useEffect(() => { if (!currentPlaylist || !isPlaying) return startPlaylist(currentPlaylist.playlistId) @@ -135,21 +133,6 @@ const DiscoverPage = () => { } }, [currentPlaylist, isPlaying, startPlaylist, confirmPlaylist, refetchViewCounts]) - // 현재 재생 시간 업데이트 - useEffect(() => { - const intervalId = setInterval(() => { - if (playerRef.current) updateCurrentTime(playerRef.current.getCurrentTime()) - }, 1000) - return () => clearInterval(intervalId) - }, [updateCurrentTime]) - - // 재생, 일시정지 - useEffect(() => { - if (!playerRef.current) return - if (isPlaying) playerRef.current.playVideo() - else playerRef.current.pauseVideo() - }, [isPlaying]) - // 캐러셀 스와이프 시 현재 플레이리스트 업데이트 const handleSelectPlaylist = useCallback( (index: number) => { @@ -162,26 +145,9 @@ const DiscoverPage = () => { fetchNextPage() } }, - [setPlaylist, currentPlaylist, playlistsData, fetchNextPage, hasNextPage, isFetchingNextPage] + [playlistsData, currentPlaylist, setPlaylist, hasNextPage, isFetchingNextPage, fetchNextPage] ) - const handlePlayerStateChange = useCallback( - (event: YouTubeEvent) => { - if (event.data === window.YT.PlayerState.ENDED) nextTrack() - }, - [nextTrack] - ) - - const handlePlayPause = () => { - if (isPlaying) pause() - else play() - } - - const isCurrentlyPlaying = (() => { - if (!window.YT || !playerRef.current) return false - return isPlaying && playerRef.current.getPlayerState() === window.YT.PlayerState.PLAYING - })() - return ( {showCoachmark && } @@ -189,21 +155,16 @@ const DiscoverPage = () => { {playlistsData.map((data) => ( (isPlaying ? pause() : play())} onNext={nextTrack} onPrev={prevTrack} onSelectTrack={(trackIndex, time) => { - if (currentPlaylist) { - setPlaylist(currentPlaylist, trackIndex) - if (time !== undefined) playerRef.current?.seekTo(time, true) - if (!isPlaying) play() - } + if (currentPlaylist) setPlaylist(currentPlaylist, trackIndex, time) }} playerRef={playerRef} isMuted={isMuted} @@ -212,22 +173,16 @@ const DiscoverPage = () => { ))} - {!showCoachmark && videoId && ( + + {videoId && ( { playerRef.current = event.target - - // 현 상태 참조해서 동기화 + playerRef.current?.seekTo(currentTime, true) + if (isPlaying) playerRef.current?.playVideo() + else playerRef.current?.pauseVideo() setIsMuted(playerRef.current?.isMuted() ?? null) - - if (isPlaying) { - playerRef.current?.seekTo(currentTime, true) - playerRef.current?.playVideo() - } else { - playerRef.current?.seekTo(currentTime, true) - playerRef.current?.pauseVideo() - } }} onStateChange={handlePlayerStateChange} /> @@ -241,7 +196,6 @@ export default DiscoverPage const Slide = styled.div` flex: 0 0 100%; ` - const Page = styled.div` position: relative; ` diff --git a/src/pages/mycd/index.tsx b/src/pages/mycd/index.tsx index 29f3445b..ff8bff03 100644 --- a/src/pages/mycd/index.tsx +++ b/src/pages/mycd/index.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useCallback, useState } from 'react' +import { useEffect, useCallback, useState } from 'react' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -24,10 +24,9 @@ const MyCdPage = () => { play, pause, currentTime, - updateCurrentTime, + playerRef, } = usePlaylist() - const playerRef = useRef(null) - const [isMuted, setIsMuted] = useState(null) // TODO : 모바일 대응용 버튼으로 수정 + const [isMuted, setIsMuted] = useState(null) const { userInfo } = useAuthStore() const navigate = useNavigate() @@ -35,38 +34,23 @@ const MyCdPage = () => { useEffect(() => { if (!currentPlaylist && playlistData && userInfo) { - const { playlistId, playlistName, songs, genre } = playlistData // 필수 값 체크 - - if (playlistId && playlistName && songs) { - const convertedPlaylist = { - creator: { - creatorId: userInfo.userId, - creatorNickname: userInfo.username, - }, - playlistId, - playlistName, - genre, - songs, - representative: false, - cdItems: playlistData.onlyCdResponse?.cdItems || [], - } - - setPlaylist(convertedPlaylist, currentTrackIndex, currentTime) + const convertedPlaylist = { + creator: { + creatorId: userInfo.userId, + creatorNickname: userInfo.username, + }, + playlistId: playlistData.playlistId, + playlistName: playlistData.playlistName, + genre: playlistData.genre, + songs: playlistData.songs, + representative: false, + cdItems: playlistData.onlyCdResponse?.cdItems || [], } + + setPlaylist(convertedPlaylist, currentTrackIndex, currentTime) } }, [currentPlaylist, playlistData, userInfo, setPlaylist, currentTrackIndex, currentTime]) - // 현재 재생 시간 업데이트 - useEffect(() => { - const intervalId = setInterval(() => { - if (playerRef.current) { - updateCurrentTime(playerRef.current.getCurrentTime()) - } - }, 1000) - - return () => clearInterval(intervalId) - }, [updateCurrentTime]) - const handlePlayerStateChange = useCallback( (event: YT.OnStateChangeEvent) => { if (event.data === window.YT.PlayerState.ENDED) nextTrack() @@ -78,18 +62,6 @@ const MyCdPage = () => { ? getVideoId(currentPlaylist.songs[currentTrackIndex]?.youtubeUrl) : null - // 재생, 일시정지 - useEffect(() => { - if (!playerRef.current) return - if (isPlaying) playerRef.current.playVideo() - else playerRef.current.pauseVideo() - }, [isPlaying]) - - const handlePlayPause = () => { - if (isPlaying) pause() - else play() - } - const isCurrentlyPlaying = (() => { if (!window.YT || !playerRef.current) return false return isPlaying && playerRef.current.getPlayerState() === window.YT.PlayerState.PLAYING @@ -116,7 +88,7 @@ const MyCdPage = () => { currentTrackIndex={currentTrackIndex} currentTime={currentTime} isPlaying={isCurrentlyPlaying} - onPlayPause={handlePlayPause} + onPlayPause={() => (isPlaying ? pause() : play())} onNext={nextTrack} onPrev={prevTrack} onSelectTrack={(trackIndex, time) => { From c9450ba1b97a9283dcaa0f1d8c5378a28442bd0e Mon Sep 17 00:00:00 2001 From: gahyeon Date: Thu, 18 Sep 2025 02:40:53 +0900 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20mock=20data=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/discover/commentData.json | 50 ----------------------------- src/pages/mycd/myPlaylist.json | 40 ----------------------- 2 files changed, 90 deletions(-) delete mode 100644 src/pages/discover/commentData.json delete mode 100644 src/pages/mycd/myPlaylist.json diff --git a/src/pages/discover/commentData.json b/src/pages/discover/commentData.json deleted file mode 100644 index ec751c8e..00000000 --- a/src/pages/discover/commentData.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "id": 1, - "userId": 101, - "userName": "가현", - "content": "코멘트 테스트!", - "profileImg": "/mock/profile1.png", - "role": "owner" - }, - { - "id": 2, - "userId": 101, - "userName": "가현", - "content": "노래가 정말정말정말정말정말정말정말정말정말정말 좋아요 🥴🥴🥴🥴 ", - "profileImg": "/mock/profile1.png", - "role": "owner" - }, - { - "id": 3, - "userId": 101, - "userName": "가현", - "content": "코멘트 테스트!", - "profileImg": "/mock/profile1.png", - "role": "owner" - }, - { - "id": 4, - "userId": 101, - "userName": "가현", - "content": "코멘트 테스트!", - "profileImg": "/mock/profile1.png", - "role": "owner" - }, - { - "id": 5, - "userId": 101, - "userName": "가현", - "content": "코멘트 테스트!", - "profileImg": "/mock/profile1.png", - "role": "owner" - }, - { - "id": 6, - "userId": 101, - "userName": "가현", - "content": "코멘트 테스트!", - "profileImg": "/mock/profile1.png", - "role": "owner" - } -] diff --git a/src/pages/mycd/myPlaylist.json b/src/pages/mycd/myPlaylist.json deleted file mode 100644 index 8bfe7f06..00000000 --- a/src/pages/mycd/myPlaylist.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": 1, - "title": "mycd playlist", - "tracks": [ - { - "id": 1, - "title": "첫 번째 곡", - "youtubeUrl": "https://www.youtube.com/watch?v=9--KAnocsTY", - "youtubeThumbnail": "", - "youtubeLength": 10 - }, - { - "id": 2, - "title": "두 번째 곡", - "youtubeUrl": "https://www.youtube.com/watch?v=SC4xMk98Pdc&list=RDSC4xMk98Pdc&start_radio=1", - "youtubeThumbnail": "", - "youtubeLength": 226 - }, - { - "id": 3, - "title": "세 번째 곡", - "youtubeUrl": "https://www.youtube.com/watch?v=BYwF8ODtpvo&list=RDBYwF8ODtpvo&start_radio=1", - "youtubeThumbnail": "", - "youtubeLength": 209 - }, - { - "id": 4, - "title": "네 번째 곡", - "youtubeUrl": "https://www.youtube.com/watch?v=ia2Ph61bYzc&list=RDia2Ph61bYzc&start_radio=1", - "youtubeThumbnail": "", - "youtubeLength": 184 - } - ], - "listeners": 123, - "isOnAir": false, - "liked": false, - "genre": "K-POP", - "username": "김들락", - "userId": 12344 -} From 2ea1fc6107c9fe4a11a106af399fcad14747d0fd Mon Sep 17 00:00:00 2001 From: gahyeon Date: Fri, 19 Sep 2025 01:41:45 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20=EB=88=84=EC=A0=81=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81=20=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/lib/getAccTime.ts | 3 +++ src/shared/lib/index.ts | 1 + src/widgets/playlist/PlaylistLayout.tsx | 15 +++------------ src/widgets/playlist/ProgressBar.tsx | 9 ++++++--- 4 files changed, 13 insertions(+), 15 deletions(-) create mode 100644 src/shared/lib/getAccTime.ts diff --git a/src/shared/lib/getAccTime.ts b/src/shared/lib/getAccTime.ts new file mode 100644 index 00000000..2a1d2896 --- /dev/null +++ b/src/shared/lib/getAccTime.ts @@ -0,0 +1,3 @@ +export const getAccTime = (trackLengths: number[], trackIndex: number, currentTime: number) => { + return trackLengths.slice(0, trackIndex).reduce((acc, len) => acc + len, 0) + currentTime +} diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts index 7a5c1574..f3a8750c 100644 --- a/src/shared/lib/index.ts +++ b/src/shared/lib/index.ts @@ -1,3 +1,4 @@ export { getVideoId } from './getVideoId' export { getGenreLabel } from './getGenreLabel' export { getTrackOrderLabel } from './getTrackOrderLabel' +export { getAccTime } from './getAccTime' diff --git a/src/widgets/playlist/PlaylistLayout.tsx b/src/widgets/playlist/PlaylistLayout.tsx index b4d4ad3c..c8f2a3a8 100644 --- a/src/widgets/playlist/PlaylistLayout.tsx +++ b/src/widgets/playlist/PlaylistLayout.tsx @@ -1,4 +1,4 @@ -import { useMemo, useCallback } from 'react' +import { useCallback } from 'react' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -55,16 +55,6 @@ const PlaylistLayout = ({ const deviceType = useDevice() const isMobile = deviceType === 'mobile' - // 누적 시간 계산 - const accumulatedTime = useMemo(() => { - if (!currentPlaylist) return 0 - return ( - currentPlaylist.songs - .slice(0, currentTrackIndex) - .reduce((acc, track) => acc + track.youtubeLength, 0) + currentTime - ) - }, [currentPlaylist, currentTrackIndex, currentTime]) - const handleProgressClick = useCallback( (trackIndex: number, seconds: number) => { onSelectTrack(trackIndex, seconds) @@ -113,7 +103,8 @@ const PlaylistLayout = ({ t.youtubeLength) || []} - currentTime={accumulatedTime} + currentTime={currentTime} + currentIndex={currentTrackIndex} onClick={handleProgressClick} /> diff --git a/src/widgets/playlist/ProgressBar.tsx b/src/widgets/playlist/ProgressBar.tsx index 0be97ced..b5b3c4ae 100644 --- a/src/widgets/playlist/ProgressBar.tsx +++ b/src/widgets/playlist/ProgressBar.tsx @@ -1,11 +1,13 @@ import styled from 'styled-components' import { Mark } from '@/assets/icons' +import { getAccTime } from '@/shared/lib' import { SvgButton } from '@/shared/ui' interface ProgressBarProps { trackLengths: number[] currentTime: number + currentIndex: number onClick?: (trackIndex: number, seconds: number) => void } @@ -15,8 +17,9 @@ const formatTime = (time: number) => { .join(':') } -const ProgressBar = ({ trackLengths, currentTime, onClick }: ProgressBarProps) => { +const ProgressBar = ({ trackLengths, currentTime, currentIndex, onClick }: ProgressBarProps) => { const duration = trackLengths.reduce((acc, cur) => acc + cur, 0) + const accTime = getAccTime(trackLengths, currentIndex, currentTime) // 마커 퍼센트 계산 let sum = 0 @@ -58,7 +61,7 @@ const ProgressBar = ({ trackLengths, currentTime, onClick }: ProgressBarProps) = onClick?.(trackIndex, localTime) } - const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0 + const progressPercent = duration > 0 ? (accTime / duration) * 100 : 0 return ( @@ -75,7 +78,7 @@ const ProgressBar = ({ trackLengths, currentTime, onClick }: ProgressBarProps) = - {formatTime(currentTime)} + {formatTime(accTime)} {formatTime(duration)} From 7b6700c5faa7be95c5ed3e21d4b5102be00748d8 Mon Sep 17 00:00:00 2001 From: gahyeon Date: Fri, 19 Sep 2025 04:55:17 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20=EC=9E=AC=EC=83=9D=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20interval=20ProgressBar=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20(#104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/providers/PlayerProvider.tsx | 18 +----------------- src/widgets/playlist/PlaylistLayout.tsx | 2 -- src/widgets/playlist/ProgressBar.tsx | 18 ++++++++++++++++-- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/app/providers/PlayerProvider.tsx b/src/app/providers/PlayerProvider.tsx index 7ef96edd..b5704f75 100644 --- a/src/app/providers/PlayerProvider.tsx +++ b/src/app/providers/PlayerProvider.tsx @@ -1,12 +1,4 @@ -import { - createContext, - useState, - useContext, - useRef, - useEffect, - useCallback, - type ReactNode, -} from 'react' +import { createContext, useState, useContext, useRef, useCallback, type ReactNode } from 'react' import type { PlaylistInfo } from '@/entities/playlist' @@ -39,14 +31,6 @@ const PlaylistProvider = ({ children }: PlaylistProviderProps) => { const playerRef = useRef(null) - // 1초마다 재생 시간 자동 업데이트 - useEffect(() => { - const intervalId = setInterval(() => { - if (playerRef.current) setCurrentTime(playerRef.current.getCurrentTime()) - }, 1000) - return () => clearInterval(intervalId) - }, []) - const setPlaylist = (playlist: PlaylistInfo, trackIndex?: number, time?: number) => { setCurrentPlaylist(playlist) if (trackIndex !== undefined) setCurrentTrackIndex(trackIndex) diff --git a/src/widgets/playlist/PlaylistLayout.tsx b/src/widgets/playlist/PlaylistLayout.tsx index c8f2a3a8..0e6ef0d1 100644 --- a/src/widgets/playlist/PlaylistLayout.tsx +++ b/src/widgets/playlist/PlaylistLayout.tsx @@ -34,7 +34,6 @@ const PlaylistLayout = ({ data, currentPlaylist, currentTrackIndex, - currentTime, isPlaying, onPlayPause, onNext, @@ -103,7 +102,6 @@ const PlaylistLayout = ({ t.youtubeLength) || []} - currentTime={currentTime} currentIndex={currentTrackIndex} onClick={handleProgressClick} /> diff --git a/src/widgets/playlist/ProgressBar.tsx b/src/widgets/playlist/ProgressBar.tsx index b5b3c4ae..ca803525 100644 --- a/src/widgets/playlist/ProgressBar.tsx +++ b/src/widgets/playlist/ProgressBar.tsx @@ -1,12 +1,14 @@ +import { useEffect } from 'react' + import styled from 'styled-components' +import { usePlaylist } from '@/app/providers/PlayerProvider' import { Mark } from '@/assets/icons' import { getAccTime } from '@/shared/lib' import { SvgButton } from '@/shared/ui' interface ProgressBarProps { trackLengths: number[] - currentTime: number currentIndex: number onClick?: (trackIndex: number, seconds: number) => void } @@ -17,7 +19,19 @@ const formatTime = (time: number) => { .join(':') } -const ProgressBar = ({ trackLengths, currentTime, currentIndex, onClick }: ProgressBarProps) => { +const ProgressBar = ({ trackLengths, currentIndex, onClick }: ProgressBarProps) => { + const { currentTime, updateCurrentTime, playerRef, isPlaying } = usePlaylist() + + useEffect(() => { + if (!isPlaying) return + const id = setInterval(() => { + if (playerRef.current) { + updateCurrentTime(playerRef.current.getCurrentTime()) + } + }, 1000) + return () => clearInterval(id) + }, [isPlaying, playerRef, updateCurrentTime]) + const duration = trackLengths.reduce((acc, cur) => acc + cur, 0) const accTime = getAccTime(trackLengths, currentIndex, currentTime)