diff --git a/app.js b/app.js
index d8a1f0f3..689de7a9 100644
--- a/app.js
+++ b/app.js
@@ -1,7 +1,7 @@
// 모듈 require
const express = require("express");
const http = require("http");
-const { port: httpPort, eventMode } = require("./loadenv");
+const { port: httpPort, eventConfig } = require("./loadenv");
const logger = require("./src/modules/logger");
const { connectDatabase } = require("./src/modules/stores/mongo");
const { startSocketServer } = require("./src/modules/socket");
@@ -43,8 +43,11 @@ app.use(require("./src/middlewares/limitRate"));
app.use("/docs", require("./src/routes/docs"));
// 2023 추석 이벤트 전용 라우터입니다.
-eventMode &&
- app.use(`/events/${eventMode}`, require("./src/lottery").lotteryRouter);
+eventConfig &&
+ app.use(
+ `/events/${eventConfig.mode}`,
+ require("./src/lottery").lotteryRouter
+ );
// [Middleware] 모든 API 요청에 대하여 origin 검증
app.use(require("./src/middlewares/originValidator"));
diff --git a/loadenv.js b/loadenv.js
index 2bce4fb9..3dfc5be6 100644
--- a/loadenv.js
+++ b/loadenv.js
@@ -38,5 +38,10 @@ module.exports = {
slackWebhookUrl: {
report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional
},
- eventMode: undefined,
+ eventConfig: (process.env.EVENT_CONFIG &&
+ JSON.parse(process.env.EVENT_CONFIG)) || {
+ mode: "2023fall",
+ startAt: "2023-09-25T00:00:00+09:00",
+ endAt: "2023-10-10T00:00:00+09:00",
+ },
};
diff --git a/src/lottery/index.js b/src/lottery/index.js
index d06be299..a24af419 100644
--- a/src/lottery/index.js
+++ b/src/lottery/index.js
@@ -1,21 +1,16 @@
const express = require("express");
const {
eventStatusModel,
- eventModel,
+ questModel,
itemModel,
transactionModel,
} = require("./modules/stores/mongo");
+const { eventConfig } = require("../../loadenv");
const { buildResource } = require("../modules/adminResource");
-const { instagramRewardAction } = require("./modules/admin");
// [Routes] 기존 docs 라우터의 docs extend
-require("./routes/docs")();
-
-// [Middleware] 목표 달성 여부 검증
-const checkReward = (req, res, next) => {
- next();
-};
+eventConfig && require("./routes/docs")();
const lotteryRouter = express.Router();
@@ -26,16 +21,21 @@ lotteryRouter.use(require("../middlewares/originValidator"));
lotteryRouter.use("/global-state", require("./routes/globalState"));
lotteryRouter.use("/transactions", require("./routes/transactions"));
lotteryRouter.use("/items", require("./routes/items"));
+lotteryRouter.use("/public-notice", require("./routes/publicNotice"));
+lotteryRouter.use("/quests", require("./routes/quests"));
+
+const resources = [
+ eventStatusModel,
+ questModel,
+ itemModel,
+ transactionModel,
+].map(buildResource());
-const eventStatusResource = buildResource([instagramRewardAction])(
- eventStatusModel
-);
-const otherResources = [eventModel, itemModel, transactionModel].map(
- buildResource()
-);
+const contracts =
+ eventConfig && require(`./modules/contracts/${eventConfig.mode}`);
module.exports = {
- checkReward,
lotteryRouter,
- resources: [eventStatusResource, ...otherResources],
+ resources,
+ contracts,
};
diff --git a/src/lottery/middlewares/timestampValidator.js b/src/lottery/middlewares/timestampValidator.js
new file mode 100644
index 00000000..22511536
--- /dev/null
+++ b/src/lottery/middlewares/timestampValidator.js
@@ -0,0 +1,19 @@
+const { eventConfig } = require("../../../loadenv");
+const eventPeriod = eventConfig && {
+ startAt: new Date(eventConfig.startAt),
+ endAt: new Date(eventConfig.endAt),
+};
+
+const timestampValidator = (req, res, next) => {
+ if (
+ !eventPeriod ||
+ req.timestamp >= eventPeriod.endAt ||
+ req.timestamp < eventPeriod.startAt
+ ) {
+ return res.status(400).json({ error: "out of date" });
+ } else {
+ next();
+ }
+};
+
+module.exports = timestampValidator;
diff --git a/src/lottery/modules/admin.js b/src/lottery/modules/admin.js
deleted file mode 100644
index 337d7d96..00000000
--- a/src/lottery/modules/admin.js
+++ /dev/null
@@ -1,74 +0,0 @@
-const { useUserCreditAmount } = require("./credit");
-const { transactionModel } = require("./stores/mongo");
-const { recordAction } = require("../../modules/adminResource");
-const { eventEnv } = require("../../../loadenv");
-
-/** eventId가 없는 경우 null이 아닌 undefined를 넣어야 합니다. */
-const creditTransfer = async (userId, amount, eventId, comment) => {
- const user = await useUserCreditAmount(userId);
- await user.update(amount);
-
- const transaction = new transactionModel({
- type: "get",
- amount,
- userId,
- event: eventId,
- comment,
- });
- await transaction.save();
-
- return transaction._id;
-};
-
-/** itemId가 없는 경우 null이 아닌 undefined를 넣어야 합니다. */
-/** itemType이 없는 경우 null이 아닌 undefined를 넣어야 합니다. */
-const creditWithdraw = async (userId, amount, itemId, itemType, comment) => {
- const user = await useUserCreditAmount(userId);
- await user.update(-amount);
-
- const transaction = new transactionModel({
- type: "use",
- amount,
- userId,
- item: itemId,
- itemType,
- comment,
- });
- await transaction.save();
-
- return transaction._id;
-};
-
-const instagramRewardActionHandler = async (req, res, context) => {
- const transactionId = await creditTransfer(
- context?.record?.params?.userId,
- eventEnv.instagramReward,
- eventEnv.instagramEventId,
- eventEnv.instagramComment
- );
-
- let record = context.record.toJSON(context.currentAdmin);
- record.params.creditAmount += eventEnv.instagramReward;
-
- return {
- record,
- transactionId,
- };
-};
-const instagramRewardActionLogs = [
- "update",
- {
- action: "create",
- target: (res, req, context) => `Transaction(_id = ${res.transactionId})`,
- },
-];
-
-const instagramRewardAction = recordAction(
- "instagramReward",
- instagramRewardActionHandler,
- instagramRewardActionLogs
-);
-
-module.exports = {
- instagramRewardAction,
-};
diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js
new file mode 100644
index 00000000..1677c26e
--- /dev/null
+++ b/src/lottery/modules/contracts/2023fall.js
@@ -0,0 +1,263 @@
+const { buildQuests, completeQuest } = require("../quests");
+const mongoose = require("mongoose");
+
+/** 전체 퀘스트 목록입니다. */
+const quests = buildQuests({
+ firstLogin: {
+ name: "첫 발걸음",
+ description:
+ "로그인만 해도 송편을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 송편을 받아보세요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_firstLogin.png",
+ reward: {
+ ticket1: 1,
+ },
+ },
+ payingAndSending: {
+ name: "함께하는 택시의 여정",
+ description:
+ "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 송편을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_payingAndSending.png",
+ reward: 300,
+ maxCount: 3,
+ },
+ firstRoomCreation: {
+ name: "첫 방 개설",
+ description:
+ "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_firstRoomCreation.png",
+ reward: 50,
+ },
+ roomSharing: {
+ name: "Taxi로 모여라",
+ description:
+ "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_roomSharing.png",
+ reward: 50,
+ },
+ paying: {
+ name: "정산해요 택시의 숲",
+ description:
+ "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산하기를 요청해보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_paying.png",
+ reward: 100,
+ maxCount: 3,
+ },
+ sending: {
+ name: "송금 완료! 친구야 고마워",
+ description:
+ "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_sending.png",
+ reward: 50,
+ maxCount: 3,
+ },
+ nicknameChanging: {
+ name: "닉네임 변신",
+ description:
+ "닉네임을 변경하여 자신을 표현하세요. 마이페이지의 수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_nicknameChanging.png",
+ reward: 50,
+ },
+ accountChanging: {
+ name: "계좌 등록은 정산의 시작",
+ description:
+ "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지의 수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_accountChanging.png",
+ reward: 50,
+ },
+ adPushAgreement: {
+ name: "Taxi의 소울메이트",
+ description:
+ "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_adPushAgreement.png",
+ reward: 50,
+ },
+ eventSharingOnInstagram: {
+ name: "나만 알기에는 아까운 이벤트",
+ description:
+ "추석에 맞춰 쏟아지는 혜택들. 나만 알 순 없죠. 인스타그램 친구들에게 스토리로 공유해보아요. 이벤트 안내 페이지에서 인스타그램 스토리에 공유하기을 눌러보세요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_eventSharingOnInstagram.png",
+ reward: 100,
+ },
+ purchaseSharingOnInstagram: {
+ name: "상품 획득을 축하합니다",
+ description:
+ "이벤트를 열심히 즐긴 당신. 그 상품 획득을 축하 받을 자격이 충분합니다. 달토끼 상점에서 상품 구매 후 뜨는 인스타그램 스토리에 공유하기 버튼을 눌러 상품 획득을 공유하세요.",
+ imageUrl:
+ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_purchaseSharingOnInstagram.png",
+ reward: 100,
+ },
+});
+
+/**
+ * firstLogin 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @returns {Promise}
+ * @usage lottery/globalState/createUserGlobalStateHandler
+ */
+const completeFirstLoginQuest = async (userId, timestamp) => {
+ return await completeQuest(userId, timestamp, quests.firstLogin);
+};
+
+/**
+ * payingAndSending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @param {Object} roomObject - 방의 정보입니다.
+ * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다.
+ * @returns {Promise}
+ * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요.
+ * @usage rooms - commitPaymentHandler, rooms - settlementHandler
+ */
+const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => {
+ if (roomObject.part.length < 2) return null;
+
+ return await completeQuest(userId, timestamp, quests.payingAndSending);
+};
+
+/**
+ * firstRoomCreation 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @returns {Promise}
+ * @description 방을 만들 때마다 호출해 주세요.
+ * @usage rooms - createHandler
+ */
+const completeFirstRoomCreationQuest = async (userId, timestamp) => {
+ return await completeQuest(userId, timestamp, quests.firstRoomCreation);
+};
+
+const completeRoomSharingQuest = async () => {
+ // TODO
+};
+
+/**
+ * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @param {Object} roomObject - 방의 정보입니다.
+ * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다.
+ * @returns {Promise}
+ * @description 정산 요청이 이루어질 때마다 호출해 주세요.
+ * @usage rooms - commitPaymentHandler
+ */
+const completePayingQuest = async (userId, timestamp, roomObject) => {
+ if (roomObject.part.length < 2) return null;
+
+ return await completeQuest(userId, timestamp, quests.paying);
+};
+
+/**
+ * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @param {Object} roomObject - 방의 정보입니다.
+ * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다.
+ * @returns {Promise}
+ * @description 송금이 이루어질 때마다 호출해 주세요.
+ * @usage rooms - settlementHandler
+ */
+const completeSendingQuest = async (userId, timestamp, roomObject) => {
+ if (roomObject.part.length < 2) return null;
+
+ return await completeQuest(userId, timestamp, quests.sending);
+};
+
+/**
+ * nicknameChanging 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @returns {Promise}
+ * @description 닉네임을 변경할 때마다 호출해 주세요.
+ * @usage users - editNicknameHandler
+ */
+const completeNicknameChangingQuest = async (userId, timestamp) => {
+ return await completeQuest(userId, timestamp, quests.nicknameChanging);
+};
+
+/**
+ * accountChanging 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @param {string} newAccount - 변경된 계좌입니다.
+ * @returns {Promise}
+ * @description 계좌를 변경할 때마다 호출해 주세요.
+ * @usage users - editAccountHandler
+ */
+const completeAccountChangingQuest = async (userId, timestamp, newAccount) => {
+ if (newAccount === "") return null;
+
+ return await completeQuest(userId, timestamp, quests.accountChanging);
+};
+
+/**
+ * adPushAgreementQuest 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다.
+ * @returns {Promise}
+ * @description 알림 옵션을 변경할 때마다 호출해 주세요.
+ * @usage notifications/editOptionsHandler
+ */
+const completeAdPushAgreementQuest = async (
+ userId,
+ timestamp,
+ advertisement
+) => {
+ if (!advertisement) return null;
+
+ return await completeQuest(userId, timestamp, quests.adPushAgreement);
+};
+
+/**
+ * eventSharingOnInstagram 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @returns {Promise}
+ * @description 인스타그램 스토리에 추석 이벤트를 공유할 때마다 호출해 주세요.
+ * @usage quests - instagramEventShareHandler
+ */
+const completeEventSharingOnInstagramQuest = async (userId, timestamp) => {
+ return await completeQuest(userId, timestamp, quests.eventSharingOnInstagram);
+};
+
+/**
+ * purchaseSharingOnInstagram 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @returns {Promise}
+ * @description 인스타그램 스토리에 구매한 아이템을 공유할 때마다 호출해 주세요.
+ * @usage quests - instagramPurchaseShareHandler
+ */
+const completePurchaseSharingOnInstagramQuest = async (userId, timestamp) => {
+ return await completeQuest(
+ userId,
+ timestamp,
+ quests.purchaseSharingOnInstagram
+ );
+};
+
+module.exports = {
+ quests,
+ completeFirstLoginQuest,
+ completePayingAndSendingQuest,
+ completeFirstRoomCreationQuest,
+ completeRoomSharingQuest,
+ completePayingQuest,
+ completeSendingQuest,
+ completeNicknameChangingQuest,
+ completeAccountChangingQuest,
+ completeAdPushAgreementQuest,
+ completeEventSharingOnInstagramQuest,
+ completePurchaseSharingOnInstagramQuest,
+};
diff --git a/src/lottery/modules/credit.js b/src/lottery/modules/credit.js
deleted file mode 100644
index 2349d0c4..00000000
--- a/src/lottery/modules/credit.js
+++ /dev/null
@@ -1,24 +0,0 @@
-const { eventStatusModel } = require("../modules/stores/mongo");
-
-const useUserCreditAmount = async (userId) => {
- const eventStatus = await eventStatusModel.findOne({ userId }).lean();
- if (!eventStatus) return null;
-
- return {
- amount: eventStatus.creditAmount,
- update: async (delta) => {
- await eventStatusModel.updateOne(
- { _id: eventStatus._id },
- {
- $inc: {
- creditAmount: delta,
- },
- }
- );
- },
- };
-};
-
-module.exports = {
- useUserCreditAmount,
-};
diff --git a/src/lottery/modules/populates/transactions.js b/src/lottery/modules/populates/transactions.js
index f4206d87..bcf84863 100644
--- a/src/lottery/modules/populates/transactions.js
+++ b/src/lottery/modules/populates/transactions.js
@@ -1,11 +1,22 @@
const transactionPopulateOption = [
- { path: "event" },
{
path: "item",
select: "name imageUrl price description isDisabled stock itemType",
},
];
+const publicNoticePopulateOption = [
+ {
+ path: "userId",
+ select: "nickname",
+ },
+ {
+ path: "item",
+ select: "name price description",
+ },
+];
+
module.exports = {
transactionPopulateOption,
+ publicNoticePopulateOption,
};
diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js
new file mode 100644
index 00000000..26bf2e24
--- /dev/null
+++ b/src/lottery/modules/quests.js
@@ -0,0 +1,162 @@
+const {
+ eventStatusModel,
+ questModel,
+ itemModel,
+ transactionModel,
+} = require("./stores/mongo");
+const logger = require("../../modules/logger");
+const mongoose = require("mongoose");
+
+const { eventConfig } = require("../../../loadenv");
+const eventPeriod = eventConfig && {
+ startAt: new Date(eventConfig.startAt),
+ endAt: new Date(eventConfig.endAt),
+};
+
+const requiredQuestFields = ["name", "description", "imageUrl", "reward"];
+const buildQuests = (quests) => {
+ for (const [id, quest] of Object.entries(quests)) {
+ // quest에 필수 필드가 모두 포함되어 있는지 확인합니다.
+ const hasError = requiredQuestFields.reduce((before, field) => {
+ if (quest[field] !== undefined) return before;
+
+ logger.error(`There is no ${field} field in ${id}Quest`);
+ return true;
+ }, false);
+ if (hasError) return null;
+
+ // quest.id 필드를 설정합니다.
+ quest.id = id;
+
+ // quest.reward가 number인 경우, object로 변환합니다.
+ if (typeof quest.reward === "number") {
+ const credit = quest.reward;
+ quest.reward = {
+ credit,
+ };
+ }
+
+ // quest.reward에 누락된 필드가 있는 경우, 기본값(0)으로 설정합니다.
+ quest.reward.credit = quest.reward.credit || 0;
+ quest.reward.ticket1 = quest.reward.ticket1 || 0;
+
+ // quest.maxCount가 없는 경우, 기본값(1)으로 설정합니다.
+ quest.maxCount = quest.maxCount || 1;
+ }
+
+ return quests;
+};
+
+/**
+ * 퀘스트 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @param {Object} quest - 퀘스트의 정보입니다.
+ * @param {string} quest.id - 퀘스트의 Id입니다.
+ * @param {string} quest.name - 퀘스트의 이름입니다.
+ * @param {Object} quest.reward - 퀘스트의 완료 보상입니다.
+ * @param {number} quest.reward.credit - 퀘스트의 완료 보상 중 재화의 양입니다.
+ * @param {number} quest.reward.ticket1 - 퀘스트의 완료 보상 중 일반 티켓의 개수입니다.
+ * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다.
+ * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다.
+ */
+const completeQuest = async (userId, timestamp, quest) => {
+ try {
+ // 1단계: 유저의 EventStatus를 가져옵니다.
+ const eventStatus = await eventStatusModel.findOne({ userId }).lean();
+ if (!eventStatus) return null;
+
+ // 2단계: 이벤트 기간인지 확인합니다.
+ if (timestamp >= eventPeriod.endAt || timestamp < eventPeriod.startAt) {
+ logger.info(
+ `User ${userId} failed to complete auto-disabled ${quest.id}Quest`
+ );
+ return null;
+ }
+
+ // 3단계: 유저의 퀘스트 완료 횟수를 확인합니다.
+ const questCount = eventStatus.completedQuests.filter(
+ (completedQuestId) => completedQuestId === quest.id
+ ).length;
+ if (questCount >= quest.maxCount) {
+ logger.info(
+ `User ${userId} already completed ${quest.id}Quest ${questCount} times`
+ );
+ return null;
+ }
+
+ // 4단계: 원격으로 비활성화된 퀘스트인지 확인합니다.
+ // 비활성화된 퀘스트만 DB에 저장할 것이기 때문에, questDoc이 null이어도 오류를 발생시키면 안됩니다.
+ const questDoc = await questModel.findOne({ id: quest.id }).lean();
+ if (questDoc?.isDisabled) {
+ logger.info(
+ `User ${userId} failed to complete disabled ${quest.id}Quest`
+ );
+ return null;
+ }
+
+ // 5단계: 완료 보상 중 티켓이 있는 경우, 티켓 정보를 가져옵니다.
+ const ticket1 =
+ quest.reward.ticket1 && (await itemModel.findOne({ itemType: 1 }).lean());
+ if (quest.reward.ticket1 && !ticket1) throw "Fail to find ticket1";
+
+ // 6단계: 유저의 EventStatus를 업데이트합니다.
+ await eventStatusModel.updateOne(
+ { userId },
+ {
+ $inc: {
+ creditAmount: quest.reward.credit,
+ ticket1Amount: quest.reward.ticket1,
+ },
+ $push: {
+ completedQuests: quest.id,
+ },
+ }
+ );
+
+ // 7단계: Transaction을 생성합니다.
+ const transactionsId = [];
+ if (quest.reward.credit) {
+ const transaction = new transactionModel({
+ type: "get",
+ amount: quest.reward.credit,
+ userId,
+ questId: quest.id,
+ comment: `"${quest.name}" 퀘스트를 완료해 송편 ${quest.reward.credit}개를 획득했습니다.`,
+ });
+ await transaction.save();
+
+ transactionsId.push(transaction._id);
+ }
+ if (quest.reward.ticket1) {
+ const transaction = new transactionModel({
+ type: "use",
+ amount: 0,
+ userId,
+ questId: quest.id,
+ item: ticket1._id,
+ comment: `"${quest.name}" 퀘스트를 완료해 "${ticket1.name}" ${quest.reward.ticket1}개를 획득했습니다.`,
+ });
+ await transaction.save();
+
+ transactionsId.push(transaction._id);
+ }
+
+ logger.info(`User ${userId} successfully completed ${quest.id}Quest`);
+ return {
+ quest,
+ transactionsId,
+ };
+ } catch (err) {
+ logger.error(err);
+ logger.error(
+ `User ${userId} failed to complete ${quest.id}Quest due to exception`
+ );
+ return null;
+ }
+};
+
+module.exports = {
+ buildQuests,
+ completeQuest,
+};
diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js
index 8675851a..2e32978d 100644
--- a/src/lottery/modules/stores/mongo.js
+++ b/src/lottery/modules/stores/mongo.js
@@ -12,10 +12,9 @@ const eventStatusSchema = Schema({
ref: "User",
required: true,
},
- eventList: {
- type: [Schema.Types.ObjectId],
+ completedQuests: {
+ type: [String],
default: [],
- ref: "Event",
},
creditAmount: {
type: Number,
@@ -23,39 +22,28 @@ const eventStatusSchema = Schema({
min: 0,
validate: integerValidator,
},
-});
-
-const eventSchema = Schema({
- name: {
- type: String,
- required: true,
- },
- rewardAmount: {
+ ticket1Amount: {
type: Number,
- required: true,
+ default: 0,
min: 0,
validate: integerValidator,
},
- maxCount: {
+ ticket2Amount: {
type: Number,
- default: 1,
+ default: 0,
min: 0,
validate: integerValidator,
},
- expireat: {
- type: Date,
+});
+
+const questSchema = Schema({
+ id: {
+ type: String,
required: true,
+ unique: true,
},
isDisabled: {
type: Boolean,
- default: false,
- },
- imageUrl: {
- type: String,
- required: true,
- },
- description: {
- type: String,
required: true,
},
});
@@ -123,9 +111,8 @@ const transactionSchema = Schema({
ref: "User",
required: true,
},
- event: {
- type: Schema.Types.ObjectId,
- ref: "Event",
+ questId: {
+ type: String,
},
item: {
type: Schema.Types.ObjectId,
@@ -141,13 +128,13 @@ const transactionSchema = Schema({
},
});
transactionSchema.set("timestamps", {
- createdAt: "doneat",
+ createdAt: "createAt",
updatedAt: false,
});
module.exports = {
eventStatusModel: mongoose.model("EventStatus", eventStatusSchema),
- eventModel: mongoose.model("Event", eventSchema),
+ questModel: mongoose.model("Quest", questSchema),
itemModel: mongoose.model("Item", itemSchema),
transactionModel: mongoose.model("Transaction", transactionSchema),
};
diff --git a/src/lottery/routes/docs/eventsSchema.js b/src/lottery/routes/docs/eventsSchema.js
deleted file mode 100644
index 04fcb616..00000000
--- a/src/lottery/routes/docs/eventsSchema.js
+++ /dev/null
@@ -1,67 +0,0 @@
-/** Event에 대한 기본적인 프로퍼티를 갖고 있는 스키마입니다. */
-const eventBase = {
- type: "object",
- required: [
- "_id",
- "name",
- "rewardAmount",
- "maxCount",
- "expireat",
- "isDisabled",
- "imageUrl",
- "description",
- ],
- properties: {
- _id: {
- type: "string",
- description: "Event의 ObjectId",
- example: "OBJECT ID",
- },
- name: {
- type: "string",
- description: "이벤트의 이름",
- example: "최초 로그인 이벤트",
- },
- rewardAmount: {
- type: "number",
- description: "달성 보상",
- example: 100,
- },
- maxCount: {
- type: "number",
- description: "최대 달성 가능 횟수",
- example: 1,
- },
- expireat: {
- type: "string",
- description: "달성할 수 있는 마지막 시각",
- example: "2023-01-01 00:00:00",
- },
- isDisabled: {
- type: "boolean",
- description: "달성 불가능 여부",
- example: false,
- },
- imageUrl: {
- type: "string",
- description: "이미지 썸네일 URL",
- example: "THUMBNAIL URL",
- },
- description: {
- type: "string",
- description: "이벤트의 설명",
- example: "처음으로 이벤트 기간 중 Taxi에 로그인하면 송편을 드립니다.",
- },
- },
-};
-
-const eventsSchema = {
- event: eventBase,
- relatedEvent: {
- ...eventBase,
- description:
- "Transaction과 관련된 이벤트의 Object. 이벤트와 관련된 Transaction인 경우에만 포함됩니다.",
- },
-};
-
-module.exports = eventsSchema;
diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js
index e350e35c..4cf9630b 100644
--- a/src/lottery/routes/docs/globalState.js
+++ b/src/lottery/routes/docs/globalState.js
@@ -1,5 +1,5 @@
-const { eventMode } = require("../../../../loadenv");
-const apiPrefix = `/events/${eventMode}/global-state`;
+const { eventConfig } = require("../../../../loadenv");
+const apiPrefix = `/events/${eventConfig.mode}/global-state`;
const globalStateDocs = {};
globalStateDocs[`${apiPrefix}/`] = {
@@ -7,7 +7,7 @@ globalStateDocs[`${apiPrefix}/`] = {
tags: [`${apiPrefix}`],
summary: "Frontend에서 Global state로 관리하는 정보 반환",
description:
- "유저의 재화 개수, 이벤트 달성 상태, 추첨권 개수 등 Frontend에서 Global state로 관리할 정보를 가져옵니다. 유저에 대한 EventStatus Document가 없을 경우 새롭게 생성하며, 유일한 생성 지점입니다.",
+ "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다.",
responses: {
200: {
description: "",
@@ -16,26 +16,32 @@ globalStateDocs[`${apiPrefix}/`] = {
schema: {
type: "object",
required: [
+ "isAgreeOnTermsOfEvent",
"creditAmount",
- "eventStatus",
+ "completedQuests",
"ticket1Amount",
"ticket2Amount",
- "events",
+ "quests",
],
properties: {
+ isAgreeOnTermsOfEvent: {
+ type: "boolean",
+ description: "유저의 이벤트 참여 동의 여부",
+ example: true,
+ },
creditAmount: {
type: "number",
description: "재화 개수. 0 이상입니다.",
example: 10000,
},
- eventStatus: {
+ completedQuests: {
type: "array",
description:
- "유저가 달성한 이벤트의 배열. 여러 번 달성할 수 있는 이벤트의 경우 배열 내에 같은 이벤트가 여러 번 포함될 수 있습니다.",
+ "유저가 완료한 퀘스트의 배열. 여러 번 완료할 수 있는 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.",
items: {
type: "string",
- description: "Event의 ObjectId",
- example: "OBJECT ID",
+ description: "Quest의 Id",
+ example: "QUEST ID",
},
},
ticket1Amount: {
@@ -48,11 +54,64 @@ globalStateDocs[`${apiPrefix}/`] = {
description: "고급 티켓의 개수. 0 이상입니다.",
example: 10,
},
- events: {
+ quests: {
type: "array",
- description: "Event의 배열",
+ description: "Quest의 배열",
items: {
- $ref: "#/components/schemas/event",
+ type: "object",
+ required: [
+ "id",
+ "name",
+ "description",
+ "imageUrl",
+ "reward",
+ "maxCount",
+ ],
+ properties: {
+ id: {
+ type: "string",
+ description: "Quest의 Id",
+ example: "QUEST ID",
+ },
+ name: {
+ type: "string",
+ description: "퀘스트의 이름",
+ example: "최초 로그인 퀘스트",
+ },
+ description: {
+ type: "string",
+ description: "퀘스트의 설명",
+ example:
+ "처음으로 이벤트 기간 중 Taxi에 로그인하면 송편을 드립니다.",
+ },
+ imageUrl: {
+ type: "string",
+ description: "이미지 썸네일 URL",
+ example: "THUMBNAIL URL",
+ },
+ reward: {
+ type: "object",
+ description: "완료 보상",
+ required: ["credit", "ticket1"],
+ properties: {
+ credit: {
+ type: "number",
+ description: "완료 보상 중 재화의 개수입니다.",
+ example: 100,
+ },
+ ticket1: {
+ type: "number",
+ description: "완료 보상 중 일반 티켓의 개수입니다.",
+ example: 1,
+ },
+ },
+ },
+ maxCount: {
+ type: "number",
+ description: "최대 완료 가능 횟수",
+ example: 1,
+ },
+ },
},
},
},
@@ -63,5 +122,43 @@ globalStateDocs[`${apiPrefix}/`] = {
},
},
};
+globalStateDocs[`${apiPrefix}/create`] = {
+ get: {
+ tags: [`${apiPrefix}`],
+ summary: "Frontend에서 Global state로 관리하는 정보 생성",
+ description:
+ "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 생성합니다.",
+ requestBody: {
+ description: "Update an existent user in the store",
+ content: {
+ "application/json": {
+ schema: {
+ $ref: "#/components/schemas/createUserGlobalStateHandler",
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
+ description: "",
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ required: ["result"],
+ properties: {
+ result: {
+ type: "boolean",
+ description: "성공 여부. 항상 true입니다.",
+ example: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
module.exports = globalStateDocs;
diff --git a/src/lottery/routes/docs/globalStateSchema.js b/src/lottery/routes/docs/globalStateSchema.js
new file mode 100644
index 00000000..7a9a5260
--- /dev/null
+++ b/src/lottery/routes/docs/globalStateSchema.js
@@ -0,0 +1,15 @@
+const globalStateSchema = {
+ createUserGlobalStateHandler: {
+ type: "object",
+ required: ["phoneNumber"],
+ properties: {
+ phoneNumber: {
+ type: "string",
+ pattern: "^010-?([0-9]{3,4})-?([0-9]{4})$",
+ },
+ },
+ errorMessage: "validation: bad request",
+ },
+};
+
+module.exports = globalStateSchema;
diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js
index d1c0956e..4079795a 100644
--- a/src/lottery/routes/docs/items.js
+++ b/src/lottery/routes/docs/items.js
@@ -1,5 +1,5 @@
-const { eventMode } = require("../../../../loadenv");
-const apiPrefix = `/events/${eventMode}/items`;
+const { eventConfig } = require("../../../../loadenv");
+const apiPrefix = `/events/${eventConfig.mode}/items`;
const itemsDocs = {};
itemsDocs[`${apiPrefix}/list`] = {
diff --git a/src/lottery/routes/docs/itemsSchema.js b/src/lottery/routes/docs/itemsSchema.js
index 227f2b08..1601f237 100644
--- a/src/lottery/routes/docs/itemsSchema.js
+++ b/src/lottery/routes/docs/itemsSchema.js
@@ -75,6 +75,17 @@ const itemsSchema = {
...itemBase,
description: "랜덤박스를 구입한 경우에만 포함됩니다.",
},
+ purchaseHandler: {
+ type: "object",
+ required: ["itemId"],
+ properties: {
+ itemId: {
+ type: "string",
+ pattern: "^[a-fA-F\\d]{24}$",
+ },
+ },
+ errorMessage: "validation: bad request",
+ },
};
module.exports = itemsSchema;
diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js
new file mode 100644
index 00000000..ca30cfb9
--- /dev/null
+++ b/src/lottery/routes/docs/publicNotice.js
@@ -0,0 +1,111 @@
+const { eventConfig } = require("../../../../loadenv");
+const apiPrefix = `/events/${eventConfig.mode}/public-notice`;
+
+const publicNoticeDocs = {};
+publicNoticeDocs[`${apiPrefix}/recentTransactions`] = {
+ get: {
+ tags: [`${apiPrefix}`],
+ summary: "최근의 유의미한 상품 획득 기록 반환",
+ description: "모든 유저의 상품 획득 내역 중 유의미한 기록을 가져옵니다.",
+ responses: {
+ 200: {
+ description: "",
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ required: ["transactions"],
+ properties: {
+ transactions: {
+ type: "array",
+ description: "상품 획득 기록의 배열",
+ items: {
+ type: "string",
+ example:
+ "tu**************님께서 일반응모권을(를) 획득하셨습니다.",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
+publicNoticeDocs[`${apiPrefix}/leaderboard`] = {
+ get: {
+ tags: [`${apiPrefix}`],
+ summary: "리더보드 반환",
+ description:
+ "티켓 개수(고급 티켓은 일반 티켓 5개와 등가입니다.) 기준의 리더보드와 관련된 정보를 가져옵니다.",
+ responses: {
+ 200: {
+ description: "",
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ required: ["leaderboard"],
+ properties: {
+ leaderboard: {
+ type: "array",
+ description: "상위 20명만 포함된 리더보드",
+ items: {
+ type: "object",
+ required: [
+ "nickname",
+ "profileImageUrl",
+ "ticket1Amount",
+ "ticket2Amount",
+ "probability",
+ ],
+ properties: {
+ nickname: {
+ type: "string",
+ description: "유저의 닉네임",
+ example: "asdf",
+ },
+ profileImageUrl: {
+ type: "string",
+ description: "프로필 이미지 URL",
+ example: "IMAGE URL",
+ },
+ ticket1Amount: {
+ type: "number",
+ description: "일반 티켓의 개수. 0 이상입니다.",
+ example: 10,
+ },
+ ticket2Amount: {
+ type: "number",
+ description: "고급 티켓의 개수. 0 이상입니다.",
+ example: 10,
+ },
+ probability: {
+ type: "number",
+ description: "1등 당첨 확률",
+ example: 0.001,
+ },
+ },
+ },
+ },
+ rank: {
+ type: "number",
+ description: "유저의 리더보드 순위. 1부터 시작합니다.",
+ example: 30,
+ },
+ probability: {
+ type: "number",
+ description: "1등 당첨 확률",
+ example: 0.00003,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+module.exports = publicNoticeDocs;
diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js
new file mode 100644
index 00000000..1d6a89c5
--- /dev/null
+++ b/src/lottery/routes/docs/quests.js
@@ -0,0 +1,61 @@
+const { eventConfig } = require("../../../../loadenv");
+const apiPrefix = `/events/${eventConfig.mode}/quests`;
+
+const eventsDocs = {};
+eventsDocs[`${apiPrefix}/instagram/share-event`] = {
+ post: {
+ tags: [`${apiPrefix}`],
+ summary: "eventSharingOnInstagram 퀘스트 완료 요청",
+ description: "eventSharingOnInstagram 퀘스트의 완료를 요청합니다.",
+ responses: {
+ 200: {
+ description: "",
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ required: ["result"],
+ properties: {
+ result: {
+ type: "boolean",
+ description: "성공 여부",
+ example: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+eventsDocs[`${apiPrefix}/instagram/share-purchase`] = {
+ post: {
+ tags: [`${apiPrefix}`],
+ summary: "purchaseSharingOnInstagram 퀘스트 완료 요청",
+ description: "purchaseSharingOnInstagram 퀘스트의 완료를 요청합니다.",
+ responses: {
+ 200: {
+ description: "",
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ required: ["result"],
+ properties: {
+ result: {
+ description: "성공 여부",
+ type: "boolean",
+ example: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+};
+
+module.exports = eventsDocs;
diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js
index efec40c5..77a85f61 100644
--- a/src/lottery/routes/docs/swaggerDocs.js
+++ b/src/lottery/routes/docs/swaggerDocs.js
@@ -1,11 +1,12 @@
-const { eventMode } = require("../../../../loadenv");
+const { eventConfig } = require("../../../../loadenv");
const globalStateDocs = require("./globalState");
const itemsDocs = require("./items");
const transactionsDocs = require("./transactions");
-const eventsSchema = require("./eventsSchema");
+const questsDocs = require("./quests");
const itemsSchema = require("./itemsSchema");
-
-const apiPrefix = `/events/${eventMode}`;
+const publicNoticeDocs = require("./publicNotice");
+const globalStateSchema = require("./globalStateSchema");
+const apiPrefix = `/events/${eventConfig.mode}`;
const eventSwaggerDocs = {
tags: [
@@ -21,16 +22,26 @@ const eventSwaggerDocs = {
name: `${apiPrefix}/transactions`,
description: "이벤트 - 입출금 내역 관련 API",
},
+ {
+ name: `${apiPrefix}/quests`,
+ description: "이벤트 - 퀘스트 관련 API",
+ },
+ {
+ name: `${apiPrefix}/public-notice`,
+ description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API",
+ },
],
paths: {
...globalStateDocs,
...itemsDocs,
...transactionsDocs,
+ ...questsDocs,
+ ...publicNoticeDocs,
},
components: {
schemas: {
- ...eventsSchema,
...itemsSchema,
+ ...globalStateSchema,
},
},
};
diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js
index df1e1cb9..9bb82f41 100644
--- a/src/lottery/routes/docs/transactions.js
+++ b/src/lottery/routes/docs/transactions.js
@@ -1,5 +1,5 @@
-const { eventMode } = require("../../../../loadenv");
-const apiPrefix = `/events/${eventMode}/transactions`;
+const { eventConfig } = require("../../../../loadenv");
+const apiPrefix = `/events/${eventConfig.mode}/transactions`;
const transactionsDocs = {};
transactionsDocs[`${apiPrefix}/`] = {
@@ -21,7 +21,7 @@ transactionsDocs[`${apiPrefix}/`] = {
description: "유저의 재화 입출금 기록의 배열",
items: {
type: "object",
- required: ["_id", "type", "amount", "comment", "doneat"],
+ required: ["_id", "type", "amount", "comment", "createAt"],
properties: {
_id: {
type: "string",
@@ -39,8 +39,11 @@ transactionsDocs[`${apiPrefix}/`] = {
description: "재화의 변화량의 절댓값",
example: 50,
},
- event: {
- $ref: "#/components/schemas/relatedEvent",
+ questId: {
+ type: "string",
+ description:
+ "Transaction과 관련된 퀘스트의 Id. 퀘스트와 관련된 Transaction인 경우에만 포함됩니다.",
+ example: "QUEST ID",
},
item: {
$ref: "#/components/schemas/relatedItem",
@@ -50,7 +53,7 @@ transactionsDocs[`${apiPrefix}/`] = {
description: "입출금 내역에 대한 설명",
example: "랜덤 상자 구입 - 50개 차감",
},
- doneat: {
+ createAt: {
type: "string",
description: "입출금이 일어난 시각",
example: "2023-01-01 00:00:00",
diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js
index f0f75406..cb69d782 100644
--- a/src/lottery/routes/globalState.js
+++ b/src/lottery/routes/globalState.js
@@ -2,10 +2,19 @@ const express = require("express");
const router = express.Router();
const globalStateHandlers = require("../services/globalState");
+const { validateBody } = require("../../middlewares/ajv");
+const globalStateSchema = require("./docs/globalStateSchema");
-// 라우터 접근 시 로그인 필요
+router.get("/", globalStateHandlers.getUserGlobalStateHandler);
+
+// 아래의 Endpoint 접근 시 로그인 및 시각 체크 필요
router.use(require("../../middlewares/auth"));
+router.use(require("../middlewares/timestampValidator"));
-router.get("/", globalStateHandlers.getUserGlobalStateHandler);
+router.post(
+ "/create",
+ validateBody(globalStateSchema.createUserGlobalStateHandler),
+ globalStateHandlers.createUserGlobalStateHandler
+);
module.exports = router;
diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js
index 539288bc..21dc47ae 100644
--- a/src/lottery/routes/items.js
+++ b/src/lottery/routes/items.js
@@ -2,9 +2,20 @@ const express = require("express");
const router = express.Router();
const itemsHandlers = require("../services/items");
-const auth = require("../../middlewares/auth");
+
+const { validateParams } = require("../../middlewares/ajv");
+const itemsSchema = require("./docs/itemsSchema");
router.get("/list", itemsHandlers.listHandler);
-router.post("/purchase/:itemId", auth, itemsHandlers.purchaseHandler);
+
+// 아래의 Endpoint 접근 시 로그인 및 시각 체크 필요
+router.use(require("../../middlewares/auth"));
+router.use(require("../middlewares/timestampValidator"));
+
+router.post(
+ "/purchase/:itemId",
+ validateParams(itemsSchema.purchaseHandler),
+ itemsHandlers.purchaseHandler
+);
module.exports = router;
diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js
new file mode 100644
index 00000000..ac17e481
--- /dev/null
+++ b/src/lottery/routes/publicNotice.js
@@ -0,0 +1,13 @@
+const express = require("express");
+
+const router = express.Router();
+const publicNoticeHandlers = require("../services/publicNotice");
+
+// 상점 공지는 로그인을 요구하지 않습니다.
+router.get(
+ "/recentTransactions",
+ publicNoticeHandlers.getRecentPurchaceItemListHandler
+);
+router.get("/leaderboard", publicNoticeHandlers.getTicketLeaderboardHandler);
+
+module.exports = router;
diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js
new file mode 100644
index 00000000..032c96c7
--- /dev/null
+++ b/src/lottery/routes/quests.js
@@ -0,0 +1,11 @@
+const express = require("express");
+const router = express.Router();
+const quests = require("../services/quests");
+
+router.use(require("../../middlewares/auth"));
+router.use(require("../middlewares/timestampValidator"));
+
+router.post("/instagram/share-event", quests.instagramEventShareHandler);
+router.post("/instagram/share-purchase", quests.instagramPurchaseShareHandler);
+
+module.exports = router;
diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js
index b771430e..c36ac9f1 100644
--- a/src/lottery/services/globalState.js
+++ b/src/lottery/services/globalState.js
@@ -1,58 +1,72 @@
-const {
- eventStatusModel,
- eventModel,
- transactionModel,
- itemModel,
-} = require("../modules/stores/mongo");
+const { eventStatusModel } = require("../modules/stores/mongo");
+const { userModel } = require("../../modules/stores/mongo");
const logger = require("../../modules/logger");
+const { isLogin, getLoginInfo } = require("../../modules/auths/login");
+
+const { eventConfig } = require("../../../loadenv");
+const contracts =
+ eventConfig && require(`../modules/contracts/${eventConfig.mode}`);
+const quests = contracts ? Object.values(contracts.quests) : undefined;
const getUserGlobalStateHandler = async (req, res) => {
+ try {
+ const userId = isLogin(req) ? getLoginInfo(req).oid : null;
+ const eventStatus =
+ userId &&
+ (await eventStatusModel.findOne({ userId }, "-_id -userId -__v").lean());
+ if (eventStatus)
+ return res.json({
+ isAgreeOnTermsOfEvent: true,
+ ...eventStatus,
+ quests,
+ });
+ else
+ return res.json({
+ isAgreeOnTermsOfEvent: false,
+ completedQuests: [],
+ creditAmount: 0,
+ ticket1Amount: 0,
+ ticket2Amount: 0,
+ quests,
+ });
+ } catch (err) {
+ logger.error(err);
+ res.status(500).json({ error: "GlobalState/ : internal server error" });
+ }
+};
+
+const createUserGlobalStateHandler = async (req, res) => {
try {
let eventStatus = await eventStatusModel
.findOne({ userId: req.userOid })
.lean();
- if (!eventStatus) {
- // User마다 EventStatus를 가져야 하고, 현재 Taxi에는 회원 탈퇴 시스템이 없으므로, EventStatus가 없으면 새롭게 생성하도록 구현합니다.
- // EventStatus의 생성은 이곳에서만 이루어집니다!!
- eventStatus = new eventStatusModel({
- userId: req.userOid,
- });
- await eventStatus.save();
- }
+ if (eventStatus)
+ return res
+ .status(400)
+ .json({ error: "GlobalState/Create : already created" });
- const ticket1Amount = await transactionModel.count({
+ eventStatus = new eventStatusModel({
userId: req.userOid,
- type: "use",
- item: {
- $exists: true,
- $ne: null,
- },
- itemType: 1,
- });
- const ticket2Amount = await transactionModel.count({
- userId: req.userOid,
- type: "use",
- item: {
- $exists: true,
- $ne: null,
- },
- itemType: 2,
- });
- const events = await eventModel.find({}, "-__v").lean();
-
- res.json({
- creditAmount: eventStatus.creditAmount,
- eventStatus: eventStatus.eventList.map((id) => id.toString()),
- ticket1Amount,
- ticket2Amount,
- events,
});
+ await eventStatus.save();
+
+ //logic2. 수집한 유저 전화번호 user Scheme 에 저장
+ const user = await userModel.findOne({ _id: req.userOid });
+ user.phoneNumber = req.body.phoneNumber;
+ await user.save();
+
+ await contracts.completeFirstLoginQuest(req.userOid, req.timestamp);
+
+ res.json({ result: true });
} catch (err) {
logger.error(err);
- res.status(500).json({ error: "GlobalState/ : internal server error" });
+ res
+ .status(500)
+ .json({ error: "GlobalState/Create : internal server error" });
}
};
module.exports = {
getUserGlobalStateHandler,
+ createUserGlobalStateHandler,
};
diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js
index d47c2479..ece89fec 100644
--- a/src/lottery/services/items.js
+++ b/src/lottery/services/items.js
@@ -1,9 +1,30 @@
-const { itemModel, transactionModel } = require("../modules/stores/mongo");
+const {
+ eventStatusModel,
+ itemModel,
+ transactionModel,
+} = require("../modules/stores/mongo");
const logger = require("../../modules/logger");
-const { useUserCreditAmount } = require("../modules/credit");
+
+const updateEventStatus = async (
+ userId,
+ { creditDelta = 0, ticket1Delta = 0, ticket2Delta = 0 } = {}
+) =>
+ await eventStatusModel.updateOne(
+ { userId },
+ {
+ $inc: {
+ creditAmount: creditDelta,
+ ticket1Amount: ticket1Delta,
+ ticket2Amount: ticket2Delta,
+ },
+ }
+ );
const getRandomItem = async (req, depth) => {
- if (depth >= 10) return null;
+ if (depth >= 10) {
+ logger.error(`User ${req.userOid} failed to open random box`);
+ return null;
+ }
const items = await itemModel
.find({
@@ -22,10 +43,9 @@ const getRandomItem = async (req, depth) => {
.join(",");
logger.info(
- `[RandomBox] getRandomItem(depth=${depth}) is called by the user(id=${req.userOid}).`
- );
- logger.info(
- `[RandomBox] randomItems of the user(id=${req.userOid}) is [${dumpRandomItems}].`
+ `User ${req.userOid}'s ${
+ depth + 1
+ }th random box probability is: [${dumpRandomItems}]`
);
if (randomItems.length === 0) return null;
@@ -33,16 +53,16 @@ const getRandomItem = async (req, depth) => {
const randomItem =
randomItems[Math.floor(Math.random() * randomItems.length)];
try {
+ // 1단계: 재고를 차감합니다.
const newRandomItem = await itemModel
.findOneAndUpdate(
- { _id: randomItem._id },
+ { _id: randomItem._id, stock: { $gt: 0 } },
{
$inc: {
stock: -1,
},
},
{
- runValidators: true,
new: true,
fields: {
itemType: 0,
@@ -52,21 +72,32 @@ const getRandomItem = async (req, depth) => {
}
)
.lean();
+ if (!newRandomItem) {
+ throw new Error("The item was already sold out");
+ }
+
+ // 2단계: 유저 정보를 업데이트합니다.
+ await updateEventStatus(req.userOid, {
+ ticket1Delta: randomItem.itemType === 1 ? 1 : 0,
+ ticket2Delta: randomItem.itemType === 2 ? 1 : 0,
+ });
+ // 3단계: Transaction을 추가합니다.
const transaction = new transactionModel({
type: "use",
amount: 0,
userId: req.userOid,
item: randomItem._id,
itemType: randomItem.itemType,
- comment: `랜덤박스에서 ${randomItem.name} 획득 - 0개 차감`,
+ comment: `랜덤 박스에서 "${randomItem.name}" 1개를 획득했습니다.`,
});
await transaction.save();
return newRandomItem;
} catch (err) {
+ logger.error(err);
logger.warn(
- `[RandomBox] getRandomItem(depth=${depth}) by the user(id=${req.userOid}) failed due to ${err}.`
+ `User ${req.userOid}'s ${depth + 1}th random box failed due to exception`
);
return await getRandomItem(req, depth + 1);
@@ -87,21 +118,21 @@ const listHandler = async (_, res) => {
const purchaseHandler = async (req, res) => {
try {
+ const eventStatus = await eventStatusModel.findOne({ userId: req.userOid });
+ if (!eventStatus)
+ return res
+ .status(400)
+ .json({ error: "Items/Purchase : nonexistent eventStatus" });
+
const { itemId } = req.params;
const item = await itemModel.findOne({ _id: itemId }).lean();
if (!item)
return res.status(400).json({ error: "Items/Purchase : invalid Item" });
- const user = await useUserCreditAmount(req.userOid);
- if (!user)
- return res
- .status(400)
- .json({ error: "Items/Purchase : invalid EventStatus" });
-
// 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다.
if (item.isDisabled)
return res.status(400).json({ error: "Items/Purchase : disabled item" });
- if (user.amount < item.price)
+ if (eventStatus.creditAmount < item.price)
return res
.status(400)
.json({ error: "Items/Purchase : not enough credit" });
@@ -111,32 +142,31 @@ const purchaseHandler = async (req, res) => {
.json({ error: "Items/Purchase : item out of stock" });
// 1단계: 재고를 차감합니다.
- // 재고가 차감됐으나 유저 크레딧이 차감되지 않은 경우, 나중에 Transaction 기록 분석을 통해 오류 복구가 가능합니다.
- // 하지만 유저 크레딧이 차감됐으나 재고가 차감되지 않은 경우, 다른 유저가 품절된 상품을 구입할 수 있게 되고, 이는 다수의 유저에게 불편을 야기할 수 있습니다.
- await itemModel.updateOne(
- { _id: item._id },
+ const { modifiedCount } = await itemModel.updateOne(
+ { _id: item._id, stock: { $gt: 0 } },
{
$inc: {
stock: -1,
},
- },
- {
- runValidators: true,
}
);
+ if (modifiedCount === 0) throw new Error("The item was already sold out");
- // 2단계: 유저의 크레딧을 차감합니다.
- await user.update(-item.price);
+ // 2단계: 유저 정보를 업데이트합니다.
+ await updateEventStatus(req.userOid, {
+ creditDelta: -item.price,
+ ticket1Delta: item.itemType === 1 ? 1 : 0,
+ ticket2Delta: item.itemType === 2 ? 1 : 0,
+ });
// 3단계: Transaction을 추가합니다.
- // Transaction은 가장 마지막에 추가해야 다른 문서와의 불일치를 감지할 수 있습니다.
const transaction = new transactionModel({
type: "use",
amount: item.price,
userId: req.userOid,
item: item._id,
itemType: item.itemType,
- comment: `${item.name} 구입 - ${item.price}개 차감`,
+ comment: `송편 ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`,
});
await transaction.save();
diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js
new file mode 100644
index 00000000..e6b287fa
--- /dev/null
+++ b/src/lottery/services/publicNotice.js
@@ -0,0 +1,103 @@
+const { transactionModel } = require("../modules/stores/mongo");
+const { eventStatusModel } = require("../modules/stores/mongo");
+const { userModel } = require("../../modules/stores/mongo");
+const { isLogin, getLoginInfo } = require("../../modules/auths/login");
+const logger = require("../../modules/logger");
+const {
+ publicNoticePopulateOption,
+} = require("../modules/populates/transactions");
+
+const getRecentPurchaceItemListHandler = async (req, res) => {
+ try {
+ const transactions = (
+ await transactionModel
+ .find({ type: "use", itemType: 0 })
+ .sort({ createAt: -1 })
+ .limit(5)
+ .populate(publicNoticePopulateOption)
+ .lean()
+ ).map(
+ ({ userId, item, comment }) =>
+ `${userId.nickname}님께서 ${item.name}을(를) ${
+ comment.startsWith("송편")
+ ? "을(를) 구입하셨습니다."
+ : comment.startsWith("랜덤 박스")
+ ? "을(를) 뽑았습니다."
+ : "을(를) 획득하셨습니다."
+ }`
+ );
+ res.json({ transactions });
+ } catch (err) {
+ logger.error(err);
+ res.status(500).json({
+ error: "PublicNotice/RecentTransactions : internal server error",
+ });
+ }
+};
+
+const getTicketLeaderboardHandler = async (req, res) => {
+ try {
+ const users = await eventStatusModel
+ .find({
+ $or: [{ ticket1Amount: { $gt: 0 } }, { ticket2Amount: { $gt: 0 } }],
+ })
+ .lean();
+ const sortedUsers = users
+ .map((user) => ({
+ userId: user.userId.toString(),
+ ticket1Amount: user.ticket1Amount,
+ ticket2Amount: user.ticket2Amount,
+ weight: user.ticket1Amount + 5 * user.ticket2Amount,
+ }))
+ .sort((a, b) => -(a.weight - b.weight));
+
+ const userId = isLogin(req) ? getLoginInfo(req).oid : null;
+ let rank = -1;
+
+ const weightSum = sortedUsers.reduce((before, user, index) => {
+ if (rank < 0 && user.userId === userId) {
+ rank = index;
+ }
+ return before + user.weight;
+ }, 0);
+
+ const leaderboard = await Promise.all(
+ sortedUsers.slice(0, 20).map(async (user) => {
+ const userInfo = await userModel.findOne({ _id: user.userId }).lean();
+ if (!userInfo) {
+ logger.error(`Fail to find user ${user.userId}`);
+ return null;
+ }
+ return {
+ nickname: userInfo.nickname,
+ profileImageUrl: userInfo.profileImageUrl,
+ ticket1Amount: user.ticket1Amount,
+ ticket2Amount: user.ticket2Amount,
+ probability: user.weight / weightSum,
+ };
+ })
+ );
+ if (leaderboard.includes(null))
+ return res
+ .status(500)
+ .json({ error: "PublicNotice/Leaderboard : internal server error" });
+
+ if (rank >= 0)
+ res.json({
+ leaderboard,
+ rank: rank + 1,
+ probability: sortedUsers[rank].weight / weightSum,
+ });
+ else res.json({ leaderboard });
+ } catch (err) {
+ logger.error(err);
+ res
+ .status(500)
+ .json({ error: "PublicNotice/Leaderboard : internal server error" });
+ }
+};
+
+module.exports = {
+ getRecentPurchaceItemListHandler,
+ getTicketLeaderboardHandler,
+};
diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js
new file mode 100644
index 00000000..7212893f
--- /dev/null
+++ b/src/lottery/services/quests.js
@@ -0,0 +1,46 @@
+const logger = require("../../modules/logger");
+const contracts = require("../modules/contracts/2023fall");
+
+/**
+ * 인스타그램 스토리에 이벤트를 공유했을 때.
+ */
+const instagramEventShareHandler = async (req, res) => {
+ try {
+ const { userOid } = req;
+ const contractResult = await contracts.completeEventSharingOnInstagramQuest(
+ userOid,
+ req.timestamp
+ );
+ res.json({ result: !!contractResult });
+ } catch (err) {
+ logger.error(err);
+ res
+ .status(500)
+ .json({ error: "Quests/Instagram/ShareEvent: internal server error" });
+ }
+};
+
+/**
+ * 인스타그램 스토리에 아이템 구매 내역을 공유했을 때.
+ */
+const instagramPurchaseShareHandler = async (req, res) => {
+ try {
+ const { userOid } = req;
+ const contractResult =
+ await contracts.completePurchaseSharingOnInstagramQuest(
+ userOid,
+ req.timestamp
+ );
+ res.json({ result: !!contractResult });
+ } catch (err) {
+ logger.error(err);
+ res
+ .status(500)
+ .json({ error: "Quests/Instagram/SharePurchase: internal server error" });
+ }
+};
+
+module.exports = {
+ instagramEventShareHandler,
+ instagramPurchaseShareHandler,
+};
diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js
index 2009e540..c151d81f 100644
--- a/src/lottery/services/transactions.js
+++ b/src/lottery/services/transactions.js
@@ -8,7 +8,7 @@ const getUserTransactionsHandler = async (req, res) => {
try {
// userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다.
const transactions = await transactionModel
- .find({ userId: req.userOid }, "-userId -itemType -__v")
+ .find({ userId: req.userOid }, "-userId -__v")
.populate(transactionPopulateOption)
.lean();
if (transactions)
diff --git a/src/modules/adminResource.js b/src/modules/adminResource.js
index 655e928b..b75013b3 100644
--- a/src/modules/adminResource.js
+++ b/src/modules/adminResource.js
@@ -57,6 +57,8 @@ const defaultActionLogFeature = buildFeature({
});
const recordActionAfterHandler = (actions) => async (res, req, context) => {
+ if (!res.response) return res;
+
const actionsWrapper = Array.isArray(actions) ? actions : [actions];
for (const action of actionsWrapper) {
if (typeof action === "string") {
diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js
index c255d7b6..998aecdd 100755
--- a/src/modules/stores/mongo.js
+++ b/src/modules/stores/mongo.js
@@ -12,6 +12,7 @@ const userSchema = Schema({
ongoingRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 진행중인 방 배열
doneRoom: [{ type: Schema.Types.ObjectId, ref: "Room" }], // 참여중인 완료된 방 배열
withdraw: { type: Boolean, default: false },
+ phoneNumber: { type: String }, // 전화번호 (2023FALL 이벤트부터 추가)
ban: { type: Boolean, default: false }, //계정 정지 여부
joinat: { type: Date, required: true }, //가입 시각
agreeOnTermsOfService: { type: Boolean, default: false }, //이용약관 동의 여부
diff --git a/src/routes/admin.js b/src/routes/admin.js
index d476142e..e2ddaae4 100644
--- a/src/routes/admin.js
+++ b/src/routes/admin.js
@@ -13,7 +13,7 @@ const {
deviceTokenModel,
notificationOptionModel,
} = require("../modules/stores/mongo");
-const { eventMode } = require("../../loadenv");
+const { eventConfig } = require("../../loadenv");
const { buildResource } = require("../modules/adminResource");
const router = express.Router();
@@ -37,7 +37,7 @@ const baseResources = [
notificationOptionModel,
].map(buildResource());
const resources = baseResources.concat(
- eventMode === "2023fall" ? require("../lottery").resources : []
+ eventConfig?.mode === "2023fall" ? require("../lottery").resources : []
);
// Create router for admin page
diff --git a/src/services/auth.js b/src/services/auth.js
index bab3fc71..83b598de 100644
--- a/src/services/auth.js
+++ b/src/services/auth.js
@@ -91,6 +91,7 @@ const tryLogin = async (req, res, userData, redirectOrigin, redirectPath) => {
}
login(req, userData.sid, user.id, user._id, user.name);
+
res.redirect(new URL(redirectPath, redirectOrigin).href);
} catch (err) {
logger.error(err);
diff --git a/src/services/auth.mobile.js b/src/services/auth.mobile.js
index fac66032..0e537b33 100644
--- a/src/services/auth.mobile.js
+++ b/src/services/auth.mobile.js
@@ -36,6 +36,7 @@ const tokenLoginHandler = async (req, res) => {
login(req, user.sid, user.id, user._id, user.name);
req.session.isApp = true;
req.session.deviceToken = deviceToken;
+
return res.status(200).json({ message: "success" });
} catch (e) {
logger.error(e);
diff --git a/src/services/notifications.js b/src/services/notifications.js
index 799c8bb7..633f6739 100644
--- a/src/services/notifications.js
+++ b/src/services/notifications.js
@@ -4,6 +4,9 @@ const logger = require("../modules/logger");
const { registerDeviceToken, validateDeviceToken } = require("../modules/fcm");
+// 이벤트 코드입니다.
+const { contracts } = require("../lottery");
+
const registerDeviceTokenHandler = async (req, res) => {
try {
// 해당 FCM device token이 유효한지 검사합니다.
@@ -104,6 +107,13 @@ const editOptionsHandler = async (req, res) => {
.send("Notification/editOptions: deviceToken not found");
}
+ // 이벤트 코드입니다.
+ await contracts?.completeAdPushAgreementQuest(
+ req.userOid,
+ req.timestamp,
+ options.advertisement
+ );
+
res.status(200).json(updatedNotificationOptions);
} catch (err) {
logger.error(err);
diff --git a/src/services/rooms.js b/src/services/rooms.js
index 0456d534..d4b7557e 100644
--- a/src/services/rooms.js
+++ b/src/services/rooms.js
@@ -11,6 +11,9 @@ const {
getIsOver,
} = require("../modules/populates/rooms");
+// 이벤트 코드입니다.
+const { contracts } = require("../lottery");
+
const createHandler = async (req, res) => {
const { name, from, to, time, maxPartLength } = req.body;
@@ -81,7 +84,12 @@ const createHandler = async (req, res) => {
});
const roomObject = (await room.populate(roomPopulateOption)).toObject();
- return res.send(formatSettlement(roomObject));
+ const roomObjectFormated = formatSettlement(roomObject);
+
+ // 이벤트 코드입니다.
+ await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp);
+
+ return res.send(roomObjectFormated);
} catch (err) {
logger.error(err);
res.status(500).json({
@@ -483,6 +491,18 @@ const commitPaymentHandler = async (req, res) => {
authorId: user._id,
});
+ // 이벤트 코드입니다.
+ await contracts?.completePayingQuest(
+ req.userOid,
+ req.timestamp,
+ roomObject
+ );
+ await contracts?.completePayingAndSendingQuest(
+ req.userOid,
+ req.timestamp,
+ roomObject
+ );
+
// 수정한 방 정보를 반환합니다.
res.send(formatSettlement(roomObject, { isOver: true }));
} catch (err) {
@@ -549,6 +569,18 @@ const settlementHandler = async (req, res) => {
authorId: user._id,
});
+ // 이벤트 코드입니다.
+ await contracts?.completeSendingQuest(
+ req.userOid,
+ req.timestamp,
+ roomObject
+ );
+ await contracts?.completePayingAndSendingQuest(
+ req.userOid,
+ req.timestamp,
+ roomObject
+ );
+
// 수정한 방 정보를 반환합니다.
res.send(formatSettlement(roomObject, { isOver: true }));
} catch (err) {
diff --git a/src/services/users.js b/src/services/users.js
index 77af8c7d..f4a00d6a 100644
--- a/src/services/users.js
+++ b/src/services/users.js
@@ -2,6 +2,9 @@ const { userModel } = require("../modules/stores/mongo");
const logger = require("../modules/logger");
const aws = require("../modules/stores/aws");
+// 이벤트 코드입니다.
+const { contracts } = require("../lottery");
+
const agreeOnTermsOfServiceHandler = async (req, res) => {
try {
let user = await userModel.findOne({ id: req.userId });
@@ -34,43 +37,54 @@ const getAgreeOnTermsOfServiceHandler = async (req, res) => {
};
const editNicknameHandler = async (req, res) => {
- const newNickname = req.body.nickname;
+ try {
+ const newNickname = req.body.nickname;
+ const result = await userModel.findOneAndUpdate(
+ { id: req.userId },
+ { nickname: newNickname }
+ );
- // 닉네임을 갱신하고 결과를 반환
- await userModel
- .findOneAndUpdate({ id: req.userId }, { nickname: newNickname })
- .then((result) => {
- if (result) {
- res
- .status(200)
- .send("User/editNickname : edit user nickname successful");
- } else {
- res.status(400).send("User/editNickname : such user id does not exist");
- }
- })
- .catch((err) => {
- logger.error(err);
- res.status(500).send("User/editNickname : internal server error");
- });
+ if (result) {
+ // 이벤트 코드입니다.
+ await contracts?.completeNicknameChangingQuest(
+ req.userOid,
+ req.timestamp
+ );
+
+ res.status(200).send("User/editNickname : edit user nickname successful");
+ } else {
+ res.status(400).send("User/editNickname : such user id does not exist");
+ }
+ } catch (err) {
+ logger.error(err);
+ res.status(500).send("User/editNickname : internal server error");
+ }
};
const editAccountHandler = async (req, res) => {
- const newAccount = req.body.account;
+ try {
+ const newAccount = req.body.account;
+ const result = await userModel.findOneAndUpdate(
+ { id: req.userId },
+ { account: newAccount }
+ );
- // 계좌번호를 갱신하고 결과를 반환
- await userModel
- .findOneAndUpdate({ id: req.userId }, { account: newAccount })
- .then((result) => {
- if (result) {
- res.status(200).send("User/editAccount : edit user account successful");
- } else {
- res.status(400).send("User/editAccount : such user id does not exist");
- }
- })
- .catch((err) => {
- logger.error(err);
- res.status(500).send("User/editAccount : internal server error");
- });
+ if (result) {
+ // 이벤트 코드입니다.
+ await contracts?.completeAccountChangingQuest(
+ req.userOid,
+ req.timestamp,
+ newAccount
+ );
+
+ res.status(200).send("User/editAccount : edit user account successful");
+ } else {
+ res.status(400).send("User/editAccount : such user id does not exist");
+ }
+ } catch (err) {
+ logger.error(err);
+ res.status(500).send("User/editAccount : internal server error");
+ }
};
const editProfileImgGetPUrlHandler = async (req, res) => {