Project

GraphQL 대화 생성

프도의길 2023. 8. 28. 14:56

저번에 유저 검색하는 부분을 하였습니다 검색 한 유저와 대화를 할 수 있게  대화 생성을 만들어보겠습니다.

선택누르시 검색한 유저랑 채팅할 수 있는 대화방 생성을 만들어 보겠습니다.

먼저 schema.prisma에서 테이블을 만들어줍니다

generator client {
  provider        = "prisma-client-js"
  binaryTargets = ["native","darwin","darwin-arm64","debian-openssl-1.1.x"]
}

datasource db {
  provider = "mongodb"
  url      = env("MONGODB_URL")
}

model Account {
  id  String  @id @default(auto()) @map("_id") @db.ObjectId
  userId             String
  type               String
  provider           String
  providerAccountId  String
  refresh_token      String?  @db.String
  access_token       String?  @db.String
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String?  @db.String
  session_state      String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id  String  @id @default(auto()) @map("_id") @db.ObjectId
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id  String  @id @default(auto()) @map("_id") @db.ObjectId
  name          String?
  email         String?   @unique
  username      String?   @unique
  conversations ConversationParticipant[]
  messages      Message[]
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  id  String  @id @default(auto()) @map("_id") @db.ObjectId
  identifier String
  token      String   @unique
  expires    DateTime
  @@unique([identifier, token])
}

model Conversation {
   id              String  @id @default(auto()) @map("_id") @db.ObjectId
   messages        Message[] @relation("conversationMessages")
   participants    ConversationParticipant[]
   latestMessageId String? @unique
   latestMessage   Message?   @relation(name: "latestConversationMessage", fields: [latestMessageId], references: [id], onUpdate: NoAction, onDelete: NoAction)
   createdAt       DateTime @default(now())
   updatedAt       DateTime  @updatedAt
}


model ConversationParticipant {
   id               String  @id @default(auto()) @map("_id") @db.ObjectId
   userId           String
   user             User    @relation(fields:[userId], references:[id])
   conversationId   String 
   conversation     Conversation @relation(fields:[conversationId], references:[id])
   hasSeenLatestMessage Boolean
   createdAt        DateTime @default(now())
   updatedAt        DateTime @updatedAt
}

model Message {
   id             String  @id @default(auto()) @map("_id") @db.ObjectId
   conversation   Conversation  @relation(name: "conversationMessages", fields: [conversationId], references: [id])
   conversationId String
   senderId       String
   sender         User     @relation(fields:[senderId], references:[id])
   body           String
   isLatestIn     Conversation? @relation(name:"latestConversationMessage")
   createdAt      DateTime @default(now())
   updatedAt      DateTime @updatedAt
}

다대다 형식으로 만들어줍니다 prisma를 수정할때 무조건 npx prisma generate --schema=src/prisma/schema.prisma 해줍니다.

//backend src/graphql/resolvers/conversation.ts
import { GraphQLError } from "graphql";
import { Prisma } from "@prisma/client";
import { ApolloError } from "apollo-server-core";
import { withFilter } from "graphql-subscriptions";
import { ConversationPopulated, GraphQLContext } from "./../../util/types";
const resolvers = {
  Query: {
    conversations: async (
      _: any,
      args: any,
      context: GraphQLContext
    ): Promise<Array<ConversationPopulated>> => {
      console.log("CONVERSATIONS QUERY");
      const { session, prisma } = context;

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

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

      try {
        const conversations: any = await prisma.conversation.findMany({
          /**
           * Below has been confirmed to be the correct
           * query by the Prisma team. Has been confirmed
           * that there is an issue on their end
           * Issue seems specific to Mongo
           */
          // where: {
          //   participants: {
          //     some: {
          //       userId: {
          //         equals: userId,
          //       },
          //     },
          //   },
          // },
          include: conversationPopulated,
        });

        return conversations.filter(
          (conversation: { participants: any[] }) =>
            !!conversation.participants.find((p) => p.userId === userId)
        );
      } catch (error: any) {
        console.log("conversations Err", error);
        throw new ApolloError(error?.message);
      }
    },
  },

  Mutation: {
    createConversation: async (
      _: any,
      args: { participantIds: Array<string> },
      context: GraphQLContext
    ): Promise<{ conversationId: string }> => {
      console.log("INSIDE CREATE CONVERSATION", args);
      const { session, prisma, pubsub } = context;
      const { participantIds } = args;

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

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

      try {
        const conversation = await prisma.conversation.create({
          data: {
            participants: {
              createMany: {
                data: participantIds.map((id) => ({
                  userId: id,
                  hasSeenLatestMessage: id === userId,
                })),
              },
            },
          },
          include: conversationPopulated,
        });


        return {
          conversationId: conversation.id,
        };
      } catch (error) {
        console.log("CreateConversation Err", error);
        throw new ApolloError("Err creating conversation");
      }
    },
  },
};

export const participantPopulated =
  Prisma.validator<Prisma.ConversationParticipantInclude>()({
    user: {
      select: {
        id: true,
        username: true,
      },
    },
  });

export const conversationPopulated =
  Prisma.validator<Prisma.ConversationInclude>()({
    participants: {
      include: participantPopulated,
    },
    latestMessage: {
      include: {
        sender: {
          select: {
            id: true,
            username: true,
          },
        },
      },
    },
  });

export default resolvers;

대화 id conversationId를 리턴해준다

//frontend src/compoents/Chat/Conversations/Modal/Modal.tsx
import {
  Button,
  Input,
  Modal,
  ModalBody,
  ModalCloseButton,
  ModalContent,
  ModalHeader,
  ModalOverlay,
  Stack,
  Text,
} from "@chakra-ui/react";
import UserOperations from "../../../../graphql/operations/user";
import ConversationOperations from "../../../../graphql/operations/conversation";
import React, { useCallback, useState } from "react";
import { Observable, useLazyQuery, useMutation } from "@apollo/client";
import {
  CreateConversationData,
  SearchedUser,
  SearchUsersData,
  SearchUsersInput,
} from "@/util/types";
import UserSearchList from "./userSearchList";
import Participants from "./Participants";
import toast from "react-hot-toast";
import { Session } from "next-auth";
import { useRouter } from "next/router";

interface ModalProps {
  session: Session;
  isOpen: boolean;
  onClose: () => void;
}

const ConversationModal = ({ session, isOpen, onClose }: ModalProps) => {
  const {
    user: { id: userId },
  } = session;

  const router = useRouter();

  const [username, setUsername] = useState("");
  const [participants, setParticipants] = useState<Array<SearchedUser>>([]);
  const [searchUsers, { data, loading, error }] = useLazyQuery<any>(
    UserOperations.Queries.searchUsers
  );

  const [createConversation, { loading: createConversationLoading }] =
    useMutation<CreateConversationData>(
      ConversationOperations.Mutations.createConversation
    );
  console.log("HERE IS SEARCH DATA", data);

  const onCreateConversation = async () => {
    const participantIds = [userId, ...participants.map((p) => p.id)];

    try {
      // createConversation Mutation
      const { data } = await createConversation({
        variables: {
          participantIds,
        },
      });

      if (!data?.createConversation) {
        throw new Error("Failed to create conversation");
      }

      const {
        createConversation: { conversationId },
      } = data;

      router.push({ query: { conversationId } }); //대화방 주소

      setParticipants([]);
      setUsername("");
      onClose();

      console.log("DATA", data);
    } catch (err: any) {
      console.log("onCreateConversation", err);
      toast.error(err?.message);
    }
  };

  const onSearch = useCallback(
    async (e: React.FormEvent) => {
      e.preventDefault();
      // search user query
      searchUsers({ variables: { username } as any });
    },
    [username]
  );

  const addParticipant = (user: SearchedUser) => {
    setParticipants((prev) => [...prev, user]);
    setUsername("");
  };

  const removeParticipant = (userId: string) => {
    console.log("removeParticipant");
    setParticipants((prev) => prev.filter((p) => p.id !== userId));
  };

  return (
    <>
      <Modal isOpen={isOpen} onClose={onClose}>
        <ModalOverlay />
        <ModalContent pb={4}>
          <ModalHeader>대화 생성</ModalHeader>
          <ModalCloseButton />
          <ModalBody>
            <form onSubmit={onSearch}>
              <Stack spacing={4}>
                <Input
                  placeholder="유저를 입력해주세요."
                  value={username}
                  onChange={(e) => setUsername(e.target.value)}
                />
                <Button type="submit" disabled={!username} isLoading={loading}>
                  검색
                </Button>
              </Stack>
            </form>
            {data?.searchUsers && (
              <UserSearchList
                users={data?.searchUsers}
                username={username}
                addParticipant={addParticipant}
              />
            )}
            {participants.length !== 0 && (
              <>
                <Participants
                  participants={participants}
                  removeParticipant={removeParticipant}
                />
                <Button
                  bg="blue.100"
                  width="100%"
                  mt={6}
                  _hover={{ bg: "gray.100" }}
                  isLoading={createConversationLoading}
                  onClick={onCreateConversation}
                >
                  대화 생성
                </Button>
              </>
            )}
          </ModalBody>
        </ModalContent>
      </Modal>
    </>
  );
};

export default ConversationModal;

 

선택 누르시 대화 id가 생성이 됩니다

'Project' 카테고리의 다른 글

GraphQL 대화생성 Subscription  (0) 2023.08.29
WebSocket/ Subscription  (0) 2023.08.29
GraphQL- 유저 검색 기능  (0) 2023.08.23
Graphql Query, prisma  (0) 2023.08.22
Graphql 리액트 연결  (0) 2023.08.22