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 9575d53 commit 47ded5e
Show file tree
Hide file tree
Showing 7 changed files with 454 additions and 12 deletions.
1 change: 1 addition & 0 deletions src/app/albums/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default async function AlbumPage({ params }: AlbumPageProps) {
<TrackList
tracks={album.trackResponseDtos}
albumId={params.id}
artistId={album.artistResponseDto.uuid}
/>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BackgroundImage } from "@/components/layout/BackgroundImage";
import { getAuthCookie } from "@/lib/server-auth";
import { ThemeProvider } from "@/contexts/theme/ThemeContext";
import { UserProvider } from "@/contexts/auth/UserContext";
import { Toaster } from "@/components/ui/toaster"

// getUser 함수 수정
const getUser = async () => {
Expand Down Expand Up @@ -77,6 +78,7 @@ export default async function RootLayout({
</UserProvider>
</AuthProvider>
</ThemeProvider>
<Toaster />
</body>
</html>
);
Expand Down
10 changes: 2 additions & 8 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { cn } from "@/lib/utils";
import { AlbumSection } from "@/components/home/AlbumSection";
import { TrackSection } from "@/components/home/TrackSection";

export default function Home() {
return (
Expand All @@ -18,14 +19,7 @@ export default function Home() {
)}
>
<AlbumSection />

{/* 최신 업로드 섹션 */}
<section className="p-6 border-t border-white/10">
<h2 className="text-xl font-bold mb-4">최신 업로드</h2>
<div className="space-y-4">
{/* 트랙 리스트가 들어갈 자리 */}
</div>
</section>
<TrackSection />
</div>
</div>

Expand Down
138 changes: 138 additions & 0 deletions src/components/albums/EditTrackModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"use client";

import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
import { checkAuth } from "@/lib/auth";

interface Track {
uuid: string;
title: string;
lyric: string;
}

interface EditTrackModalProps {
track: Track;
isOpen: boolean;
onClose: () => void;
onUpdate: (track: Track) => void;
}

export function EditTrackModal({ track, isOpen, onClose, onUpdate }: EditTrackModalProps) {
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const [form, setForm] = useState({
title: track.title,
lyric: track.lyric,
});

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

try {
setIsSubmitting(true);
const { accessToken } = await checkAuth();

const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/tracks/${track.uuid}`,
{
method: "PATCH",
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify({
title: form.title.trim(),
lyric: form.lyric.trim(),
}),
}
);

if (!response.ok) throw new Error("트랙 수정에 실패했습니다.");

const updatedTrack = { ...track, ...form };
onUpdate(updatedTrack);

toast({
title: "트랙 수정 완료",
description: "트랙이 수정되었습니다.",
variant: "default",
});

onClose();
} catch (error) {
toast({
variant: "destructive",
title: "트랙 수정 실패",
description: error instanceof Error ? error.message : "트랙 수정에 실패했습니다.",
});
} finally {
setIsSubmitting(false);
}
};

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>트랙 수정</DialogTitle>
<DialogDescription className="hidden">트랙 정보를 수정해주세요.</DialogDescription>
</DialogHeader>

<form onSubmit={handleSubmit} className="space-y-6 mt-4">
<div>
<label className="text-sm font-medium mb-2 block">트랙 제목</label>
<Input
value={form.title}
onChange={(e) =>
setForm((prev) => ({ ...prev, title: e.target.value }))
}
className="bg-white/5 border-white/10"
maxLength={50}
required
disabled={isSubmitting}
/>
</div>

<div>
<label className="text-sm font-medium mb-2 block">가사</label>
<Textarea
value={form.lyric}
onChange={(e) =>
setForm((prev) => ({ ...prev, lyric: e.target.value }))
}
className="bg-white/5 border-white/10 min-h-[120px] resize-none"
maxLength={1000}
required
disabled={isSubmitting}
/>
</div>

<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={onClose}
disabled={isSubmitting}
>
취소
</Button>
<Button type="submit" disabled={isSubmitting}>
수정하기
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
}
133 changes: 133 additions & 0 deletions src/components/albums/TrackActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import { MoreVertical, Edit2, Trash2 } from "lucide-react";
import { useState } from "react";
import { useToast } from "@/hooks/use-toast";
import { checkAuth } from "@/lib/auth";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { EditTrackModal } from "./EditTrackModal";

interface Track {
uuid: string;
title: string;
lyric: string;
}

interface TrackActionsProps {
track: Track;
onUpdate: (track: Track) => void;
onDelete: (trackId: string) => void;
}

export function TrackActions({ track, onUpdate, onDelete }: TrackActionsProps) {
const { toast } = useToast();
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);

const handleDelete = async () => {
try {
setIsDeleting(true);
const { accessToken } = await checkAuth();

const response = await fetch(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/tracks/${track.uuid}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${accessToken}`,
},
credentials: "include",
}
);

if (!response.ok) throw new Error("트랙 삭제에 실패했습니다.");

onDelete(track.uuid);
toast({
title: "트랙 삭제 완료",
variant: "default",
description: "트랙이 삭제되었습니다.",
});
} catch (error) {
toast({
variant: "destructive",
title: "트랙 삭제 실패",
description: error instanceof Error ? error.message : "트랙 삭제에 실패했습니다.",
});
} finally {
setIsDeleting(false);
setShowDeleteDialog(false);
}
};

return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="p-2 hover:bg-white/5 rounded-full">
<MoreVertical className="w-4 h-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem
onClick={() => setShowEditModal(true)}
>
<Edit2 className="w-4 h-4 mr-2" />
수정
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setShowDeleteDialog(true)}
className="text-red-500 focus:text-red-500"
>
<Trash2 className="w-4 h-4 mr-2" />
삭제
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>트랙을 삭제하시겠습니까?</AlertDialogTitle>
<AlertDialogDescription>
이 작업은 되돌릴 수 없습니다.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>취소</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={isDeleting}
className="bg-destructive hover:bg-destructive/90"
>
삭제
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<EditTrackModal
track={track}
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
onUpdate={onUpdate}
/>
</>
);
}
34 changes: 30 additions & 4 deletions src/components/albums/TrackList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,34 @@ import { Play, Music, Loader2 } 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";

interface Track {
uuid: string;
title: string;
duration: number;
lyric: string;
}

interface TrackListProps {
tracks: Track[];
albumId: string;
artistId: string;
}

export function TrackList({ tracks: initialTracks, albumId }: TrackListProps) {
export function TrackList({ tracks: initialTracks, albumId, artistId }: 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 isOwner = isAuthenticated && user?.uuid === artistId;

const fetchMoreTracks = async () => {
if (isLoading || !hasMore) return;

Expand Down Expand Up @@ -76,7 +85,7 @@ export function TrackList({ tracks: initialTracks, albumId }: TrackListProps) {
) : (
<div className="space-y-1">
{tracks.map((track, index) => (
<button
<div
key={track.uuid}
className={cn(
"w-full px-4 py-3",
Expand All @@ -97,11 +106,28 @@ export function TrackList({ tracks: initialTracks, albumId }: TrackListProps) {
</div>
<Play className="w-4 h-4 absolute left-2 opacity-0 group-hover:opacity-100 transition-all duration-300" />
<div className="flex-1 text-left">{track.title}</div>
<div className="text-sm text-muted-foreground">
<div className="text-sm text-muted-foreground mr-4">
{formatDuration(track.duration)}
</div>
{isOwner && (
<TrackActions
track={track}
onUpdate={(updatedTrack) => {
setTracks(prev =>
prev.map(t => t.uuid === updatedTrack.uuid ? {
...t,
title: updatedTrack.title,
lyric: updatedTrack.lyric
} : t)
);
}}
onDelete={(deletedTrackId) => {
setTracks(prev => prev.filter(t => t.uuid !== deletedTrackId));
}}
/>
)}
</div>
</button>
</div>
))}

{hasMore && (
Expand Down
Loading

0 comments on commit 47ded5e

Please sign in to comment.