diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 80eea25..3809265 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,7 @@ import MobileMembers from './pages/mobile/public/Members'; import MobileAlbum from './pages/mobile/public/Album'; import MobileAlbumDetail from './pages/mobile/public/AlbumDetail'; import MobileAlbumGallery from './pages/mobile/public/AlbumGallery'; +import MobileTrackDetail from './pages/mobile/public/TrackDetail'; import MobileSchedule from './pages/mobile/public/Schedule'; // 관리자 페이지 @@ -94,6 +95,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/frontend/src/pages/mobile/public/AlbumDetail.jsx b/frontend/src/pages/mobile/public/AlbumDetail.jsx index 0f88b29..2493024 100644 --- a/frontend/src/pages/mobile/public/AlbumDetail.jsx +++ b/frontend/src/pages/mobile/public/AlbumDetail.jsx @@ -261,7 +261,8 @@ function MobileAlbumDetail() { {displayTracks?.map((track) => (
navigate(`/album/${encodeURIComponent(album.title)}/track/${encodeURIComponent(track.title)}`)} + className="flex items-center gap-3 py-2.5 px-3 rounded-xl hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer" > {String(track.track_number).padStart(2, '0')} diff --git a/frontend/src/pages/mobile/public/TrackDetail.jsx b/frontend/src/pages/mobile/public/TrackDetail.jsx new file mode 100644 index 0000000..8ec4082 --- /dev/null +++ b/frontend/src/pages/mobile/public/TrackDetail.jsx @@ -0,0 +1,299 @@ +import { useMemo } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { motion } from 'framer-motion'; +import { Clock, User, Music, Mic2, ChevronLeft, ChevronRight } from 'lucide-react'; +import { getTrack } from '../../../api/public/albums'; + +// 유튜브 URL에서 비디오 ID 추출 +const getYoutubeVideoId = (url) => { + if (!url) return null; + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\n?#]+)/, + ]; + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) return match[1]; + } + return null; +}; + +// 쉼표 기준 줄바꿈 처리 +const formatCredit = (text) => { + if (!text) return null; + return text.split(',').map((item, index) => ( + {item.trim()} + )); +}; + +// 모바일 곡 상세 페이지 +function TrackDetail() { + const { name: albumName, trackTitle } = useParams(); + const navigate = useNavigate(); + + // useQuery로 트랙 데이터 로드 + const { data: track, isLoading: loading, error } = useQuery({ + queryKey: ['track', albumName, trackTitle], + queryFn: () => getTrack(albumName, trackTitle), + enabled: !!albumName && !!trackTitle, + }); + + const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.music_video_url), [track?.music_video_url]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (error || !track) { + return ( +
+

트랙을 찾을 수 없습니다.

+
+ ); + } + + return ( + + {/* 헤더 - 뒤로가기 */} +
+ +
+

{track.album?.title}

+

{track.title}

+
+
+ + {/* 트랙 정보 헤더 */} +
+
+ {/* 앨범 커버 */} + + {track.album?.title} + + + {/* 트랙 정보 */} +
+
+ {track.is_title_track === 1 && ( + + TITLE + + )} + + Track {String(track.track_number).padStart(2, '0')} + +
+ +

{track.title}

+ +

+ {track.album?.album_type} · {track.album?.title} +

+ + {track.duration && ( +
+ + {track.duration} +
+ )} +
+
+
+ + {/* 뮤직비디오 섹션 */} + {youtubeVideoId && ( + +

+
+ 뮤직비디오 +

+
+