diff --git a/loadenv.js b/loadenv.js
index cbf47d88..4e589543 100644
--- a/loadenv.js
+++ b/loadenv.js
@@ -43,7 +43,18 @@ 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: "2024fall",
+ credit: {
+ name: "송편코인",
+ initialAmount: 0,
+ },
+ period: {
+ startAt: new Date("2024-09-07T00:00:00+09:00"),
+ endAt: new Date("2024-09-24T00:00:00+09:00"),
+ },
+ }, // optional
naverMap: {
apiId: process.env.NAVER_MAP_API_ID, // optional
apiKey: process.env.NAVER_MAP_API_KEY, //optional
diff --git a/src/lottery/index.js b/src/lottery/index.js
index d485dfe1..a00e3598 100644
--- a/src/lottery/index.js
+++ b/src/lottery/index.js
@@ -28,10 +28,10 @@ lotteryRouter.use(require("../middlewares/originValidator"));
// [Router] APIs
lotteryRouter.use("/globalState", require("./routes/globalState"));
-lotteryRouter.use("/invite", require("./routes/invite"));
+lotteryRouter.use("/invites", require("./routes/invites"));
lotteryRouter.use("/transactions", require("./routes/transactions"));
lotteryRouter.use("/items", require("./routes/items"));
-lotteryRouter.use("/publicNotice", require("./routes/publicNotice"));
+// lotteryRouter.use("/publicNotice", require("./routes/publicNotice"));
lotteryRouter.use("/quests", require("./routes/quests"));
// [AdminJS] AdminJS에 표시할 Resource 생성
diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js
index 72a62deb..a415b0af 100644
--- a/src/lottery/modules/contracts.js
+++ b/src/lottery/modules/contracts.js
@@ -13,19 +13,10 @@ const quests = buildQuests({
firstLogin: {
name: "첫 발걸음",
description:
- "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.",
+ "이벤트 참여만 해도 송편코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 송편코인을 받아 보세요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png",
- reward: 50,
- },
- payingAndSending: {
- name: "함께하는 택시의 여정",
- description:
- "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.",
- imageUrl:
- "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_payingAndSending.png",
- reward: 150,
- maxCount: 0,
+ reward: 200,
},
firstRoomCreation: {
name: "첫 방 개설",
@@ -33,33 +24,33 @@ const quests = buildQuests({
"원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstRoomCreation.png",
- reward: 50,
+ reward: 500,
},
roomSharing: {
- name: "너 T야? Taxi",
+ name: "이 택시팟은 진짜 유명한 택시팟임",
description:
- "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.",
+ "방을 공유해 친구들을 택시팟에 초대해 보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_roomSharing.png",
- reward: 50,
+ reward: 500,
isApiRequired: true,
},
- paying: {
- name: "정산해요 택시의 숲",
+ fareSettlement: {
+ name: "정산의 신, 신팍스",
description:
- "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산하기를 요청해보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.",
+ "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산을 요청해 보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_paying.png",
- reward: 100,
+ reward: 2000,
maxCount: 0,
},
- sending: {
+ farePayment: {
name: "송금 완료면 I am 신뢰에요",
description:
"2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_sending.png",
- reward: 50,
+ reward: 2000,
maxCount: 0,
},
nicknameChanging: {
@@ -68,7 +59,7 @@ const quests = buildQuests({
"닉네임을 변경하여 자신을 표현하세요. 마이페이지의 수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_nicknameChanging.png",
- reward: 50,
+ reward: 500,
},
accountChanging: {
name: "계좌 등록을 해야 능률이 올라갑니다",
@@ -76,7 +67,7 @@ const quests = buildQuests({
"정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지의 수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_accountChanging.png",
- reward: 50,
+ reward: 500,
},
adPushAgreement: {
name: "Taxi의 소울메이트",
@@ -84,25 +75,30 @@ const quests = buildQuests({
"Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_adPushAgreement.png",
- reward: 50,
+ reward: 500,
},
eventSharing: {
- name: "너 나랑 ㅌ태태택 (1명)",
- description:
- "내가 초대한 사람이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!",
+ name: "Taxi를 아십니까",
+ description: "내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png",
- reward: 50,
+ reward: 700,
maxCount: 0,
},
- eventSharing5: {
- name: "너 나랑 ㅌ태태택 (5명)",
+ dailyAttendance: {
+ name: "하루 한 번 Taxi!",
description:
- "내가 초대한 사람이 5명이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!",
- imageUrl:
- "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png",
- reward: 250,
- maxCount: 0,
+ "매일 Taxi에 접속하여 출석 체크를 하면 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요. 송편코인을 얻으려면 출석 체크 페이지에서 출석 버튼을 눌러야 해요.",
+ imageUrl: "",
+ reward: 700,
+ maxCount: 17,
+ isApiRequired: true,
+ },
+ itemPurchase: {
+ name: "itemPurchase",
+ description: "itemPurchase",
+ imageUrl: "",
+ reward: 500,
},
});
@@ -111,40 +107,12 @@ const quests = buildQuests({
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @returns {Promise}
- * @usage lottery/globalState/createUserGlobalStateHandler
+ * @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 - commitSettlementHandler, rooms - commitPaymentHandler
- */
-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 (
- !eventPeriod ||
- 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입니다.
@@ -158,7 +126,7 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => {
};
/**
- * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
+ * fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {Object} roomObject - 방의 정보입니다.
@@ -169,9 +137,9 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => {
* @description 정산 요청이 이루어질 때마다 호출해 주세요.
* @usage rooms - commitSettlementHandler
*/
-const completePayingQuest = async (userId, timestamp, roomObject) => {
+const completeFareSettlementQuest = async (userId, timestamp, roomObject) => {
logger.info(
- `User ${userId} requested to complete payingQuest in Room ${roomObject._id}`
+ `User ${userId} requested to complete fareSettlementQuest in Room ${roomObject._id}`
);
if (roomObject.part.length < 2) return null;
@@ -182,11 +150,11 @@ const completePayingQuest = async (userId, timestamp, roomObject) => {
)
return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다.
- return await completeQuest(userId, timestamp, quests.paying);
+ return await completeQuest(userId, timestamp, quests.fareSettlement);
};
/**
- * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
+ * farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {Object} roomObject - 방의 정보입니다.
@@ -197,9 +165,9 @@ const completePayingQuest = async (userId, timestamp, roomObject) => {
* @description 송금이 이루어질 때마다 호출해 주세요.
* @usage rooms - commitPaymentHandler
*/
-const completeSendingQuest = async (userId, timestamp, roomObject) => {
+const completeFarePaymentQuest = async (userId, timestamp, roomObject) => {
logger.info(
- `User ${userId} requested to complete sendingQuest in Room ${roomObject._id}`
+ `User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}`
);
if (roomObject.part.length < 2) return null;
@@ -210,7 +178,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => {
)
return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다.
- return await completeQuest(userId, timestamp, quests.sending);
+ return await completeQuest(userId, timestamp, quests.farePayment);
};
/**
@@ -241,13 +209,13 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => {
};
/**
- * adPushAgreementQuest 퀘스트의 완료를 요청합니다.
+ * adPushAgreement 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다.
* @returns {Promise}
* @description 알림 옵션을 변경할 때마다 호출해 주세요.
- * @usage notifications/editOptionsHandler
+ * @usage notifications - editOptionsHandler
*/
const completeAdPushAgreementQuest = async (
userId,
@@ -260,38 +228,36 @@ const completeAdPushAgreementQuest = async (
};
/**
- * eventSharing, eventSharing5 퀘스트의 완료를 요청합니다.
+ * eventSharing 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @returns {Promise}
- * @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요.
+ * @usage lottery/globalState - createUserGlobalStateHandler
*/
const completeEventSharingQuest = async (userId, timestamp) => {
- const eventSharingResult = await completeQuest(
- userId,
- timestamp,
- quests.eventSharing
- );
- if (!eventSharingResult || eventSharingResult.questCount % 5 !== 0)
- return [eventSharingResult, null];
+ return await completeQuest(userId, timestamp, quests.eventSharing);
+};
- const eventSharing5Result = await completeQuest(
- userId,
- timestamp,
- quests.eventSharing5
- );
- return [eventSharingResult, eventSharing5Result];
+/**
+ * itemPurchase 퀘스트의 완료를 요청합니다.
+ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
+ * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
+ * @returns {Promise}
+ * @description 상품을 구입할 때마다 호출해 주세요.
+ */
+const completeItemPurchaseQuest = async (userId, timestamp) => {
+ return await completeQuest(userId, timestamp, quests.itemPurchase);
};
module.exports = {
quests,
completeFirstLoginQuest,
- completePayingAndSendingQuest,
completeFirstRoomCreationQuest,
- completePayingQuest,
- completeSendingQuest,
+ completeFareSettlementQuest,
+ completeFarePaymentQuest,
completeNicknameChangingQuest,
completeAccountChangingQuest,
completeAdPushAgreementQuest,
completeEventSharingQuest,
+ completeItemPurchaseQuest,
};
diff --git a/src/lottery/modules/populates/transactions.js b/src/lottery/modules/populates/transactions.js
index 6d965258..a09428c5 100644
--- a/src/lottery/modules/populates/transactions.js
+++ b/src/lottery/modules/populates/transactions.js
@@ -1,8 +1,7 @@
const transactionPopulateOption = [
{
- path: "item",
- select:
- "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType",
+ path: "itemId",
+ select: "name imageUrl",
},
];
diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js
index 04c6cd4c..0d79ac81 100644
--- a/src/lottery/modules/quests.js
+++ b/src/lottery/modules/quests.js
@@ -14,6 +14,7 @@ const eventPeriod = eventConfig && {
};
const requiredQuestFields = ["name", "description", "imageUrl", "reward"];
+
const buildQuests = (quests) => {
for (const [id, quest] of Object.entries(quests)) {
// quest에 필수 필드가 모두 포함되어 있는지 확인합니다.
@@ -61,7 +62,7 @@ const buildQuests = (quests) => {
* @param {number} quest.reward.credit - 퀘스트의 완료 보상 중 재화의 양입니다.
* @param {number} quest.reward.ticket1 - 퀘스트의 완료 보상 중 일반 티켓의 개수입니다.
* @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다.
- * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다.
+ * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다.
*/
const completeQuest = async (userId, timestamp, quest) => {
try {
@@ -118,7 +119,10 @@ const completeQuest = async (userId, timestamp, quest) => {
ticket1Amount: quest.reward.ticket1,
},
$push: {
- completedQuests: quest.id,
+ completedQuests: {
+ questId: quest.id,
+ completedAt: timestamp,
+ },
},
}
);
@@ -143,7 +147,7 @@ const completeQuest = async (userId, timestamp, quest) => {
amount: 0,
userId,
questId: quest.id,
- item: ticket1._id,
+ itemId: ticket1._id,
comment: `"${quest.name}" 퀘스트를 완료해 "${ticket1.name}" ${quest.reward.ticket1}개를 획득했습니다.`,
});
await transaction.save();
diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js
index 600c99ec..09b6c80e 100644
--- a/src/lottery/modules/stores/mongo.js
+++ b/src/lottery/modules/stores/mongo.js
@@ -10,6 +10,17 @@ const integerValidator = {
message: "{VALUE} is not an integer value",
};
+const completedQuestSchema = Schema({
+ questId: {
+ type: String,
+ required: true,
+ },
+ completedAt: {
+ type: Date,
+ required: true,
+ },
+});
+
const eventStatusSchema = Schema({
userId: {
type: Schema.Types.ObjectId,
@@ -17,7 +28,7 @@ const eventStatusSchema = Schema({
required: true,
},
completedQuests: {
- type: [String],
+ type: [completedQuestSchema],
default: [],
},
creditAmount: {
@@ -42,17 +53,11 @@ const eventStatusSchema = Schema({
type: Boolean,
default: false,
},
- group: {
- type: Number,
- required: true,
- min: 1,
- validate: integerValidator,
- }, // 소속된 새터반
inviter: {
type: Schema.Types.ObjectId,
ref: "User",
}, // 이 사용자를 초대한 사용자
- isEnabledInviteUrl: {
+ isInviteUrlEnabled: {
type: Boolean,
default: false,
}, // 초대 링크 활성화 여부
@@ -101,7 +106,13 @@ const itemSchema = Schema({
required: true,
min: 0,
validate: integerValidator,
- },
+ }, // 의미 없는 값, 기존 코드와의 호환성을 위해 남겨둡니다.
+ realStock: {
+ type: Number,
+ required: true,
+ min: 1,
+ validate: integerValidator,
+ }, // 상품의 실제 재고
itemType: {
type: Number,
enum: [0, 1, 2, 3],
@@ -124,13 +135,13 @@ const transactionSchema = Schema({
type: String,
enum: ["get", "use"],
required: true,
- },
+ }, // get: 재화 획득, use: 재화 사용
amount: {
type: Number,
required: true,
min: 0,
validate: integerValidator,
- },
+ }, // 재화의 변화량의 절댓값
userId: {
type: Schema.Types.ObjectId,
ref: "User",
@@ -138,22 +149,23 @@ const transactionSchema = Schema({
},
questId: {
type: String,
- },
- item: {
+ }, // 완료한 퀘스트의 ID
+ itemId: {
type: Schema.Types.ObjectId,
ref: `${modelNamePrefix}Item`,
- },
- itemType: {
+ }, // 획득한 상품의 ID
+ itemAmount: {
type: Number,
- enum: [0, 1, 2, 3],
- },
+ min: 1,
+ validate: integerValidator,
+ }, // 획득한 상품의 개수
comment: {
type: String,
required: true,
},
});
transactionSchema.set("timestamps", {
- createdAt: "createAt",
+ createdAt: "createdAt",
updatedAt: false,
});
diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js
index 4af3493e..1bbf23f4 100644
--- a/src/lottery/routes/docs/globalState.js
+++ b/src/lottery/routes/docs/globalState.js
@@ -5,24 +5,21 @@ const globalStateDocs = {};
globalStateDocs[`${apiPrefix}/`] = {
get: {
tags: [`${apiPrefix}`],
- summary: "Frontend에서 Global state로 관리하는 정보 반환",
+ summary: "Frontend에서 Global State로 관리하는 정보 반환",
description:
- "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다.",
+ "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리하는 정보를 가져옵니다.",
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
type: "object",
required: [
"isAgreeOnTermsOfEvent",
- "isEligible",
+ "isBanned",
"creditAmount",
- "groupCreditAmount",
- "completedQuests",
- "group",
"quests",
+ "completedQuests",
],
properties: {
isAgreeOnTermsOfEvent: {
@@ -30,44 +27,19 @@ globalStateDocs[`${apiPrefix}/`] = {
description: "유저의 이벤트 참여 동의 여부",
example: true,
},
- isEligible: {
- type: "boolean",
- description: "유저의 이벤트 참여 가능 여부",
- example: true,
- },
- creditAmount: {
- type: "number",
- description: "재화 개수. 0 이상입니다.",
- example: 1000,
- },
- groupCreditAmount: {
- type: "number",
- description: "소속 새터반에 소속된 유저의 전체 재화 개수",
- example: 35000,
- },
- completedQuests: {
- type: "array",
- description:
- "유저가 완료한 퀘스트의 배열. 여러 번 완료할 수 있는 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.",
- items: {
- type: "string",
- description: "Quest의 Id",
- example: "QUEST ID",
- },
- },
isBanned: {
type: "boolean",
- description: "해당 유저 제재 대상 여부",
+ description: "유저의 이벤트 참여 제한 여부",
example: false,
},
- group: {
+ creditAmount: {
type: "number",
- description: "유저의 소속 새터반",
- example: 16,
+ description: "유저의 재화 개수. 0 이상의 정수입니다.",
+ example: 1000,
},
quests: {
type: "array",
- description: "Quest의 배열",
+ description: "전체 퀘스트의 배열",
items: {
type: "object",
required: [
@@ -82,7 +54,7 @@ globalStateDocs[`${apiPrefix}/`] = {
properties: {
id: {
type: "string",
- description: "Quest의 Id",
+ description: "퀘스트의 Id",
example: "QUEST ID",
},
name: {
@@ -98,34 +70,54 @@ globalStateDocs[`${apiPrefix}/`] = {
},
imageUrl: {
type: "string",
- description: "이미지 썸네일 URL",
+ description: "퀘스트의 썸네일 이미지 URL",
example: "THUMBNAIL URL",
},
reward: {
type: "object",
- description: "완료 보상",
required: ["credit"],
properties: {
credit: {
type: "number",
- description: "완료 보상 중 재화의 개수입니다.",
+ description: "퀘스트의 완료 보상 중 재화의 개수",
example: 100,
},
},
},
maxCount: {
type: "number",
- description: "최대 완료 가능 횟수",
+ description: "퀘스트의 최대 완료 가능 횟수",
example: 1,
},
isApiRequired: {
type: "boolean",
- description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청할 수 있는지 여부`,
+ description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청해야 하는지의 여부`,
example: false,
},
},
},
},
+ completedQuests: {
+ type: "array",
+ description:
+ "유저가 완료한 퀘스트의 배열. 여러 번 완료한 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.",
+ items: {
+ type: "object",
+ required: ["id", "completedAt"],
+ properties: {
+ id: {
+ type: "string",
+ description: "퀘스트의 Id",
+ example: "QUEST ID",
+ },
+ completedAt: {
+ type: "string",
+ description: "퀘스트의 완료 시각",
+ example: "2023-01-01 00:00:00",
+ },
+ },
+ },
+ },
},
},
},
@@ -137,11 +129,10 @@ globalStateDocs[`${apiPrefix}/`] = {
globalStateDocs[`${apiPrefix}/create`] = {
post: {
tags: [`${apiPrefix}`],
- summary: "Frontend에서 Global state로 관리하는 정보 생성",
+ summary: "Frontend에서 Global State로 관리할 정보 생성",
description:
- "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 생성합니다.",
+ "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리할 정보를 생성합니다.",
requestBody: {
- description: "",
content: {
"application/json": {
schema: {
@@ -152,7 +143,6 @@ globalStateDocs[`${apiPrefix}/create`] = {
},
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
diff --git a/src/lottery/routes/docs/invite.js b/src/lottery/routes/docs/invites.js
similarity index 60%
rename from src/lottery/routes/docs/invite.js
rename to src/lottery/routes/docs/invites.js
index 3a3972da..cfe37214 100644
--- a/src/lottery/routes/docs/invite.js
+++ b/src/lottery/routes/docs/invites.js
@@ -1,25 +1,23 @@
const { eventConfig } = require("../../../../loadenv");
-const apiPrefix = `/events/${eventConfig?.mode}/invite`;
+const apiPrefix = `/events/${eventConfig?.mode}/invites`;
-const inviteDocs = {};
-inviteDocs[`${apiPrefix}/search/:inviter`] = {
+const invitesDocs = {};
+invitesDocs[`${apiPrefix}/search/{inviter}`] = {
get: {
tags: [`${apiPrefix}`],
- summary: "초대자 정보 조회",
- description: "초대자의 정보를 조회합니다.",
- requestBody: {
- description: "",
- content: {
- "application/json": {
- schema: {
- $ref: "#/components/schemas/searchInviterHandler",
- },
- },
+ summary: "초대한 유저의 정보 반환",
+ description: "초대한 유저의 정보를 가져옵니다.",
+ parameters: [
+ {
+ in: "path",
+ name: "inviter",
+ required: true,
+ description: "초대한 유저의 eventStatus ObjectId",
+ example: "INVITER ID",
},
- },
+ ],
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
@@ -28,13 +26,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = {
properties: {
nickname: {
type: "string",
- description: "초대자의 닉네임",
- example: "asdf",
+ description: "초대한 유저의 닉네임",
+ example: "static",
},
profileImageUrl: {
type: "string",
- description: "초대자의 프로필 이미지 URL",
- example: "IMAGE URL",
+ description: "초대한 유저의 프로필 이미지 URL",
+ example: "PROFILE URL",
},
},
},
@@ -44,14 +42,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = {
},
},
};
-inviteDocs[`${apiPrefix}/create`] = {
+invitesDocs[`${apiPrefix}/create`] = {
post: {
tags: [`${apiPrefix}`],
summary: "초대 링크 생성",
description: "초대 링크를 생성합니다.",
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
@@ -72,4 +69,4 @@ inviteDocs[`${apiPrefix}/create`] = {
},
};
-module.exports = inviteDocs;
+module.exports = invitesDocs;
diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js
index b08aeaf7..063e5f45 100644
--- a/src/lottery/routes/docs/items.js
+++ b/src/lottery/routes/docs/items.js
@@ -2,15 +2,13 @@ const { eventConfig } = require("../../../../loadenv");
const apiPrefix = `/events/${eventConfig?.mode}/items`;
const itemsDocs = {};
-itemsDocs[`${apiPrefix}/list`] = {
+itemsDocs[`${apiPrefix}/`] = {
get: {
tags: [`${apiPrefix}`],
- summary: "상점에서 판매하는 모든 상품의 목록 반환",
- description:
- "상점에서 판매하는 모든 상품의 목록을 가져옵니다. 매진된 상품도 가져옵니다.",
+ summary: "상점에서 판매하는 상품의 목록 반환",
+ description: "상점에서 판매하는 상품의 목록을 가져옵니다.",
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
@@ -19,9 +17,61 @@ itemsDocs[`${apiPrefix}/list`] = {
properties: {
items: {
type: "array",
- description: "Item의 배열",
+ description: "상품의 배열",
items: {
- $ref: "#/components/schemas/item",
+ type: "object",
+ required: [
+ "_id",
+ "name",
+ "description",
+ "imageUrl",
+ "price",
+ "isDisabled",
+ "itemType",
+ ],
+ properties: {
+ _id: {
+ type: "string",
+ description: "상품의 ObjectId",
+ example: "ITEM ID",
+ },
+ name: {
+ type: "string",
+ description: "상품의 이름",
+ example: "진짜 송편",
+ },
+ description: {
+ type: "string",
+ description: "상품의 설명",
+ example: "먹을 수 있는 송편입니다.",
+ },
+ imageUrl: {
+ type: "string",
+ description: "상품의 썸네일 이미지 URL",
+ example: "THUMBNAIL URL",
+ },
+ instagramStoryStickerImageUrl: {
+ type: "string",
+ description: "인스타그램 스토리 스티커 이미지 URL",
+ example: "STICKER URL",
+ },
+ price: {
+ type: "number",
+ description: "상품의 가격. 0 이상의 정수입니다.",
+ example: 400,
+ },
+ isDisabled: {
+ type: "boolean",
+ description: "상품의 판매 중지 여부",
+ example: false,
+ },
+ itemType: {
+ type: "number",
+ description:
+ "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.",
+ example: 0,
+ },
+ },
},
},
},
@@ -32,56 +82,229 @@ itemsDocs[`${apiPrefix}/list`] = {
},
},
};
-itemsDocs[`${apiPrefix}/purchase/:itemId`] = {
- post: {
+itemsDocs[`${apiPrefix}/{itemId}`] = {
+ get: {
tags: [`${apiPrefix}`],
- summary: "상품 구매",
- description: "상품을 구매합니다.",
- requestBody: {
- description: "",
- content: {
- "application/json": {
- schema: {
- $ref: "#/components/schemas/purchaseHandler",
+ summary: "상점에서 판매하는 특정 상품의 정보 반환",
+ description: "상점에서 판매하는 특정 상품의 정보를 가져옵니다.",
+ parameters: [
+ {
+ in: "path",
+ name: "itemId",
+ required: true,
+ description: "상품 정보를 조회할 ObjectId",
+ example: "ITEM ID",
+ },
+ ],
+ responses: {
+ 200: {
+ content: {
+ "application/json": {
+ schema: {
+ type: "object",
+ required: ["item"],
+ properties: {
+ item: {
+ type: "object",
+ required: [
+ "_id",
+ "name",
+ "description",
+ "imageUrl",
+ "price",
+ "isDisabled",
+ "itemType",
+ ],
+ description: "상품의 정보",
+ properties: {
+ _id: {
+ type: "string",
+ description: "상품의 ObjectId",
+ example: "ITEM ID",
+ },
+ name: {
+ type: "string",
+ description: "상품의 이름",
+ example: "진짜 송편",
+ },
+ description: {
+ type: "string",
+ description: "상품의 설명",
+ example: "먹을 수 있는 송편입니다.",
+ },
+ imageUrl: {
+ type: "string",
+ description: "상품의 썸네일 이미지 URL",
+ example: "THUMBNAIL URL",
+ },
+ instagramStoryStickerImageUrl: {
+ type: "string",
+ description: "인스타그램 스토리 스티커 이미지 URL",
+ example: "STICKER URL",
+ },
+ price: {
+ type: "number",
+ description: "상품의 가격. 0 이상의 정수입니다.",
+ example: 400,
+ },
+ isDisabled: {
+ type: "boolean",
+ description: "상품의 판매 중지 여부",
+ example: false,
+ },
+ itemType: {
+ type: "number",
+ description:
+ "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.",
+ example: 0,
+ },
+ },
+ },
+ },
+ },
},
},
},
},
+ },
+};
+itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = {
+ get: {
+ tags: [`${apiPrefix}`],
+ summary: "상품 리더보드 반환",
+ description: "상품 리더보드를 가져옵니다. 일반 상품만 리더보드를 갖습니다.",
+ parameters: [
+ {
+ in: "path",
+ name: "itemId",
+ required: true,
+ description: "리더보드를 조회할 상품의 ObjectId",
+ example: "ITEM ID",
+ },
+ ],
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
type: "object",
- required: ["result"],
+ required: ["leaderboard", "totalAmount", "totalUser"],
properties: {
- result: {
- type: "boolean",
- description: "성공 여부. 항상 true입니다.",
- example: true,
+ leaderboard: {
+ type: "array",
+ description: "상품 리더보드. 상위 20등까지만 반환됩니다.",
+ items: {
+ type: "object",
+ required: [
+ "nickname",
+ "profileImageUrl",
+ "amount",
+ "probability",
+ "rank",
+ ],
+ properties: {
+ nickname: {
+ type: "string",
+ description: "유저의 닉네임",
+ example: "static",
+ },
+ profileImageUrl: {
+ type: "string",
+ description: "유저의 프로필 이미지 URL",
+ example: "PROFILE URL",
+ },
+ amount: {
+ type: "number",
+ description: "유저가 상품을 구입한 횟수",
+ example: 3,
+ },
+ probability: {
+ type: "number",
+ description: "유저가 상품에 당첨될 확률",
+ example: 0.1,
+ },
+ rank: {
+ type: "number",
+ description: "순위",
+ example: 1,
+ },
+ },
+ },
+ },
+ totalAmount: {
+ type: "number",
+ description: "상품의 총 판매량",
+ example: 100,
+ },
+ totalUser: {
+ type: "number",
+ description: "상품을 구입한 유저의 수",
+ example: 50,
+ },
+ rank: {
+ type: "number",
+ description: "현재 유저의 리더보드 순위. 1부터 시작합니다.",
+ example: 1,
},
- reward: {
- $ref: "#/components/schemas/rewardItem",
+ amount: {
+ type: "number",
+ description: "현재 유저가 상품을 구입한 횟수",
+ example: 3,
+ },
+ probability: {
+ type: "number",
+ description: "현재 유저가 상품에 당첨될 확률",
+ example: 0.1,
},
},
},
},
},
},
- 400: {
- description:
- "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.",
+ },
+ },
+};
+itemsDocs[`${apiPrefix}/purchase/{itemId}`] = {
+ post: {
+ tags: [`${apiPrefix}`],
+ summary: "상품 구입",
+ description: "상품을 구입합니다.",
+ parameters: [
+ {
+ in: "path",
+ name: "itemId",
+ required: true,
+ description: "리더보드를 조회할 상품의 ObjectId",
+ example: "ITEM ID",
+ },
+ ],
+ requestBody: {
+ content: {
+ "application/json": {
+ schema: {
+ $ref: "#/components/schemas/purchaseItemHandlerBody",
+ },
+ },
+ },
+ },
+ responses: {
+ 200: {
content: {
"application/json": {
schema: {
type: "object",
- required: ["error"],
+ required: ["result"],
properties: {
- error: {
- type: "string",
- description: "",
- example: "checkBanned: banned user",
+ result: {
+ type: "boolean",
+ description: "성공 여부. 항상 true입니다.",
+ example: true,
+ },
+ isJackpot: {
+ type: "boolean",
+ description:
+ "대박 여부. 랜덤박스를 구입한 경우에만 포함됩니다.",
+ example: true,
},
},
},
diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js
index 23a410b2..bcf2cc78 100644
--- a/src/lottery/routes/docs/publicNotice.js
+++ b/src/lottery/routes/docs/publicNotice.js
@@ -2,7 +2,7 @@ const { eventConfig } = require("../../../../loadenv");
const apiPrefix = `/events/${eventConfig?.mode}/publicNotice`;
const publicNoticeDocs = {};
-// 다음 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다.
+// 다음 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다.
//
// publicNoticeDocs[`${apiPrefix}/recentTransactions`] = {
// get: {
@@ -35,75 +35,75 @@ const publicNoticeDocs = {};
// },
// },
// };
-publicNoticeDocs[`${apiPrefix}/leaderboard`] = {
- get: {
- tags: [`${apiPrefix}`],
- summary: "리더보드 반환",
- description:
- "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.",
- responses: {
- 200: {
- description: "",
- content: {
- "application/json": {
- schema: {
- type: "object",
- required: ["leaderboard"],
- properties: {
- leaderboard: {
- type: "array",
- description: "이벤트에 참여한 새터반 전체가 포함된 리더보드",
- items: {
- type: "object",
- required: [
- "group",
- "creditAmount",
- "mvpNickname",
- "mvpProfileImageUrl",
- ],
- properties: {
- group: {
- type: "number",
- description: "새터반",
- example: 16,
- },
- creditAmount: {
- type: "number",
- description: "새터반에 소속된 유저의 전체 재화 개수",
- example: 3000,
- },
- mvpNickname: {
- type: "string",
- description:
- "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임",
- example: "asdf",
- },
- mvpProfileImageUrl: {
- type: "string",
- description: "MVP의 프로필 이미지 URL",
- example: "IMAGE URL",
- },
- },
- },
- },
- group: {
- type: "number",
- description: "유저의 소속 새터반",
- example: 16,
- },
- rank: {
- type: "number",
- description:
- "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.",
- example: 1,
- },
- },
- },
- },
- },
- },
- },
- },
-};
+// publicNoticeDocs[`${apiPrefix}/leaderboard`] = {
+// get: {
+// tags: [`${apiPrefix}`],
+// summary: "리더보드 반환",
+// description:
+// "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.",
+// responses: {
+// 200: {
+// description: "",
+// content: {
+// "application/json": {
+// schema: {
+// type: "object",
+// required: ["leaderboard"],
+// properties: {
+// leaderboard: {
+// type: "array",
+// description: "이벤트에 참여한 새터반 전체가 포함된 리더보드",
+// items: {
+// type: "object",
+// required: [
+// "group",
+// "creditAmount",
+// "mvpNickname",
+// "mvpProfileImageUrl",
+// ],
+// properties: {
+// group: {
+// type: "number",
+// description: "새터반",
+// example: 16,
+// },
+// creditAmount: {
+// type: "number",
+// description: "새터반에 소속된 유저의 전체 재화 개수",
+// example: 3000,
+// },
+// mvpNickname: {
+// type: "string",
+// description:
+// "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임",
+// example: "asdf",
+// },
+// mvpProfileImageUrl: {
+// type: "string",
+// description: "MVP의 프로필 이미지 URL",
+// example: "IMAGE URL",
+// },
+// },
+// },
+// },
+// group: {
+// type: "number",
+// description: "유저의 소속 새터반",
+// example: 16,
+// },
+// rank: {
+// type: "number",
+// description:
+// "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.",
+// example: 1,
+// },
+// },
+// },
+// },
+// },
+// },
+// },
+// },
+// };
module.exports = publicNoticeDocs;
diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js
index 14694f3e..42a9c022 100644
--- a/src/lottery/routes/docs/quests.js
+++ b/src/lottery/routes/docs/quests.js
@@ -2,24 +2,22 @@ const { eventConfig } = require("../../../../loadenv");
const apiPrefix = `/events/${eventConfig?.mode}/quests`;
const questsDocs = {};
-questsDocs[`${apiPrefix}/complete/:questId`] = {
+questsDocs[`${apiPrefix}/complete/{questId}`] = {
post: {
tags: [`${apiPrefix}`],
summary: "퀘스트 완료 요청",
description: "퀘스트의 완료를 요청합니다.",
- requestBody: {
- description: "",
- content: {
- "application/json": {
- schema: {
- $ref: "#/components/schemas/completeHandler",
- },
- },
+ parameters: [
+ {
+ in: "path",
+ name: "questId",
+ required: true,
+ description: "완료를 요청할 퀘스트의 ID",
+ example: "QUEST ID",
},
- },
+ ],
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
@@ -36,25 +34,6 @@ questsDocs[`${apiPrefix}/complete/:questId`] = {
},
},
},
- 400: {
- description:
- "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.",
- content: {
- "application/json": {
- schema: {
- type: "object",
- required: ["error"],
- properties: {
- error: {
- type: "string",
- description: "",
- example: "checkBanned: banned user",
- },
- },
- },
- },
- },
- },
},
},
};
diff --git a/src/lottery/routes/docs/schemas/globalStateSchema.js b/src/lottery/routes/docs/schemas/globalStateSchema.js
index 0c3c55e6..15055525 100644
--- a/src/lottery/routes/docs/schemas/globalStateSchema.js
+++ b/src/lottery/routes/docs/schemas/globalStateSchema.js
@@ -6,7 +6,6 @@ const globalStateZod = {
createUserGlobalStateHandler: z
.object({
phoneNumber: z.string().regex(user.phoneNumber),
- group: z.number().gte(1).lte(26),
inviter: z.string().regex(objectId),
})
.partial({ inviter: true }),
diff --git a/src/lottery/routes/docs/schemas/inviteSchema.js b/src/lottery/routes/docs/schemas/invitesSchema.js
similarity index 67%
rename from src/lottery/routes/docs/schemas/inviteSchema.js
rename to src/lottery/routes/docs/schemas/invitesSchema.js
index e3016557..dfc33c5c 100644
--- a/src/lottery/routes/docs/schemas/inviteSchema.js
+++ b/src/lottery/routes/docs/schemas/invitesSchema.js
@@ -2,12 +2,12 @@ const { z } = require("zod");
const { zodToSchemaObject } = require("../../../../routes/docs/utils");
const { objectId } = require("../../../../modules/patterns");
-const inviteZod = {
+const invitesZod = {
searchInviterHandler: z.object({
inviter: z.string().regex(objectId),
}),
};
-const inviteSchema = zodToSchemaObject(inviteZod);
+const invitesSchema = zodToSchemaObject(invitesZod);
-module.exports = { inviteSchema, inviteZod };
+module.exports = { invitesZod, invitesSchema };
diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js
index 80912cfb..d224ba70 100644
--- a/src/lottery/routes/docs/schemas/itemsSchema.js
+++ b/src/lottery/routes/docs/schemas/itemsSchema.js
@@ -1,98 +1,22 @@
-/* Item에 대한 기본적인 프로퍼티를 갖고 있는 스키마입니다.
- * TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다.
- */
-const itemBase = {
- type: "object",
- required: [
- "_id",
- "name",
- "imageUrl",
- "price",
- "description",
- "isDisabled",
- "stock",
- ],
- properties: {
- _id: {
- type: "string",
- description: "Item의 ObjectId",
- example: "OBJECT ID",
- },
- name: {
- type: "string",
- description: "상품의 이름",
- example: "진짜송편",
- },
- imageUrl: {
- type: "string",
- description: "이미지 썸네일 URL",
- example: "THUMBNAIL URL",
- },
- instagramStoryStickerImageUrl: {
- type: "string",
- description: "인스타그램 스토리 스티커 이미지 URL",
- example: "STICKER URL",
- },
- price: {
- type: "number",
- description: "상품의 가격. 0 이상입니다.",
- example: 400,
- },
- description: {
- type: "string",
- description: "상품의 설명",
- example: "맛있는 송편입니다.",
- },
- isDisabled: {
- type: "boolean",
- description: "판매 중지 여부",
- example: false,
- },
- stock: {
- type: "number",
- description: "남은 상품 재고. 재고가 있는 경우 1, 없는 경우 0입니다.",
- example: 1,
- },
- },
-};
+const { z } = require("zod");
+const { zodToSchemaObject } = require("../../../../routes/docs/utils");
+const { objectId } = require("../../../../modules/patterns");
-/** itemBase에 itemType(상품 유형) 프로퍼티가 추가된 스키마입니다. */
-const itemWithType = {
- type: itemBase.type,
- required: itemBase.required.concat(["itemType"]),
- properties: {
- ...itemBase.properties,
- itemType: {
- type: "number",
- description:
- "상품 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.",
- example: 0,
- },
- },
+const itemsZod = {
+ getItemHandler: z.object({
+ itemId: z.string().regex(objectId),
+ }),
+ getItemLeaderboardHandler: z.object({
+ itemId: z.string().regex(objectId),
+ }),
+ purchaseItemHandlerParams: z.object({
+ itemId: z.string().regex(objectId),
+ }),
+ purchaseItemHandlerBody: z.object({
+ amount: z.number().int().positive(),
+ }),
};
-const itemsSchema = {
- item: itemWithType,
- relatedItem: {
- ...itemWithType,
- description:
- "Transaction과 관련된 아이템의 Object. 아이템과 관련된 Transaction인 경우에만 포함됩니다.",
- },
- rewardItem: {
- ...itemBase,
- description: "랜덤박스를 구입한 경우에만 포함됩니다.",
- },
- purchaseHandler: {
- type: "object",
- required: ["itemId"],
- properties: {
- itemId: {
- type: "string",
- pattern: "^[a-fA-F\\d]{24}$",
- },
- },
- errorMessage: "validation: bad request",
- },
-};
+const itemsSchema = zodToSchemaObject(itemsZod);
-module.exports = itemsSchema;
+module.exports = { itemsZod, itemsSchema };
diff --git a/src/lottery/routes/docs/schemas/questsSchema.js b/src/lottery/routes/docs/schemas/questsSchema.js
index 2efd11cd..8daf560d 100644
--- a/src/lottery/routes/docs/schemas/questsSchema.js
+++ b/src/lottery/routes/docs/schemas/questsSchema.js
@@ -2,7 +2,9 @@ const { z } = require("zod");
const { zodToSchemaObject } = require("../../../../routes/docs/utils");
const questsZod = {
- completeHandler: z.object({ questId: z.enum(["roomSharing"]) }),
+ completeQuestHandler: z.object({
+ questId: z.enum(["roomSharing", "dailyAttendance"]),
+ }),
};
const questsSchema = zodToSchemaObject(questsZod);
diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js
index 38ca8298..0b6702da 100644
--- a/src/lottery/routes/docs/swaggerDocs.js
+++ b/src/lottery/routes/docs/swaggerDocs.js
@@ -1,13 +1,13 @@
const globalStateDocs = require("./globalState");
-const inviteDocs = require("./invite");
+const invitesDocs = require("./invites");
const itemsDocs = require("./items");
const publicNoticeDocs = require("./publicNotice");
const questsDocs = require("./quests");
const transactionsDocs = require("./transactions");
const { globalStateSchema } = require("./schemas/globalStateSchema");
-const { inviteSchema } = require("./schemas/inviteSchema");
-const itemsSchema = require("./schemas/itemsSchema");
+const { invitesSchema } = require("./schemas/invitesSchema");
+const { itemsSchema } = require("./schemas/itemsSchema");
const { questsSchema } = require("./schemas/questsSchema");
const { eventConfig } = require("../../../../loadenv");
@@ -20,41 +20,39 @@ const eventSwaggerDocs = {
description: "이벤트 - Global State 관련 API",
},
{
- name: `${apiPrefix}/invite`,
+ name: `${apiPrefix}/invites`,
description: "이벤트 - 초대 링크 관련 API",
},
- // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다.
- //
- // {
- // name: `${apiPrefix}/items`,
- // description: "이벤트 - 아이템 관련 API",
- // },
{
- name: `${apiPrefix}/publicNotice`,
- description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API",
+ name: `${apiPrefix}/items`,
+ description: "이벤트 - 상품 관련 API",
},
+ // {
+ // name: `${apiPrefix}/publicNotice`,
+ // description: "이벤트 - 상품 구매, 뽑기, 획득 공지 관련 API",
+ // },
{
name: `${apiPrefix}/quests`,
description: "이벤트 - 퀘스트 관련 API",
},
{
name: `${apiPrefix}/transactions`,
- description: "이벤트 - 입출금 내역 관련 API",
+ description: "이벤트 - 재화 입출금 내역 관련 API",
},
],
paths: {
...globalStateDocs,
- ...inviteDocs,
- //...itemsDocs,
- ...publicNoticeDocs,
+ ...invitesDocs,
+ ...itemsDocs,
+ // ...publicNoticeDocs,
...questsDocs,
...transactionsDocs,
},
components: {
schemas: {
...globalStateSchema,
- ...inviteSchema,
- //...itemsSchema,
+ ...invitesSchema,
+ ...itemsSchema,
...questsSchema,
},
},
diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js
index fa78238b..a041b949 100644
--- a/src/lottery/routes/docs/transactions.js
+++ b/src/lottery/routes/docs/transactions.js
@@ -6,10 +6,9 @@ transactionsDocs[`${apiPrefix}/`] = {
get: {
tags: [`${apiPrefix}`],
summary: "재화 입출금 내역 반환",
- description: "유저의 재화 입출금 내역을 가져옵니다.",
+ description: "재화 입출금 내역을 가져옵니다.",
responses: {
200: {
- description: "",
content: {
"application/json": {
schema: {
@@ -18,16 +17,11 @@ transactionsDocs[`${apiPrefix}/`] = {
properties: {
transactions: {
type: "array",
- description: "유저의 재화 입출금 기록의 배열",
+ description: "유저의 재화 입출금 내역의 배열",
items: {
type: "object",
- required: ["_id", "type", "amount", "comment", "createAt"],
+ required: ["type", "amount", "comment", "createdAt"],
properties: {
- _id: {
- type: "string",
- description: "Transaction의 ObjectId",
- example: "OBJECT ID",
- },
type: {
type: "string",
description:
@@ -41,18 +35,33 @@ transactionsDocs[`${apiPrefix}/`] = {
},
questId: {
type: "string",
- description:
- "Transaction과 관련된 퀘스트의 Id. 퀘스트와 관련된 Transaction인 경우에만 포함됩니다.",
+ description: "입출금 내역과 관련된 퀘스트의 Id",
example: "QUEST ID",
},
+ item: {
+ type: "object",
+ required: ["name", "imageUrl"],
+ properties: {
+ name: {
+ type: "string",
+ description: "상품의 이름",
+ example: "랜덤 상자",
+ },
+ imageUrl: {
+ type: "string",
+ description: "상품의 썸네일 이미지 URL",
+ example: "IMAGE URL",
+ },
+ },
+ },
comment: {
type: "string",
description: "입출금 내역에 대한 설명",
example: "랜덤 상자 구입 - 50개 차감",
},
- createAt: {
+ createdAt: {
type: "string",
- description: "입출금이 일어난 시각",
+ description: "입출금 내역이 생성된 시각",
example: "2023-01-01 00:00:00",
},
},
diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js
index 1f2b4327..c4f37d39 100644
--- a/src/lottery/routes/globalState.js
+++ b/src/lottery/routes/globalState.js
@@ -1,7 +1,8 @@
const express = require("express");
+const router = express.Router();
+
const { validateBody } = require("../../middlewares/zod");
const { globalStateZod } = require("./docs/schemas/globalStateSchema");
-const router = express.Router();
const globalStateHandlers = require("../services/globalState");
router.get("/", globalStateHandlers.getUserGlobalStateHandler);
diff --git a/src/lottery/routes/invite.js b/src/lottery/routes/invite.js
deleted file mode 100644
index eafa09cb..00000000
--- a/src/lottery/routes/invite.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const express = require("express");
-const { validateParams } = require("../../middlewares/zod");
-const { inviteZod } = require("./docs/schemas/inviteSchema");
-const router = express.Router();
-const inviteHandlers = require("../services/invite");
-
-router.get(
- "/search/:inviter",
- validateParams(inviteZod.searchInviterHandler),
- inviteHandlers.searchInviterHandler
-);
-
-// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요
-router.use(require("../../middlewares/auth"));
-router.use(require("../middlewares/checkBanned"));
-router.use(require("../middlewares/timestampValidator"));
-
-router.post("/create", inviteHandlers.createInviteUrlHandler);
-
-module.exports = router;
diff --git a/src/lottery/routes/invites.js b/src/lottery/routes/invites.js
new file mode 100644
index 00000000..65e4271e
--- /dev/null
+++ b/src/lottery/routes/invites.js
@@ -0,0 +1,21 @@
+const express = require("express");
+const router = express.Router();
+
+const { validateParams } = require("../../middlewares/zod");
+const { invitesZod } = require("./docs/schemas/invitesSchema");
+const invitesHandlers = require("../services/invites");
+
+router.get(
+ "/search/:inviter",
+ validateParams(invitesZod.searchInviterHandler),
+ invitesHandlers.searchInviterHandler
+);
+
+// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요
+router.use(require("../../middlewares/auth"));
+router.use(require("../middlewares/checkBanned"));
+router.use(require("../middlewares/timestampValidator"));
+
+router.post("/create", invitesHandlers.createInviteUrlHandler);
+
+module.exports = router;
diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js
index 5cdf98a8..0de8be18 100644
--- a/src/lottery/routes/items.js
+++ b/src/lottery/routes/items.js
@@ -1,23 +1,32 @@
const express = require("express");
-
const router = express.Router();
-// TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다.
+
+const { validateBody, validateParams } = require("../../middlewares/zod");
+const { itemsZod } = require("./docs/schemas/itemsSchema");
const itemsHandlers = require("../services/items");
-const itemsSchema = require("./docs/schemas/itemsSchema");
-// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다.
-//
-// router.get("/list", itemsHandlers.listHandler);
+router.get("/", itemsHandlers.getItemsHandler);
+router.get(
+ "/:itemId",
+ validateParams(itemsZod.getItemHandler),
+ itemsHandlers.getItemHandler
+);
+router.get(
+ "/leaderboard/:itemId",
+ validateParams(itemsZod.getItemLeaderboardHandler),
+ itemsHandlers.getItemLeaderboardHandler
+);
-// // 아래의 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(itemsZod.purchaseItemHandlerParams),
+ validateBody(itemsZod.purchaseItemHandlerBody),
+ itemsHandlers.purchaseItemHandler
+);
module.exports = router;
diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js
index 4698a193..f6646061 100644
--- a/src/lottery/routes/publicNotice.js
+++ b/src/lottery/routes/publicNotice.js
@@ -3,10 +3,10 @@ const express = require("express");
const router = express.Router();
const publicNoticeHandlers = require("../services/publicNotice");
-router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler);
-
-// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다.
+// 아래의 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다.
//
+// router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler);
+
// router.get(
// "/recentTransactions",
// publicNoticeHandlers.getRecentPurchaceItemListHandler
diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js
index 4941c8d2..e9845434 100644
--- a/src/lottery/routes/quests.js
+++ b/src/lottery/routes/quests.js
@@ -1,18 +1,19 @@
const express = require("express");
+const router = express.Router();
+
const { validateParams } = require("../../middlewares/zod");
const { questsZod } = require("./docs/schemas/questsSchema");
-const router = express.Router();
const questsHandlers = require("../services/quests");
-// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요
+// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요
router.use(require("../../middlewares/auth"));
router.use(require("../middlewares/checkBanned"));
router.use(require("../middlewares/timestampValidator"));
router.post(
"/complete/:questId",
- validateParams(questsZod.completeHandler),
- questsHandlers.completeHandler
+ validateParams(questsZod.completeQuestHandler),
+ questsHandlers.completeQuestHandler
);
module.exports = router;
diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js
index aee05d90..f9e375ca 100644
--- a/src/lottery/routes/transactions.js
+++ b/src/lottery/routes/transactions.js
@@ -1,6 +1,6 @@
const express = require("express");
-
const router = express.Router();
+
const transactionsHandlers = require("../services/transactions");
// 아래의 Endpoint 접근 시 로그인 필요
diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js
index 5459f851..ee01f47e 100644
--- a/src/lottery/services/globalState.js
+++ b/src/lottery/services/globalState.js
@@ -8,61 +8,57 @@ const { eventConfig } = require("../../../loadenv");
const contracts = require("../modules/contracts");
const quests = Object.values(contracts.quests);
-// 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다.
-const checkIsUserEligible = (user) => {
- // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다.
- if (nodeEnv !== "production") return true;
+// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다.
+//
+// // 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다.
+// const checkIsUserEligible = (user) => {
+// // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다.
+// if (nodeEnv !== "production") return true;
- const kaistId = parseInt(user?.subinfo?.kaist || "0");
- return 20240001 <= kaistId && kaistId <= 20241500;
-};
+// 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
- .findOne({ userId }, "completedQuests creditAmount isBanned group")
+ .findOne({ userId }, "completedQuests creditAmount isBanned")
.lean());
if (!eventStatus)
return res.json({
isAgreeOnTermsOfEvent: false,
- isEligible: checkIsUserEligible(user) || !!user?.isAdmin, // 테스트를 위해 관리자인 경우 true로 설정합니다. 하지만 관리자이더라도 이벤트에 참여할 수 없습니다.
- completedQuests: [],
+ isBanned: false,
creditAmount: 0,
- group: 0,
- groupCreditAmount: 0,
quests,
+ completedQuests: [],
});
// 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 && groupCreditAmountReal !== 0)
- return res
- .status(500)
- .json({ error: "GlobalState/ : internal server error" });
+ // const groupCreditAmount = await eventStatusModel.aggregate([
+ // {
+ // $match: {
+ // group: eventStatus.group,
+ // },
+ // },
+ // {
+ // $group: {
+ // _id: null,
+ // creditAmount: { $sum: "$creditAmount" },
+ // },
+ // },
+ // ]);
+ // const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount;
+ // if (!groupCreditAmountReal && groupCreditAmountReal !== 0)
+ // return res
+ // .status(500)
+ // .json({ error: "GlobalState/ : internal server error" });
return res.json({
- isAgreeOnTermsOfEvent: true,
- isEligible: true,
...eventStatus,
- groupCreditAmount: groupCreditAmountReal,
+ isAgreeOnTermsOfEvent: true,
quests,
});
} catch (err) {
@@ -79,7 +75,7 @@ const createUserGlobalStateHandler = async (req, res) => {
if (eventStatus)
return res
.status(400)
- .json({ error: "GlobalState/Create : already created" });
+ .json({ error: "GlobalState/create : already created" });
/* Request의 inviter 필드가 설정되어 있는데,
1. 해당되는 유저가 이벤트에 참여하지 않았거나,
@@ -88,47 +84,53 @@ const createUserGlobalStateHandler = async (req, res) => {
에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */
const inviterStatus =
req.body.inviter &&
- (await eventStatusModel.findOne({ _id: req.body.inviter }).lean());
+ (await eventStatusModel.findById(req.body.inviter).lean());
if (
req.body.inviter &&
(!inviterStatus ||
inviterStatus.isBanned ||
- !inviterStatus.isEnabledInviteUrl)
+ !inviterStatus.isInviteUrlEnabled)
)
return res.status(400).json({
- error: "GlobalState/Create : inviter did not participate in the event",
+ error: "GlobalState/create : invalid inviter",
});
- const user = await userModel.findOne({ _id: req.userOid });
+ const user = await userModel.findById(req.userOid);
if (!user)
return res
.status(500)
- .json({ error: "GlobalState/Create : internal server error" });
+ .json({ error: "GlobalState/create : internal server error" });
// 유저가 이벤트에 참여할 수 있는지 확인합니다.
- const isEligible = checkIsUserEligible(user);
- if (!isEligible)
- return res.status(400).json({
- error: "GlobalState/Create : not eligible to participate in the event",
- });
-
- // 수집한 전화번호를 User Document에 저장합니다.
- // 다른 이벤트 참여 과정에서 문제가 생길 수 있으므로, 이벤트 참여 자격이 있는 경우에만 저장합니다.
- user.phoneNumber = req.body.phoneNumber;
- await user.save();
+ // const isEligible = checkIsUserEligible(user);
+ // if (!isEligible)
+ // return res.status(400).json({
+ // error: "GlobalState/create : not eligible to participate in the event",
+ // });
+
+ // 필요한 경우 유저의 전화번호를 업데이트합니다.
+ if (user.phoneNumber !== req.body.phoneNumber) {
+ if (user.phoneNumber) {
+ logger.info(`Past user phone number: ${user.phoneNumber}`);
+ logger.info(`Update user phone number: ${req.body.phoneNumber}`);
+ }
+
+ user.phoneNumber = req.body.phoneNumber;
+ await user.save();
+ }
// EventStatus Document를 생성합니다.
eventStatus = new eventStatusModel({
userId: req.userOid,
creditAmount: eventConfig?.credit.initialAmount ?? 0,
- group: req.body.group,
- inviter: req.body.inviter,
+ inviter: inviterStatus?.userId ?? undefined,
});
await eventStatus.save();
+ // 퀘스트를 완료 처리합니다.
await contracts.completeFirstLoginQuest(req.userOid, req.timestamp);
- if (req.body.inviter) {
+ if (inviterStatus) {
await contracts.completeEventSharingQuest(req.userOid, req.timestamp);
await contracts.completeEventSharingQuest(
inviterStatus.userId,
@@ -141,7 +143,7 @@ const createUserGlobalStateHandler = async (req, res) => {
logger.error(err);
res
.status(500)
- .json({ error: "GlobalState/Create : internal server error" });
+ .json({ error: "GlobalState/create : internal server error" });
}
};
diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js
deleted file mode 100644
index c7871273..00000000
--- a/src/lottery/services/invite.js
+++ /dev/null
@@ -1,66 +0,0 @@
-const { eventStatusModel } = require("../modules/stores/mongo");
-const { userModel } = require("../../modules/stores/mongo");
-const logger = require("../../modules/logger");
-
-const { eventConfig } = require("../../../loadenv");
-
-const searchInviterHandler = async (req, res) => {
- try {
- const { inviter } = req.params;
- const inviterStatus = await eventStatusModel.findOne({ _id: inviter });
- if (
- !inviterStatus ||
- !inviterStatus.isEnabledInviteUrl ||
- inviterStatus.isBanned
- )
- return res.status(400).json({ error: "Invite/Search : invalid inviter" });
-
- const inviterInfo = await userModel.findOne({ _id: inviterStatus.userId });
- if (!inviterInfo)
- return res
- .status(500)
- .json({ error: "Invite/Search : internal server error" });
-
- return res.json({
- nickname: inviterInfo.nickname,
- profileImageUrl: inviterInfo.profileImageUrl,
- });
- } catch (err) {
- logger.error(err);
- res.status(500).json({ error: "Invite/Search : internal server error" });
- }
-};
-
-const createInviteUrlHandler = async (req, res) => {
- try {
- const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`;
-
- if (req.eventStatus.isEnabledInviteUrl) return res.json({ inviteUrl });
-
- const eventStatus = await eventStatusModel
- .findOneAndUpdate(
- {
- _id: req.eventStatus._id,
- isEnabledInviteUrl: false,
- },
- {
- isEnabledInviteUrl: true,
- }
- )
- .lean();
- if (!eventStatus)
- return res
- .status(500)
- .json({ error: "Invite/Create : internal server error" });
-
- return res.json({ inviteUrl });
- } catch (err) {
- logger.error(err);
- res.status(500).json({ error: "Invite/Create : internal server error" });
- }
-};
-
-module.exports = {
- searchInviterHandler,
- createInviteUrlHandler,
-};
diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js
new file mode 100644
index 00000000..6479bce8
--- /dev/null
+++ b/src/lottery/services/invites.js
@@ -0,0 +1,73 @@
+const { eventStatusModel } = require("../modules/stores/mongo");
+const { userModel } = require("../../modules/stores/mongo");
+const logger = require("../../modules/logger");
+
+const { eventConfig } = require("../../../loadenv");
+
+const searchInviterHandler = async (req, res) => {
+ try {
+ /* 1. 해당되는 유저가 이벤트에 참여하지 않았거나,
+ 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나,
+ 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면,
+ 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */
+ const inviterStatus = await eventStatusModel
+ .findById(req.params.inviter)
+ .lean();
+ if (
+ !inviterStatus ||
+ inviterStatus.isBanned ||
+ !inviterStatus.isInviteUrlEnabled
+ )
+ return res
+ .status(400)
+ .json({ error: "Invites/search : invalid inviter" });
+
+ // 해당되는 유저의 닉네임과 프로필 이미지를 가져옵니다.
+ const inviter = await userModel
+ .findById(inviterStatus.userId, "nickname profileImageUrl")
+ .lean();
+ if (!inviter)
+ return res
+ .status(500)
+ .json({ error: "Invites/search : internal server error" });
+
+ return res.json(inviter);
+ } catch (err) {
+ logger.error(err);
+ res.status(500).json({ error: "Invites/search : internal server error" });
+ }
+};
+
+const createInviteUrlHandler = async (req, res) => {
+ try {
+ const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`;
+
+ // 이미 초대 링크가 활성화된 경우 링크를 즉시 반환합니다.
+ if (req.eventStatus.isInviteUrlEnabled) return res.json({ inviteUrl });
+
+ // 초대 링크를 활성화합니다.
+ const { modifiedCount } = await eventStatusModel.updateOne(
+ {
+ _id: req.eventStatus._id,
+ isInviteUrlEnabled: false,
+ },
+ {
+ isInviteUrlEnabled: true,
+ }
+ );
+ if (modifiedCount !== 1)
+ return res
+ .status(500)
+ .json({ error: "Invites/create : internal server error" });
+
+ return res.json({ inviteUrl });
+ } catch (err) {
+ logger.error(err);
+ res.status(500).json({ error: "Invites/create : internal server error" });
+ }
+};
+
+module.exports = {
+ searchInviterHandler,
+ createInviteUrlHandler,
+};
diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js
index 9189bae4..eb00535d 100644
--- a/src/lottery/services/items.js
+++ b/src/lottery/services/items.js
@@ -3,9 +3,159 @@ const {
itemModel,
transactionModel,
} = require("../modules/stores/mongo");
+const { userModel } = require("../../modules/stores/mongo");
+const { isLogin, getLoginInfo } = require("../../modules/auths/login");
const logger = require("../../modules/logger");
const { eventConfig } = require("../../../loadenv");
+const contracts = require("../modules/contracts");
+
+const getItemsHandler = async (req, res) => {
+ try {
+ const items = await itemModel
+ .find(
+ {},
+ "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType"
+ )
+ .lean();
+ res.json({ items });
+ } catch (err) {
+ logger.error(err);
+ res.status(500).json({ error: "Items/ : internal server error" });
+ }
+};
+
+const getItemHandler = async (req, res) => {
+ try {
+ const { itemId } = req.params;
+ const item = await itemModel
+ .findById(
+ itemId,
+ "_id name description imageUrl instagramStoryStickerImageUrl price isDisabled itemType"
+ )
+ .lean();
+ if (!item) return res.status(400).json({ error: "Items/ : invalid item" });
+
+ res.json({ item });
+ } catch (err) {
+ logger.error(err);
+ res.status(500).json({ error: "Items/ : internal server error" });
+ }
+};
+
+// 유도 과정은 services/publicNotice.js 파일에 정의된 calculateProbabilityV2 함수의 주석 참조
+const calculateWinProbability = (realStock, users, amount, totalAmount) => {
+ if (users.length <= realStock) return 1;
+
+ const base = Math.pow(
+ 1 - realStock / users.length,
+ users.length / totalAmount
+ );
+ return 1 - Math.pow(base, amount);
+};
+
+const getItemLeaderboardHandler = async (req, res) => {
+ try {
+ // 상품 정보를 가져옵니다.
+ const { itemId } = req.params;
+ const item = await itemModel.findOne({ _id: itemId, itemType: 0 }).lean();
+ if (!item)
+ return res
+ .status(400)
+ .json({ error: "Items/leaderboard : invalid item" });
+
+ // 해당 상품을 구매한 유저들의 목록을 가져옵니다.
+ const users = await transactionModel.aggregate([
+ {
+ $match: {
+ type: "use",
+ itemId: item._id,
+ },
+ },
+ {
+ $group: {
+ _id: "$userId",
+ amount: { $sum: "$itemAmount" },
+ },
+ },
+ {
+ $lookup: {
+ from: eventStatusModel.collection.name,
+ localField: "_id",
+ foreignField: "userId",
+ as: "eventStatus",
+ },
+ },
+ {
+ $match: {
+ "eventStatus.0.isBanned": false,
+ },
+ },
+ {
+ $sort: { amount: -1 },
+ },
+ ]);
+
+ // 리더보드 생성을 위해 필요한 정보를 계산합니다.
+ const totalAmount = users.reduce((acc, user) => acc + user.amount, 0);
+ const rankMap = new Map(
+ users
+ .map((user) => user.amount)
+ .reduce((acc, amount, index) => {
+ if (acc.length === 0 || acc[acc.length - 1][0] !== amount) {
+ acc.push([amount, index + 1]);
+ }
+ return acc;
+ }, [])
+ );
+
+ // 리더보드를 생성합니다.
+ const leaderboardBase = users.map((user) => ({
+ userId: user._id,
+ amount: user.amount,
+ probability: calculateWinProbability(
+ item.realStock,
+ users,
+ user.amount,
+ totalAmount
+ ),
+ rank: rankMap.get(user.amount),
+ }));
+ const leaderboard = await Promise.all(
+ leaderboardBase
+ .filter((user) => user.rank <= 20)
+ .map(async (user) => {
+ const userInfo = await userModel.findById(user.userId).lean();
+ return {
+ nickname: userInfo.nickname,
+ profileImageUrl: userInfo.profileImageUrl,
+ amount: user.amount,
+ probability: user.probability,
+ rank: user.rank,
+ };
+ })
+ );
+
+ const userId = isLogin(req) ? getLoginInfo(req).oid : null;
+ const user = leaderboardBase.find(
+ (user) => user.userId.toString() === userId
+ );
+
+ return res.json({
+ leaderboard,
+ totalAmount,
+ totalUser: users.length,
+ amount: user?.amount,
+ probability: user?.probability,
+ rank: user?.rank,
+ });
+ } catch (err) {
+ logger.error(err);
+ res
+ .status(500)
+ .json({ error: "Items/leaderboard : internal server error" });
+ }
+};
const updateEventStatus = async (
userId,
@@ -22,199 +172,217 @@ const updateEventStatus = async (
}
);
-const hideItemStock = (item) => {
- item.stock = item.stock > 0 ? 1 : 0;
- return item;
-};
+// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다.
+//
+// const getRandomItem = async (req, depth) => {
+// if (depth >= 10) {
+// logger.error(`User ${req.userOid} failed to open random box`);
+// return null;
+// }
-const getRandomItem = async (req, depth) => {
- if (depth >= 10) {
- logger.error(`User ${req.userOid} failed to open random box`);
- return null;
- }
-
- const items = await itemModel
- .find({
- isRandomItem: true,
- stock: { $gt: 0 },
- isDisabled: false,
- })
- .lean();
- const randomItems = items
- .map((item) => Array(item.randomWeight).fill(item))
- .reduce((a, b) => a.concat(b), []);
- const dumpRandomItems = randomItems
- .map((item) => item._id.toString())
- .join(",");
-
- logger.info(
- `User ${req.userOid}'s ${
- depth + 1
- }th random box probability is: [${dumpRandomItems}]`
- );
+// const items = await itemModel
+// .find({
+// isRandomItem: true,
+// stock: { $gt: 0 },
+// isDisabled: false,
+// })
+// .lean();
+// const randomItems = items
+// .map((item) => Array(item.randomWeight).fill(item))
+// .reduce((a, b) => a.concat(b), []);
+// const dumpRandomItems = randomItems
+// .map((item) => item._id.toString())
+// .join(",");
- if (randomItems.length === 0) return null;
+// logger.info(
+// `User ${req.userOid}'s ${
+// depth + 1
+// }th random box probability is: [${dumpRandomItems}]`
+// );
- const randomItem =
- randomItems[Math.floor(Math.random() * randomItems.length)];
- try {
- // 1단계: 재고를 차감합니다.
- const newRandomItem = await itemModel
- .findOneAndUpdate(
- { _id: randomItem._id, stock: { $gt: 0 } },
- {
- $inc: {
- stock: -1,
- },
- },
- {
- new: true,
- fields: {
- itemType: 0,
- isRandomItem: 0,
- randomWeight: 0,
- },
- }
- )
- .lean();
- if (!newRandomItem) {
- throw new Error(`Item ${randomItem._id.toString()} was already sold out`);
- }
+// if (randomItems.length === 0) return null;
- // 2단계: 유저 정보를 업데이트합니다.
- await updateEventStatus(req.userOid, {
- ticket1Delta: randomItem.itemType === 1 ? 1 : 0,
- ticket2Delta: randomItem.itemType === 2 ? 1 : 0,
- });
+// const randomItem =
+// randomItems[Math.floor(Math.random() * randomItems.length)];
+// try {
+// // 1단계: 재고를 차감합니다.
+// const newRandomItem = await itemModel
+// .findOneAndUpdate(
+// { _id: randomItem._id, stock: { $gt: 0 } },
+// {
+// $inc: {
+// stock: -1,
+// },
+// },
+// {
+// new: true,
+// fields: {
+// itemType: 0,
+// isRandomItem: 0,
+// randomWeight: 0,
+// },
+// }
+// )
+// .lean();
+// if (!newRandomItem) {
+// throw new Error(`Item ${randomItem._id.toString()} was already sold out`);
+// }
- // 3단계: Transaction을 추가합니다.
- const transaction = new transactionModel({
- type: "use",
- amount: 0,
- userId: req.userOid,
- item: randomItem._id,
- itemType: randomItem.itemType,
- comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`,
- });
- await transaction.save();
+// // 2단계: 유저 정보를 업데이트합니다.
+// await updateEventStatus(req.userOid, {
+// ticket1Delta: randomItem.itemType === 1 ? 1 : 0,
+// ticket2Delta: randomItem.itemType === 2 ? 1 : 0,
+// });
- return newRandomItem;
- } catch (err) {
- logger.error(err);
- logger.warn(
- `User ${req.userOid}'s ${depth + 1}th random box failed due to exception`
- );
+// // 3단계: Transaction을 추가합니다.
+// const transaction = new transactionModel({
+// type: "use",
+// amount: 0,
+// userId: req.userOid,
+// itemId: randomItem._id,
+// comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`,
+// });
+// await transaction.save();
- return await getRandomItem(req, depth + 1);
- }
-};
+// return newRandomItem;
+// } catch (err) {
+// logger.error(err);
+// logger.warn(
+// `User ${req.userOid}'s ${depth + 1}th random box failed due to exception`
+// );
-const listHandler = async (_, res) => {
- try {
- const items = await itemModel
- .find(
- {},
- "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType"
- )
- .lean();
- res.json({ items: items.map(hideItemStock) });
- } catch (err) {
- logger.error(err);
- res.status(500).json({ error: "Items/List : internal server error" });
- }
-};
+// return await getRandomItem(req, depth + 1);
+// }
+// };
-const purchaseHandler = async (req, res) => {
+const purchaseItemHandler = async (req, res) => {
try {
const { itemId } = req.params;
- const item = await itemModel.findOne({ _id: itemId }).lean();
+ const item = await itemModel.findById(itemId).lean();
if (!item)
- return res.status(400).json({ error: "Items/Purchase : invalid Item" });
+ return res.status(400).json({ error: "Items/purchase : invalid Item" });
- // 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다.
+ const { amount } = req.body;
+ const totalPrice = item.price * amount;
+
+ // 구매 가능 조건: 재화가 충분하며, 재고가 남아있으며, 판매 중인 상품이어야 합니다.
if (item.isDisabled)
- return res.status(400).json({ error: "Items/Purchase : disabled item" });
- if (req.eventStatus.creditAmount < item.price)
+ return res.status(400).json({ error: "Items/purchase : disabled item" });
+ if (req.eventStatus.creditAmount < totalPrice)
return res
.status(400)
- .json({ error: "Items/Purchase : not enough credit" });
- if (item.stock <= 0)
+ .json({ error: "Items/purchase : not enough credit" });
+ if (item.stock < amount)
return res
.status(400)
- .json({ error: "Items/Purchase : item out of stock" });
+ .json({ error: "Items/purchase : item out of stock" });
// 1단계: 재고를 차감합니다.
const { modifiedCount } = await itemModel.updateOne(
- { _id: item._id, stock: { $gt: 0 } },
- {
- $inc: {
- stock: -1,
- },
- }
+ { _id: item._id, stock: { $gte: amount } },
+ { $inc: { stock: -amount } }
);
if (modifiedCount === 0)
return res
.status(400)
- .json({ error: "Items/Purchase : item out of stock" });
+ .json({ error: "Items/purchase : item out of stock" });
- // 2단계: 유저 정보를 업데이트합니다.
- await updateEventStatus(req.userOid, {
- creditDelta: -item.price,
- ticket1Delta: item.itemType === 1 ? 1 : 0,
- ticket2Delta: item.itemType === 2 ? 1 : 0,
- });
+ if (item.itemType !== 3) {
+ // 랜덤박스가 아닌 상품을 구입한 경우
+ // 2단계: 유저 정보를 업데이트합니다.
+ await updateEventStatus(req.userOid, {
+ creditDelta: -totalPrice,
+ ticket1Delta: item.itemType === 1 ? amount : 0,
+ ticket2Delta: item.itemType === 2 ? amount : 0,
+ });
- // 3단계: Transaction을 추가합니다.
- const transaction = new transactionModel({
- type: "use",
- amount: item.price,
- userId: req.userOid,
- item: item._id,
- itemType: item.itemType,
- comment: `${eventConfig?.credit.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`,
- });
- await transaction.save();
+ // 3단계: 출금 내역을 추가합니다.
+ const transaction = new transactionModel({
+ type: "use",
+ amount: totalPrice,
+ userId: req.userOid,
+ itemId: item._id,
+ itemAmount: amount,
+ comment: `${eventConfig?.credit.name} ${totalPrice}개를 사용해 "${item.name}" ${amount}개를 획득했습니다.`,
+ });
+ await transaction.save();
- // 4단계: 랜덤박스인 경우 아이템을 추첨합니다.
- if (item.itemType !== 3) return res.json({ result: true });
+ // 4단계: 퀘스트를 완료 처리합니다.
+ await contracts.completeItemPurchaseQuest(
+ req.userOid,
+ transaction.createdAt
+ );
- const randomItem = await getRandomItem(req, 0);
- if (!randomItem) {
- // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다.
- // TODO: Transactions 도입 후 이 코드는 삭제합니다.
- logger.info(`User ${req.userOid}'s status will be restored`);
+ return res.json({ result: true });
+ } else {
+ // 랜덤박스를 구입한 경우
+ // 2단계: 대박(40%)인지 쪽박(60%)인지 결정합니다.
+ const isJackpot = Math.random() < 0.4;
+ const creditDelta = isJackpot ? totalPrice : -totalPrice;
- await transactionModel.deleteOne({ _id: transaction._id });
- await updateEventStatus(req.userOid, {
- creditDelta: item.price,
- });
- await itemModel.updateOne(
- { _id: item._id },
- {
- $inc: {
- stock: 1,
- },
- }
- );
+ // 3단계: 유저 정보를 업데이트합니다.
+ await updateEventStatus(req.userOid, { creditDelta });
- logger.info(`User ${req.userOid}'s status was successfully restored`);
+ // 4단계: 입출금 내역을 추가합니다.
+ if (isJackpot) {
+ const transaction = new transactionModel({
+ type: "get",
+ amount: creditDelta,
+ userId: req.userOid,
+ itemId: item._id,
+ itemAmount: amount,
+ comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용해 대박을 터뜨렸습니다.`,
+ });
+ await transaction.save();
+ } else {
+ const transaction = new transactionModel({
+ type: "use",
+ amount: creditDelta,
+ userId: req.userOid,
+ itemId: item._id,
+ itemAmount: amount,
+ comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용했지만 쪽박을 맞았습니다.`,
+ });
+ await transaction.save();
+ }
- return res
- .status(500)
- .json({ error: "Items/Purchase : random box error" });
+ return res.json({ result: true, isJackpot });
}
- res.json({
- result: true,
- reward: hideItemStock(randomItem),
- });
+ // const randomItem = await getRandomItem(req, 0);
+ // if (!randomItem) {
+ // // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다.
+ // // TODO: Transactions 도입 후 이 코드는 삭제합니다.
+ // logger.info(`User ${req.userOid}'s status will be restored`);
+
+ // await transactionModel.deleteOne({ _id: transaction._id });
+ // await updateEventStatus(req.userOid, {
+ // creditDelta: item.price,
+ // });
+ // await itemModel.updateOne(
+ // { _id: item._id },
+ // {
+ // $inc: {
+ // stock: 1,
+ // },
+ // }
+ // );
+
+ // logger.info(`User ${req.userOid}'s status was successfully restored`);
+
+ // return res
+ // .status(500)
+ // .json({ error: "Items/purchase : random box error" });
+ // }
} catch (err) {
logger.error(err);
- res.status(500).json({ error: "Items/Purchase : internal server error" });
+ res.status(500).json({ error: "Items/purchase : internal server error" });
}
};
module.exports = {
- listHandler,
- purchaseHandler,
+ getItemsHandler,
+ getItemHandler,
+ getItemLeaderboardHandler,
+ purchaseItemHandler,
};
diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js
index ae1472bc..5a0c6ae6 100644
--- a/src/lottery/services/quests.js
+++ b/src/lottery/services/quests.js
@@ -3,20 +3,38 @@ const logger = require("../../modules/logger");
const contracts = require("../modules/contracts");
-const completeHandler = async (req, res) => {
+const completeQuestHandler = async (req, res) => {
try {
const quest = contracts.quests[req.params.questId];
if (!quest || !quest.isApiRequired)
- return res.status(400).json({ error: "Quests/Complete: invalid Quest" });
+ return res.status(400).json({ error: "Quests/complete: invalid quest" });
+
+ // 출석 체크 퀘스트는 하루에 1번만 완료하도록 제한합니다.
+ if (quest.id === "dailyAttendance") {
+ const todayMidnight = new Date(req.timestamp);
+ todayMidnight.setHours(0, 0, 0, 0);
+
+ const tomorrowMidnight = new Date(todayMidnight);
+ tomorrowMidnight.setDate(tomorrowMidnight.getDate() + 1);
+
+ // 오늘 완료된 dailyAttendance 퀘스트가 있는지 확인합니다.
+ const completedQuest = req.eventStatus.completedQuests.find(
+ ({ questId, completedAt }) =>
+ questId === quest.id &&
+ completedAt >= todayMidnight &&
+ completedAt < tomorrowMidnight
+ );
+ if (completedQuest) return res.json({ result: false });
+ }
const result = await completeQuest(req.userOid, req.timestamp, quest);
res.json({ result: !!result }); // boolean으로 변환하기 위해 !!를 사용합니다.
} catch (err) {
logger.error(err);
- res.status(500).json({ error: "Quests/Complete: internal server error" });
+ res.status(500).json({ error: "Quests/complete: internal server error" });
}
};
module.exports = {
- completeHandler,
+ completeQuestHandler,
};
diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js
index 1d920870..fe976e28 100644
--- a/src/lottery/services/transactions.js
+++ b/src/lottery/services/transactions.js
@@ -1,25 +1,34 @@
const { transactionModel } = require("../modules/stores/mongo");
+const {
+ transactionPopulateOption,
+} = require("../modules/populates/transactions");
const logger = require("../../modules/logger");
-const hideItemStock = (transaction) => {
- if (transaction.item) {
- transaction.item.stock = transaction.item.stock > 0 ? 1 : 0;
+const formatTransaction = (transaction) => {
+ if (transaction.itemId) {
+ transaction.item = transaction.itemId;
+ delete transaction.itemId;
}
return transaction;
};
const getUserTransactionsHandler = async (req, res) => {
try {
- // userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다.
const transactions = await transactionModel
- .find({ userId: req.userOid }, "_id type amount questId comment createAt")
+ .find(
+ { userId: req.userOid },
+ "type amount questId itemId comment createdAt"
+ )
+ .populate(transactionPopulateOption)
.lean();
- if (transactions)
- res.json({
- transactions,
- });
- else
- res.status(500).json({ error: "Transactions/ : internal server error" });
+ if (!transactions)
+ return res
+ .status(500)
+ .json({ error: "Transactions/ : internal server error" });
+
+ res.json({
+ transactions: transactions.map(formatTransaction),
+ });
} catch (err) {
logger.error(err);
res.status(500).json({ error: "Transactions/ : internal server error" });
diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js
new file mode 100644
index 00000000..f70b9550
--- /dev/null
+++ b/src/middlewares/ban.js
@@ -0,0 +1,19 @@
+const { validateServiceBanRecord } = require("../modules/ban");
+
+const serviceMapper = new Map([
+ ["/rooms/create", "service"],
+ ["/rooms/join", "service"],
+]);
+
+const banMiddleware = async (req, res, next) => {
+ const banErrorMessage = await validateServiceBanRecord(
+ req,
+ serviceMapper.get(req.originalUrl)
+ );
+ if (banErrorMessage !== undefined) {
+ return res.status(400).json({ error: banErrorMessage });
+ }
+ next();
+};
+
+module.exports = banMiddleware;
diff --git a/src/modules/ban.js b/src/modules/ban.js
new file mode 100644
index 00000000..4068db78
--- /dev/null
+++ b/src/modules/ban.js
@@ -0,0 +1,45 @@
+const logger = require("./logger");
+const { banModel } = require("./stores/mongo");
+
+/**
+ * @param {*} req
+ * @param {String} service
+ */
+const validateServiceBanRecord = async (req, service) => {
+ let banRecord = undefined;
+
+ try {
+ // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴
+ const bans = await banModel
+ .find({
+ userSid: req.session.loginInfo.sid,
+ expireAt: {
+ $gte: req.timestamp,
+ },
+ serviceName: service,
+ })
+ .sort({ expireAt: -1 });
+ if (bans.length > 0) {
+ // 가장 expireAt이 큰 정지 기록만 반환함.
+ banRecord = bans[0];
+ }
+ } catch (err) {
+ logger.error(
+ "Error occured while validateServiceBanRecord: " + err.message
+ );
+ return;
+ }
+ if (banRecord !== undefined) {
+ const formattedExpireAt = banRecord.expireAt
+ .toISOString()
+ .replace("T", " ")
+ .split(".")[0];
+ const banErrorMessage = `${req.originalUrl} : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from service until ${formattedExpireAt}.`;
+ return banErrorMessage;
+ }
+ return;
+};
+
+module.exports = {
+ validateServiceBanRecord,
+};
diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js
index 8f837775..f236b829 100755
--- a/src/modules/stores/mongo.js
+++ b/src/modules/stores/mongo.js
@@ -28,35 +28,21 @@ const userSchema = Schema({
const banSchema = Schema({
// 정지 시킬 사용자를 기제함.
- userId: { type: mongoose.Types.ObjectId, ref: "User", required: true },
+ userSid: { type: String, required: true },
// 정지 사유
- reason: {
+ reason: { type: String, required: true },
+ bannedAt: { type: Date, required: true }, // 정지 당한 시각
+ expireAt: { type: Date, required: true }, // 정지 만료 시각
+ // 정지를 당한 서비스를 기제함
+ serviceName: {
type: String,
required: true,
+ // 필요시 이곳에 정지를 시킬 서비스를 추가함.
+ enum: [
+ "service", // service: 방 생성/참여 제한
+ "2023-fall-event", // xxxx-xxxx-event: 특정 이벤트 참여 제한
+ ],
},
- bannedAt: {
- type: Date, // 정지 당한 시각
- required: true,
- },
- expireAt: {
- type: Date, // 정지 만료 시각
- required: true,
- },
- services: [
- {
- // 정지를 당한 서비스를 기제함
- serviceName: {
- type: String,
- required: true,
- // 필요시 이곳에 정지를 시킬 서비스를 추가함.
- enum: [
- "all", // all -> 과거/미래 모든 서비스 및 이벤트 이용 제한
- "service", // service -> 방 생성/참여 제한
- "2023-fall-event", // event -> 특정 이벤트 참여 제한
- ],
- },
- },
- ],
});
const participantSchema = Schema({
diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js
index 3ac66e6b..0c9303a2 100644
--- a/src/routes/docs/rooms.js
+++ b/src/routes/docs/rooms.js
@@ -72,6 +72,12 @@ roomsDocs[`${apiPrefix}/create`] = {
},
},
examples: {
+ "방 생성 기능이 정지당한 경우": {
+ value: {
+ error:
+ "Rooms/join : user monday is temporarily restricted from creating rooms until 2024-08-23 15:00:00.",
+ },
+ },
"출발지와 도착지가 같음": {
value: {
error: "Rooms/create : locations are same",
@@ -309,6 +315,12 @@ roomsDocs[`${apiPrefix}/join`] = {
},
},
examples: {
+ "방 참여 기능이 정지당한 경우": {
+ value: {
+ error:
+ "Rooms/join : user monday is temporarily restricted from joining rooms until 2024-08-23 15:00:00.",
+ },
+ },
"사용자가 참여하는 진행 중 방이 5개 이상": {
value: {
error: "Rooms/join : participating in too many rooms",
diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js
index deaeb113..0f19ecde 100644
--- a/src/routes/docs/users.js
+++ b/src/routes/docs/users.js
@@ -330,7 +330,7 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = {
},
};
-usersDocs[`${apiPrefix}/isBanned`] = {
+usersDocs[`${apiPrefix}/getBanRecord`] = {
get: {
tags: [tag],
summary: "본인의 현재 정지 기록을 가져움",
@@ -344,10 +344,10 @@ usersDocs[`${apiPrefix}/isBanned`] = {
type: "array",
items: {
properties: {
- userId: {
+ userSid: {
type: "string",
- description: "사용자의 ObjectId",
- pattern: objectId.source,
+ description: "사용자의 SSO ID",
+ pattern: "monday-sid",
},
reason: {
type: "string",
@@ -364,85 +364,10 @@ usersDocs[`${apiPrefix}/isBanned`] = {
description: "정지 만료 시각",
example: "2024-05-21 12:00",
},
- services: {
- type: "array",
- items: {
- properties: {
- serviceName: {
- type: "string",
- description: "정지를 당한 서비스 또는 이벤트 이름",
- },
- },
- },
- },
- },
- },
- },
- },
- },
- },
- 400: {
- content: {
- "text/html": {
- example: "Users/isBanned : there is no ban record",
- },
- },
- },
- 500: {
- content: {
- "text/html": {
- example: "Users/isBanned : internal server error",
- },
- },
- },
- },
- },
-};
-
-usersDocs[`${apiPrefix}/getBanRecord`] = {
- get: {
- tags: [tag],
- summary: "본인의 모든 정지 기록을 가져움",
- description:
- "정지 기록들 중 본인인 경우에 해당하는 정지 기록을 모두 가져옴",
- responses: {
- 200: {
- content: {
- "application/json": {
- schema: {
- type: "array",
- items: {
- properties: {
- userId: {
- type: "string",
- description: "사용자의 ObjectId",
- pattern: objectId.source,
- },
- reason: {
+ serviceName: {
type: "string",
- description: "정지 사유",
- example: "미정산",
- },
- bannedAt: {
- type: "date",
- description: "정지 당한 시각",
- example: "2024-05-20 12:00",
- },
- expireAt: {
- type: "date",
- description: "정지 만료 시각",
- example: "2024-05-21 12:00",
- },
- services: {
- type: "array",
- items: {
- properties: {
- serviceName: {
- type: "string",
- description: "정지를 당한 서비스 또는 이벤트 이름",
- },
- },
- },
+ description: "정지를 당한 서비스 또는 이벤트 이름",
+ example: "2023-fall-event",
},
},
},
diff --git a/src/routes/rooms.js b/src/routes/rooms.js
index 6345fa6f..b8bad634 100644
--- a/src/routes/rooms.js
+++ b/src/routes/rooms.js
@@ -35,6 +35,9 @@ router.get(
// 이후 API 접근 시 로그인 필요
router.use(require("../middlewares/auth"));
+// 방 생성/참여전 ban 여부 확인
+router.use(require("../middlewares/ban"));
+
// 특정 id 방 세부사항 보기
router.get(
"/info",
diff --git a/src/routes/users.js b/src/routes/users.js
index d5366155..de3c0b1d 100755
--- a/src/routes/users.js
+++ b/src/routes/users.js
@@ -56,9 +56,6 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler);
// 프로필 이미지를 기본값으로 재설정합니다.
router.get("/resetProfileImg", userHandlers.resetProfileImgHandler);
-// 유저의 현재 유효한 서비스 정지 기록들만 반환합니다.
-router.get("/isBanned", userHandlers.isBannedHandler);
-
// 유저의 서비스 정지 기록들을 모두 반환합니다.
router.get("/getBanRecord", userHandlers.getBanRecordHandler);
diff --git a/src/services/rooms.js b/src/services/rooms.js
index 218a23d5..0ef798fe 100644
--- a/src/services/rooms.js
+++ b/src/services/rooms.js
@@ -586,12 +586,7 @@ const commitSettlementHandler = async (req, res) => {
});
// 이벤트 코드입니다.
- await contracts?.completePayingQuest(
- req.userOid,
- req.timestamp,
- roomObject
- );
- await contracts?.completePayingAndSendingQuest(
+ await contracts?.completeFareSettlementQuest(
req.userOid,
req.timestamp,
roomObject
@@ -664,12 +659,7 @@ const commitPaymentHandler = async (req, res) => {
});
// 이벤트 코드입니다.
- await contracts?.completeSendingQuest(
- req.userOid,
- req.timestamp,
- roomObject
- );
- await contracts?.completePayingAndSendingQuest(
+ await contracts?.completeFarePaymentQuest(
req.userOid,
req.timestamp,
roomObject
diff --git a/src/services/users.js b/src/services/users.js
index 5d8ee632..8c51c736 100644
--- a/src/services/users.js
+++ b/src/services/users.js
@@ -200,27 +200,14 @@ const resetProfileImgHandler = async (req, res) => {
}
};
-const isBannedHandler = async (req, res) => {
- try {
- // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴
- const result = await banModel.find({
- userId: req.userOid,
- expireAt: {
- $gte: req.timestamp,
- },
- });
- if (!result)
- return res.status(500).send("Users/isBanned : internal server error");
- res.status(200).json(result);
- } catch (err) {
- res.status(500).send("Users/isBanned : internal server error");
- }
-};
-
const getBanRecordHandler = async (req, res) => {
try {
- // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴
- const result = await banModel.find({ userId: req.userOid });
+ // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴
+ const result = await banModel
+ .find({
+ userSid: req.session.loginInfo.sid,
+ })
+ .sort({ expireAt: -1 });
if (!result)
return res.status(500).send("Users/getBanRecord : internal server error");
res.status(200).json(result);
@@ -238,6 +225,5 @@ module.exports = {
editProfileImgDoneHandler,
resetNicknameHandler,
resetProfileImgHandler,
- isBannedHandler,
getBanRecordHandler,
};