-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
454 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
/> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.