곡 상세 페이지 트랙 변경 시 스크롤/애니메이션 개선
- ScrollToTop: PC 레이아웃의 main 요소 스크롤 초기화 추가 - TrackDetail: key prop으로 트랙 변경 시 컴포넌트 리마운트 - TrackDetail: main 요소 스크롤 초기화 (PC는 main에서 스크롤) - 수록곡 선택 시 Link 대신 button + navigate 사용 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
eae56df146
commit
a7bc2e9800
3 changed files with 41 additions and 9 deletions
|
|
@ -16,6 +16,12 @@ function ScrollToTop() {
|
||||||
if (mobileContent) {
|
if (mobileContent) {
|
||||||
mobileContent.scrollTop = 0;
|
mobileContent.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PC 레이아웃 스크롤 컨테이너 초기화
|
||||||
|
const main = document.querySelector('main');
|
||||||
|
if (main) {
|
||||||
|
main.scrollTop = 0;
|
||||||
|
}
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useMemo, useEffect } from 'react';
|
import { useState, useMemo, useEffect, useLayoutEffect } from 'react';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
@ -40,6 +40,11 @@ function MobileTrackDetail() {
|
||||||
const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.video_url), [track?.video_url]);
|
const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.video_url), [track?.video_url]);
|
||||||
const videoLabel = track?.video_type === 'special' ? '스페셜 영상' : '뮤직비디오';
|
const videoLabel = track?.video_type === 'special' ? '스페셜 영상' : '뮤직비디오';
|
||||||
|
|
||||||
|
// 트랙 변경 시 스크롤 맨 위로
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||||
|
}, [trackTitle]);
|
||||||
|
|
||||||
// 가사 펼침 상태
|
// 가사 펼침 상태
|
||||||
const [showFullLyrics, setShowFullLyrics] = useState(false);
|
const [showFullLyrics, setShowFullLyrics] = useState(false);
|
||||||
|
|
||||||
|
|
@ -93,7 +98,7 @@ function MobileTrackDetail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-4">
|
<div key={trackTitle} className="pb-4">
|
||||||
{/* 트랙 정보 헤더 */}
|
{/* 트랙 정보 헤더 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useLayoutEffect } from 'react';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
|
@ -40,6 +40,14 @@ function PCTrackDetail() {
|
||||||
const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.video_url), [track?.video_url]);
|
const youtubeVideoId = useMemo(() => getYoutubeVideoId(track?.video_url), [track?.video_url]);
|
||||||
const videoLabel = track?.video_type === 'special' ? '스페셜 영상' : '뮤직비디오';
|
const videoLabel = track?.video_type === 'special' ? '스페셜 영상' : '뮤직비디오';
|
||||||
|
|
||||||
|
// 트랙 변경 시 스크롤 맨 위로
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const main = document.querySelector('main');
|
||||||
|
if (main) {
|
||||||
|
main.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}, [trackTitle]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
@ -61,7 +69,13 @@ function PCTrackDetail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }} className="py-12">
|
<motion.div
|
||||||
|
key={trackTitle}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="py-12"
|
||||||
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
{/* 브레드크럼 네비게이션 */}
|
{/* 브레드크럼 네비게이션 */}
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
|
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
|
||||||
|
|
@ -218,11 +232,18 @@ function PCTrackDetail() {
|
||||||
{track.otherTracks?.map((t) => {
|
{track.otherTracks?.map((t) => {
|
||||||
const isCurrent = t.title === track.title;
|
const isCurrent = t.title === track.title;
|
||||||
return (
|
return (
|
||||||
<Link
|
<button
|
||||||
key={t.id}
|
key={t.id}
|
||||||
to={`/album/${encodeURIComponent(track.album?.title || albumName)}/track/${encodeURIComponent(t.title)}`}
|
type="button"
|
||||||
replace={!isCurrent}
|
onClick={() => {
|
||||||
className={`group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
|
if (!isCurrent) {
|
||||||
|
navigate(
|
||||||
|
`/album/${encodeURIComponent(track.album?.title || albumName)}/track/${encodeURIComponent(t.title)}`,
|
||||||
|
{ replace: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full group flex items-center gap-3 px-3 py-2.5 rounded-xl transition-all ${
|
||||||
isCurrent ? 'bg-primary text-white' : 'hover:bg-gray-50'
|
isCurrent ? 'bg-primary text-white' : 'hover:bg-gray-50'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -259,7 +280,7 @@ function PCTrackDetail() {
|
||||||
<span className={`text-xs tabular-nums ${isCurrent ? 'text-white/70' : 'text-gray-400'}`}>
|
<span className={`text-xs tabular-nums ${isCurrent ? 'text-white/70' : 'text-gray-400'}`}>
|
||||||
{t.duration || ''}
|
{t.duration || ''}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue