Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions src/features/swipe/ui/SwipeCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useCallback } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { useLocation, useNavigate, useParams } from 'react-router-dom'

import useEmblaCarousel from 'embla-carousel-react'
import styled from 'styled-components'
Expand All @@ -9,37 +9,45 @@ import type { PlaylistInfo } from '@/entities/playlist'
interface SwipeCarouselProps {
children: React.ReactNode
data: PlaylistInfo[]
axis: 'x' | 'y'
onSelectIndexChange?: (activeIndex: number) => void
basePath: string
}

const SwipeCarousel = ({ children, data, onSelectIndexChange }: SwipeCarouselProps) => {
const SwipeCarousel = ({
children,
data,
axis,
onSelectIndexChange,
basePath,
}: SwipeCarouselProps) => {
const navigate = useNavigate()
const { id: playlistId } = useParams()
const location = useLocation()

const initialIndex =
!isNaN(Number(playlistId)) && Number(playlistId) > 0
? data.findIndex((p) => p.playlistId === Number(playlistId))
: 0

const [emblaRef, emblaApi] = useEmblaCarousel({
axis: 'y',
loop: false,
axis,
loop: axis === 'x',
startIndex: initialIndex > 0 ? initialIndex : 0, // 매치 실패 시 0번으로
containScroll: axis === 'x' && data.length <= 3 ? false : 'trimSnaps',
})

// 슬라이드 선택 시 URL 업데이트
const onSelect = useCallback(() => {
if (!emblaApi || data.length === 0) return

const selectedIndex = emblaApi.selectedScrollSnap()
console.log('selectedIndex ', selectedIndex)
onSelectIndexChange?.(selectedIndex) // 부모에 알림
const newId = data[selectedIndex]?.playlistId

const newId = data[selectedIndex]?.playlistId
if (newId != null && playlistId !== String(newId)) {
navigate(`/discover/${newId}`, { replace: true })
navigate(`${basePath}/${newId}${location.search}`, { replace: true })
}
}, [emblaApi, data, navigate, playlistId])
}, [emblaApi, data, navigate, playlistId, onSelectIndexChange, basePath, location.search])

useEffect(() => {
if (!emblaApi) return
Expand All @@ -51,9 +59,17 @@ const SwipeCarousel = ({ children, data, onSelectIndexChange }: SwipeCarouselPro
}, [emblaApi, onSelect])

return (
<EmblaViewport ref={emblaRef}>
<EmblaContainer>{children}</EmblaContainer>
</EmblaViewport>
<>
{axis === 'x' ? (
<div ref={emblaRef}>
<HorizontalContainer>{children}</HorizontalContainer>
</div>
) : (
<EmblaViewport ref={emblaRef}>
<VerticaContainer>{children}</VerticaContainer>
</EmblaViewport>
)}
</>
)
}

Expand All @@ -64,8 +80,13 @@ const EmblaViewport = styled.div`
overflow: hidden;
`

const EmblaContainer = styled.div`
const VerticaContainer = styled.div`
display: flex;
flex-direction: column;
height: 100%;
`
Comment on lines +83 to 87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

오타 수정 필요.

VerticaContainer는 오타입니다. VerticalContainer로 수정해야 합니다.

다음 diff를 적용하세요:

-const VerticaContainer = styled.div`
+const VerticalContainer = styled.div`
   display: flex;
   flex-direction: column;
   height: 100%;
 `

그리고 69번 라인의 사용처도 함께 수정:

-          <VerticaContainer>{children}</VerticaContainer>
+          <VerticalContainer>{children}</VerticalContainer>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/features/swipe/ui/SwipeCarousel.tsx around lines 83 to 87 (and usage at
line 69), rename the styled component identifier from VerticaContainer to
VerticalContainer to fix the typo; update the styled declaration name and all
references (including the use at line 69) to VerticalContainer so the component
compiles and imports match the corrected name.


const HorizontalContainer = styled.div`
display: flex;
touch-action: pan-x pinch-zoom;
`
7 changes: 6 additions & 1 deletion src/pages/discover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
} = useShufflePlaylists()

// shuffleData에서 실제 playlist 배열만 추출
const shufflePlaylists = shuffleData?.pages.flatMap((page) => page.content) ?? []

Check warning on line 63 in src/pages/discover/index.tsx

View workflow job for this annotation

GitHub Actions / Build and Lint

The 'shufflePlaylists' logical expression could make the dependencies of useMemo Hook (at line 97) change on every render. Move it inside the useMemo callback. Alternatively, wrap the initialization of 'shufflePlaylists' in its own useMemo() Hook

// PlaylistDetailResponse → PlaylistInfo 변환
const playlistAsInfo = useMemo(() => {
Expand Down Expand Up @@ -151,7 +151,12 @@
return (
<Page>
{showCoachmark && <DiscoverCoachMark onClose={handleCloseCoachmark} />}
<SwipeCarousel data={playlistsData} onSelectIndexChange={handleSelectPlaylist}>
<SwipeCarousel
data={playlistsData}
onSelectIndexChange={handleSelectPlaylist}
axis="y"
basePath="/discover"
>
{playlistsData.map((data) => (
<Slide key={data.playlistId}>
<PlaylistLayout
Expand Down
66 changes: 53 additions & 13 deletions src/pages/mycd/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useCallback, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useEffect, useCallback, useState, useMemo } from 'react'
import { useLocation, useNavigate, useParams } from 'react-router-dom'

import styled from 'styled-components'

Expand All @@ -10,6 +10,7 @@ import { useMyCdList, useMyLikedCdList } from '@/entities/playlist/model/useMyCd
import { useAuthStore } from '@/features/auth/store/authStore'
import { useChatSocket } from '@/features/chat/model/sendMessage'
import { HeaderTab, PlaylistCarousel } from '@/pages/mycd/ui'
import type { MyCdTab } from '@/pages/mycd/ui/HeaderTab'
import { getVideoId } from '@/shared/lib'
import { useDevice } from '@/shared/lib/useDevice'
import { flexColCenter, flexRowCenter } from '@/shared/styles/mixins'
Expand All @@ -29,22 +30,29 @@ const MyCdPage = () => {
currentTime,
playerRef,
} = usePlaylist()

const [isMuted, setIsMuted] = useState<boolean | null>(null)
const [selectedTab, setSelectedTab] = useState<'MY' | 'LIKE'>('MY')
const { userInfo } = useAuthStore()
const navigate = useNavigate()
const deviceType = useDevice()
const isMobile = deviceType === 'mobile'

const handleTabSelect = (tab: 'MY' | 'LIKE') => {
setSelectedTab(tab)
}
const { id: routePlaylistId } = useParams<{ id?: string }>()
const { search } = useLocation()
const typeParam = new URLSearchParams(search).get('type')
const [selectedTab, setSelectedTab] = useState<MyCdTab>(typeParam === 'LIKE' ? 'LIKE' : 'MY')

const myCdPlaylist = useMyCdList('RECENT')
const likedCdPlaylist = useMyLikedCdList('RECENT')

const playlistQuery = selectedTab === 'MY' ? myCdPlaylist : likedCdPlaylist
const playlistData = playlistQuery.data

const playlistData = useMemo(
() => playlistQuery.data?.filter((p) => selectedTab !== 'MY' || p.isPublic), // my 탭일 경우 isPublic true만 필터링

[playlistQuery.data, selectedTab]
)

const isError = playlistQuery.isError

const [centerPlaylist, setCenterPlaylist] = useState<{
Expand All @@ -53,33 +61,65 @@ const MyCdPage = () => {
}>({ playlistId: null, playlistName: '' })

useEffect(() => {
if (playlistData && playlistData.length > 0 && centerPlaylist.playlistId === null) {
if (playlistQuery.isLoading || !playlistData) return

const routeId = routePlaylistId ? Number(routePlaylistId) : null
const found = routeId ? playlistData.find((p) => p.playlistId === routeId) : null

if (found) {
setCenterPlaylist({
playlistId: found.playlistId,
playlistName: found.playlistName,
})
} else if (playlistData.length > 0) {
const first = playlistData[0]
setCenterPlaylist({
playlistId: first.playlistId,
playlistName: first.playlistName,
})

const path =
selectedTab === 'LIKE' ? `/mycd/${first.playlistId}?type=LIKE` : `/mycd/${first.playlistId}`

navigate(path, { replace: true })
}
}, [playlistData, centerPlaylist.playlistId])
}, [playlistData, playlistQuery.isLoading, routePlaylistId, navigate, selectedTab])

/* 좋아요 탭 선택 시 url query param 반영 */
const handleTabSelect = (tab: MyCdTab) => {
setSelectedTab(tab)

const basePath = centerPlaylist.playlistId ? `/mycd/${centerPlaylist.playlistId}` : '/mycd'

const path = tab === 'LIKE' ? `${basePath}?type=LIKE` : basePath

// LoopCarousel 센터 컨텐츠 변경 핸들러
navigate(path, { replace: true })
}

/* 캐러셀 드래그 시 URL 갱신 */
const handleCenterChange = useCallback(
(playlist: { playlistId: number; playlistName: string }) => {
if (playlist) {
setCenterPlaylist({
playlistId: playlist.playlistId,
playlistName: playlist.playlistName,
})

const path =
selectedTab === 'LIKE'
? `/mycd/${playlist.playlistId}?type=LIKE`
: `/mycd/${playlist.playlistId}`

navigate(path, { replace: true })
}
},
[]
[navigate, selectedTab]
)

/* 플레이리스트 세팅 */
const { data: playlistDetail } = usePlaylistDetail(centerPlaylist.playlistId)

useEffect(() => {
if (playlistDetail && userInfo) {
// 이미 같은 playlist면 재설정 안함
if (currentPlaylist?.playlistId === playlistDetail.playlistId) return

const convertedPlaylist = {
Expand Down
7 changes: 3 additions & 4 deletions src/pages/mycd/tracklist/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { useLocation } from 'react-router-dom'
import { useParams } from 'react-router-dom'

import { usePlaylistDetail } from '@/entities/playlist'
import PlaylistInfo from '@/widgets/playlist/PlaylistInfo'

const MyCdInfoPage = () => {
const location = useLocation()
const { playlistId } = location.state as { playlistId: number }
const { data, isLoading, isError } = usePlaylistDetail(Number(playlistId))
const { id } = useParams<{ id: string }>()
const { data, isLoading, isError } = usePlaylistDetail(Number(id))

return <PlaylistInfo playlistData={data} isLoading={isLoading} isError={isError} />
}
Expand Down
37 changes: 31 additions & 6 deletions src/pages/mycd/ui/PlaylistCarousel.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useState, useCallback, type Key } from 'react'
import { useState, useCallback, useEffect, type Key } from 'react'
import { useParams } from 'react-router-dom'

import styled from 'styled-components'

import type { CdCustomData } from '@/entities/playlist'
import type { CdCustomData, PlaylistInfo } from '@/entities/playlist'
import { SwipeCarousel } from '@/features/swipe'
import { flexRowCenter } from '@/shared/styles/mixins'
import { Cd } from '@/shared/ui'
import LoopCarousel from '@/shared/ui/LoopCarousel'

interface CarouselPlaylist {
playlistId: number
Expand All @@ -19,8 +20,18 @@ interface PlaylistCarouselProps {
}

const PlaylistCarousel = ({ data, onCenterChange }: PlaylistCarouselProps) => {
const { id: playlistId } = useParams()
const [activeIndex, setActiveIndex] = useState(0)

// url 기준으로 active index 동기화
useEffect(() => {
if (!playlistId) return

const index = data.findIndex((p) => p.playlistId === Number(playlistId))

if (index >= 0) setActiveIndex(index)
}, [playlistId, data])

const handleSelectIndex = useCallback(
(index: number) => {
setActiveIndex(index)
Expand All @@ -35,20 +46,34 @@ const PlaylistCarousel = ({ data, onCenterChange }: PlaylistCarouselProps) => {
[data, onCenterChange]
)

const playlistInfoData: PlaylistInfo[] = data.map((p) => ({
playlistId: p.playlistId,
playlistName: p.playlistName,
creator: { creatorId: '0', creatorNickname: '' },
genre: '',
songs: [],
isPublic: true,
}))
Comment on lines +49 to +56

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

playlistInfoData 배열이 컴포넌트가 리렌더링될 때마다 새로 생성되고 있습니다. data prop이 변경되지 않는 한 이 값은 동일하게 유지될 수 있으므로, 불필요한 계산을 피하고 성능을 최적화하기 위해 useMemo로 감싸는 것을 권장합니다.1

  const playlistInfoData: PlaylistInfo[] = useMemo(
    () =>
      data.map((p) => ({
        playlistId: p.playlistId,
        playlistName: p.playlistName,
        creator: { creatorId: '0', creatorNickname: '' },
        genre: '',
        songs: [],
        isPublic: true,
      })),
    [data]
  )

Style Guide References

Footnotes

  1. 불필요한 리렌더링을 줄이고 성능을 최적화하기 위해 useMemo와 같은 React 최적화 기법을 적절하게 사용해야 합니다. 이 경우, data prop이 변경될 때만 playlistInfoData를 다시 계산하도록 useMemo를 사용하는 것이 좋습니다.


return (
<LoopCarousel onSelectIndex={handleSelectIndex}>
<SwipeCarousel
data={playlistInfoData}
onSelectIndexChange={handleSelectIndex}
axis="x"
basePath="/mycd"
>
{data.map((slide, index: Key) => (
<EmblaSlide key={index}>
<Slide $active={activeIndex === index}>
<Cd
variant="carousel"
variant="mycd"
bgColor="none"
stickers={activeIndex === index ? slide.cdResponse.cdItems : []}
/>
</Slide>
</EmblaSlide>
))}
</LoopCarousel>
</SwipeCarousel>
)
}

Expand Down
4 changes: 2 additions & 2 deletions src/shared/config/routesConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export const routesConfig: RouteConfig[] = [
isPrivate: true,
isNotSuspense: true,
children: [
{ path: '', component: MyCdPage },
{ path: 'tracklist', component: MyCdInfoPage },
{ path: ':id?', component: MyCdPage },
{ path: ':id?/tracklist', component: MyCdInfoPage },
],
},

Expand Down
2 changes: 2 additions & 0 deletions src/shared/ui/Cd.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface CdProps {
| 'carousel'
| 'responsive'
| 'home'
| 'mycd'
bgColor?: 'none' | 'default' | 'dark'
stickers?: CdCustomData[]
isPublic?: boolean
Expand Down Expand Up @@ -137,6 +138,7 @@ const sizeMap = {
carousel: { container: 180, base: 180, borderRadius: 0 },
responsive: { borderRadius: 10 },
home: { container: 180, base: 180, borderRadius: 0 },
mycd: { container: 260, base: 260, borderRadius: 0 },
} as const

interface StyleProps {
Expand Down
2 changes: 1 addition & 1 deletion src/widgets/playlist/ActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const ActionBar = ({
if (type === 'DISCOVER') {
navigate(`/discover/${playlistId}/tracklist`)
} else {
navigate(`/mycd/tracklist`, { state: { playlistId } })
navigate(`/mycd/${playlistId}/tracklist`)
}
}

Expand Down
Loading