From b2bc6709454944fcfa4e9baa98531ee3f6eb9907 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 9 Feb 2024 11:52:08 +0900 Subject: [PATCH 01/31] Add: enable quest system --- loadenv.js | 7 +- src/lottery/modules/contracts/2024spring.js | 271 ++++++++++++++++++++ 2 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/lottery/modules/contracts/2024spring.js diff --git a/loadenv.js b/loadenv.js index 789e21db..dd63a12a 100644 --- a/loadenv.js +++ b/loadenv.js @@ -43,5 +43,10 @@ module.exports = { slackWebhookUrl: { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, - eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional + eventConfig: (process.env.EVENT_CONFIG && + JSON.parse(process.env.EVENT_CONFIG)) || { + mode: "2024spring", + startAt: "2024-02-16T00:00:00+09:00", + endAt: "2024-03-19T00:00:00+09:00", + }, // optional }; diff --git a/src/lottery/modules/contracts/2024spring.js b/src/lottery/modules/contracts/2024spring.js new file mode 100644 index 00000000..e318483c --- /dev/null +++ b/src/lottery/modules/contracts/2024spring.js @@ -0,0 +1,271 @@ +const { buildQuests, completeQuest } = require("../quests"); +const mongoose = require("mongoose"); +const logger = require("../../../modules/logger"); + +const { eventConfig } = require("../../../../loadenv"); +const eventPeriod = eventConfig && { + startAt: new Date(eventConfig.startAt), + endAt: new Date(eventConfig.endAt), +}; + +/** 전체 퀘스트 목록입니다. */ +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, + isApiRequired: true, + }, + 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, + isApiRequired: true, + }, + purchaseSharingOnInstagram: { + name: "상품 획득을 축하합니다", + description: + "이벤트를 열심히 즐긴 당신. 그 상품 획득을 축하 받을 자격이 충분합니다. 달토끼 상점에서 상품 구매 후 뜨는 인스타그램 스토리에 공유하기 버튼을 눌러 상품 획득을 공유하세요.", + imageUrl: + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_purchaseSharingOnInstagram.png", + reward: 100, + isApiRequired: true, + }, +}); + +/** + * 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 {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. + * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. + * @param {Date} roomObject.time - 출발 시각입니다. + * @returns {Promise} + * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. + * @usage rooms - commitPaymentHandler, rooms - settlementHandler + */ +const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { + logger.info( + `User ${userId} requested to complete payingAndSendingQuest in Room ${roomObject._id}` + ); + + if (roomObject.part.length < 2) return null; + if ( + roomObject.time >= eventPeriod.endAt || + roomObject.time < eventPeriod.startAt + ) + 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); +}; + +/** + * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {Object} roomObject - 방의 정보입니다. + * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. + * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. + * @param {Date} roomObject.time - 출발 시각입니다. + * @returns {Promise} + * @description 정산 요청이 이루어질 때마다 호출해 주세요. + * @usage rooms - commitPaymentHandler + */ +const completePayingQuest = async (userId, timestamp, roomObject) => { + logger.info( + `User ${userId} requested to complete payingQuest in Room ${roomObject._id}` + ); + + if (roomObject.part.length < 2) return null; + if ( + roomObject.time >= eventPeriod.endAt || + roomObject.time < eventPeriod.startAt + ) + 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 {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. + * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. + * @param {Date} roomObject.time - 출발 시각입니다. + * @returns {Promise} + * @description 송금이 이루어질 때마다 호출해 주세요. + * @usage rooms - settlementHandler + */ +const completeSendingQuest = async (userId, timestamp, roomObject) => { + logger.info( + `User ${userId} requested to complete sendingQuest in Room ${roomObject._id}` + ); + + if (roomObject.part.length < 2) return null; + if ( + roomObject.time >= eventPeriod.endAt || + roomObject.time < eventPeriod.startAt + ) + 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); +}; + +module.exports = { + quests, + completeFirstLoginQuest, + completePayingAndSendingQuest, + completeFirstRoomCreationQuest, + completePayingQuest, + completeSendingQuest, + completeNicknameChangingQuest, + completeAccountChangingQuest, + completeAdPushAgreementQuest, +}; From e832dbff3939c7868847a02b9fb6c28aedb33324 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 14:42:53 +0900 Subject: [PATCH 02/31] Add: mongoose model name prefix --- src/lottery/modules/stores/mongo.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 0f495f67..eec7195a 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -138,9 +138,24 @@ transactionSchema.set("timestamps", { updatedAt: false, }); +const { eventConfig } = require("../../../../loadenv"); + +// 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. +// 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. +const modelNamePrefix = + (eventConfig && + (eventConfig.mode === "2023fall" ? "" : `${eventConfig.mode}/`)) ?? + ""; + module.exports = { - eventStatusModel: mongoose.model("EventStatus", eventStatusSchema), - questModel: mongoose.model("Quest", questSchema), - itemModel: mongoose.model("Item", itemSchema), - transactionModel: mongoose.model("Transaction", transactionSchema), + eventStatusModel: mongoose.model( + `${modelNamePrefix}EventStatus`, + eventStatusSchema + ), + questModel: mongoose.model(`${modelNamePrefix}Quest`, questSchema), + itemModel: mongoose.model(`${modelNamePrefix}Item`, itemSchema), + transactionModel: mongoose.model( + `${modelNamePrefix}Transaction`, + transactionSchema + ), }; From df4dfa1769842a0224d8b56c7430d133d255e36e Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 15:07:10 +0900 Subject: [PATCH 03/31] Fix: AdminJS error --- src/lottery/index.js | 16 ++++++++-------- src/lottery/modules/stores/mongo.js | 4 +--- src/routes/admin.js | 9 ++++----- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/lottery/index.js b/src/lottery/index.js index 9fc6f2c4..2ec3d319 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -29,19 +29,19 @@ lotteryRouter.use("/items", require("./routes/items")); lotteryRouter.use("/public-notice", require("./routes/publicNotice")); lotteryRouter.use("/quests", require("./routes/quests")); -const itemResource = buildResource([ - addOneItemStockAction, - addFiveItemStockAction, -])(itemModel); -const otherResources = [eventStatusModel, questModel, transactionModel].map( - buildResource() -); +// [AdminJS] AdminJS에 표시할 Resource 생성 +const resources = eventConfig && [ + buildResource()(eventStatusModel), + buildResource()(questModel), + buildResource([addOneItemStockAction, addFiveItemStockAction])(itemModel), + buildResource()(transactionModel), +]; const contracts = eventConfig && require(`./modules/contracts/${eventConfig.mode}`); module.exports = { lotteryRouter, - resources: [itemResource, ...otherResources], + resources: resources ?? [], contracts, }; diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index eec7195a..008adbcf 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -143,9 +143,7 @@ const { eventConfig } = require("../../../../loadenv"); // 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. // 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. const modelNamePrefix = - (eventConfig && - (eventConfig.mode === "2023fall" ? "" : `${eventConfig.mode}/`)) ?? - ""; + eventConfig && eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; module.exports = { eventStatusModel: mongoose.model( diff --git a/src/routes/admin.js b/src/routes/admin.js index e2ddaae4..3734017c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -25,7 +25,7 @@ router.use(require("../middlewares/auth")); // Registration of the mongoose adapter AdminJS.registerAdapter(AdminJSMongoose); -const baseResources = [ +const resources = [ userModel, roomModel, locationModel, @@ -35,10 +35,9 @@ const baseResources = [ adminLogModel, deviceTokenModel, notificationOptionModel, -].map(buildResource()); -const resources = baseResources.concat( - eventConfig?.mode === "2023fall" ? require("../lottery").resources : [] -); +] + .map(buildResource()) + .concat(require("../lottery").resources); // Create router for admin page const adminJS = new AdminJS({ resources }); From 4bc495cf6ae300f30a7c68fd7ffad7dc94d3e74c Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 18:38:08 +0900 Subject: [PATCH 04/31] Refactor: update quests --- src/lottery/modules/contracts/2024spring.js | 46 ++++++++++----------- src/lottery/modules/quests.js | 3 +- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/lottery/modules/contracts/2024spring.js b/src/lottery/modules/contracts/2024spring.js index e318483c..c909c3c0 100644 --- a/src/lottery/modules/contracts/2024spring.js +++ b/src/lottery/modules/contracts/2024spring.js @@ -13,21 +13,19 @@ const quests = buildQuests({ firstLogin: { name: "첫 발걸음", description: - "로그인만 해도 송편을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 송편을 받아보세요.", + "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_firstLogin.png", - reward: { - ticket1: 1, - }, + reward: 50, }, payingAndSending: { name: "함께하는 택시의 여정", description: - "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 송편을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", + "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_payingAndSending.png", - reward: 300, - maxCount: 3, + reward: 150, + maxCount: 0, }, firstRoomCreation: { name: "첫 방 개설", @@ -38,7 +36,7 @@ const quests = buildQuests({ reward: 50, }, roomSharing: { - name: "Taxi로 모여라", + name: "너 T야? Taxi", description: "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", imageUrl: @@ -53,19 +51,19 @@ const quests = buildQuests({ imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_paying.png", reward: 100, - maxCount: 3, + maxCount: 0, }, sending: { - name: "송금 완료! 친구야 고마워", + name: "송금 완료면 I am 신뢰에요", description: "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_sending.png", reward: 50, - maxCount: 3, + maxCount: 0, }, nicknameChanging: { - name: "닉네임 변신", + name: "닉네임 폼 미쳤다", description: "닉네임을 변경하여 자신을 표현하세요. 마이페이지수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", imageUrl: @@ -73,7 +71,7 @@ const quests = buildQuests({ reward: 50, }, accountChanging: { - name: "계좌 등록은 정산의 시작", + name: "계좌 등록을 해야 능률이 올라갑니다", description: "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.", imageUrl: @@ -88,23 +86,23 @@ const quests = buildQuests({ "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_adPushAgreement.png", reward: 50, }, - eventSharingOnInstagram: { - name: "나만 알기에는 아까운 이벤트", + eventSharing: { + name: "너 나랑 ㅌ태태택 (1명)", description: - "추석에 맞춰 쏟아지는 혜택들. 나만 알 순 없죠. 인스타그램 친구들에게 스토리로 공유해보아요. 이벤트 안내 페이지에서 인스타그램 스토리에 공유하기을 눌러보세요.", + "내가 초대한 사람이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 앱 내의 공유 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", imageUrl: "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_eventSharingOnInstagram.png", - reward: 100, - isApiRequired: true, + reward: 50, + maxCount: 0, }, - purchaseSharingOnInstagram: { - name: "상품 획득을 축하합니다", + eventSharing5: { + name: "너 나랑 ㅌ태태택 (5명)", description: - "이벤트를 열심히 즐긴 당신. 그 상품 획득을 축하 받을 자격이 충분합니다. 달토끼 상점에서 상품 구매 후 뜨는 인스타그램 스토리에 공유하기 버튼을 눌러 상품 획득을 공유하세요.", + "내가 초대한 사람이 5명이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 앱 내의 공유 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_purchaseSharingOnInstagram.png", - reward: 100, - isApiRequired: true, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_eventSharingOnInstagram.png", + reward: 250, + maxCount: 0, }, }); diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index c994a141..1a1835b5 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -78,10 +78,11 @@ const completeQuest = async (userId, timestamp, quest) => { } // 3단계: 유저의 퀘스트 완료 횟수를 확인합니다. + // maxCount가 0인 경우, 무제한으로 퀘스트를 완료할 수 있습니다. const questCount = eventStatus.completedQuests.filter( (completedQuestId) => completedQuestId === quest.id ).length; - if (questCount >= quest.maxCount) { + if (quest.maxCount > 0 && questCount >= quest.maxCount) { logger.info( `User ${userId} already completed ${quest.id}Quest ${questCount} times` ); From 368a1722c246bb9fda04500881153200d0ef3fc8 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 18:49:18 +0900 Subject: [PATCH 05/31] Add: creditName, initialCreditAmount field in eventConfig environment variable --- loadenv.js | 2 ++ src/lottery/modules/quests.js | 2 +- src/lottery/services/globalState.js | 2 +- src/lottery/services/items.js | 4 +++- src/lottery/services/publicNotice.js | 10 +++++++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/loadenv.js b/loadenv.js index dd63a12a..8d90fcfa 100644 --- a/loadenv.js +++ b/loadenv.js @@ -46,6 +46,8 @@ module.exports = { eventConfig: (process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG)) || { mode: "2024spring", + creditName: "넙죽코인", + initialCreditAmount: 0, startAt: "2024-02-16T00:00:00+09:00", endAt: "2024-03-19T00:00:00+09:00", }, // optional diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 1a1835b5..1b222f56 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -127,7 +127,7 @@ const completeQuest = async (userId, timestamp, quest) => { amount: quest.reward.credit, userId, questId: quest.id, - comment: `"${quest.name}" 퀘스트를 완료해 송편 ${quest.reward.credit}개를 획득했습니다.`, + comment: `"${quest.name}" 퀘스트를 완료해 ${eventConfig?.creditName} ${quest.reward.credit}개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 13ce9bb7..131e0977 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -47,7 +47,7 @@ const createUserGlobalStateHandler = async (req, res) => { eventStatus = new eventStatusModel({ userId: req.userOid, - creditAmount: 100, // 초기 송편 개수는 0개가 아닌 100개입니다. + creditAmount: eventConfig?.initialCreditAmount ?? 0, }); await eventStatus.save(); diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 21c4310c..cff16c40 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -5,6 +5,8 @@ const { } = require("../modules/stores/mongo"); const logger = require("../../modules/logger"); +const { eventConfig } = require("../../../loadenv"); + const updateEventStatus = async ( userId, { creditDelta = 0, ticket1Delta = 0, ticket2Delta = 0 } = {} @@ -169,7 +171,7 @@ const purchaseHandler = async (req, res) => { userId: req.userOid, item: item._id, itemType: item.itemType, - comment: `송편 ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, + comment: `${eventConfig?.creditName} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index f81aaf87..75080290 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -1,5 +1,7 @@ -const { transactionModel } = require("../modules/stores/mongo"); -const { eventStatusModel } = require("../modules/stores/mongo"); +const { + eventStatusModel, + transactionModel, +} = require("../modules/stores/mongo"); const { userModel } = require("../../modules/stores/mongo"); const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const logger = require("../../modules/logger"); @@ -7,6 +9,8 @@ const { publicNoticePopulateOption, } = require("../modules/populates/transactions"); +const { eventConfig } = require("../../../loadenv"); + /** * getValueRank 사용자의 상품 구매 내역 또는 경품 추첨 내역의 순위 결정을 위한 가치를 평가하는 함수 * 상품 가격이 높을수록, 상품 구매 일시가 최근일 수록 가치가 높습니다. @@ -48,7 +52,7 @@ const getRecentPurchaceItemListHandler = async (req, res) => { .slice(0, 5) .map(({ userId, item, comment, createAt }) => ({ text: `${userId.nickname}님께서 ${item.name}${ - comment.startsWith("송편") + comment.startsWith(eventConfig?.creditName) ? "을(를) 구입하셨습니다." : comment.startsWith("랜덤박스") ? "을(를) 뽑았습니다." From 681dfd695164f84f733e38aea138b1526220ee0d Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 19:57:55 +0900 Subject: [PATCH 06/31] Add: group, inviter field into EventStatus model --- src/lottery/modules/stores/mongo.js | 9 ++++++ src/lottery/routes/docs/globalState.js | 29 ++++++-------------- src/lottery/routes/docs/globalStateSchema.js | 11 +++++++- src/lottery/services/globalState.js | 17 ++++++++++-- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 008adbcf..d67bcba4 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -37,6 +37,15 @@ const eventStatusSchema = Schema({ isBanned: { type: Boolean, }, + group: { + type: Number, + min: 1, + validate: integerValidator, + }, // 소속된 새터반 + inviter: { + type: Schema.Types.ObjectId, + ref: "User", + }, // 이 사용자를 초대한 사용자 }); const questSchema = Schema({ diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index d1dbff44..ca6cb706 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -19,8 +19,7 @@ globalStateDocs[`${apiPrefix}/`] = { "isAgreeOnTermsOfEvent", "creditAmount", "completedQuests", - "ticket1Amount", - "ticket2Amount", + "group", "quests", ], properties: { @@ -44,15 +43,15 @@ globalStateDocs[`${apiPrefix}/`] = { example: "QUEST ID", }, }, - ticket1Amount: { - type: "number", - description: "일반 티켓의 개수. 0 이상입니다.", - example: 10, + isBanned: { + type: "boolean", + description: "해당 유저 제재 대상 여부", + example: false, }, - ticket2Amount: { + group: { type: "number", - description: "고급 티켓의 개수. 0 이상입니다.", - example: 10, + description: "유저의 소속 새터반", + example: 16, }, quests: { type: "array", @@ -93,18 +92,13 @@ globalStateDocs[`${apiPrefix}/`] = { reward: { type: "object", description: "완료 보상", - required: ["credit", "ticket1"], + required: ["credit"], properties: { credit: { type: "number", description: "완료 보상 중 재화의 개수입니다.", example: 100, }, - ticket1: { - type: "number", - description: "완료 보상 중 일반 티켓의 개수입니다.", - example: 1, - }, }, }, maxCount: { @@ -120,11 +114,6 @@ globalStateDocs[`${apiPrefix}/`] = { }, }, }, - isBanned: { - type: "boolean", - description: "해당 유저 제재 대상 여부", - example: false, - }, }, }, }, diff --git a/src/lottery/routes/docs/globalStateSchema.js b/src/lottery/routes/docs/globalStateSchema.js index 7a9a5260..5bdafc43 100644 --- a/src/lottery/routes/docs/globalStateSchema.js +++ b/src/lottery/routes/docs/globalStateSchema.js @@ -1,12 +1,21 @@ const globalStateSchema = { createUserGlobalStateHandler: { type: "object", - required: ["phoneNumber"], + required: ["phoneNumber", "group"], properties: { phoneNumber: { type: "string", pattern: "^010-?([0-9]{3,4})-?([0-9]{4})$", }, + group: { + type: "integer", + minimum: 1, + maximum: 40, // TODO: 실제로 '전기' 새터반이 몇 반까지 있는지 확인히 수정해야 합니다. + }, + inviter: { + type: "string", + pattern: "^[a-fA-F\\d]{24}$", + }, }, errorMessage: "validation: bad request", }, diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 131e0977..7ce3119f 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -13,7 +13,9 @@ const getUserGlobalStateHandler = async (req, res) => { const userId = isLogin(req) ? getLoginInfo(req).oid : null; const eventStatus = userId && - (await eventStatusModel.findOne({ userId }, "-_id -userId -__v").lean()); + (await eventStatusModel + .findOne({ userId }, "completedQuests creditAmount isBanned group") + .lean()); if (eventStatus) return res.json({ isAgreeOnTermsOfEvent: true, @@ -25,8 +27,7 @@ const getUserGlobalStateHandler = async (req, res) => { isAgreeOnTermsOfEvent: false, completedQuests: [], creditAmount: 0, - ticket1Amount: 0, - ticket2Amount: 0, + group: 0, quests, }); } catch (err) { @@ -45,9 +46,19 @@ const createUserGlobalStateHandler = async (req, res) => { .status(400) .json({ error: "GlobalState/Create : already created" }); + if ( + req.body.inviter && + (await eventStatusModel.findOne({ _id: req.body.inviter }).lean()) + ) + return res.status(400).json({ + error: "GlobalState/Create : inviter did not participate in the event", + }); + eventStatus = new eventStatusModel({ userId: req.userOid, creditAmount: eventConfig?.initialCreditAmount ?? 0, + group: req.body.group, + inviter: req.body.inviter, }); await eventStatus.save(); From 65bd8ca0168f7167353a2bccf6c9fd7a2d7fb5c7 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 21:11:20 +0900 Subject: [PATCH 07/31] Add: getGroupLeaderboardHandler --- src/lottery/routes/docs/publicNotice.js | 83 +++++-------------------- src/lottery/routes/publicNotice.js | 2 +- src/lottery/services/publicNotice.js | 46 ++++++++++++++ 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index e5ce2f55..cc95f148 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -38,7 +38,7 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { tags: [`${apiPrefix}`], summary: "리더보드 반환", description: - "티켓 개수(고급 티켓은 일반 티켓 5개와 등가입니다.) 기준의 리더보드와 관련된 정보를 가져옵니다.", + "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", responses: { 200: { description: "", @@ -46,89 +46,38 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { "application/json": { schema: { type: "object", - required: [ - "leaderboard", - "totalTicket1Amount", - "totalTicket2Amount", - "totalUserAmount", - ], + required: ["leaderboard"], properties: { leaderboard: { type: "array", - description: "상위 20명만 포함된 리더보드", + description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", items: { type: "object", - required: [ - "nickname", - "profileImageUrl", - "ticket1Amount", - "ticket2Amount", - "probability", - "probabilityV2", - ], + required: ["group", "creditAmount"], properties: { - nickname: { - type: "string", - description: "유저의 닉네임", - example: "asdf", - }, - profileImageUrl: { - type: "string", - description: "프로필 이미지 URL", - example: "IMAGE URL", - }, - ticket1Amount: { - type: "number", - description: "일반 티켓의 개수. 0 이상입니다.", - example: 10, - }, - ticket2Amount: { + group: { type: "number", - description: "고급 티켓의 개수. 0 이상입니다.", - example: 10, + description: "새터반", + example: 16, }, - probability: { + creditAmount: { type: "number", - description: "1등 당첨 확률", - example: 0.001, - }, - probabilityV2: { - type: "number", - description: "근사적인 상품 당첨 확률", - example: 0.015, + description: "새터반에 소속된 유저의 전체 재화 개수", + example: 3000, }, }, }, }, - totalTicket1Amount: { - type: "number", - description: "전체 일반 티켓의 수", - example: 300, - }, - totalTicket2Amount: { - type: "number", - description: "전체 고급 티켓의 수", - example: 100, - }, - totalUserAmount: { + group: { type: "number", - description: "리더보드에 포함된 유저의 수", - example: 100, + description: "유저의 소속 새터반", + example: 16, }, rank: { type: "number", - description: "유저의 리더보드 순위. 1부터 시작합니다.", - example: 30, - }, - probability: { - type: "number", - description: "1등 당첨 확률", - example: 0.00003, - }, - probabilityV2: { - type: "number", - description: "근사적인 상품 당첨 확률", - example: 0.00045, + description: + "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", + example: 1, }, }, }, diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js index ac17e481..d1ca04a8 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -8,6 +8,6 @@ router.get( "/recentTransactions", publicNoticeHandlers.getRecentPurchaceItemListHandler ); -router.get("/leaderboard", publicNoticeHandlers.getTicketLeaderboardHandler); +router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); module.exports = router; diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 75080290..255f66f2 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -92,6 +92,7 @@ const calculateProbabilityV2 = (users, weightSum, weight) => { return 1 - Math.pow(base, weight); }; +// 2023 가을 이벤트를 위한 리더보드 API 핸들러입니다. const getTicketLeaderboardHandler = async (req, res) => { try { const users = await eventStatusModel @@ -176,7 +177,52 @@ const getTicketLeaderboardHandler = async (req, res) => { } }; +// 2024 봄 이벤트를 위한 리더보드 API 핸들러입니다. +const getGroupLeaderboardHandler = async (req, res) => { + try { + const leaderboard = await eventStatusModel.aggregate([ + { + $group: { + _id: "$group", + creditAmount: { $sum: "$creditAmount" }, + }, + }, // group을 기준으로 사용자들의 creditAmount를 합산합니다. + { + $project: { + _id: false, + group: "$_id", + creditAmount: true, + }, + }, // _id 필드의 이름을 group으로 변경합니다. + { + $sort: { + creditAmount: -1, + group: 1, + }, + }, // creditAmount를 기준으로 내림차순 정렬합니다. creditAmount가 같을 경우 group을 기준으로 오름차순 정렬합니다. + ]); + + const userId = isLogin(req) ? getLoginInfo(req).oid : null; + const user = userId && (await eventStatusModel.findOne({ userId }).lean()); + if (user) { + res.json({ + leaderboard, + group: user.group, + rank: leaderboard.findIndex((group) => group.group === user.group) + 1, + }); + } else { + res.json({ leaderboard }); + } + } catch (err) { + logger.error(err); + res + .status(500) + .json({ error: "PublicNotice/Leaderboard : internal server error" }); + } +}; + module.exports = { getRecentPurchaceItemListHandler, getTicketLeaderboardHandler, + getGroupLeaderboardHandler, }; From 7f5458a6f3a7cedb54e9e2513902a0c5027512b1 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 21:25:39 +0900 Subject: [PATCH 08/31] Refactor: getUserTransactionsHandler --- src/lottery/routes/docs/transactions.js | 3 --- src/lottery/services/transactions.js | 8 ++------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js index 9bb82f41..90794593 100644 --- a/src/lottery/routes/docs/transactions.js +++ b/src/lottery/routes/docs/transactions.js @@ -45,9 +45,6 @@ transactionsDocs[`${apiPrefix}/`] = { "Transaction과 관련된 퀘스트의 Id. 퀘스트와 관련된 Transaction인 경우에만 포함됩니다.", example: "QUEST ID", }, - item: { - $ref: "#/components/schemas/relatedItem", - }, comment: { type: "string", description: "입출금 내역에 대한 설명", diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js index 695cf3bf..1d920870 100644 --- a/src/lottery/services/transactions.js +++ b/src/lottery/services/transactions.js @@ -1,8 +1,5 @@ const { transactionModel } = require("../modules/stores/mongo"); const logger = require("../../modules/logger"); -const { - transactionPopulateOption, -} = require("../modules/populates/transactions"); const hideItemStock = (transaction) => { if (transaction.item) { @@ -15,12 +12,11 @@ const getUserTransactionsHandler = async (req, res) => { try { // userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다. const transactions = await transactionModel - .find({ userId: req.userOid }, "-userId -__v") - .populate(transactionPopulateOption) + .find({ userId: req.userOid }, "_id type amount questId comment createAt") .lean(); if (transactions) res.json({ - transactions: transactions.map(hideItemStock), + transactions, }); else res.status(500).json({ error: "Transactions/ : internal server error" }); From 00206a4a83db22ccfde2244f8c54baaa8918d724 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 13 Feb 2024 22:01:29 +0900 Subject: [PATCH 09/31] Add: completeEventSharingQuest --- app.js | 2 +- src/lottery/modules/contracts/2024spring.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index a26c4b46..7f862f8a 100644 --- a/app.js +++ b/app.js @@ -50,7 +50,7 @@ app.use(require("./src/middlewares/limitRate")); // [Router] Swagger (API 문서) app.use("/docs", require("./src/routes/docs")); -// 2023 추석 이벤트 전용 라우터입니다. +// [Router] 이벤트 전용 라우터입니다. eventConfig && app.use( `/events/${eventConfig.mode}`, diff --git a/src/lottery/modules/contracts/2024spring.js b/src/lottery/modules/contracts/2024spring.js index c909c3c0..62de3c2b 100644 --- a/src/lottery/modules/contracts/2024spring.js +++ b/src/lottery/modules/contracts/2024spring.js @@ -256,6 +256,20 @@ const completeAdPushAgreementQuest = async ( return await completeQuest(userId, timestamp, quests.adPushAgreement); }; +/** + * eventSharing, eventSharing5 퀘스트의 완료를 요청합니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @returns {Promise} + * @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요. + */ +const completeEventSharingQuest = async (userId, timestamp) => { + return [ + await completeQuest(userId, timestamp, quests.eventSharing), + await completeQuest(userId, timestamp, quests.eventSharing5), + ]; +}; + module.exports = { quests, completeFirstLoginQuest, @@ -266,4 +280,5 @@ module.exports = { completeNicknameChangingQuest, completeAccountChangingQuest, completeAdPushAgreementQuest, + completeEventSharingQuest, }; From 2014bddc1ec10247c8ac140601a5123b7ddb1319 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 14 Feb 2024 00:42:00 +0900 Subject: [PATCH 10/31] Add: validate KAIST student id in global-state/create endpoint --- src/lottery/routes/docs/globalStateSchema.js | 2 +- src/lottery/services/globalState.js | 25 ++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/lottery/routes/docs/globalStateSchema.js b/src/lottery/routes/docs/globalStateSchema.js index 5bdafc43..5c1b2de7 100644 --- a/src/lottery/routes/docs/globalStateSchema.js +++ b/src/lottery/routes/docs/globalStateSchema.js @@ -10,7 +10,7 @@ const globalStateSchema = { group: { type: "integer", minimum: 1, - maximum: 40, // TODO: 실제로 '전기' 새터반이 몇 반까지 있는지 확인히 수정해야 합니다. + maximum: 26, }, inviter: { type: "string", diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 7ce3119f..9351a4f6 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -54,6 +54,26 @@ const createUserGlobalStateHandler = async (req, res) => { error: "GlobalState/Create : inviter did not participate in the event", }); + const user = await userModel.findOne({ _id: req.userOid }); + if (!user) + return res + .status(500) + .json({ error: "GlobalState/Create : internal server error" }); + + // 24학번 학사 과정 학생이 아닌 경우 이벤트에 참여할 수 없습니다. + const kaistId = parseInt(user.subinfo?.kaist || "0"); + if (!(20240001 <= kaistId && kaistId <= 20241500)) { + return res.status(400).json({ + error: "GlobalState/Create : not an undergraduate freshman", + }); + } + + // 수집한 전화번호를 User Document에 저장합니다. + // 다른 이벤트 참여 과정에서 문제가 생길 수 있으므로, 이벤트 참여 자격이 있는 경우에만 저장합니다. + user.phoneNumber = req.body.phoneNumber; + await user.save(); + + // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, creditAmount: eventConfig?.initialCreditAmount ?? 0, @@ -62,11 +82,6 @@ const createUserGlobalStateHandler = async (req, res) => { }); 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 }); From dac8fbec399cb34d2d8a054cd61479e86cf51442 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 14 Feb 2024 00:44:11 +0900 Subject: [PATCH 11/31] Refactor: rename some router paths --- src/lottery/index.js | 4 ++-- src/lottery/routes/docs/globalState.js | 2 +- src/lottery/routes/docs/publicNotice.js | 2 +- src/lottery/routes/docs/swaggerDocs.js | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lottery/index.js b/src/lottery/index.js index 2ec3d319..466b0b31 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -23,10 +23,10 @@ const lotteryRouter = express.Router(); lotteryRouter.use(require("../middlewares/originValidator")); // [Router] APIs -lotteryRouter.use("/global-state", require("./routes/globalState")); +lotteryRouter.use("/globalState", require("./routes/globalState")); lotteryRouter.use("/transactions", require("./routes/transactions")); lotteryRouter.use("/items", require("./routes/items")); -lotteryRouter.use("/public-notice", require("./routes/publicNotice")); +lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); lotteryRouter.use("/quests", require("./routes/quests")); // [AdminJS] AdminJS에 표시할 Resource 생성 diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index ca6cb706..6f181265 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/global-state`; +const apiPrefix = `/events/${eventConfig.mode}/globalState`; const globalStateDocs = {}; globalStateDocs[`${apiPrefix}/`] = { diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index cc95f148..aa31aa06 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/public-notice`; +const apiPrefix = `/events/${eventConfig.mode}/publicNotice`; const publicNoticeDocs = {}; publicNoticeDocs[`${apiPrefix}/recentTransactions`] = { diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 73fa7b66..278528cc 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -14,7 +14,7 @@ const apiPrefix = `/events/${eventConfig.mode}`; const eventSwaggerDocs = { tags: [ { - name: `${apiPrefix}/global-state`, + name: `${apiPrefix}/globalState`, description: "이벤트 - Global State 관련 API", }, { @@ -22,7 +22,7 @@ const eventSwaggerDocs = { description: "이벤트 - 아이템 관련 API", }, { - name: `${apiPrefix}/public-notice`, + name: `${apiPrefix}/publicNotice`, description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API", }, { From bf8eb9ed1d456288dc49988ea0322382e177365e Mon Sep 17 00:00:00 2001 From: static Date: Wed, 14 Feb 2024 01:10:59 +0900 Subject: [PATCH 12/31] Add: groupCreditAmount field in the response of globalState endpoint --- src/lottery/routes/docs/globalState.js | 8 +++++- src/lottery/services/globalState.js | 36 +++++++++++++++++++++----- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 6f181265..d6150601 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -18,6 +18,7 @@ globalStateDocs[`${apiPrefix}/`] = { required: [ "isAgreeOnTermsOfEvent", "creditAmount", + "groupCreditAmount", "completedQuests", "group", "quests", @@ -31,7 +32,12 @@ globalStateDocs[`${apiPrefix}/`] = { creditAmount: { type: "number", description: "재화 개수. 0 이상입니다.", - example: 10000, + example: 1000, + }, + groupCreditAmount: { + type: "number", + description: "소속 새터반에 소속된 유저의 전체 재화 개수", + example: 35000, }, completedQuests: { type: "array", diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 9351a4f6..ea76a92b 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -16,20 +16,42 @@ const getUserGlobalStateHandler = async (req, res) => { (await eventStatusModel .findOne({ userId }, "completedQuests creditAmount isBanned group") .lean()); - if (eventStatus) - return res.json({ - isAgreeOnTermsOfEvent: true, - ...eventStatus, - quests, - }); - else + if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, completedQuests: [], creditAmount: 0, group: 0, + groupCreditAmount: 0, quests, }); + + // group이 eventStatus.group과 같은 사용자들의 creditAmount를 합산합니다. + const groupCreditAmount = await eventStatusModel.aggregate([ + { + $match: { + group: eventStatus.group, + }, + }, + { + $group: { + _id: null, + creditAmount: { $sum: "$creditAmount" }, + }, + }, + ]); + const groupCreditAmountReal = groupCreditAmount[0]?.creditAmount; + if (!groupCreditAmountReal) + return res + .status(500) + .json({ error: "GlobalState/ : internal server error" }); + + return res.json({ + isAgreeOnTermsOfEvent: true, + ...eventStatus, + groupCreditAmount: groupCreditAmountReal, + quests, + }); } catch (err) { logger.error(err); res.status(500).json({ error: "GlobalState/ : internal server error" }); From 4b77ffe8b1a7f0565a1ca63a330f96764c0bf1a2 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 14 Feb 2024 01:17:18 +0900 Subject: [PATCH 13/31] Fix: start time of the event --- loadenv.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loadenv.js b/loadenv.js index 8d90fcfa..b6f24caf 100644 --- a/loadenv.js +++ b/loadenv.js @@ -48,7 +48,7 @@ module.exports = { mode: "2024spring", creditName: "넙죽코인", initialCreditAmount: 0, - startAt: "2024-02-16T00:00:00+09:00", + startAt: "2024-02-23T00:00:00+09:00", endAt: "2024-03-19T00:00:00+09:00", }, // optional }; From d80b24241d4aa1ef5039565489d3b6246d42cf1e Mon Sep 17 00:00:00 2001 From: static Date: Wed, 14 Feb 2024 23:57:35 +0900 Subject: [PATCH 14/31] Remove: contracts/2023fall.js --- loadenv.js | 2 - src/lottery/index.js | 92 +++--- src/lottery/middlewares/timestampValidator.js | 1 - .../{contracts/2024spring.js => contracts.js} | 15 +- src/lottery/modules/contracts/2023fall.js | 271 ------------------ src/lottery/modules/quests.js | 7 +- src/lottery/modules/stores/mongo.js | 15 +- src/lottery/services/globalState.js | 7 +- src/lottery/services/items.js | 3 +- src/lottery/services/publicNotice.js | 3 +- src/lottery/services/quests.js | 6 +- src/routes/admin.js | 1 - 12 files changed, 80 insertions(+), 343 deletions(-) rename src/lottery/modules/{contracts/2024spring.js => contracts.js} (97%) delete mode 100644 src/lottery/modules/contracts/2023fall.js diff --git a/loadenv.js b/loadenv.js index b6f24caf..d4d281c8 100644 --- a/loadenv.js +++ b/loadenv.js @@ -46,8 +46,6 @@ module.exports = { eventConfig: (process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG)) || { mode: "2024spring", - creditName: "넙죽코인", - initialCreditAmount: 0, startAt: "2024-02-23T00:00:00+09:00", endAt: "2024-03-19T00:00:00+09:00", }, // optional diff --git a/src/lottery/index.js b/src/lottery/index.js index 466b0b31..e4066938 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -1,47 +1,49 @@ -const express = require("express"); -const { - eventStatusModel, - questModel, - itemModel, - transactionModel, -} = require("./modules/stores/mongo"); - -const { buildResource } = require("../modules/adminResource"); -const { - addOneItemStockAction, - addFiveItemStockAction, -} = require("./modules/items"); - const { eventConfig } = require("../../loadenv"); -// [Routes] 기존 docs 라우터의 docs extend -eventConfig && require("./routes/docs")(); - -const lotteryRouter = express.Router(); - -// [Middleware] 모든 API 요청에 대하여 origin 검증 -lotteryRouter.use(require("../middlewares/originValidator")); - -// [Router] APIs -lotteryRouter.use("/globalState", require("./routes/globalState")); -lotteryRouter.use("/transactions", require("./routes/transactions")); -lotteryRouter.use("/items", require("./routes/items")); -lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); -lotteryRouter.use("/quests", require("./routes/quests")); - -// [AdminJS] AdminJS에 표시할 Resource 생성 -const resources = eventConfig && [ - buildResource()(eventStatusModel), - buildResource()(questModel), - buildResource([addOneItemStockAction, addFiveItemStockAction])(itemModel), - buildResource()(transactionModel), -]; - -const contracts = - eventConfig && require(`./modules/contracts/${eventConfig.mode}`); - -module.exports = { - lotteryRouter, - resources: resources ?? [], - contracts, -}; +if (eventConfig) { + // [Routes] 기존 docs 라우터의 docs extend + require("./routes/docs")(); + + const express = require("express"); + const lotteryRouter = express.Router(); + + // [Middleware] 모든 API 요청에 대하여 origin 검증 + lotteryRouter.use(require("../middlewares/originValidator")); + + // [Router] APIs + lotteryRouter.use("/globalState", require("./routes/globalState")); + lotteryRouter.use("/transactions", require("./routes/transactions")); + lotteryRouter.use("/items", require("./routes/items")); + lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); + lotteryRouter.use("/quests", require("./routes/quests")); + + // [AdminJS] AdminJS에 표시할 Resource 생성 + const { buildResource } = require("../modules/adminResource"); + const { + eventStatusModel, + questModel, + itemModel, + transactionModel, + } = require("./modules/stores/mongo"); + const { + addOneItemStockAction, + addFiveItemStockAction, + } = require("./modules/items"); + + const resources = [ + buildResource()(eventStatusModel), + buildResource()(questModel), + buildResource([addOneItemStockAction, addFiveItemStockAction])(itemModel), + buildResource()(transactionModel), + ]; + + module.exports = { + lotteryRouter, + resources, + contracts: require("./modules/contracts"), + }; +} else { + module.exports = { + resources: [], + }; +} diff --git a/src/lottery/middlewares/timestampValidator.js b/src/lottery/middlewares/timestampValidator.js index 22511536..781ec265 100644 --- a/src/lottery/middlewares/timestampValidator.js +++ b/src/lottery/middlewares/timestampValidator.js @@ -6,7 +6,6 @@ const eventPeriod = eventConfig && { const timestampValidator = (req, res, next) => { if ( - !eventPeriod || req.timestamp >= eventPeriod.endAt || req.timestamp < eventPeriod.startAt ) { diff --git a/src/lottery/modules/contracts/2024spring.js b/src/lottery/modules/contracts.js similarity index 97% rename from src/lottery/modules/contracts/2024spring.js rename to src/lottery/modules/contracts.js index 62de3c2b..21ee4eaf 100644 --- a/src/lottery/modules/contracts/2024spring.js +++ b/src/lottery/modules/contracts.js @@ -1,13 +1,19 @@ -const { buildQuests, completeQuest } = require("../quests"); +const { buildQuests, completeQuest: buildCompleteQuest } = require("./quests"); const mongoose = require("mongoose"); -const logger = require("../../../modules/logger"); +const logger = require("../../modules/logger"); -const { eventConfig } = require("../../../../loadenv"); +const { eventConfig } = require("../../../loadenv"); const eventPeriod = eventConfig && { startAt: new Date(eventConfig.startAt), endAt: new Date(eventConfig.endAt), }; +/** 재화 정보입니다. */ +const creditInfo = { + name: "넙죽코인", + initialAmount: 0, +}; + /** 전체 퀘스트 목록입니다. */ const quests = buildQuests({ firstLogin: { @@ -106,6 +112,8 @@ const quests = buildQuests({ }, }); +const completeQuest = buildCompleteQuest(creditInfo.name); + /** * firstLogin 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. @@ -271,6 +279,7 @@ const completeEventSharingQuest = async (userId, timestamp) => { }; module.exports = { + creditInfo, quests, completeFirstLoginQuest, completePayingAndSendingQuest, diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js deleted file mode 100644 index e318483c..00000000 --- a/src/lottery/modules/contracts/2023fall.js +++ /dev/null @@ -1,271 +0,0 @@ -const { buildQuests, completeQuest } = require("../quests"); -const mongoose = require("mongoose"); -const logger = require("../../../modules/logger"); - -const { eventConfig } = require("../../../../loadenv"); -const eventPeriod = eventConfig && { - startAt: new Date(eventConfig.startAt), - endAt: new Date(eventConfig.endAt), -}; - -/** 전체 퀘스트 목록입니다. */ -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, - isApiRequired: true, - }, - 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, - isApiRequired: true, - }, - purchaseSharingOnInstagram: { - name: "상품 획득을 축하합니다", - description: - "이벤트를 열심히 즐긴 당신. 그 상품 획득을 축하 받을 자격이 충분합니다. 달토끼 상점에서 상품 구매 후 뜨는 인스타그램 스토리에 공유하기 버튼을 눌러 상품 획득을 공유하세요.", - imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_purchaseSharingOnInstagram.png", - reward: 100, - isApiRequired: true, - }, -}); - -/** - * 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 {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. - * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. - * @param {Date} roomObject.time - 출발 시각입니다. - * @returns {Promise} - * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler, rooms - settlementHandler - */ -const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { - logger.info( - `User ${userId} requested to complete payingAndSendingQuest in Room ${roomObject._id}` - ); - - if (roomObject.part.length < 2) return null; - if ( - roomObject.time >= eventPeriod.endAt || - roomObject.time < eventPeriod.startAt - ) - 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); -}; - -/** - * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. - * @param {Object} roomObject - 방의 정보입니다. - * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. - * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. - * @param {Date} roomObject.time - 출발 시각입니다. - * @returns {Promise} - * @description 정산 요청이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitPaymentHandler - */ -const completePayingQuest = async (userId, timestamp, roomObject) => { - logger.info( - `User ${userId} requested to complete payingQuest in Room ${roomObject._id}` - ); - - if (roomObject.part.length < 2) return null; - if ( - roomObject.time >= eventPeriod.endAt || - roomObject.time < eventPeriod.startAt - ) - 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 {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. - * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. - * @param {Date} roomObject.time - 출발 시각입니다. - * @returns {Promise} - * @description 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - settlementHandler - */ -const completeSendingQuest = async (userId, timestamp, roomObject) => { - logger.info( - `User ${userId} requested to complete sendingQuest in Room ${roomObject._id}` - ); - - if (roomObject.part.length < 2) return null; - if ( - roomObject.time >= eventPeriod.endAt || - roomObject.time < eventPeriod.startAt - ) - 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); -}; - -module.exports = { - quests, - completeFirstLoginQuest, - completePayingAndSendingQuest, - completeFirstRoomCreationQuest, - completePayingQuest, - completeSendingQuest, - completeNicknameChangingQuest, - completeAccountChangingQuest, - completeAdPushAgreementQuest, -}; diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 1b222f56..fde78c0d 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -52,6 +52,7 @@ const buildQuests = (quests) => { /** * 퀘스트 완료를 요청합니다. + * @param {string} creditName - 재화의 이름입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} quest - 퀘스트의 정보입니다. @@ -63,8 +64,10 @@ const buildQuests = (quests) => { * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다. */ -const completeQuest = async (userId, timestamp, quest) => { +const completeQuest = (creditName) => async (userId, timestamp, quest) => { try { + if (!eventPeriod) throw new Error("eventPeriod is not defined"); + // 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다. const eventStatus = await eventStatusModel.findOne({ userId }).lean(); if (!eventStatus || eventStatus.isBanned) return null; @@ -127,7 +130,7 @@ const completeQuest = async (userId, timestamp, quest) => { amount: quest.reward.credit, userId, questId: quest.id, - comment: `"${quest.name}" 퀘스트를 완료해 ${eventConfig?.creditName} ${quest.reward.credit}개를 획득했습니다.`, + comment: `"${quest.name}" 퀘스트를 완료해 ${creditName} ${quest.reward.credit}개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index d67bcba4..68392246 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -1,6 +1,12 @@ const mongoose = require("mongoose"); const Schema = mongoose.Schema; +// 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. +// 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. +const { eventConfig } = require("../../../../loadenv"); +const modelNamePrefix = + eventConfig && eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; + const integerValidator = { validator: Number.isInteger, message: "{VALUE} is not an integer value", @@ -131,7 +137,7 @@ const transactionSchema = Schema({ }, item: { type: Schema.Types.ObjectId, - ref: "Item", + ref: `${modelNamePrefix}Item`, }, itemType: { type: Number, @@ -147,13 +153,6 @@ transactionSchema.set("timestamps", { updatedAt: false, }); -const { eventConfig } = require("../../../../loadenv"); - -// 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. -// 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. -const modelNamePrefix = - eventConfig && eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; - module.exports = { eventStatusModel: mongoose.model( `${modelNamePrefix}EventStatus`, diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index ea76a92b..059f12b1 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -4,9 +4,8 @@ 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 contracts = eventConfig && require("../modules/contracts"); +const quests = contracts && Object.values(contracts.quests); const getUserGlobalStateHandler = async (req, res) => { try { @@ -98,7 +97,7 @@ const createUserGlobalStateHandler = async (req, res) => { // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, - creditAmount: eventConfig?.initialCreditAmount ?? 0, + creditAmount: contracts.creditInfo.initialAmount, group: req.body.group, inviter: req.body.inviter, }); diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index cff16c40..707a8b00 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -6,6 +6,7 @@ const { const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); +const contracts = eventConfig && require("../modules/contracts"); const updateEventStatus = async ( userId, @@ -171,7 +172,7 @@ const purchaseHandler = async (req, res) => { userId: req.userOid, item: item._id, itemType: item.itemType, - comment: `${eventConfig?.creditName} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, + comment: `${contracts.creditInfo.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 255f66f2..251efd67 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -10,6 +10,7 @@ const { } = require("../modules/populates/transactions"); const { eventConfig } = require("../../../loadenv"); +const contracts = eventConfig && require("../modules/contracts"); /** * getValueRank 사용자의 상품 구매 내역 또는 경품 추첨 내역의 순위 결정을 위한 가치를 평가하는 함수 @@ -52,7 +53,7 @@ const getRecentPurchaceItemListHandler = async (req, res) => { .slice(0, 5) .map(({ userId, item, comment, createAt }) => ({ text: `${userId.nickname}님께서 ${item.name}${ - comment.startsWith(eventConfig?.creditName) + comment.startsWith(contracts.creditInfo.name) ? "을(를) 구입하셨습니다." : comment.startsWith("랜덤박스") ? "을(를) 뽑았습니다." diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js index 5dd4f0e5..a25e8238 100644 --- a/src/lottery/services/quests.js +++ b/src/lottery/services/quests.js @@ -2,13 +2,11 @@ const { completeQuest } = require("../modules/quests"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); -const quests = eventConfig - ? require(`../modules/contracts/${eventConfig.mode}`).quests - : undefined; +const contracts = eventConfig && require("../modules/contracts"); const completeHandler = async (req, res) => { try { - const quest = quests[req.params.questId]; + const quest = contracts.quests[req.params.questId]; if (!quest || !quest.isApiRequired) return res.status(400).json({ error: "Quests/Complete: invalid Quest" }); diff --git a/src/routes/admin.js b/src/routes/admin.js index 3734017c..da20d60d 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -13,7 +13,6 @@ const { deviceTokenModel, notificationOptionModel, } = require("../modules/stores/mongo"); -const { eventConfig } = require("../../loadenv"); const { buildResource } = require("../modules/adminResource"); const router = express.Router(); From 6fc078a90168d6777b59747e27b497ba757b7a9e Mon Sep 17 00:00:00 2001 From: static Date: Thu, 15 Feb 2024 00:04:55 +0900 Subject: [PATCH 15/31] Remove: nullity checks for eventConfig in lottery module --- src/lottery/middlewares/timestampValidator.js | 2 +- src/lottery/modules/contracts.js | 2 +- src/lottery/modules/quests.js | 4 +--- src/lottery/modules/stores/mongo.js | 3 +-- src/lottery/services/globalState.js | 5 ++--- src/lottery/services/items.js | 3 +-- src/lottery/services/publicNotice.js | 3 +-- src/lottery/services/quests.js | 3 +-- 8 files changed, 9 insertions(+), 16 deletions(-) diff --git a/src/lottery/middlewares/timestampValidator.js b/src/lottery/middlewares/timestampValidator.js index 781ec265..f4333b32 100644 --- a/src/lottery/middlewares/timestampValidator.js +++ b/src/lottery/middlewares/timestampValidator.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../loadenv"); -const eventPeriod = eventConfig && { +const eventPeriod = { startAt: new Date(eventConfig.startAt), endAt: new Date(eventConfig.endAt), }; diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 21ee4eaf..89b23e13 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -3,7 +3,7 @@ const mongoose = require("mongoose"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); -const eventPeriod = eventConfig && { +const eventPeriod = { startAt: new Date(eventConfig.startAt), endAt: new Date(eventConfig.endAt), }; diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index fde78c0d..a37e2406 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -8,7 +8,7 @@ const logger = require("../../modules/logger"); const mongoose = require("mongoose"); const { eventConfig } = require("../../../loadenv"); -const eventPeriod = eventConfig && { +const eventPeriod = { startAt: new Date(eventConfig.startAt), endAt: new Date(eventConfig.endAt), }; @@ -66,8 +66,6 @@ const buildQuests = (quests) => { */ const completeQuest = (creditName) => async (userId, timestamp, quest) => { try { - if (!eventPeriod) throw new Error("eventPeriod is not defined"); - // 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다. const eventStatus = await eventStatusModel.findOne({ userId }).lean(); if (!eventStatus || eventStatus.isBanned) return null; diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 68392246..1944d49c 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -4,8 +4,7 @@ const Schema = mongoose.Schema; // 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. // 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. const { eventConfig } = require("../../../../loadenv"); -const modelNamePrefix = - eventConfig && eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; +const modelNamePrefix = eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; const integerValidator = { validator: Number.isInteger, diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 059f12b1..b9e61576 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -3,9 +3,8 @@ 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"); -const quests = contracts && Object.values(contracts.quests); +const contracts = require("../modules/contracts"); +const quests = Object.values(contracts.quests); const getUserGlobalStateHandler = async (req, res) => { try { diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 707a8b00..7de95841 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -5,8 +5,7 @@ const { } = require("../modules/stores/mongo"); const logger = require("../../modules/logger"); -const { eventConfig } = require("../../../loadenv"); -const contracts = eventConfig && require("../modules/contracts"); +const contracts = require("../modules/contracts"); const updateEventStatus = async ( userId, diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 251efd67..615254ab 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -9,8 +9,7 @@ const { publicNoticePopulateOption, } = require("../modules/populates/transactions"); -const { eventConfig } = require("../../../loadenv"); -const contracts = eventConfig && require("../modules/contracts"); +const contracts = require("../modules/contracts"); /** * getValueRank 사용자의 상품 구매 내역 또는 경품 추첨 내역의 순위 결정을 위한 가치를 평가하는 함수 diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js index a25e8238..ae1472bc 100644 --- a/src/lottery/services/quests.js +++ b/src/lottery/services/quests.js @@ -1,8 +1,7 @@ const { completeQuest } = require("../modules/quests"); const logger = require("../../modules/logger"); -const { eventConfig } = require("../../../loadenv"); -const contracts = eventConfig && require("../modules/contracts"); +const contracts = require("../modules/contracts"); const completeHandler = async (req, res) => { try { From af8f92fc2967f3da3d12201d96ae279e1d417877 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 15 Feb 2024 00:11:10 +0900 Subject: [PATCH 16/31] Refactor: disable unused endpoints in lottery router --- src/lottery/routes/items.js | 24 ++++++++++++++---------- src/lottery/routes/publicNotice.js | 14 +++++++++----- src/lottery/routes/quests.js | 1 + src/lottery/routes/transactions.js | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 7d1513f5..e7be69a8 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -1,4 +1,5 @@ const express = require("express"); +const { eventConfig } = require("../../../loadenv"); const router = express.Router(); const itemsHandlers = require("../services/items"); @@ -6,17 +7,20 @@ const itemsHandlers = require("../services/items"); const { validateParams } = require("../../middlewares/ajv"); const itemsSchema = require("./docs/itemsSchema"); -router.get("/list", itemsHandlers.listHandler); +// 아래의 Endpoint는 2023년 가을학기 이벤트 때에만 접근 가능 +if (eventConfig.mode === "2023fall") { + router.get("/list", itemsHandlers.listHandler); -// 아래의 Endpoint 접근 시 로그인, 블록드리스트 및 시각 체크 필요 -router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); -router.use(require("../middlewares/timestampValidator")); + // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 + router.use(require("../../middlewares/auth")); + router.use(require("../middlewares/checkBanned")); + router.use(require("../middlewares/timestampValidator")); -router.post( - "/purchase/:itemId", - validateParams(itemsSchema.purchaseHandler), - itemsHandlers.purchaseHandler -); + 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 index d1ca04a8..9764d754 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -1,13 +1,17 @@ const express = require("express"); +const { eventConfig } = require("../../../loadenv"); const router = express.Router(); const publicNoticeHandlers = require("../services/publicNotice"); -// 상점 공지는 로그인을 요구하지 않습니다. -router.get( - "/recentTransactions", - publicNoticeHandlers.getRecentPurchaceItemListHandler -); router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); +// 아래의 Endpoint는 2023년 가을학기 이벤트 때에만 접근 가능 +if (eventConfig.mode === "2023fall") { + router.get( + "/recentTransactions", + publicNoticeHandlers.getRecentPurchaceItemListHandler + ); +} + module.exports = router; diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js index 2aa55624..45862c51 100644 --- a/src/lottery/routes/quests.js +++ b/src/lottery/routes/quests.js @@ -6,6 +6,7 @@ const questsHandlers = require("../services/quests"); const { validateParams } = require("../../middlewares/ajv"); const questsSchema = require("./docs/questsSchema"); +// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 router.use(require("../../middlewares/auth")); router.use(require("../middlewares/checkBanned")); router.use(require("../middlewares/timestampValidator")); diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js index 391e0cf6..aee05d90 100644 --- a/src/lottery/routes/transactions.js +++ b/src/lottery/routes/transactions.js @@ -3,7 +3,7 @@ const express = require("express"); const router = express.Router(); const transactionsHandlers = require("../services/transactions"); -// 라우터 접근 시 로그인 필요 +// 아래의 Endpoint 접근 시 로그인 필요 router.use(require("../../middlewares/auth")); router.get("/", transactionsHandlers.getUserTransactionsHandler); From a9e73ac59a3c882d92edb37d9294a2df36d11039 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 15 Feb 2024 00:30:39 +0900 Subject: [PATCH 17/31] Fix: groupCreditAmount cannot be zero --- src/lottery/services/globalState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index b9e61576..8659f667 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -39,7 +39,7 @@ const getUserGlobalStateHandler = async (req, res) => { }, ]); const groupCreditAmountReal = groupCreditAmount[0]?.creditAmount; - if (!groupCreditAmountReal) + if (!groupCreditAmountReal && groupCreditAmountReal !== 0) return res .status(500) .json({ error: "GlobalState/ : internal server error" }); From e8c9a9cc1c0cc265b83f23394f5f95f32f351e69 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 15 Feb 2024 01:26:11 +0900 Subject: [PATCH 18/31] Refactor: minor changes --- src/lottery/routes/docs/questsSchema.js | 6 +----- src/lottery/services/globalState.js | 7 ++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/lottery/routes/docs/questsSchema.js b/src/lottery/routes/docs/questsSchema.js index 3432cc8c..e4d6786c 100644 --- a/src/lottery/routes/docs/questsSchema.js +++ b/src/lottery/routes/docs/questsSchema.js @@ -5,11 +5,7 @@ const questsSchema = { properties: { questId: { type: "string", - enum: [ - "roomSharing", - "eventSharingOnInstagram", - "purchaseSharingOnInstagram", - ], + enum: ["roomSharing"], }, }, }, diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 8659f667..e65acbe2 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -2,6 +2,7 @@ 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 { nodeEnv } = require("../../../loadenv"); const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); @@ -81,8 +82,12 @@ const createUserGlobalStateHandler = async (req, res) => { .json({ error: "GlobalState/Create : internal server error" }); // 24학번 학사 과정 학생이 아닌 경우 이벤트에 참여할 수 없습니다. + // 테스트를 위해, production 환경에서만 학번을 확인합니다. const kaistId = parseInt(user.subinfo?.kaist || "0"); - if (!(20240001 <= kaistId && kaistId <= 20241500)) { + if ( + nodeEnv === "production" && + !(20240001 <= kaistId && kaistId <= 20241500) + ) { return res.status(400).json({ error: "GlobalState/Create : not an undergraduate freshman", }); From f1811aed9e289bacad85ec6cab7e12de7daadfa7 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 18 Feb 2024 11:06:13 +0900 Subject: [PATCH 19/31] Add: null checking before using eventConfig --- src/lottery/middlewares/timestampValidator.js | 3 ++- src/lottery/modules/contracts.js | 5 ++++- src/lottery/modules/quests.js | 8 ++++++-- src/lottery/modules/stores/mongo.js | 3 ++- src/lottery/routes/docs/globalState.js | 4 ++-- src/lottery/routes/docs/items.js | 2 +- src/lottery/routes/docs/publicNotice.js | 2 +- src/lottery/routes/docs/quests.js | 2 +- src/lottery/routes/docs/swaggerDocs.js | 2 +- src/lottery/routes/docs/transactions.js | 2 +- src/lottery/routes/items.js | 2 +- src/lottery/routes/publicNotice.js | 2 +- 12 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/lottery/middlewares/timestampValidator.js b/src/lottery/middlewares/timestampValidator.js index f4333b32..22511536 100644 --- a/src/lottery/middlewares/timestampValidator.js +++ b/src/lottery/middlewares/timestampValidator.js @@ -1,11 +1,12 @@ const { eventConfig } = require("../../../loadenv"); -const eventPeriod = { +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 ) { diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 89b23e13..a63515bb 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -3,7 +3,7 @@ const mongoose = require("mongoose"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); -const eventPeriod = { +const eventPeriod = eventConfig && { startAt: new Date(eventConfig.startAt), endAt: new Date(eventConfig.endAt), }; @@ -144,6 +144,7 @@ const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { if (roomObject.part.length < 2) return null; if ( + !eventPeriod || roomObject.time >= eventPeriod.endAt || roomObject.time < eventPeriod.startAt ) @@ -183,6 +184,7 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { if (roomObject.part.length < 2) return null; if ( + !eventPeriod || roomObject.time >= eventPeriod.endAt || roomObject.time < eventPeriod.startAt ) @@ -210,6 +212,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => { if (roomObject.part.length < 2) return null; if ( + !eventPeriod || roomObject.time >= eventPeriod.endAt || roomObject.time < eventPeriod.startAt ) diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index a37e2406..78cb890c 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -8,7 +8,7 @@ const logger = require("../../modules/logger"); const mongoose = require("mongoose"); const { eventConfig } = require("../../../loadenv"); -const eventPeriod = { +const eventPeriod = eventConfig && { startAt: new Date(eventConfig.startAt), endAt: new Date(eventConfig.endAt), }; @@ -71,7 +71,11 @@ const completeQuest = (creditName) => async (userId, timestamp, quest) => { if (!eventStatus || eventStatus.isBanned) return null; // 2단계: 이벤트 기간인지 확인합니다. - if (timestamp >= eventPeriod.endAt || timestamp < eventPeriod.startAt) { + if ( + !eventPeriod || + timestamp >= eventPeriod.endAt || + timestamp < eventPeriod.startAt + ) { logger.info( `User ${userId} failed to complete auto-disabled ${quest.id}Quest` ); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 1944d49c..68392246 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -4,7 +4,8 @@ const Schema = mongoose.Schema; // 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. // 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. const { eventConfig } = require("../../../../loadenv"); -const modelNamePrefix = eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; +const modelNamePrefix = + eventConfig && eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; const integerValidator = { validator: Number.isInteger, diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index d6150601..29a73de5 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/globalState`; +const apiPrefix = `/events/${eventConfig?.mode}/globalState`; const globalStateDocs = {}; globalStateDocs[`${apiPrefix}/`] = { @@ -114,7 +114,7 @@ globalStateDocs[`${apiPrefix}/`] = { }, isApiRequired: { type: "boolean", - description: `/events/${eventConfig.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청할 수 있는지 여부`, + description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청할 수 있는지 여부`, example: false, }, }, diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 1137dc2b..b08aeaf7 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/items`; +const apiPrefix = `/events/${eventConfig?.mode}/items`; const itemsDocs = {}; itemsDocs[`${apiPrefix}/list`] = { diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index aa31aa06..2a2b0516 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/publicNotice`; +const apiPrefix = `/events/${eventConfig?.mode}/publicNotice`; const publicNoticeDocs = {}; publicNoticeDocs[`${apiPrefix}/recentTransactions`] = { diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js index cd624688..71268b2c 100644 --- a/src/lottery/routes/docs/quests.js +++ b/src/lottery/routes/docs/quests.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/quests`; +const apiPrefix = `/events/${eventConfig?.mode}/quests`; const eventsDocs = {}; eventsDocs[`${apiPrefix}/complete/:questId`] = { diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 278528cc..ee985e42 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -9,7 +9,7 @@ const globalStateSchema = require("./globalStateSchema"); const questsSchema = require("./questsSchema"); const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}`; +const apiPrefix = `/events/${eventConfig?.mode}`; const eventSwaggerDocs = { tags: [ diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js index 90794593..fa78238b 100644 --- a/src/lottery/routes/docs/transactions.js +++ b/src/lottery/routes/docs/transactions.js @@ -1,5 +1,5 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig.mode}/transactions`; +const apiPrefix = `/events/${eventConfig?.mode}/transactions`; const transactionsDocs = {}; transactionsDocs[`${apiPrefix}/`] = { diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index e7be69a8..176053e9 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -8,7 +8,7 @@ const { validateParams } = require("../../middlewares/ajv"); const itemsSchema = require("./docs/itemsSchema"); // 아래의 Endpoint는 2023년 가을학기 이벤트 때에만 접근 가능 -if (eventConfig.mode === "2023fall") { +if (eventConfig?.mode === "2023fall") { router.get("/list", itemsHandlers.listHandler); // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js index 9764d754..11ad927b 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -7,7 +7,7 @@ const publicNoticeHandlers = require("../services/publicNotice"); router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); // 아래의 Endpoint는 2023년 가을학기 이벤트 때에만 접근 가능 -if (eventConfig.mode === "2023fall") { +if (eventConfig?.mode === "2023fall") { router.get( "/recentTransactions", publicNoticeHandlers.getRecentPurchaceItemListHandler From 72bc8a0225577cfbec5cea576b92a798f61ed273 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 18 Feb 2024 11:09:43 +0900 Subject: [PATCH 20/31] Docs: hide ununsed endpoints --- src/lottery/routes/docs/publicNotice.js | 64 +++++++++++++------------ src/lottery/routes/docs/swaggerDocs.js | 14 +++--- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index 2a2b0516..069850ac 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -2,37 +2,39 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/publicNotice`; 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**************님께서 일반응모권을(를) 획득하셨습니다.", - }, - }, - }, - }, - }, - }, - }, - }, - }, -}; +// 다음 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// +// 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}`], diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index ee985e42..9b1d21f8 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -17,10 +17,12 @@ const eventSwaggerDocs = { name: `${apiPrefix}/globalState`, description: "이벤트 - Global State 관련 API", }, - { - name: `${apiPrefix}/items`, - description: "이벤트 - 아이템 관련 API", - }, + // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다. + // + // { + // name: `${apiPrefix}/items`, + // description: "이벤트 - 아이템 관련 API", + // }, { name: `${apiPrefix}/publicNotice`, description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API", @@ -36,7 +38,7 @@ const eventSwaggerDocs = { ], paths: { ...globalStateDocs, - ...itemsDocs, + //...itemsDocs, ...publicNoticeDocs, ...questsDocs, ...transactionsDocs, @@ -44,7 +46,7 @@ const eventSwaggerDocs = { components: { schemas: { ...globalStateSchema, - ...itemsSchema, + //...itemsSchema, ...questsSchema, }, }, From f8cc2d0b3b60cf05d248643925dd6d99b588fc3c Mon Sep 17 00:00:00 2001 From: static Date: Sun, 18 Feb 2024 11:21:57 +0900 Subject: [PATCH 21/31] Remove: legacy support codes --- src/lottery/modules/stores/mongo.js | 4 +--- src/lottery/routes/items.js | 25 ++++++++++++------------- src/lottery/routes/publicNotice.js | 13 ++++++------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 68392246..cb6b4e84 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -2,10 +2,8 @@ const mongoose = require("mongoose"); const Schema = mongoose.Schema; // 이벤트마다 사용된 모델을 구분하기 위해 이름에 Prefix를 붙입니다. -// 2023년 가을학기 이벤트 때에는 Prefix를 사용하지 않았으므로, 해당 경우에는 Prefix를 붙이지 않습니다. const { eventConfig } = require("../../../../loadenv"); -const modelNamePrefix = - eventConfig && eventConfig.mode !== "2023fall" ? eventConfig.mode : ""; +const modelNamePrefix = eventConfig?.mode ?? ""; const integerValidator = { validator: Number.isInteger, diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 176053e9..87d29c53 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -7,20 +7,19 @@ const itemsHandlers = require("../services/items"); const { validateParams } = require("../../middlewares/ajv"); const itemsSchema = require("./docs/itemsSchema"); -// 아래의 Endpoint는 2023년 가을학기 이벤트 때에만 접근 가능 -if (eventConfig?.mode === "2023fall") { - router.get("/list", itemsHandlers.listHandler); +// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// +// router.get("/list", itemsHandlers.listHandler); - // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 - router.use(require("../../middlewares/auth")); - router.use(require("../middlewares/checkBanned")); - router.use(require("../middlewares/timestampValidator")); +// // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 +// router.use(require("../../middlewares/auth")); +// router.use(require("../middlewares/checkBanned")); +// router.use(require("../middlewares/timestampValidator")); - router.post( - "/purchase/:itemId", - validateParams(itemsSchema.purchaseHandler), - itemsHandlers.purchaseHandler - ); -} +// 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 index 11ad927b..e47d6d9a 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -6,12 +6,11 @@ const publicNoticeHandlers = require("../services/publicNotice"); router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); -// 아래의 Endpoint는 2023년 가을학기 이벤트 때에만 접근 가능 -if (eventConfig?.mode === "2023fall") { - router.get( - "/recentTransactions", - publicNoticeHandlers.getRecentPurchaceItemListHandler - ); -} +// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// +// router.get( +// "/recentTransactions", +// publicNoticeHandlers.getRecentPurchaceItemListHandler +// ); module.exports = router; From ec7060c41168778eb1fc3a3c9222e033e0e27986 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 18 Feb 2024 23:39:33 +0900 Subject: [PATCH 22/31] Add: mvp related fields in the response of leaderboard api --- src/lottery/routes/docs/publicNotice.js | 18 +++++++++++++++++- src/lottery/services/publicNotice.js | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index 069850ac..23a410b2 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -55,7 +55,12 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", items: { type: "object", - required: ["group", "creditAmount"], + required: [ + "group", + "creditAmount", + "mvpNickname", + "mvpProfileImageUrl", + ], properties: { group: { type: "number", @@ -67,6 +72,17 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { description: "새터반에 소속된 유저의 전체 재화 개수", example: 3000, }, + mvpNickname: { + type: "string", + description: + "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", + example: "asdf", + }, + mvpProfileImageUrl: { + type: "string", + description: "MVP의 프로필 이미지 URL", + example: "IMAGE URL", + }, }, }, }, diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 615254ab..87c2ccdb 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -180,7 +180,7 @@ const getTicketLeaderboardHandler = async (req, res) => { // 2024 봄 이벤트를 위한 리더보드 API 핸들러입니다. const getGroupLeaderboardHandler = async (req, res) => { try { - const leaderboard = await eventStatusModel.aggregate([ + const leaderboardWithoutMvp = await eventStatusModel.aggregate([ { $group: { _id: "$group", @@ -201,6 +201,25 @@ const getGroupLeaderboardHandler = async (req, res) => { }, }, // creditAmount를 기준으로 내림차순 정렬합니다. creditAmount가 같을 경우 group을 기준으로 오름차순 정렬합니다. ]); + const leaderboard = await Promise.all( + leaderboardWithoutMvp.map(async (group) => { + const mvp = await eventStatusModel + .find({ group: group.group }) + .sort({ creditAmount: -1 }) + .limit(1) // Aggreation을 사용하는 것보다, sort와 limit을 바로 붙여 사용하는 것이 더 효율적입니다. + .lean(); + if (!mvp) throw new Error(`Fail to find MVP in group ${group.group}`); + + const mvpInfo = await userModel.findOne({ _id: mvp[0].userId }).lean(); + if (!mvpInfo) throw new Error(`Fail to find user ${mvp[0].userId}`); + + return { + ...group, + mvpNickname: mvpInfo.nickname, + mvpProfileImageUrl: mvpInfo.profileImageUrl, + }; + }) + ); const userId = isLogin(req) ? getLoginInfo(req).oid : null; const user = userId && (await eventStatusModel.findOne({ userId }).lean()); From 1583d5a2d7404dbcb5b41bd131aaebc5ff3ad125 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 19 Feb 2024 23:01:29 +0900 Subject: [PATCH 23/31] Refactor: move creditInfo into eventConfig --- loadenv.js | 10 ++++++++-- src/lottery/middlewares/timestampValidator.js | 4 ++-- src/lottery/modules/contracts.js | 15 +++------------ src/lottery/modules/quests.js | 9 ++++----- src/lottery/routes/items.js | 1 - src/lottery/routes/publicNotice.js | 1 - src/lottery/services/globalState.js | 3 ++- src/lottery/services/items.js | 4 ++-- src/lottery/services/publicNotice.js | 4 ++-- 9 files changed, 23 insertions(+), 28 deletions(-) diff --git a/loadenv.js b/loadenv.js index d4d281c8..871f25a2 100644 --- a/loadenv.js +++ b/loadenv.js @@ -46,7 +46,13 @@ module.exports = { eventConfig: (process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG)) || { mode: "2024spring", - startAt: "2024-02-23T00:00:00+09:00", - endAt: "2024-03-19T00:00:00+09:00", + credit: { + name: "넙죽코인", + initialAmount: 0, + }, + period: { + startAt: "2024-02-23T00:00:00+09:00", + endAt: "2024-03-19T00:00:00+09:00", + }, }, // optional }; diff --git a/src/lottery/middlewares/timestampValidator.js b/src/lottery/middlewares/timestampValidator.js index 22511536..5df2973c 100644 --- a/src/lottery/middlewares/timestampValidator.js +++ b/src/lottery/middlewares/timestampValidator.js @@ -1,7 +1,7 @@ const { eventConfig } = require("../../../loadenv"); const eventPeriod = eventConfig && { - startAt: new Date(eventConfig.startAt), - endAt: new Date(eventConfig.endAt), + startAt: new Date(eventConfig.period.startAt), + endAt: new Date(eventConfig.period.endAt), }; const timestampValidator = (req, res, next) => { diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index a63515bb..1237b7ba 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -1,17 +1,11 @@ -const { buildQuests, completeQuest: buildCompleteQuest } = require("./quests"); +const { buildQuests, completeQuest } = require("./quests"); const mongoose = require("mongoose"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); const eventPeriod = eventConfig && { - startAt: new Date(eventConfig.startAt), - endAt: new Date(eventConfig.endAt), -}; - -/** 재화 정보입니다. */ -const creditInfo = { - name: "넙죽코인", - initialAmount: 0, + startAt: new Date(eventConfig.period.startAt), + endAt: new Date(eventConfig.period.endAt), }; /** 전체 퀘스트 목록입니다. */ @@ -112,8 +106,6 @@ const quests = buildQuests({ }, }); -const completeQuest = buildCompleteQuest(creditInfo.name); - /** * firstLogin 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. @@ -282,7 +274,6 @@ const completeEventSharingQuest = async (userId, timestamp) => { }; module.exports = { - creditInfo, quests, completeFirstLoginQuest, completePayingAndSendingQuest, diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 78cb890c..84f75d1d 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -9,8 +9,8 @@ const mongoose = require("mongoose"); const { eventConfig } = require("../../../loadenv"); const eventPeriod = eventConfig && { - startAt: new Date(eventConfig.startAt), - endAt: new Date(eventConfig.endAt), + startAt: new Date(eventConfig.period.startAt), + endAt: new Date(eventConfig.period.endAt), }; const requiredQuestFields = ["name", "description", "imageUrl", "reward"]; @@ -52,7 +52,6 @@ const buildQuests = (quests) => { /** * 퀘스트 완료를 요청합니다. - * @param {string} creditName - 재화의 이름입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} quest - 퀘스트의 정보입니다. @@ -64,7 +63,7 @@ const buildQuests = (quests) => { * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다. */ -const completeQuest = (creditName) => async (userId, timestamp, quest) => { +const completeQuest = async (userId, timestamp, quest) => { try { // 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다. const eventStatus = await eventStatusModel.findOne({ userId }).lean(); @@ -132,7 +131,7 @@ const completeQuest = (creditName) => async (userId, timestamp, quest) => { amount: quest.reward.credit, userId, questId: quest.id, - comment: `"${quest.name}" 퀘스트를 완료해 ${creditName} ${quest.reward.credit}개를 획득했습니다.`, + comment: `"${quest.name}" 퀘스트를 완료해 ${eventConfig?.credit.name} ${quest.reward.credit}개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 87d29c53..e71a950d 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -1,5 +1,4 @@ const express = require("express"); -const { eventConfig } = require("../../../loadenv"); const router = express.Router(); const itemsHandlers = require("../services/items"); diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js index e47d6d9a..4698a193 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -1,5 +1,4 @@ const express = require("express"); -const { eventConfig } = require("../../../loadenv"); const router = express.Router(); const publicNoticeHandlers = require("../services/publicNotice"); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index e65acbe2..02683261 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -4,6 +4,7 @@ const logger = require("../../modules/logger"); const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const { nodeEnv } = require("../../../loadenv"); +const { eventConfig } = require("../../../loadenv"); const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); @@ -101,7 +102,7 @@ const createUserGlobalStateHandler = async (req, res) => { // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, - creditAmount: contracts.creditInfo.initialAmount, + creditAmount: eventConfig?.credit.initialAmount, group: req.body.group, inviter: req.body.inviter, }); diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 7de95841..9189bae4 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -5,7 +5,7 @@ const { } = require("../modules/stores/mongo"); const logger = require("../../modules/logger"); -const contracts = require("../modules/contracts"); +const { eventConfig } = require("../../../loadenv"); const updateEventStatus = async ( userId, @@ -171,7 +171,7 @@ const purchaseHandler = async (req, res) => { userId: req.userOid, item: item._id, itemType: item.itemType, - comment: `${contracts.creditInfo.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, + comment: `${eventConfig?.credit.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 87c2ccdb..2d473e79 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -9,7 +9,7 @@ const { publicNoticePopulateOption, } = require("../modules/populates/transactions"); -const contracts = require("../modules/contracts"); +const { eventConfig } = require("../../../loadenv"); /** * getValueRank 사용자의 상품 구매 내역 또는 경품 추첨 내역의 순위 결정을 위한 가치를 평가하는 함수 @@ -52,7 +52,7 @@ const getRecentPurchaceItemListHandler = async (req, res) => { .slice(0, 5) .map(({ userId, item, comment, createAt }) => ({ text: `${userId.nickname}님께서 ${item.name}${ - comment.startsWith(contracts.creditInfo.name) + comment.startsWith(eventConfig?.credit.name) ? "을(를) 구입하셨습니다." : comment.startsWith("랜덤박스") ? "을(를) 뽑았습니다." From 833aade793bdae147954d8dbf56dae2fbf36bf41 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 19 Feb 2024 23:19:43 +0900 Subject: [PATCH 24/31] Refactor: restore index.js --- src/lottery/index.js | 84 +++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/src/lottery/index.js b/src/lottery/index.js index e4066938..0cddcb43 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -1,49 +1,47 @@ +const express = require("express"); +const { + eventStatusModel, + questModel, + itemModel, + transactionModel, +} = require("./modules/stores/mongo"); + +const { buildResource } = require("../modules/adminResource"); +const { + addOneItemStockAction, + addFiveItemStockAction, +} = require("./modules/items"); + const { eventConfig } = require("../../loadenv"); +const contracts = eventConfig && require("./modules/contracts"); + +// [Routes] 기존 docs 라우터의 docs extend +eventConfig && require("./routes/docs")(); + +const lotteryRouter = express.Router(); -if (eventConfig) { - // [Routes] 기존 docs 라우터의 docs extend - require("./routes/docs")(); - - const express = require("express"); - const lotteryRouter = express.Router(); - - // [Middleware] 모든 API 요청에 대하여 origin 검증 - lotteryRouter.use(require("../middlewares/originValidator")); - - // [Router] APIs - lotteryRouter.use("/globalState", require("./routes/globalState")); - lotteryRouter.use("/transactions", require("./routes/transactions")); - lotteryRouter.use("/items", require("./routes/items")); - lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); - lotteryRouter.use("/quests", require("./routes/quests")); - - // [AdminJS] AdminJS에 표시할 Resource 생성 - const { buildResource } = require("../modules/adminResource"); - const { - eventStatusModel, - questModel, - itemModel, - transactionModel, - } = require("./modules/stores/mongo"); - const { - addOneItemStockAction, - addFiveItemStockAction, - } = require("./modules/items"); - - const resources = [ +// [Middleware] 모든 API 요청에 대하여 origin 검증 +lotteryRouter.use(require("../middlewares/originValidator")); + +// [Router] APIs +lotteryRouter.use("/globalState", require("./routes/globalState")); +lotteryRouter.use("/transactions", require("./routes/transactions")); +lotteryRouter.use("/items", require("./routes/items")); +lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); +lotteryRouter.use("/quests", require("./routes/quests")); + +// [AdminJS] AdminJS에 표시할 Resource 생성 +const resources = + (eventConfig && [ buildResource()(eventStatusModel), buildResource()(questModel), buildResource([addOneItemStockAction, addFiveItemStockAction])(itemModel), buildResource()(transactionModel), - ]; - - module.exports = { - lotteryRouter, - resources, - contracts: require("./modules/contracts"), - }; -} else { - module.exports = { - resources: [], - }; -} + ]) || + []; + +module.exports = { + lotteryRouter, + contracts, + resources, +}; From 339f516f6ee2de12688f5cce29aa2b3416cdc8ba Mon Sep 17 00:00:00 2001 From: static Date: Mon, 19 Feb 2024 23:24:13 +0900 Subject: [PATCH 25/31] Refactor: update MongoDB schema --- src/lottery/modules/stores/mongo.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index cb6b4e84..9efe9a96 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -40,9 +40,11 @@ const eventStatusSchema = Schema({ }, isBanned: { type: Boolean, + default: false, }, group: { type: Number, + required: true, min: 1, validate: integerValidator, }, // 소속된 새터반 From f357050613be20801bcbefd4ba1c760918190179 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 19 Feb 2024 23:33:21 +0900 Subject: [PATCH 26/31] Fix: invalid optional chaining for groupCreditAmount --- src/lottery/services/globalState.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 02683261..d7256876 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -40,7 +40,7 @@ const getUserGlobalStateHandler = async (req, res) => { }, }, ]); - const groupCreditAmountReal = groupCreditAmount[0]?.creditAmount; + const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; if (!groupCreditAmountReal && groupCreditAmountReal !== 0) return res .status(500) From 8e21e396040332766a8f3b0e542bab98d4843a5d Mon Sep 17 00:00:00 2001 From: static Date: Mon, 19 Feb 2024 23:43:13 +0900 Subject: [PATCH 27/31] Fix: invalid null checkings --- src/lottery/services/globalState.js | 2 +- src/lottery/services/publicNotice.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index d7256876..f986f8bc 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -102,7 +102,7 @@ const createUserGlobalStateHandler = async (req, res) => { // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, - creditAmount: eventConfig?.credit.initialAmount, + creditAmount: eventConfig?.credit.initialAmount ?? 0, group: req.body.group, inviter: req.body.inviter, }); diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 2d473e79..dd3dc91b 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -208,7 +208,8 @@ const getGroupLeaderboardHandler = async (req, res) => { .sort({ creditAmount: -1 }) .limit(1) // Aggreation을 사용하는 것보다, sort와 limit을 바로 붙여 사용하는 것이 더 효율적입니다. .lean(); - if (!mvp) throw new Error(`Fail to find MVP in group ${group.group}`); + if (mvp?.length !== 1) + throw new Error(`Fail to find MVP in group ${group.group}`); const mvpInfo = await userModel.findOne({ _id: mvp[0].userId }).lean(); if (!mvpInfo) throw new Error(`Fail to find user ${mvp[0].userId}`); From 6509a41c2f45145678e2c06fa1ed97c484b54884 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 19 Feb 2024 23:59:11 +0900 Subject: [PATCH 28/31] Add: eligibility field into the response of globalState api --- src/lottery/routes/docs/globalState.js | 6 ++++++ src/lottery/services/globalState.js | 26 +++++++++++++++++--------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 29a73de5..858539de 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -17,6 +17,7 @@ globalStateDocs[`${apiPrefix}/`] = { type: "object", required: [ "isAgreeOnTermsOfEvent", + "eligibility", "creditAmount", "groupCreditAmount", "completedQuests", @@ -29,6 +30,11 @@ globalStateDocs[`${apiPrefix}/`] = { description: "유저의 이벤트 참여 동의 여부", example: true, }, + eligibility: { + type: "boolean", + description: "유저의 이벤트 참여 가능 여부", + example: true, + }, creditAmount: { type: "number", description: "재화 개수. 0 이상입니다.", diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index f986f8bc..91942888 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -8,9 +8,20 @@ const { eventConfig } = require("../../../loadenv"); const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); +// 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. +const checkUserEligibility = async (user) => { + // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. + if (nodeEnv !== "production") return true; + + const kaistId = parseInt(user?.subinfo?.kaist || "0"); + return 20240001 <= kaistId && kaistId <= 20241500; +}; + const getUserGlobalStateHandler = async (req, res) => { try { const userId = isLogin(req) ? getLoginInfo(req).oid : null; + const user = userId && (await userModel.findOne({ _id: userId }).lean()); + const eventStatus = userId && (await eventStatusModel @@ -19,6 +30,7 @@ const getUserGlobalStateHandler = async (req, res) => { if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, + eligibility: await checkUserEligibility(user), completedQuests: [], creditAmount: 0, group: 0, @@ -48,6 +60,7 @@ const getUserGlobalStateHandler = async (req, res) => { return res.json({ isAgreeOnTermsOfEvent: true, + eligibility: true, ...eventStatus, groupCreditAmount: groupCreditAmountReal, quests, @@ -82,17 +95,12 @@ const createUserGlobalStateHandler = async (req, res) => { .status(500) .json({ error: "GlobalState/Create : internal server error" }); - // 24학번 학사 과정 학생이 아닌 경우 이벤트에 참여할 수 없습니다. - // 테스트를 위해, production 환경에서만 학번을 확인합니다. - const kaistId = parseInt(user.subinfo?.kaist || "0"); - if ( - nodeEnv === "production" && - !(20240001 <= kaistId && kaistId <= 20241500) - ) { + // 유저가 이벤트에 참여할 수 있는지 확인합니다. + const eligibility = await checkUserEligibility(user); + if (!eligibility) return res.status(400).json({ - error: "GlobalState/Create : not an undergraduate freshman", + error: "GlobalState/Create : not eligible to participate in the event", }); - } // 수집한 전화번호를 User Document에 저장합니다. // 다른 이벤트 참여 과정에서 문제가 생길 수 있으므로, 이벤트 참여 자격이 있는 경우에만 저장합니다. From a170133e2d28f56d5d924c057487cfc067b87aab Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 00:01:11 +0900 Subject: [PATCH 29/31] Remove: unnecessary async keyword --- src/lottery/services/globalState.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 91942888..28cbc34f 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -9,7 +9,7 @@ const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); // 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. -const checkUserEligibility = async (user) => { +const checkUserEligibility = (user) => { // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. if (nodeEnv !== "production") return true; @@ -30,7 +30,7 @@ const getUserGlobalStateHandler = async (req, res) => { if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, - eligibility: await checkUserEligibility(user), + eligibility: checkUserEligibility(user), completedQuests: [], creditAmount: 0, group: 0, @@ -96,7 +96,7 @@ const createUserGlobalStateHandler = async (req, res) => { .json({ error: "GlobalState/Create : internal server error" }); // 유저가 이벤트에 참여할 수 있는지 확인합니다. - const eligibility = await checkUserEligibility(user); + const eligibility = checkUserEligibility(user); if (!eligibility) return res.status(400).json({ error: "GlobalState/Create : not eligible to participate in the event", From 8db1c1cb03f13767ea5d2de411649f43e41b5480 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 00:10:04 +0900 Subject: [PATCH 30/31] Refactor: naming convention --- src/lottery/routes/docs/globalState.js | 4 ++-- src/lottery/services/globalState.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 858539de..4af3493e 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -17,7 +17,7 @@ globalStateDocs[`${apiPrefix}/`] = { type: "object", required: [ "isAgreeOnTermsOfEvent", - "eligibility", + "isEligible", "creditAmount", "groupCreditAmount", "completedQuests", @@ -30,7 +30,7 @@ globalStateDocs[`${apiPrefix}/`] = { description: "유저의 이벤트 참여 동의 여부", example: true, }, - eligibility: { + isEligible: { type: "boolean", description: "유저의 이벤트 참여 가능 여부", example: true, diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 28cbc34f..76f8b15a 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -9,7 +9,7 @@ const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); // 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. -const checkUserEligibility = (user) => { +const checkIsUserEligible = (user) => { // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. if (nodeEnv !== "production") return true; @@ -30,7 +30,7 @@ const getUserGlobalStateHandler = async (req, res) => { if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, - eligibility: checkUserEligibility(user), + isEligible: checkIsUserEligible(user), completedQuests: [], creditAmount: 0, group: 0, @@ -60,7 +60,7 @@ const getUserGlobalStateHandler = async (req, res) => { return res.json({ isAgreeOnTermsOfEvent: true, - eligibility: true, + isEligible: true, ...eventStatus, groupCreditAmount: groupCreditAmountReal, quests, @@ -96,8 +96,8 @@ const createUserGlobalStateHandler = async (req, res) => { .json({ error: "GlobalState/Create : internal server error" }); // 유저가 이벤트에 참여할 수 있는지 확인합니다. - const eligibility = checkUserEligibility(user); - if (!eligibility) + const isEligible = checkIsUserEligible(user); + if (!isEligible) return res.status(400).json({ error: "GlobalState/Create : not eligible to participate in the event", }); From 89b6f89838adcb5ce6ac91f57273359710b2f676 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 20 Feb 2024 00:17:51 +0900 Subject: [PATCH 31/31] Refactor: apply new quest images --- src/lottery/modules/contracts.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 1237b7ba..0ea00f14 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -15,7 +15,7 @@ const quests = buildQuests({ description: "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_firstLogin.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png", reward: 50, }, payingAndSending: { @@ -23,7 +23,7 @@ const quests = buildQuests({ description: "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_payingAndSending.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_payingAndSending.png", reward: 150, maxCount: 0, }, @@ -32,7 +32,7 @@ const quests = buildQuests({ description: "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_firstRoomCreation.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstRoomCreation.png", reward: 50, }, roomSharing: { @@ -40,7 +40,7 @@ const quests = buildQuests({ description: "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_roomSharing.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_roomSharing.png", reward: 50, isApiRequired: true, }, @@ -49,7 +49,7 @@ const quests = buildQuests({ description: "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산하기를 요청해보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_paying.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_paying.png", reward: 100, maxCount: 0, }, @@ -58,7 +58,7 @@ const quests = buildQuests({ description: "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_sending.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_sending.png", reward: 50, maxCount: 0, }, @@ -67,7 +67,7 @@ const quests = buildQuests({ description: "닉네임을 변경하여 자신을 표현하세요. 마이페이지수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_nicknameChanging.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_nicknameChanging.png", reward: 50, }, accountChanging: { @@ -75,7 +75,7 @@ const quests = buildQuests({ description: "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_accountChanging.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_accountChanging.png", reward: 50, }, adPushAgreement: { @@ -83,7 +83,7 @@ const quests = buildQuests({ description: "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_adPushAgreement.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_adPushAgreement.png", reward: 50, }, eventSharing: { @@ -91,7 +91,7 @@ const quests = buildQuests({ description: "내가 초대한 사람이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 앱 내의 공유 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_eventSharingOnInstagram.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", reward: 50, maxCount: 0, }, @@ -100,7 +100,7 @@ const quests = buildQuests({ description: "내가 초대한 사람이 5명이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 앱 내의 공유 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_eventSharingOnInstagram.png", + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", reward: 250, maxCount: 0, },