Skip to content

Commit

Permalink
트랙 재생 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
ipcgrdn committed Jan 16, 2025
1 parent 4754e4b commit f1958de
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 65 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.2.1",
"@radix-ui/react-slider": "^1.2.2",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
Expand Down
8 changes: 8 additions & 0 deletions src/app/albums/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export default async function AlbumPage({ params }: AlbumPageProps) {
tracks={album.trackResponseDtos}
albumId={params.id}
artistId={album.artistResponseDto.uuid}
album={{
title: album.albumResponseDto.title,
artImage: album.albumResponseDto.artImage,
}}
artist={{
name: album.artistResponseDto.name,
artistImage: album.artistResponseDto.artistImage,
}}
/>
</div>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getAuthCookie } from "@/lib/server-auth";
import { ThemeProvider } from "@/contexts/theme/ThemeContext";
import { UserProvider } from "@/contexts/auth/UserContext";
import { Toaster } from "@/components/ui/toaster"
import { AudioProvider } from "@/contexts/audio/AudioContext";

// getUser 함수 수정
const getUser = async () => {
Expand Down Expand Up @@ -68,13 +69,15 @@ export default async function RootLayout({
<ThemeProvider>
<AuthProvider initialAuth={isAuthenticated}>
<UserProvider initialUser={initialUser}>
<BackgroundImage />
<div className="relative min-h-screen w-full overflow-hidden">
<div className="fixed inset-0 backdrop-blur-[2px] bg-white/[0.01] dark:bg-transparent" />
<AudioProvider>
<BackgroundImage />
<div className="relative min-h-screen w-full overflow-hidden">
<div className="fixed inset-0 backdrop-blur-[2px] bg-white/[0.01] dark:bg-transparent" />
<Sidebar />
<Header />
<main className="relative pt-24">{children}</main>
</div>
</div>
</AudioProvider>
</UserProvider>
</AuthProvider>
</ThemeProvider>
Expand Down
73 changes: 66 additions & 7 deletions src/components/albums/TrackList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use client'

import { cn } from "@/lib/utils";
import { Play, Music, Loader2 } from "lucide-react";
import { Play, Music, Loader2, Pause } from "lucide-react";
import { formatDuration } from "@/lib/format";
import { useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";
import { TrackActions } from "@/components/albums/TrackActions";
import { useAuth } from "@/contexts/auth/AuthContext";
import { useUser } from "@/contexts/auth/UserContext";
import { useAudio } from "@/contexts/audio/AudioContext";

interface Track {
uuid: string;
Expand All @@ -20,16 +21,31 @@ interface TrackListProps {
tracks: Track[];
albumId: string;
artistId: string;
album: {
title: string;
artImage: string;
};
artist: {
name: string;
artistImage: string;
};
}

export function TrackList({ tracks: initialTracks, albumId, artistId }: TrackListProps) {
export function TrackList({
tracks: initialTracks,
albumId,
artistId,
album,
artist
}: TrackListProps) {
const { isAuthenticated } = useAuth();
const { user } = useUser();
const [tracks, setTracks] = useState<Track[]>(initialTracks);
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const { ref, inView } = useInView();
const { currentTrack, isPlaying, play, pause } = useAudio();

const isOwner = isAuthenticated && user?.uuid === artistId;

Expand Down Expand Up @@ -73,6 +89,38 @@ export function TrackList({ tracks: initialTracks, albumId, artistId }: TrackLis

const hasNoTracks = !tracks || tracks.length === 0;

const handlePlayPause = (track: Track) => {
const fullTrack = {
trackResponseDto: {
uuid: track.uuid,
title: track.title,
duration: track.duration,
lyric: track.lyric,
artUrl: album.artImage,
},
albumResponseDto: {
uuid: albumId,
title: album.title,
artImage: album.artImage,
},
artistResponseDto: {
uuid: artistId,
name: artist.name,
artistImage: artist.artistImage,
},
};

if (currentTrack?.trackResponseDto.uuid === track.uuid) {
if (isPlaying) {
pause();
} else {
play(fullTrack);
}
} else {
play(fullTrack);
}
};

return (
<div className="p-8 pt-4">
{hasNoTracks ? (
Expand All @@ -93,18 +141,29 @@ export function TrackList({ tracks: initialTracks, albumId, artistId }: TrackLis
"rounded-xl",
"transition-all duration-300",
"hover:bg-white/5",
"group relative"
"group relative",
currentTrack?.trackResponseDto.uuid === track.uuid && "bg-white/5"
)}
>
<div className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="absolute inset-0 bg-gradient-to-r from-white/5 to-transparent" />
</div>

<div className="relative flex items-center gap-4 w-full">
<div className="w-8 text-sm text-muted-foreground group-hover:opacity-0 transition-opacity">
{String(index + 1).padStart(2, '0')}
</div>
<Play className="w-4 h-4 absolute left-2 opacity-0 group-hover:opacity-100 transition-all duration-300" />
<button
onClick={() => handlePlayPause(track)}
className="w-8 flex items-center justify-center"
>
<div className="text-sm text-muted-foreground group-hover:opacity-0 transition-opacity">
{String(index + 1).padStart(2, '0')}
</div>
{currentTrack?.trackResponseDto.uuid === track.uuid && isPlaying ? (
<Pause className="w-4 h-4 absolute opacity-0 group-hover:opacity-100 transition-all duration-300" />
) : (
<Play className="w-4 h-4 absolute opacity-0 group-hover:opacity-100 transition-all duration-300" />
)}
</button>

<div className="flex-1 text-left">{track.title}</div>
<div className="text-sm text-muted-foreground mr-4">
{formatDuration(track.duration)}
Expand Down
41 changes: 31 additions & 10 deletions src/components/home/TrackSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,38 @@ import { formatDuration } from "@/lib/format";
import { useEffect, useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useAudio } from "@/contexts/audio/AudioContext";

interface Track {
trackResponseDto: {
uuid: string;
title: string;
duration: number;
artUrl: string;
lyric: string;
};
albumResponseDto: {
uuid: string;
title: string;
artImage: string;
};
artistResponseDto: {
uuid: string;
name: string;
artistImage: string;
};
}

export function TrackSection() {
const [tracks, setTracks] = useState<Track[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { currentTrack, isPlaying, play, pause } = useAudio();

const fetchTracks = async () => {
try {
setIsLoading(true);
setError(null);

console.log("Fetching tracks..."); // 디버깅 로그

const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/tracks?page=0&pageSize=10`,
Expand All @@ -43,19 +46,19 @@ export function TrackSection() {
}
);

console.log("Response status:", response.status); // 디버깅 로그

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
console.log("Received data:", data); // 디버깅 로그

setTracks(data);
} catch (error) {
console.error("Error fetching tracks:", error); // 디버깅 로그
setError(error instanceof Error ? error.message : "트랙 목록을 불러오는데 실패했습니다.");
setError(
error instanceof Error
? error.message
: "트랙 목록을 불러오는데 실패했습니다."
);
} finally {
setIsLoading(false);
}
Expand All @@ -65,6 +68,18 @@ export function TrackSection() {
fetchTracks();
}, []);

const handlePlayPause = (track: Track) => {
if (currentTrack?.trackResponseDto.uuid === track.trackResponseDto.uuid) {
if (isPlaying) {
pause();
} else {
play(track);
}
} else {
play(track);
}
};

if (error) {
return (
<section className="p-6 border-t border-white/10">
Expand Down Expand Up @@ -93,15 +108,19 @@ export function TrackSection() {
) : (
<div className="space-y-1">
{tracks.map((track) => (
<div
<button
key={track.trackResponseDto.uuid}
onClick={() => handlePlayPause(track)}
className={cn(
"w-full px-4 py-3",
"flex items-center gap-4",
"rounded-xl",
"transition-all duration-300",
"hover:bg-white/5",
"group relative"
"group relative",
"text-left",
currentTrack?.trackResponseDto.uuid === track.trackResponseDto.uuid &&
"bg-white/5"
)}
>
<div className="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300">
Expand All @@ -125,13 +144,15 @@ export function TrackSection() {
<Link
href={`/profile/${track.artistResponseDto.uuid}`}
className="hover:underline truncate"
onClick={(e) => e.stopPropagation()}
>
{track.artistResponseDto.name}
</Link>
<span></span>
<Link
href={`/albums/${track.albumResponseDto.uuid}`}
className="hover:underline truncate"
onClick={(e) => e.stopPropagation()}
>
{track.albumResponseDto.title}
</Link>
Expand All @@ -141,7 +162,7 @@ export function TrackSection() {
{formatDuration(track.trackResponseDto.duration)}
</div>
</div>
</div>
</button>
))}
</div>
)}
Expand Down
Loading

0 comments on commit f1958de

Please sign in to comment.