diff --git a/apps/client/src/pages/home/components/suggest-music-section.tsx b/apps/client/src/pages/home/components/suggest-music-section.tsx index a32c19c44..bf1cf1ce8 100644 --- a/apps/client/src/pages/home/components/suggest-music-section.tsx +++ b/apps/client/src/pages/home/components/suggest-music-section.tsx @@ -1,3 +1,4 @@ +import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Box } from '@confeti/design-system'; @@ -6,25 +7,55 @@ import { HOME_QUERY_OPTIONS } from '@shared/apis/home/home-queries'; import { MusicList } from '@shared/components'; import MusicInfo from '@shared/components/music-list/music-info'; import { useMusicPlayer } from '@shared/hooks/use-music-player'; -import { SuggestMusicPerformanceResponse } from '@shared/types/home-response'; +import { RecommendPerformances } from '@shared/types/home-response'; -const SuggestMusicSection = ({ - data, -}: { - data: SuggestMusicPerformanceResponse; -}) => { - const musicIdList: string[] | undefined = undefined; +import { PERFORMANCE_SIZE, SONG_SIZE } from '../constants/recommend'; - const { data: suggestMusic, isPending } = useQuery({ - ...HOME_QUERY_OPTIONS.SUGGEST_MUSIC(data.performanceId, musicIdList), +interface SuggestMusicSectionProps { + onClickDetail: (type: string, typeId: number) => void; +} + +const SuggestMusicSection = ({ onClickDetail }: SuggestMusicSectionProps) => { + const { data, isPending } = useQuery({ + ...HOME_QUERY_OPTIONS.RECOMMEND_PERFORMANCES(PERFORMANCE_SIZE, SONG_SIZE), }); - if (!isPending && !suggestMusic?.musics) { + const performances: RecommendPerformances[] = data?.performances ?? []; + const [currentIndex, setCurrentIndex] = useState(0); + const currentPerformance = performances[currentIndex]; + + const musicData = useMemo( + () => + currentPerformance?.songs.map((song) => ({ + musicId: song.songId, + trackName: song.songName, + artistName: song.artistName, + artworkUrl: song.artworkUrl, + previewUrl: song.previewUrl, + })) ?? [], + [currentPerformance], + ); + + const { musicList, onClickPlayToggle, audioRef, audioEvents, stopAudio } = + useMusicPlayer(musicData); + + if (!isPending && !performances) { return null; } - const { musicList, onClickPlayToggle, audioRef, audioEvents } = - useMusicPlayer(suggestMusic?.musics ?? []); + const handleDotClick = (index: number) => { + if (!performances.length) return; + if (index === currentIndex) return; + if (index < 0 || index >= performances.length) return; + + stopAudio(); + setCurrentIndex(index); + }; + + const handleClickDetail = () => { + if (!currentPerformance) return; + onClickDetail(currentPerformance.type, currentPerformance.typeId); + }; return (
- + { ticketingResult, latestPerformancesResult, suggestPerformanceResult, - suggestMusicPerformanceResult, ] = useSuspenseQueries({ queries: [ USER_QUERY_OPTIONS.PROFILE(), HOME_QUERY_OPTIONS.TICKETING(), HOME_QUERY_OPTIONS.LATEST_PERFORMANCES(), HOME_QUERY_OPTIONS.SUGGEST_PERFORMANCE(), - HOME_QUERY_OPTIONS.SUGGEST_MUSIC_PERFORMANCE(), ], }); @@ -25,6 +23,5 @@ export const useHomeQueries = () => { ticketing: ticketingResult.data, latestPerformances: latestPerformancesResult.data, suggestPerformance: suggestPerformanceResult.data, - suggestMusicPerformance: suggestMusicPerformanceResult.data, }; }; diff --git a/apps/client/src/pages/home/page/home-page.tsx b/apps/client/src/pages/home/page/home-page.tsx index f2fa0d41c..4310620d5 100644 --- a/apps/client/src/pages/home/page/home-page.tsx +++ b/apps/client/src/pages/home/page/home-page.tsx @@ -20,13 +20,8 @@ import * as styles from './home-page.css'; const HomePage = () => { const navigateToDetail = useNavigateToDetail(); - const { - userName, - ticketing, - latestPerformances, - suggestPerformance, - suggestMusicPerformance, - } = useHomeQueries(); + const { userName, ticketing, latestPerformances, suggestPerformance } = + useHomeQueries(); const formattedCarouselData = latestPerformances.performances.map( (performance) => ({ @@ -66,9 +61,7 @@ const HomePage = () => { - {suggestMusicPerformance && ( - - )} + diff --git a/apps/client/src/shared/apis/home/home-queries.ts b/apps/client/src/shared/apis/home/home-queries.ts index 741d6f9c1..c02107ed5 100644 --- a/apps/client/src/shared/apis/home/home-queries.ts +++ b/apps/client/src/shared/apis/home/home-queries.ts @@ -6,8 +6,7 @@ import { END_POINT } from '@shared/constants/api'; import { HOME_QUERY_KEY } from '@shared/constants/query-key'; import { CarouselPerformancesResponse, - SuggestMusicPerformanceResponse, - SuggestMusicResponse, + RecommendPerformancesResponse, SuggestPerformanceResponse, TicketingPerformancesResponse, } from '@shared/types/home-response'; @@ -31,15 +30,13 @@ export const HOME_QUERY_OPTIONS = { queryKey: HOME_QUERY_KEY.SUGGEST_PERFORMANCE(), queryFn: getSuggestPerformance, }), - SUGGEST_MUSIC_PERFORMANCE: () => + RECOMMEND_PERFORMANCES: (performanceSize: number, songSize: number) => queryOptions({ - queryKey: HOME_QUERY_KEY.SUGGEST_MUSIC_PERFORMANCE(), - queryFn: getSuggestMusicPerformance, - }), - SUGGEST_MUSIC: (performanceId: number, musicIds?: string[]) => - queryOptions({ - queryKey: HOME_QUERY_KEY.SUGGEST_MUSIC(performanceId), - queryFn: () => getSuggestMusic(performanceId, musicIds), + queryKey: HOME_QUERY_KEY.RECOMMEND_PERFORMANCES( + performanceSize, + songSize, + ), + queryFn: () => getRecommendPerformances(performanceSize, songSize), }), }; @@ -67,41 +64,13 @@ export const getSuggestPerformance = return response.data; }; -export const getSuggestMusicPerformance = - async (): Promise => { - try { - const response = await get>( - END_POINT.GET_SUGGEST_MUSIC_PERFORMANCE, - ); - - if (!response.data) { - return null; - } - - return response.data; - } catch (error) { - console.error(error); - return null; - } - }; - -export const getSuggestMusic = async ( - performanceId: number, - musicIds?: string[], -): Promise => { - try { - const query = new URLSearchParams(); - - query.append('performanceId', String(performanceId)); - musicIds?.forEach((id) => query.append('musicId', id)); - - const url = `performances/recommend/musics?${query.toString()}`; - - const response = await get>(url); +export const getRecommendPerformances = async ( + performanceSize: number, + songSize: number, +): Promise => { + const response = await get>( + END_POINT.GET_SUGGEST_MUSIC_PERFORMANCE(performanceSize, songSize), + ); - return response.data || { musics: [] }; - } catch (error) { - console.error(error); - return { musics: [] }; - } + return response.data ?? { performances: [] }; }; diff --git a/apps/client/src/shared/components/music-list/music-info.tsx b/apps/client/src/shared/components/music-list/music-info.tsx index 1a8642a15..9310f77e2 100644 --- a/apps/client/src/shared/components/music-list/music-info.tsx +++ b/apps/client/src/shared/components/music-list/music-info.tsx @@ -5,27 +5,35 @@ import * as styles from './music-info.css'; interface MusicInfoProps { title: string; - total?: number; - current?: number; + posterUrl: string; + total: number; + current: number; onDotClick?: (index: number) => void; + onClickDetail?: () => void; } const MusicInfo = ({ title, - total = 2, - current = 0, + posterUrl, + total, + current, onDotClick, + onClickDetail, }: MusicInfoProps) => { const showDots = total > 1; return (
-
+
+ +

{title}

-

공연 상세정보 확인하기

+

+ 공연 상세정보 확인하기 +

diff --git a/apps/client/src/shared/components/music-list/music-list.tsx b/apps/client/src/shared/components/music-list/music-list.tsx index ed17a0bb1..40196c2c5 100644 --- a/apps/client/src/shared/components/music-list/music-list.tsx +++ b/apps/client/src/shared/components/music-list/music-list.tsx @@ -7,8 +7,8 @@ import * as styles from './music-list.css'; interface Music { musicId: string; - artworkUrl: string; trackName: string; + artworkUrl: string; artistName: string; isPlaying?: boolean; progress?: number; diff --git a/apps/client/src/shared/constants/api.ts b/apps/client/src/shared/constants/api.ts index 2a826f98a..9b6cf3e4f 100644 --- a/apps/client/src/shared/constants/api.ts +++ b/apps/client/src/shared/constants/api.ts @@ -50,7 +50,8 @@ export const END_POINT = { GET_TICKETING: '/performances/reservation', GET_LATEST_PERFORMANCES: '/performances/info', GET_SUGGEST_PERFORMANCE: '/performances/recommend', - GET_SUGGEST_MUSIC_PERFORMANCE: '/performances/recommend/performance', + GET_SUGGEST_MUSIC_PERFORMANCE: (performanceSize: number, songSize: number) => + `performances/v4/song/recommend?performanceSize=${performanceSize}&songSize=${songSize}`, //타임 테이블 GET_AVAILABLE_FESTIVALS: '/user/timetables/festivals', diff --git a/apps/client/src/shared/constants/query-key.ts b/apps/client/src/shared/constants/query-key.ts index cccc12bb2..cfeddc690 100644 --- a/apps/client/src/shared/constants/query-key.ts +++ b/apps/client/src/shared/constants/query-key.ts @@ -31,10 +31,11 @@ export const HOME_QUERY_KEY = { LATEST_PERFORMANCES: () => [...HOME_QUERY_KEY.ALL, 'latest-performances'], TICKETING: () => [...HOME_QUERY_KEY.ALL, 'ticketing'], SUGGEST_PERFORMANCE: () => [...HOME_QUERY_KEY.ALL, 'suggest-performance'], - SUGGEST_MUSIC_PERFORMANCE: () => [...HOME_QUERY_KEY.ALL, 'suggest-music'], - SUGGEST_MUSIC: (performanceId: number) => [ + RECOMMEND_PERFORMANCES: (performanceSize: number, songSize: number) => [ ...HOME_QUERY_KEY.ALL, - performanceId, + 'recommend-performances', + performanceSize, + songSize, ], } as const; diff --git a/apps/client/src/shared/hooks/use-music-player.ts b/apps/client/src/shared/hooks/use-music-player.ts index b6f9b1f35..d56030a8c 100644 --- a/apps/client/src/shared/hooks/use-music-player.ts +++ b/apps/client/src/shared/hooks/use-music-player.ts @@ -10,10 +10,11 @@ export const useMusicPlayer = (data: musics[]) => { const [duration, setDuration] = useState(30); const stopAudio = () => { - const audio = audioRef.current; - if (!audio) return; - audio.pause(); - audio.currentTime = 0; + if (!audioRef.current) return; + + audioRef.current.pause(); + audioRef.current.currentTime = 0; + setCurrentPlayingId(null); setIsAudioPlaying(false); setCurrentTime(0); @@ -24,35 +25,33 @@ export const useMusicPlayer = (data: musics[]) => { if (!selectedMusic || !selectedMusic.previewUrl || !audioRef.current) return; - const audio = audioRef.current; - - if (currentPlayingId === musicId && !audio.paused) { + if (currentPlayingId === musicId && !audioRef.current.paused) { stopAudio(); return; } - audio.src = selectedMusic.previewUrl; - audio.play().catch((e) => console.error('재생 오류', e)); + audioRef.current.src = selectedMusic.previewUrl; + audioRef.current.play().catch((e) => console.error('재생 오류', e)); setCurrentPlayingId(musicId); }; const audioEvents = useMemo( () => ({ onLoadedMetadata: () => { - const a = audioRef.current; - if (!a) return; - const d = Number.isFinite(a.duration) ? a.duration : 30; + if (!audioRef.current) return; + + const d = Number.isFinite(audioRef.current.duration) + ? audioRef.current.duration + : 30; setDuration(d); }, onTimeUpdate: () => { - const a = audioRef.current; - if (!a) return; - setCurrentTime(a.currentTime); + if (!audioRef.current) return; + setCurrentTime(audioRef.current.currentTime); }, onSeeked: () => { - const a = audioRef.current; - if (!a) return; - setCurrentTime(a.currentTime); + if (!audioRef.current) return; + setCurrentTime(audioRef.current.currentTime); }, onPlay: () => { setIsAudioPlaying(true); diff --git a/apps/client/src/shared/pages/error/error.css.ts b/apps/client/src/shared/pages/error/error.css.ts index b22caedcb..8ca6933a3 100644 --- a/apps/client/src/shared/pages/error/error.css.ts +++ b/apps/client/src/shared/pages/error/error.css.ts @@ -5,7 +5,7 @@ import { themeVars } from '@confeti/design-system/styles'; export const container = style({ ...themeVars.display.flexJustifyAlignCenter, flexDirection: 'column', - height: 'calc(100dvh - 14rem)', + flex: 1, gap: '2rem', }); diff --git a/apps/client/src/shared/types/home-response.ts b/apps/client/src/shared/types/home-response.ts index 8fa1e28f1..55c5b8050 100644 --- a/apps/client/src/shared/types/home-response.ts +++ b/apps/client/src/shared/types/home-response.ts @@ -63,3 +63,23 @@ export interface SuggestMusicPerformanceResponse { export interface SuggestMusicResponse { musics: musics[]; } + +export interface RecommendSong { + songId: string; + songName: string; + artistName: string; + artworkUrl: string; + previewUrl: string; +} + +export interface RecommendPerformances { + typeId: number; + type: 'concert' | 'festival'; + title: string; + posterUrl: string; + songs: RecommendSong[]; +} + +export interface RecommendPerformancesResponse { + performances: RecommendPerformances[]; +} diff --git a/packages/design-system/src/components/music-item/music-item.css.ts b/packages/design-system/src/components/music-item/music-item.css.ts index 9d5f0134d..6cacd758b 100644 --- a/packages/design-system/src/components/music-item/music-item.css.ts +++ b/packages/design-system/src/components/music-item/music-item.css.ts @@ -157,15 +157,18 @@ export const playerTransparent = style({ export const progressSvg = style({ position: 'absolute', - inset: '-1.5px', - transform: 'rotate(-90deg)', + width: '44px', + height: '44px', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%) rotate(-90deg)', pointerEvents: 'none', }); export const progressCircle = style({ fill: 'none', stroke: themeVars.color.confeti_lime, - strokeWidth: 1, + strokeWidth: 2, strokeDasharray: `${CIRC}`, strokeDashoffset: `${CIRC}`, transition: 'stroke-dashoffset 0.25s linear',