Project

GraphQL 메시지 보내기

프도의길 2023. 9. 5. 21:06

input 메시지를 서버로 보내기 위해서 백엔드에서 먼저 구현해줍니다.

//backend src/graphql/typeDefs
import gql from "graphql-tag";

const typeDefs = gql`
  type Message {
    id: String
    sender: User
    body: String
    createdAt: Date
  }

  type Query {
    messages(conversationId: String): [Message]
  }

  type Mutation {
    sendMessage(
      id: String
      senderId: String
      conversationId: String
      body: String
    ): Boolean
  }

  type Subscription {
    messageSent(conversationId: String): Message
  }
`;

export default typeDefs;

Query는 메시지를 가져오는거고 Mutation은 위 메시지 보내기 부분입니다

subscription은 메시지 구독 정의한 부분을 실시간 통신하기 위해 적용합니다 

resolvers로 가겠습니다.

//backend src/graphql/resolvers/message.ts
import { conversationPopulated } from "./conversation";
import { Prisma } from "@prisma/client";
import { GraphQLError } from "graphql";
import { withFilter } from "graphql-subscriptions";
import {
  GraphQLContext,
  MessagePopulated,
  SendMessageArgments,
  MessageSentSubscriptionPayload,
} from "../../util/types";
import { userIsConversationPraticipant } from "../../util/functions";

const resolvers = {
  Query: {
    messages: async function (
      _: any,
      args: { conversationId: string },
      context: GraphQLContext
    ): Promise<Array<MessagePopulated>> {
      const { session, prisma } = context;
      const { conversationId } = args;

      if (!session?.user) {
        throw new GraphQLError("Not authorized");
      }

      const {
        user: { id: userId },
      } = session;

      //대화가 존재하는지 확인하고 사용자가 참가인지 확인
      const conversation = await prisma.conversation.findUnique({
        where: {
          id: conversationId,
        },
        include: conversationPopulated,
      });

      if (!conversation) {
        throw new GraphQLError("Conversation Not Found");
      }

      const allowedToView = userIsConversationPraticipant(
        conversation.participants,
        userId
      );
      //대화 참가자에 내가 있는지 확인
      if (!allowedToView) {
        throw new GraphQLError("Not Authorized");
      }

      try {
        const messages = await prisma.message.findMany({
          where: {
            conversationId,
          },
          include: messagePopulated,
          orderBy: {
            createdAt: "desc",
          },
        });

        return messages;
        return [
          { body: "hey dude this is a super mesaage" } as MessagePopulated,
        ];
      } catch (err: any) {
        console.log("messages erro", err);
        throw new GraphQLError(err.message);
      }
    },
  },
  Mutation: {
    sendMessage: async function (
      _: any,
      args: SendMessageArgments,
      context: GraphQLContext
    ): Promise<boolean> {
      const { session, prisma, pubsub } = context;

      if (!session?.user) {
        throw new GraphQLError("Not authorized");
      }
      const { id: userId } = session.user;
      const { id: messageId, senderId, conversationId, body } = args;

      if (userId !== senderId) {
        throw new GraphQLError("Not authorized");
      }

      try {
        // create new message

        const newMessage = await prisma.message.create({
          data: {
            id: messageId,
            senderId,
            conversationId,
            body,
          },
          include: messagePopulated,
        });

        //find ConversationParticipant
        const participant = await prisma.conversationParticipant.findFirst({
          where: {
            userId,
            conversationId,
          },
        });

        if (!participant) {
          throw new GraphQLError("참여자가 존재하지 않습니다.");
        }

        //update conversation
        const conversation = await prisma.conversation.update({
          where: {
            id: conversationId,
          },
          data: {
            latestMessageId: newMessage.id,
            participants: {
              update: {
                where: {
                  id: participant.id,
                },
                data: {
                  hasSeenLatestMessage: true,
                },
              },
              updateMany: {
                where: {
                  NOT: {
                    userId,
                  },
                },
                data: {
                  hasSeenLatestMessage: false,
                },
              },
            },
          },
          include: conversationPopulated,
        });
        //오더가 생성 됐어요
        pubsub.publish("MESSAGE_SENT", { messageSent: newMessage });
        // pubsub.publish("CONVERSATION_UPDATED", {
        //   conversationUpdated: {
        //     conversation,
        //   },
        // });
      } catch (err) {
        console.log("sendMessage err", err);
        throw new GraphQLError("ERR sending message");
      }

      return true;
    },
  },
  Subscription: {
    messageSent: {
      subscribe: withFilter(
        (_: any, __: any, context: GraphQLContext) => {
          const { pubsub } = context;
          return pubsub.asyncIterator(["MESSAGE_SENT"]); //구독하고 있는 클라이언트에게 오더데이터 알려줄게요
        },
        (
          payload: MessageSentSubscriptionPayload,
          args: { conversationId: string },
          context: GraphQLContext
        ) => {
          return payload.messageSent.conversationId === args.conversationId;
        }
      ),
    },
  },
};

export const messagePopulated = Prisma.validator<Prisma.MessageInclude>()({
  sender: {
    select: {
      id: true,
      username: true,
    },
  },
});

export default resolvers;

query부분에서 conversationId로 클릭한 대화 메시지(대화리스트)를 가져오기를 위해 만들어졌습니다.

mutation에서 publish이벤트의 발생을 알리고 Subscribe에  asyncIterator로 이벤트를 감지하요 이를 구독하고 있는 클라이언트에게 데이터를 전송할 수 있는 api를 제공합니다

pubsub.publish("MESSAGE_SENT", { messageSent: newMessage });

하지만 예를 들어 제주도대화에 있는 사용자가 메시지를 호출 했을 때 서울대화에 있는 메시지에게 요청이 가면 안 되겠죠? 이를 해결하기 위해 저희는 Subscription withFilter 을 활용했습니다. 말 그대로 특정 이벤트를 구독하고 있는 모든 사용자에게 알림을 주는 것이 아닌 특정 조건을 만족하는 사용자에게만 알림을 주는 것이죠.

Subscribe블로그 참조-> (https://happy8131.tistory.com/121)

다시 프론트로 넘어옵니다

//frontend src/graphql/operations/messages.ts
import { gql } from "@apollo/client";

export const MessageFields = `
id
sender {
  id
  username
}
body
createdAt
`;

export default {
  Query: {
    messages: gql`
    query Messages($conversationId:String!){
          messages(conversationId:$conversationId){
            ${MessageFields}
          }
    }
    `,
  },
  Mutation: {
    sendMessage: gql`
      mutation SendMessage(
        $id: String!
        $senderId: String!
        $conversationId: String!
        $body: String!
      ) {
        sendMessage(
          id: $id
          senderId: $senderId
          conversationId: $conversationId
          body: $body
        )
      }
    `,
  },
  Subscription: {
    messageSent: gql`
      subscription MessageSent($conversationId: String!) {
        messageSent(conversationId: $conversationId){
          ${MessageFields}
        }
      }
    `,
  },
};

onSendMessage를 만들어줍니다.

//frontend src/component/Chat/Feed/Messages/Input.tsx
import { useMutation } from "@apollo/client";
import { Box, Input } from "@chakra-ui/react";
import { Session } from "next-auth";
import React, { useState } from "react";
import toast from "react-hot-toast";
import { SendMessageArgments } from "../../../../../../backend/src/util/types";
import MessagesOperations from "../../../../graphql/operations/messages";
import { ObjectID } from "bson";

interface MessageInputProps {
  session: Session;
  conversationId: string;
}
const MessageInput = ({ session, conversationId }: MessageInputProps) => {
  const [messageBody, setMessageBody] = useState("");
  const [sendMessage] = useMutation<SendMessageArgments>(
    MessagesOperations.Mutation.sendMessage
  );

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

    try {
      const { id: senderId } = session.user;
      const newId = new ObjectID().toString();
      const newMessage: SendMessageArgments = {
        id: newId,
        senderId,
        conversationId,
        body: messageBody,
      };
      const { data, errors } = await sendMessage({
        variables: {
          ...newMessage,
        },
      });

      if (!data?.sendMessage || errors) {
        throw new Error("메세지 보내기 실패");
      }
    } catch (err: any) {
      console.log("onSendMessage Err", err);
      toast.error(err.message);
    }
  };

  return (
    <Box px={4} py={6} width="100%`">
      <form onSubmit={onSendMessage}>
        <Input
          value={messageBody}
          onChange={(e) => setMessageBody(e.target.value)}
          placeholder="메시지를 입력해주세요."
          size="md"
          resize="none"
          _focus={{
            boxShadow: "none",
            border: "1px solid",
            borderColor: "whiteAlpha.300",
          }}
        />
      </form>
    </Box>
  );
};

export default MessageInput;

useMutation가져와서 sendMessage를 가져옵니다

인자로 id를 가져오는데  moongo와 함께 작업라려면 bson을 가져옵니다 npm i bson 설치해줍니다

해당 메시지 아이디가 되고 senderId는 사용자로부터 오는 id가 됩니다 conversationId는 대화 id를 가져옵니다

body에 iuput에 메시지를 가져옵니다 그리고 이 데이터들을 mutation에 sendMessage로 전달합니다.

//frontend src//components/Chat/Feed/Messages.tsx
import SkeletonLoader from "@/components/common/SkeletonLoader";
import { MessagesData, MessageSubscriptionData } from "@/util/types";
import { useQuery } from "@apollo/client";
import { Flex, Stack, Text } from "@chakra-ui/react";
import { useEffect } from "react";
import toast from "react-hot-toast";
import MessagesOperations from "../../../../graphql/operations/messages";

interface MessagesProps {
  userId: string;
  conversationId: string;
}

const Messages = ({ userId, conversationId }: MessagesProps) => {
  const { data, loading, error, subscribeToMore } = useQuery(
    MessagesOperations.Query.messages,
    {
      variables: {
        conversationId,
      },
      onError: ({ message }) => {
        toast.error(message);
      },
    }
  );

  if (error) {
    return null;
  }

  const subscribeToMoreMessages = (conversationId: string) => {
    subscribeToMore({
      document: MessagesOperations.Subscription.messageSent,
      variables: {
        conversationId,
      },
      updateQuery: (prev, { subscriptionData }: MessageSubscriptionData) => {
        if (!subscriptionData) return prev;

        const newMessage = subscriptionData.data.messageSent;

        return Object.assign({}, prev, {
          messages: [newMessage, ...prev.messages],
        });
      },
    });
  };

  useEffect(() => {
    subscribeToMoreMessages(conversationId);
  }, [conversationId]);

  console.log("HERE IS MESSAGES DATA", data);

  return (
    <Flex direction="column" justify="flex-end" overflow="hidden">
      {loading && (
        <Stack spacing={4} px={4}>
          <SkeletonLoader count={4} height="60px" width="320px" />
          <span>Loading Messages</span>
        </Stack>
      )}
      {data?.messages && (
        <Flex direction="column-reverse" overflowY="scroll" height="100%">
          {data.messages.map((message: any) => (
            <Text key={message.body}>{message.body}</Text>
          ))}
        </Flex>
      )}
    </Flex>
  );
};

export default Messages;

subscribeToMoreMessages 함수에서 어떤 구독인지 conversationId를 전달 해줘야 합니다.

그러면

실시간 채팅이됩니다.

fireBox웹 브라우저에 안녕하세요라고 엔터쳐보겠습니다

이렇게 바로 실시간으로 업데이트가 됩니다.!

'Project' 카테고리의 다른 글

GraphQL 메시지 대화 삭제  (0) 2023.09.20
GraphQL 메시지 읽음으로 표시  (0) 2023.09.18
GraphQL 대화생성 Subscription  (0) 2023.08.29
WebSocket/ Subscription  (0) 2023.08.29
GraphQL 대화 생성  (0) 2023.08.28