diff --git a/src/modules/auths/login.js b/src/modules/auths/login.js index f8e4308c..8181c991 100644 --- a/src/modules/auths/login.js +++ b/src/modules/auths/login.js @@ -27,7 +27,7 @@ const login = (req, sid, id, oid, name) => { const logout = (req) => { // 로그아웃 전 socket.io 소켓들 연결부터 끊기 const io = req.app.get("io"); - if (io) io.in(req.session.id).disconnectSockets(true); + if (io) io.in(`session-${req.session.id}`).disconnectSockets(true); req.session.destroy((err) => { if (err) logger.error(err); diff --git a/src/modules/populates/rooms.js b/src/modules/populates/rooms.js index 18cbedf9..7243d648 100644 --- a/src/modules/populates/rooms.js +++ b/src/modules/populates/rooms.js @@ -7,7 +7,7 @@ const roomPopulateOption = [ { path: "to", select: "_id koName enName" }, { path: "part", - select: "-_id user settlementStatus", + select: "-_id user settlementStatus readAt", populate: { path: "user", select: "_id id name nickname profileImageUrl" }, }, ]; @@ -29,13 +29,14 @@ const formatSettlement = ( roomObject.part = roomObject.part.map((participantSubDocument) => { const { _id, name, nickname, profileImageUrl } = participantSubDocument.user; - const { settlementStatus } = participantSubDocument; + const { settlementStatus, readAt } = participantSubDocument; return { _id, name, nickname, profileImageUrl, isSettlement: includeSettlement ? settlementStatus : undefined, + readAt: readAt ?? roomObject.madeat, }; }); roomObject.settlementTotal = includeSettlement diff --git a/src/modules/socket.js b/src/modules/socket.js index 3da4370e..bca99161 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -206,6 +206,30 @@ const emitChatEvent = async (io, chat) => { } }; +const emitUpdateEvent = async (io, roomId) => { + try { + // 방의 모든 사용자에게 socket 메세지 업데이트 이벤트를 발생시킵니다. + if (!io || !roomId) { + throw new IllegalArgumentsException(); + } + + const { name, part } = await roomModel.findById(roomId, "name part"); + + if (!name || !part) { + throw new IllegalArgumentsException(); + } + + part.forEach(({ user }) => io.in(`user-${user}`).emit("chat_update"), { + roomId, + }); + + return true; + } catch (err) { + logger.error(err); + return false; + } +}; + // https://socket.io/how-to/use-with-express-session 참고 const startSocketServer = (server) => { const io = new Server(server, { @@ -266,5 +290,6 @@ const startSocketServer = (server) => { module.exports = { transformChatsForRoom, emitChatEvent, + emitUpdateEvent, startSocketServer, }; diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 998aecdd..119e0c0d 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -35,6 +35,7 @@ const participantSchema = Schema({ enum: ["not-departed", "paid", "send-required", "sent"], default: "not-departed", }, + readAt: { type: Date }, }); const deviceTokenSchema = Schema({ @@ -118,6 +119,7 @@ const locationSchema = Schema({ latitude: { type: Number }, // 이후 required: true 로 수정 필요 longitude: { type: Number }, // 이후 required: true 로 수정 필요 }); + const chatSchema = Schema({ roomId: { type: Schema.Types.ObjectId, ref: "Room", required: true }, type: { diff --git a/src/routes/chats.js b/src/routes/chats.js index 812755f6..f689348c 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -54,6 +54,17 @@ router.post( chatsHandlers.sendChatHandler ); +/** + * 채팅 읽은 시각 업데이트 요청을 처리합니다. + * 같은 방에 있는 user들에게 업데이트를 요청합니다. + */ +router.post( + "/read", + body("roomId").isMongoId(), + validator, + chatsHandlers.readChatHandler +); + // 채팅 이미지를 업로드할 수 있는 Presigned-url을 발급합니다. router.post( "/uploadChatImg/getPUrl", diff --git a/src/services/chats.js b/src/services/chats.js index 4a76b1a4..abcbd073 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,7 +1,12 @@ const { chatModel, userModel, roomModel } = require("../modules/stores/mongo"); const { chatPopulateOption } = require("../modules/populates/chats"); +const { roomPopulateOption } = require("../modules/populates/rooms"); const aws = require("../modules/stores/aws"); -const { transformChatsForRoom, emitChatEvent } = require("../modules/socket"); +const { + transformChatsForRoom, + emitChatEvent, + emitUpdateEvent, +} = require("../modules/socket"); const logger = require("../modules/logger"); const chatCount = 60; @@ -170,6 +175,51 @@ const sendChatHandler = async (req, res) => { } }; +const readChatHandler = async (req, res) => { + try { + const io = req.app.get("io"); + const { userId } = req; + const { roomId } = req.body; + const user = await userModel.findOne({ id: userId }); + + if (!userId || !user) { + return res.status(500).send("Chat/read : internal server error"); + } + if (!io) { + return res.status(403).send("Chat/read : socket did not connected"); + } + + const roomObject = await roomModel + .findOneAndUpdate( + { + _id: roomId, + part: { + $elemMatch: { + user: user._id, + }, + }, + }, + { + $set: { "part.$[updater].readAt": req.timestamp }, + }, + { + new: true, + arrayFilters: [{ "updater.user": { $eq: user._id } }], + } + ) + .lean(); + + if (!roomObject) { + return res.status(404).send("Chat/read : cannot find room info"); + } + + if (await emitUpdateEvent(io, roomId)) res.json({ result: true }); + else res.status(500).send("Chat/read : failed to emit socket events"); + } catch (e) { + res.status(500).send("Chat/read : internal server error"); + } +}; + const uploadChatImgGetPUrlHandler = async (req, res) => { try { const { type, roomId } = req.body; @@ -284,4 +334,5 @@ module.exports = { sendChatHandler, uploadChatImgGetPUrlHandler, uploadChatImgDoneHandler, + readChatHandler, };