ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 10. 메세지 수정
    사이드프로젝트 2024. 7. 23. 17:01

    https://www.youtube.com/watch?v=-xXASlyU0Ck&t=2007s

    를 보며 실습하며 정리하는 글

     

    ---

    이어서 수정기능을 만들어준다. 

    Shad cn에서 dialog를 검색하고 인스톨해준다.

    https://ui.shadcn.com/docs/components/dialog

     

    Dialog

    A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

    ui.shadcn.com

    npx shadcn-ui@latest add dialog

     

    이걸 설치하고, 예시코드를 붙여넣어보면 label이 없다고 빨간줄이 그일텐데, label도 검색해서 install 해준다.

    https://ui.shadcn.com/docs/components/label

     

    Label

    Renders an accessible label associated with controls.

    ui.shadcn.com

    npx shadcn-ui@latest add label

     

    그리고 Message Action에 Delete 할때와 비슷하게 만들어준다.

    import {
      Dialog,
      DialogContent,
      DialogDescription,
      DialogFooter,
      DialogHeader,
      DialogTitle,
      DialogTrigger,
    } from "@/components/ui/dialog";
    import { Input } from "@/components/ui/input";
    import { Label } from "@/components/ui/label";
    
    export function EditAlert() {
      return (
        <Dialog>
          <DialogTrigger asChild>
            {/* 삭제할때와 마찬가지로, 빈 버튼. id만 넣어줌 */}
            <button id="trigger-edit"></button>
          </DialogTrigger>
          <DialogContent className="sm:max-w-[425px]">
            <DialogHeader>
              <DialogTitle>Edit profile</DialogTitle>
              <DialogDescription>
                Make changes to your profile here. Click save when youre done.
              </DialogDescription>
            </DialogHeader>
            <div className="grid gap-4 py-4">
              <div className="grid grid-cols-4 items-center gap-4">
                <Label htmlFor="name" className="text-right">
                  Name
                </Label>
                <Input
                  id="name"
                  defaultValue="Pedro Duarte"
                  className="col-span-3"
                />
              </div>
              <div className="grid grid-cols-4 items-center gap-4">
                <Label htmlFor="username" className="text-right">
                  Username
                </Label>
                <Input
                  id="username"
                  defaultValue="@peduarte"
                  className="col-span-3"
                />
              </div>
            </div>
            <DialogFooter>
              <Button type="submit">Save changes</Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      );
    }

     

    // components/listMessages.tsx

    "use client";
    
    import { useMessage } from "@/lib/store/messages";
    import React from "react";
    import Message from "./Message";
    import { DeleteAlert, EditAlert } 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 />
          {/* Edit 추가 */}
          <EditAlert />
        </div>
      );
    };
    
    export default ListMessages;

    // 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
              onClick={() => {
                document.getElementById("trigger-edit")?.click();
                setActionMessage(message);
              }}
            >
              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;

     

    이제 다시 MessageActions.tsx로 가서 EditAlert를 조금 손봐준다. Delete 때랑 거의 비슷하다. state를 가져와서 Input에 Default value로 넣어줌으로써 수정할 메세지가 바로 보이게끔 했다.

    export function EditAlert() {
      const actionMessage = useMessage((state) => state.actionMessage);
    
      return (
        <Dialog>
          <DialogTrigger asChild>
            {/* 삭제할때와 마찬가지로, 빈 버튼. id만 넣어줌 */}
            <button id="trigger-edit"></button>
          </DialogTrigger>
          <DialogContent className="w-full">
            <DialogHeader>
              <DialogTitle>Edit profile</DialogTitle>
              <DialogDescription>
                Make changes to your profile here. Click save when youre done.
              </DialogDescription>
            </DialogHeader>
    
            <Input id="username" defaultValue={actionMessage?.text} />
    
            <DialogFooter>
              <Button type="submit">Save changes</Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      );
    }

     

     

    이제 에딧 버튼을 클릭해보면 고칠 메세지가 잘 나온다. 

    이제 supabase를 연결해서 디비에있는 데이터를 수정할 차례다.

    delete 할때와 거의 비슷하게 Edit Alert를 수정해준다. supabase 연결하고, 수정 리퀘스트를 보낸다.

    export function EditAlert() {
      const actionMessage = useMessage((state) => state.actionMessage);
      const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>;
    
      const handleEdit = async () => {
        const supabase = createClient();
        const text = inputRef.current.value;
        if (text) {
          const { error } = await supabase
            .from("messages")
            .update({ text, is_edit: true })
            .eq("id", actionMessage?.id!);
    
          if (error) {
            toast.error(error.message);
          } else {
            toast.success("Successfully update a message");
          }
    
          document.getElementById("trigger-edit")?.click();
        }
      };
    
      return (
        <Dialog>
          <DialogTrigger asChild>
            {/* 삭제할때와 마찬가지로, 빈 버튼. id만 넣어줌 */}
            <button id="trigger-edit"></button>
          </DialogTrigger>
          <DialogContent className="w-full">
            <DialogHeader>
              <DialogTitle>Edit Message</DialogTitle>
              <DialogDescription>Edit Message</DialogDescription>
            </DialogHeader>
    
            <Input defaultValue={actionMessage?.text} ref={inputRef} />
    
            <DialogFooter>
              <Button type="submit" onClick={handleEdit}>
                Save changes
              </Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      );
    }

     

    이제 메세지를 수정하고 새로고침해보면 수정되는 걸 볼 수 있다.

     

    그리고 이번에도 역시 Optimistic Update를 해준다.

    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;
      optimisticDeleteMessage: (messageId: string) => void;
      optimisticUpdateMessage: (message: IMessage) => 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),
          };
        }),
      optimisticUpdateMessage: (updateMessage) =>
        set((state) => {
          return {
            messages: state.messages.filter((message) => {
              if (message.id == updateMessage.id) {
                message.text = updateMessage.text;
                message.is_edit = updateMessage.is_edit;
              }
              return message;
            }),
          };
        }),
    }));

     

    다시 components/MessageActions.tsx 안의 EditAlert

    export function EditAlert() {
      const actionMessage = useMessage((state) => state.actionMessage);
      const optimisticUpdateMessage = useMessage(
        (state) => state.optimisticUpdateMessage
      );
      const inputRef = useRef() as React.MutableRefObject<HTMLInputElement>;
    
      const handleEdit = async () => {
        const supabase = createClient();
        const text = inputRef.current.value.trim();
        if (text) {
          optimisticUpdateMessage({
            ...actionMessage,
            text,
            is_edit: true,
          } as IMessage);
          const { error } = await supabase
            .from("messages")
            .update({ text, is_edit: true })
            .eq("id", actionMessage?.id!);
    
          if (error) {
            toast.error(error.message);
          } else {
            toast.success("Successfully update a message");
          }
    
          document.getElementById("trigger-edit")?.click();
        } else {
          // 메세지가 없을 경우 처리. Delete Alert을 띄운다.
          document.getElementById("trigger-edit")?.click();
          document.getElementById("trigger-delete")?.click();
        }
      };
    
      return (
        <Dialog>
          <DialogTrigger asChild>
            {/* 삭제할때와 마찬가지로, 빈 버튼. id만 넣어줌 */}
            <button id="trigger-edit"></button>
          </DialogTrigger>
          <DialogContent className="w-full">
            <DialogHeader>
              <DialogTitle>Edit Message</DialogTitle>
              <DialogDescription>Edit Message</DialogDescription>
            </DialogHeader>
    
            <Input defaultValue={actionMessage?.text} ref={inputRef} />
    
            <DialogFooter>
              <Button type="submit" onClick={handleEdit}>
                Save changes
              </Button>
            </DialogFooter>
          </DialogContent>
        </Dialog>
      );
    }

    이제 수정을 누르면 바로 optimistic update가 되는 걸 볼 수 있다.

    또, 여기서 메세지를 다 지우고 Edit을 할경우 Delete Alert을 띄우도록 했다.

     

    이제 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
              onClick={() => {
                document.getElementById("trigger-edit")?.click();
                setActionMessage(message);
              }}
            >
              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>
                {
                  // 요기!! 수정되었다고 표시
                  message.is_edit && (
                    <h1 className="text-sm text-gray-400">edited</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;

     

    밑에 html 부분에 요기!! 수정되었다고 표시 쪽에 보면 메세지의 is_edit이 true일 때, edited가 표시되도록 한 걸 볼 수 있다.

     

    Github : https://github.com/Wunhyeon/Next-Supabase-Chat/tree/10.UpdateMessage

     

    GitHub - Wunhyeon/Next-Supabase-Chat

    Contribute to Wunhyeon/Next-Supabase-Chat development by creating an account on GitHub.

    github.com

     

    '사이드프로젝트' 카테고리의 다른 글

    12. Arrow down & notification  (4) 2024.07.24
    11.실시간 통신  (3) 2024.07.23
    9. Message Delete  (7) 2024.07.23
    8.Message Menu  (1) 2024.07.22
    6. 메세지 리스트 표시하기  (4) 2024.07.22

    댓글

Designed by Tistory.