diff --git a/src/schedules/autoSettlement.ts b/src/schedules/autoSettlement.ts new file mode 100644 index 00000000..4e07a4a6 --- /dev/null +++ b/src/schedules/autoSettlement.ts @@ -0,0 +1,54 @@ +import type { Express } from "express"; +import { emitChatEvent } from "@/modules/socket"; +import { userModel, roomModel } from "@/modules/stores/mongo"; +import logger from "@/modules/logger"; + +const MS_PER_MINUTE = 60000; + +// 탑승자가 1명인 상태로 탑승 시간이 지난 방에 대해서 정산 완료 처리 +const autoSettlement = (app: Express) => async () => { + try { + const io = app.get("io"); + const expiredDate = new Date(Date.now() - 60 * MS_PER_MINUTE).toISOString(); + const arrivalDate = new Date(Date.now()).toISOString(); + const candidateRooms = await roomModel.find({ + $and: [ + { time: { $gte: expiredDate } }, + { time: { $lte: arrivalDate } }, + { "part.0": { $exists: true }, "part.1": { $exists: false } }, + { "part.0.settlementStatus": { $nin: ["paid", "sent"] } }, // "sent"의 경우 로직상 불가능 하지만, 문서화 측면에서 의도적으로 남겨두었음. + ], + }); + + await Promise.all( + candidateRooms.map(async ({ _id: roomId, part }) => { + const user = await userModel.findById(part![0].user._id); + // 정산 채팅을 보냅니다. + await emitChatEvent(io, { + roomId: roomId.toString(), + type: "settlement", + content: user!.id, + authorId: user!._id.toString(), + time: null, + }); + + // 1명의 참여자만 존재하는 room에 대하여 정산 완료 처리. + await roomModel.findByIdAndUpdate(roomId, { + ["part.0.settlementStatus"]: "paid", + settlementTotal: 1, + }); + + // Atomic update로 각 Room을 한번에 제거 및 추가함. + // 아토믹하게 처리하지 않을 경우 각 Promise가 동일한 user의 여러 ongoingRoom 또는 doneRoom을 동시에 수정하여 경합조건이 발생할 수 있음에 유의. + await userModel.findByIdAndUpdate(user!._id, { + $pull: { ongoingRoom: roomId }, + $push: { doneRoom: roomId }, + }); + }) + ); + } catch (err) { + logger.error(err); + } +}; + +export default autoSettlement; diff --git a/src/schedules/index.ts b/src/schedules/index.ts index f714dd61..ea1bb96f 100644 --- a/src/schedules/index.ts +++ b/src/schedules/index.ts @@ -5,11 +5,12 @@ import notifyBeforeDepart from "./notifyBeforeDepart"; import notifyAfterArrival from "./notifyAfterArrival"; import updateMajorTaxiFare from "./updateMajorTaxiFare"; import updateMinorTaxiFare from "./updateMinorTaxiFare"; +import autoSettlement from "./autoSettlement"; const registerSchedules = (app: Express) => { cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); cron.schedule("*/10 * * * *", notifyAfterArrival(app)); - + cron.schedule("1,11,21,31,41,51 * * * *", autoSettlement(app)); if (naverMap.apiId && naverMap.apiKey) { cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); diff --git a/src/services/rooms.js b/src/services/rooms.js index 73ebeba3..76350db0 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -485,6 +485,7 @@ const searchHandler = async (req, res) => { const searchByUserHandler = async (req, res) => { try { + // lean()이 적용된 user를 response에 반환해줘야 하기 때문에 user를 한 번 더 지정한다. const user = await userModel .findOne({ id: req.userId }) .populate({