-
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
populate 관련 참조
https://www.zerocho.com/category/MongoDB/post/59a66f8372262500184b5363
https://charles098.tistory.com/172
'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