Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#456 퀘스트 시스템 활성화 #465

Merged
merged 32 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b2bc670
Add: enable quest system
kmc7468 Feb 9, 2024
e832dbf
Add: mongoose model name prefix
kmc7468 Feb 13, 2024
df4dfa1
Fix: AdminJS error
kmc7468 Feb 13, 2024
4bc495c
Refactor: update quests
kmc7468 Feb 13, 2024
368a172
Add: creditName, initialCreditAmount field in eventConfig environment…
kmc7468 Feb 13, 2024
681dfd6
Add: group, inviter field into EventStatus model
kmc7468 Feb 13, 2024
65bd8ca
Add: getGroupLeaderboardHandler
kmc7468 Feb 13, 2024
7f5458a
Refactor: getUserTransactionsHandler
kmc7468 Feb 13, 2024
00206a4
Add: completeEventSharingQuest
kmc7468 Feb 13, 2024
2014bdd
Add: validate KAIST student id in global-state/create endpoint
kmc7468 Feb 13, 2024
dac8fbe
Refactor: rename some router paths
kmc7468 Feb 13, 2024
bf8eb9e
Add: groupCreditAmount field in the response of globalState endpoint
kmc7468 Feb 13, 2024
4b77ffe
Fix: start time of the event
kmc7468 Feb 13, 2024
d80b242
Remove: contracts/2023fall.js
kmc7468 Feb 14, 2024
6fc078a
Remove: nullity checks for eventConfig in lottery module
kmc7468 Feb 14, 2024
af8f92f
Refactor: disable unused endpoints in lottery router
kmc7468 Feb 14, 2024
a9e73ac
Fix: groupCreditAmount cannot be zero
kmc7468 Feb 14, 2024
e8c9a9c
Refactor: minor changes
kmc7468 Feb 14, 2024
f1811ae
Add: null checking before using eventConfig
kmc7468 Feb 18, 2024
72bc8a0
Docs: hide ununsed endpoints
kmc7468 Feb 18, 2024
f8cc2d0
Remove: legacy support codes
kmc7468 Feb 18, 2024
ec7060c
Add: mvp related fields in the response of leaderboard api
kmc7468 Feb 18, 2024
1583d5a
Refactor: move creditInfo into eventConfig
kmc7468 Feb 19, 2024
833aade
Refactor: restore index.js
kmc7468 Feb 19, 2024
339f516
Refactor: update MongoDB schema
kmc7468 Feb 19, 2024
f357050
Fix: invalid optional chaining for groupCreditAmount
kmc7468 Feb 19, 2024
8e21e39
Fix: invalid null checkings
kmc7468 Feb 19, 2024
6509a41
Add: eligibility field into the response of globalState api
kmc7468 Feb 19, 2024
a170133
Remove: unnecessary async keyword
kmc7468 Feb 19, 2024
8db1c1c
Refactor: naming convention
kmc7468 Feb 19, 2024
89b6f89
Refactor: apply new quest images
kmc7468 Feb 19, 2024
4b7251a
Merge branch 'dev' into #456-enable-quest-system
kmc7468 Feb 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand Down
7 changes: 6 additions & 1 deletion loadenv.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-23T00:00:00+09:00",
endAt: "2024-03-19T00:00:00+09:00",
}, // optional
kmc7468 marked this conversation as resolved.
Show resolved Hide resolved
};
92 changes: 47 additions & 45 deletions src/lottery/index.js
Original file line number Diff line number Diff line change
@@ -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("/global-state", require("./routes/globalState"));
lotteryRouter.use("/transactions", require("./routes/transactions"));
lotteryRouter.use("/items", require("./routes/items"));
lotteryRouter.use("/public-notice", require("./routes/publicNotice"));
lotteryRouter.use("/quests", require("./routes/quests"));

const itemResource = buildResource([
addOneItemStockAction,
addFiveItemStockAction,
])(itemModel);
const otherResources = [eventStatusModel, questModel, transactionModel].map(
buildResource()
);

const contracts =
eventConfig && require(`./modules/contracts/${eventConfig.mode}`);

module.exports = {
lotteryRouter,
resources: [itemResource, ...otherResources],
contracts,
};
if (eventConfig) {
// [Routes] 기존 docs 라우터의 docs extend
require("./routes/docs")();

const express = require("express");
const lotteryRouter = express.Router();

kmc7468 marked this conversation as resolved.
Show resolved Hide resolved
// [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: [],
};
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,37 @@
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: {
name: "첫 발걸음",
description:
"로그인만 해도 송편을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 송편을 받아보세요.",
"로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_firstLogin.png",
reward: {
kmc7468 marked this conversation as resolved.
Show resolved Hide resolved
ticket1: 1,
},
reward: 50,
},
payingAndSending: {
name: "함께하는 택시의 여정",
description:
"2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 송편을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 <b>+버튼</b>을 눌러 확인할 수 있어요.",
"2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 <b>+버튼</b>을 눌러 확인할 수 있어요.",
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: "첫 방 개설",
Expand All @@ -38,7 +42,7 @@ const quests = buildQuests({
reward: 50,
},
roomSharing: {
name: "Taxi로 모여라",
name: "너 T야? Taxi",
description:
"방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 <b>공유하기</b> 버튼을 찾을 수 있어요.",
imageUrl:
Expand All @@ -53,27 +57,27 @@ 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명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 <b>+버튼</b>을 눌러 확인할 수 있어요.",
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:
"닉네임을 변경하여 자신을 표현하세요. <b>마이페이지</b>의 <b>수정하기</b> 버튼을 눌러 닉네임을 수정할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2023fall/quest_nicknameChanging.png",
reward: 50,
},
accountChanging: {
name: "계좌 등록은 정산의 시작",
name: "계좌 등록을 해야 능률이 올라갑니다",
description:
"정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. <b>마이페이지</b>의 <b>수정하기</b> 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.",
imageUrl:
Expand All @@ -88,26 +92,28 @@ 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:
"추석에 맞춰 쏟아지는 혜택들. 나만 알 순 없죠. 인스타그램 친구들에게 스토리로 공유해보아요. <b>이벤트 안내 페이지</b>에서 <b>인스타그램 스토리에 공유하기</b>을 눌러보세요.",
"내가 초대한 사람이 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:
"이벤트를 열심히 즐긴 당신. 그 상품 획득을 축하 받을 자격이 충분합니다. <b>달토끼 상점</b>에서 상품 구매 후 뜨는 <b>인스타그램 스토리에 공유하기</b> 버튼을 눌러 상품 획득을 공유하세요.",
"내가 초대한 사람이 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,
},
});

const completeQuest = buildCompleteQuest(creditInfo.name);

kmc7468 marked this conversation as resolved.
Show resolved Hide resolved
/**
* firstLogin 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
Expand Down Expand Up @@ -138,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
)
Expand Down Expand Up @@ -177,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
)
Expand Down Expand Up @@ -204,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
)
Expand Down Expand Up @@ -258,7 +267,22 @@ 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 = {
creditInfo,
quests,
completeFirstLoginQuest,
completePayingAndSendingQuest,
Expand All @@ -268,4 +292,5 @@ module.exports = {
completeNicknameChangingQuest,
completeAccountChangingQuest,
completeAdPushAgreementQuest,
completeEventSharingQuest,
};
14 changes: 10 additions & 4 deletions src/lottery/modules/quests.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const buildQuests = (quests) => {

/**
* 퀘스트 완료를 요청합니다.
* @param {string} creditName - 재화의 이름입니다.
kmc7468 marked this conversation as resolved.
Show resolved Hide resolved
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {Object} quest - 퀘스트의 정보입니다.
Expand All @@ -63,25 +64,30 @@ 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 {
// 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다.
const eventStatus = await eventStatusModel.findOne({ userId }).lean();
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`
);
return null;
}

// 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`
);
Expand Down Expand Up @@ -126,7 +132,7 @@ const completeQuest = async (userId, timestamp, quest) => {
amount: quest.reward.credit,
userId,
questId: quest.id,
comment: `"${quest.name}" 퀘스트를 완료해 송편 ${quest.reward.credit}개를 획득했습니다.`,
comment: `"${quest.name}" 퀘스트를 완료해 ${creditName} ${quest.reward.credit}개를 획득했습니다.`,
kmc7468 marked this conversation as resolved.
Show resolved Hide resolved
});
await transaction.save();

Expand Down
Loading
Loading