ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 11. 1:1 채팅방 API, 모든 내 대화방 가져오기, 그룹채팅방 만들기, 채팅방 이름 바꾸기
    MERN Stack ChatApp 2024. 8. 5. 17:43

    https://www.youtube.com/watch?v=HArC6GxkMMI&list=PLKhlp2qtUcSZsGkxAdgnPcHioRr-4guZf&index=10

     

    이번에는 1:1 채팅방 만들기를 하겠다.

    카톡이나 슬랙등에서 1:1 채팅을 해본 경험은 다들 있을 것이다.

    이 때, 기존의 채팅내역이 있다면 기존 채팅방을 불러오면 되지만, 없다면 새로운 1:1 채팅방을 만들게 된다.

    서버 => 라우터 => 컨트롤러 순으로 가겠다.

    // backend/server.js
    
    const express = require("express");
    const { chats } = require("./data/data");
    const dotenv = require("dotenv");
    const connectDB = require("./config/db");
    const userRoutes = require("./routes/userRoutes");
    const chatRoutes = require("./routes/chatRoutes");
    const { notFound, errorHandler } = require("./middleware/errorMiddleware");
    dotenv.config();
    
    const app = express();
    const PORT = process.env.PORT || 4000;
    connectDB();
    app.use(express.json()); // body parser (request body)
    
    app.get("/", (req, res) => {
      res.send("API is Running");
    });
    
    app.use("/api/user", userRoutes);
    app.use("/api/chat", chatRoutes); // 추가
    
    app.use(notFound); // 위 라우터에 걸리지 않는 url은 여기 걸린다.
    app.use(errorHandler); // 위에서부터 내려오는 에러들을 처리해주도록.
    
    app.listen(PORT, () => {
      console.log("Server Started on PORT : ", PORT);
    });

     

    // backend/routes/userRoutes.js
    
    const express = require("express");
    
    const { protect } = require("../middleware/authMiddleware");
    const { accessChat } = require("../controller/chatController");
    
    const router = express.Router();
    
    router.route("/").post(protect, accessChat);
    // router.route('/').get(protect,fetchChats);
    // router.route("/group").post(protect, createGroupChat);
    // router.route("/rename").put(protect, renameGroup);
    // router.route("/groupremove").delete(protect, removeFromGroup);
    // router.route("/groupadd").put(protect, addToGroup);
    
    module.exports = router;

     

     

    // backend/controller/chatController.js
    
    const asyncHandler = require("express-async-handler");
    const Chat = require("../models/chatModel");
    const User = require("../models/userModel");
    
    const accessChat = asyncHandler(async (req, res) => {
      const { userId } = req.body;
    
      if (!userId) {
        return res.sendStatus(400);
      }
    
      let isChat = await Chat.find({
        isGroupChat: false,
        $and: [{ users: { $eq: req.user._id } }, { users: { $eq: userId } }],
      })
        .populate("users", "-password") // user에서 password 빼고 Select
        .populate("latestMessage");
    
      //   isChat = await User.populate(isChat, {
      //     path: "latestMessage.sender",
      //     select: "name pic email",
      //   });
    
      //   console.log("isChat 2 : ", isChat);
    
      if (isChat.length > 0) {
        res.send(isChat[0]);
      } else {
        let chatData = {
          chatName: "sender",
          isGroupChat: false,
          users: [req.user._id, userId],
        };
    
        try {
          const createdChat = await Chat.create(chatData);
    
          const FullChat = await Chat.findOne({ _id: createdChat._id }).populate(
            "users",
            "-password"
          );
    
          res.status(200).send(FullChat);
        } catch (err) {
          throw new Error(err.message);
        }
      }
    });
    
    module.exports = { accessChat };

     

    컨트롤러에서 보면 먼저, isGroupChat 이 false이고 (1:1 대화이니), 참여한 유저들의 아이디가 내아이디 (req.userId)와, 내가 1:1 대화방을 찾고 싶은 유저의 아이디 (userId를 req.body()에서 받아와서) AND 조건으로 찾고있는 걸 볼 수 있다. populate는 JOIN과 같은거라 이해했다. 아래 참조자료 링크 달아놈.

    만약 채팅방이 있다면 기존 채팅방의 정보를 반환해주고, 없다면 새로 만들어서 보내준다.

     

    ++ 동영상에서는 Select를 할 때 유저를 찾을 때 $elemMatch라는 거를 사용하고 있는데, 아마도 조건 넣는 거 같은데(범위라던지, 일치하는 텍스트. groupBy와 LIKE 같은게 짬뽕된거라고 이해했다. ), 넣어주면 에러가 나고, 넣어줄 필요도 없는 것 같아 그냥 뺐다. 또, 아이디가 정확히 일치해야 하는데, 물론 uuid로 만들어져 그럴 가능성은 거의 없겠지만, 찾는 아이디를 완전히 포함하는 다른 아이디가 있을 수도 있지 않을까... 그렇다면 정확히 맞춰줘야하니 그냥 eq만 넣어주는 게 좋을 것 같다. 혹시 나중에 필요하면 다시 넣겠다.

     

    다음으로 모든 내 대화방을 가져와보겠다.

    chatRoute.js에 다음 구문을 추가해주고,

    router.route("/").get(protect, fetchChats);
     

     

    컨트롤러에 들어가 만들어주고, 라우터에서 임포트해주자.

     

    // backend/controller/chatController.js
    
    const fetchChats = asyncHandler(async (req, res) => {
      try {
        console.log("asdf");
        const result = await Chat.find({ users: { $eq: req.user._id } })
          .populate("users", "-password")
          .populate("groupAdmin", "-password")
          .populate("latestMessage")
          .sort({ updatedAt: -1 });
        res.status(200).send(result);
      } catch (err) {
        throw new Error(err.message);
      }
    });
    
    module.exports = { accessChat, fetchChats };

    chatController에 위와 같은 메서드를 만들어준다. 

    채팅방에서 로그인한 유저가 속한 모든 채팅방을 가져오고 있고, join으로 관련 정보들도 모두 가져오고 있다.

    그리고 sort해주고 있다.

     

    다음으로 그룹 채팅방 만들기

    라우터에 추가

    router.route("/group").post(protect, createGroupChat);
     

     

    // backend/controller/chatController.js
    
    const createGroupChat = asyncHandler(async (req, res) => {
      if (!req.body.users || !req.body.name) {
        return res.status(400).send({ message: "Please Fill all the fields" });
      }
    
      let users = JSON.parse(req.body.users);
      if (users.length < 2) {
        // group chat이니깐
        return res
          .status(400)
          .send("More than 2 users are required to from a group chat");
      }
    
      users.push(req.user); // 모든 초대하는 유저 + 지금 로그인한 유저가 있어야 그룹챗이니깐. (지금 로그인한 유저가 채팅방을 만드는 거다.)
    
      try {
        const groupChat = await Chat.create({
          chatName: req.body.name,
          users: users,
          isGroupChat: true,
          groupAdmin: req.user,
        });
    
        const fullGroupChat = await Chat.findOne({ _id: groupChat._id })
          .populate("users", "-password")
          .populate("groupAdmin", "-password");
    
        res.status(200).json(fullGroupChat);
      } catch (err) {
        throw new Error(err.message);
      }
    });
    
    module.exports = { accessChat, fetchChats, createGroupChat };

     

    별달리 설명할 거는 없다. 코드를 보면 된다. 주석도 달아놨다.

    그냥 주의할 거는 req로 받은 유저들만 추가해주는 게 아니라, 현재 로그인한 그룹챗을 만드는 사람도 유저에 포함시켜서 넣어줘야 한다. 또, 이 로그인한 유저가 이 채팅방의 장이다.

     

    다음으로 채팅방 이름 바꾸기

    // backend/controller/chatController.js
    
    const renameGroup = asyncHandler(async (req, res) => {
      const { chatId, chatName } = req.body;
    
      const updatedChat = await Chat.findByIdAndUpdate(
        chatId,
        {
          chatName,
        },
        {
          new: true,
        }
      )
        .populate("users", "-password")
        .populate("groupAdmin", "-password");
    
      if (!updatedChat) {
        res.status(404);
        throw new Error("Chat Not Found");
      } else {
        res.json(updatedChat);
      }
    });
    
    module.exports = { accessChat, fetchChats, createGroupChat, renameGroup };

    메서드 추가

    // backend/routes/userRoutes.js
    
    router.route("/rename").put(protect, renameGroup);

    라우트 추가.

     

    딱히 설명할만한 부분은 없다.

     

    그룹챗에 유저 추가, 삭제

     

    // chatRoute에 추가

    router.route("/groupadd").put(protect, addToGroup);
    router.route("/groupremove").put(protect, removeFromGroup);

    // chatController에 추가

    const addToGroup = asyncHandler(async (req, res) => {
      const { chatId, userId } = req.body;
    
      const added = await Chat.findByIdAndUpdate(
        chatId,
        { $push: { users: userId } },
        { new: true }
      )
        .populate("users", "-password")
        .populate("groupAdmin", "-password");
    
      if (!added) {
        res.status(400);
        throw new Error("Chat Not Found");
      } else {
        res.json(added);
      }
    });
    
    const removeFromGroup = asyncHandler(async (req, res) => {
      const { chatId, userId } = req.body;
    
      const removed = await Chat.findByIdAndUpdate(
        chatId,
        { $pull: { users: userId } },
        { new: true }
      )
        .populate("users", "-password")
        .populate("groupAdmin", "-password");
    
      if (!removed) {
        res.status(400);
        throw new Error("Chat Not Found");
      } else {
        res.json(removed);
      }
    });

     

    딱히 설명할 부분은 없지만 주목할만한 부분을 보면

    $push 라는 걸로 배열에 추가해주고, $pull 이라는 걸로 배열에서 빼준다는 점.

    no-sql이라서 그런지 push에 중복된 유저를 계속 추가해 줄 수 있다는 점. $pull 로 빼면 중복된 것들 한번에 다 빠진다는 점.

    (고치기 그렇게 어려운 건 아니라서 일단은 그냥 두겠다.)

     

    빨리 소켓통신 부분으로 가고 싶다...

     

     

    github : https://github.com/Wunhyeon/ChatApp-MERNStack/tree/11.ChatRoomAPI

     

    GitHub - Wunhyeon/ChatApp-MERNStack

    Contribute to Wunhyeon/ChatApp-MERNStack development by creating an account on GitHub.

    github.com

     

     

     

    populate 관련 참조

    https://www.zerocho.com/category/MongoDB/post/59a66f8372262500184b5363

     

    (MongoDB) Mongoose(몽구스) populate

    안녕하세요. 이번 시간에는 몽구스의 편리한 기능 중 하나인 populate에 대해 알아보겠습니다. 몽고DB를 사용하다보면 하나의 다큐먼트가 다른 다큐먼트의 ObjectId를 쓰는 경우가 있습니다. 그럴 때

    www.zerocho.com

    https://charles098.tistory.com/172

     

    [ MongoDB ] mongoose populate

    populate는 join과 유사한 개념이다. 우선 공식문서를 보면 populate를 아래와 같이 기술하고 있다. Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s).

    charles098.tistory.com

     

    'MERN Stack ChatApp' 카테고리의 다른 글

    13. 채팅방 프론트 - 그룹챗 만들기  (0) 2024.08.13
    12. 채팅방 프론트. 로그인보완 (서버사이트 상태저장)  (0) 2024.08.06
    10. AuthMiddleware  (0) 2024.08.05
    9. Find Users API  (0) 2024.08.05
    8.Login  (0) 2024.08.05

    댓글

Designed by Tistory.