-
9. Message Delete사이드프로젝트 2024. 7. 23. 03:34
https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s
를 보며 실습하며 정리하는 글
-----
저번시간에 Message Menu를 만든거에 이어서 Delete를 만들거다.
먼저 ShadCN에 가서 Alert Dialog를 검색하고 인스톨해준다.
https://ui.shadcn.com/docs/components/alert-dialog
npx shadcn-ui@latest add alert-dialog
그리고 새로운 컴포넌트를 만들어줄거다.
components/MessageActions.tsx 파일을 만들어준다.
import React from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; export function DeleteAlert() { return ( <AlertDialog> <AlertDialogTrigger asChild> <button id="trigger-delete"></button> // button에 id만 들어가있고 안에 내용이 없기 때문에 안보이는 버튼이다. </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Continue</AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); } const MessageActions = () => { return; }; export default MessageActions;
위에서 주의해서 봐야할 점은 button에 id만 넣어놓고 안에 내용이 없기 때문에 버튼이 보이지 않는다는 것이다.
그리고 위에서 만들어준 DeleteAlert를 components/ListMessages.tsx에 넣어준다.
"use client"; import { useMessage } from "@/lib/store/messages"; import React from "react"; import Message from "./Message"; import { DeleteAlert } from "./MessageActions"; const ListMessages = () => { const messages = useMessage((state) => state.messages); // zustand에 전역으로 저장된 state를 불러옴. return ( <div className="flex-1 flex flex-col p-5 h-full overflow-y-auto"> <div className="flex-1"></div> <div className="space-y-7"> {messages.map((value, idx) => { return <Message key={idx} message={value} />; })} </div> <DeleteAlert /> // <= 요기 </div> ); }; export default ListMessages;
그리고
components/message.tsx로 이동해 MessageMenu에서 삭제 버튼에 해당하는 곳에 이렇게 넣어준다.
const MessageMenu = () => { return ( <DropdownMenu> <DropdownMenuTrigger> <Ellipsis /> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuLabel>Action</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem>Edit</DropdownMenuItem> <DropdownMenuItem // 요기. Delete button의 아이디를 찾아내서 클릭해주는 함수 onClick={() => { document.getElementById("trigger-delete")?.click(); }} > Delete </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); };
이렇게 해주는 이유는 모든 대화마다 Dropdown menu에 Delete와 Edit 버튼을 추가해서 렌더링 해주는 것보다, 버튼 하나 생성해서 재사용가능하게 해주는 편이 더 좋기 때문이다.
여기까지 하면 버튼을 클릭했을때 경고문구가 잘 나올 것이다.
그런데 여기서 문제가, 현재 어떤 대화를 선택했는지를 알려주고 있지 않다. 그래서 어떤 대화를 삭제요청을 보내야할지가 없다.
이제 그 작업을 해주자.
먼저 lib/store/messages.ts로 이동해
import { create } from "zustand"; export type IMessage = { created_at: string; id: string; is_deleted: string | null; is_edit: boolean; text: string; user_id: string; users: { avatar_url: string; created_at: string; deleted_at: string | null; display_name: string; id: string; updated_at: string | null; } | null; }; interface MessageState { messages: IMessage[]; addMessage: (message: IMessage) => void; actionMessage: IMessage | undefined; setActionMessage: (message: IMessage | undefined) => void; } export const useMessage = create<MessageState>()((set) => ({ messages: [], addMessage: (message) => set((state) => ({ messages: [...state.messages, message] })), // 기존 state에 담겨있던 message들 + 이번에 작성한 메세지 actionMessage: undefined, setActionMessage: (message) => set(() => ({ actionMessage: message })), }));
actionMessage와 setActionMessage라는 state를 추가해준다.
그리고 다시 components/Message.tsx로 이동해서
import { IMessage, useMessage } from "@/lib/store/messages"; import Image from "next/image"; import React from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Ellipsis } from "lucide-react"; import { useUser } from "@/lib/store/user"; const MessageMenu = ({ message }: { message: IMessage }) => { // 1. props로 메세지를 받는다. const setActionMessage = useMessage((state) => state.setActionMessage); // 2. setActionMessage를 state에서 받아온다. return ( <DropdownMenu> <DropdownMenuTrigger> <Ellipsis /> </DropdownMenuTrigger> <DropdownMenuContent> <DropdownMenuLabel>Action</DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem>Edit</DropdownMenuItem> <DropdownMenuItem // 요기 onClick={() => { document.getElementById("trigger-delete")?.click(); setActionMessage(message); // 3. setActionMessage로 actionMessage에 props로 받아온 메세지를 설정해준다. }} > Delete </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ); }; const Message = ({ message }: { message: IMessage }) => { const user = useUser((state) => state.user); // 유저 정보 가져오기 return ( <div className="flex gap-2"> <div> <Image src={message.users?.avatar_url!} alt={message.users?.display_name!} width={40} height={40} className="rounded-full right-2" /> </div> <div className="flex-1"> <div className="flex items-center justify-between"> <div className="flex items-center gap-1"> <h1 className="font-bold">{message.users?.display_name}</h1> <h1 className="text-sm text-gray-400"> {new Date(message.created_at).toDateString()} </h1> </div> {/* props로 메세지를 넘겨준다. */} {message.users?.id === user?.id && <MessageMenu message={message} />} </div> <p className="text-gray-300">{message.text}</p> </div> </div> ); }; export default Message;
1. MessageMen에서 props로 메세지를 받도록 해주고,
2. `const setActionMessage = useMessage((state) => state.setActionMessage); `// 2. setActionMessage를 state에서 받아온다.
3. <DropdownMenuItem
// 요기
onClick={() => {
document.getElementById("trigger-delete")?.click();
setActionMessage(message); // 3. setActionMessage로 actionMessage에 props로 받아온 메세지를 설정해준다.
}}
>그리고 아래 Messages에서 MessageMenu에 Props를 넘겨주는 걸 볼 수 있다.
이렇게되면 어떤 메세지에 액션(삭제 또는 수정)을 해야할 지 알 수 있다.
이제 Message Actions로 이동해, 위에서 setActionMessage를 이용해 상태를 set한 actionMessage 를 불러올 수 있다.
// components/MessageActions.tsx
import React from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { useMessage } from "@/lib/store/messages"; export function DeleteAlert() { const actionMessage = useMessage((state) => state.actionMessage); // 앞에서 action Message 설정해준 걸 불러온다. return ( <AlertDialog> <AlertDialogTrigger asChild> <button id="trigger-delete"></button> </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. {/* 테스트를 위해 */} {actionMessage?.text} </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction>Continue</AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); } const MessageActions = () => { return; }; export default MessageActions;
테스트를 위해 텍스트를 그대로 불러오게 해줬는데, 잘 나온다.
이제 메세지의 아이디를 받아서 Delete 리퀘스트를 보내주면 된다.
components/MessageActions.tsx에서 작업해주자.
import React from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { useMessage } from "@/lib/store/messages"; import { createClient } from "@/utils/supabase/client"; import { toast } from "sonner"; export function DeleteAlert() { const actionMessage = useMessage((state) => state.actionMessage); // 앞에서 action Message 설정해준 걸 불러온다. const handleDeleteMessage = async () => { const supabase = createClient(); // 요기 추가됨. const { data, error } = await supabase .from("messages") .delete() .eq("id", actionMessage?.id!); // eq는 쿼리에서 WHERE 과 같은 뜻이다. 아마도 equal을 표현한게 아닐까 추측해본다. if (error) { toast.error(error.message); } else { toast.success("Successfully Delete a Message"); } }; return ( <AlertDialog> <AlertDialogTrigger asChild> <button id="trigger-delete"></button> </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. {/* 테스트를 위해 */} {actionMessage?.text} </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction onClick={handleDeleteMessage}> Continue </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); } const MessageActions = () => { return; }; export default MessageActions;
supabase 클라이언트 객체를 가져와 delete요청을 보내주고 있다. ORM문법과 거의 유사하다. 그냥 ORM이라고 봐도 될듯.
.eq는 SQL의 WHERE과 같은 역할을 한다. 그러니깐
await supabase
.from("messages")
.delete()
.eq("id", actionMessage?.id!);는 DELETE FROM messages WHERE id = actionMessageID 이 쿼리랑 같은 거다.
그리고 밑에 에러처리도 해줬다.
이제 삭제를 해보면 삭제가 잘 된다. 새로고침을 해줘야 메세지가 사라지지만.
이것도 전시간에 INSERT 할때 optimistic update를 해줬던 것처럼 optimistic update로 지워줘보자.
lib/store/messages.ts로 이동해서 optimisticDeleteMessages 메서드를 만들어줬다.
import { create } from "zustand"; export type IMessage = { created_at: string; id: string; is_deleted: string | null; is_edit: boolean; text: string; user_id: string; users: { avatar_url: string; created_at: string; deleted_at: string | null; display_name: string; id: string; updated_at: string | null; } | null; }; interface MessageState { messages: IMessage[]; addMessage: (message: IMessage) => void; actionMessage: IMessage | undefined; setActionMessage: (message: IMessage | undefined) => void; optimisticDeleteMessage: (messageId: string) => void; } export const useMessage = create<MessageState>()((set) => ({ messages: [], addMessage: (message) => set((state) => ({ messages: [...state.messages, message] })), // 기존 state에 담겨있던 message들 + 이번에 작성한 메세지 actionMessage: undefined, setActionMessage: (message) => set(() => ({ actionMessage: message })), optimisticDeleteMessage: (messageId) => set((state) => { return { messages: state.messages.filter((message) => message.id != messageId), }; }), }));
이제 components/MessageActions.tsx로 이동해 이걸 사용해준다.
import React from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { useMessage } from "@/lib/store/messages"; import { createClient } from "@/utils/supabase/client"; import { toast } from "sonner"; export function DeleteAlert() { const actionMessage = useMessage((state) => state.actionMessage); // 앞에서 action Message 설정해준 걸 불러온다. const optimisticDeleteMessage = useMessage( // optimisticDelete 불러오기 (state) => state.optimisticDeleteMessage ); const handleDeleteMessage = async () => { const supabase = createClient(); optimisticDeleteMessage(actionMessage?.id!); // optimisticDelete 사용 const { data, error } = await supabase .from("messages") .delete() .eq("id", actionMessage?.id!); // eq는 쿼리에서 WHERE 과 같은 뜻이다. 아마도 equal을 표현한게 아닐까 추측해본다. if (error) { toast.error(error.message); } else { toast.success("Successfully Delete a Message"); } }; return ( <AlertDialog> <AlertDialogTrigger asChild> <button id="trigger-delete"></button> </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle> <AlertDialogDescription> This action cannot be undone. This will permanently delete your account and remove your data from our servers. {/* 테스트를 위해 */} {actionMessage?.text} </AlertDialogDescription> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction onClick={handleDeleteMessage}> Continue </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); } const MessageActions = () => { return; }; export default MessageActions;
이제 삭제를 눌러보면 대화가 삭제되는 것을 볼 수 있다.
그런데 아직은 문제가, 생성(optimistic update) -> 삭제 (optimistic update) -> 새로고침 하면 삭제가 제대로 안된 걸 볼 수 있는데, 이는 생성을 할 때 optimistic update로 supabase DB에 있는 아이디로 메세지 컴포넌트를 만든 게 아니라, uuid라이브러리를 통해 임의로 만들어진 아이디로 메세지를 생성해줬기 때문에, 생성된 아이디와 삭제하는 아이디가 실제 DB와 맞지 않아서다. 이문제는 나중에 고쳐보기로 하겠다.
github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/9.DeleteMessage
'사이드프로젝트' 카테고리의 다른 글
11.실시간 통신 (3) 2024.07.23 10. 메세지 수정 (3) 2024.07.23 8.Message Menu (1) 2024.07.22 6. 메세지 리스트 표시하기 (4) 2024.07.22 5. Message 보내기. - 메세지 테이블 만들기, Policy 생성, type 생성 (8) 2024.07.22