From 3cc48b99ccf5843e687257559e0f16f66ed89b41 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 13:35:13 +0900 Subject: [PATCH 01/13] Add: /events/2023fall/global-state/create endpoint --- src/lottery/modules/quests.js | 13 ++++----- src/lottery/routes/globalState.js | 1 + src/lottery/services/globalState.js | 44 +++++++++++++++++++++++------ src/lottery/services/items.js | 12 ++++---- 4 files changed, 47 insertions(+), 23 deletions(-) diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 50371daa..cd3a36c1 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -58,7 +58,11 @@ const buildQuests = (quests) => { */ const completeQuest = async (userId, eventPeriod, quest) => { try { - // 1단계: 이벤트 기간인지 확인합니다. + // 1단계: 유저의 EventStatus를 가져옵니다. + const eventStatus = await eventStatusModel.findOne({ userId }).lean(); + if (!eventStatus) return null; + + // 2단계: 이벤트 기간인지 확인합니다. const now = Date.now(); if (now >= eventPeriod.end || now < eventPeriod.start) { logger.info( @@ -67,13 +71,6 @@ const completeQuest = async (userId, eventPeriod, quest) => { return null; } - // 2단계: 유저의 EventStatus를 가져옵니다. 없으면 새롭게 생성합니다. - let eventStatus = await eventStatusModel.findOne({ userId }).lean(); - if (!eventStatus) { - eventStatus = new eventStatusModel({ userId }); - await eventStatus.save(); - } - // 3단계: 유저의 퀘스트 완료 횟수를 확인합니다. const questCount = eventStatus.completedQuests.filter( (completedQuestId) => completedQuestId === quest.id diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js index f0f75406..4cb17a4a 100644 --- a/src/lottery/routes/globalState.js +++ b/src/lottery/routes/globalState.js @@ -7,5 +7,6 @@ const globalStateHandlers = require("../services/globalState"); router.use(require("../../middlewares/auth")); router.get("/", globalStateHandlers.getUserGlobalStateHandler); +router.post("/create", globalStateHandlers.createUserGlobalStateHandler); module.exports = router; diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index ff55dc22..f4caa970 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -2,21 +2,20 @@ const { eventStatusModel } = require("../modules/stores/mongo"); const logger = require("../../modules/logger"); const { eventMode } = require("../../../loadenv"); -const quests = eventMode - ? Object.values(require(`../modules/contracts/${eventMode}`).quests) +const contract = eventMode + ? require(`../modules/contracts/${eventMode}`) : undefined; +const quests = contract ? Object.values(contract.quests) : undefined; const getUserGlobalStateHandler = async (req, res) => { try { - let eventStatus = await eventStatusModel + const eventStatus = await eventStatusModel .findOne({ userId: req.userOid }) .lean(); - if (!eventStatus) { - eventStatus = new eventStatusModel({ - userId: req.userOid, - }); - await eventStatus.save(); - } + if (!eventStatus) + return res + .status(400) + .json({ error: "GlobalState/ : nonexistent eventStatus" }); res.json({ creditAmount: eventStatus.creditAmount, @@ -31,6 +30,33 @@ const getUserGlobalStateHandler = async (req, res) => { } }; +const createUserGlobalStateHandler = async (req, res) => { + try { + let eventStatus = await eventStatusModel + .findOne({ userId: req.userOid }) + .lean(); + if (eventStatus) + return res + .status(400) + .json({ error: "GlobalState/Create : already created" }); + + eventStatus = new eventStatusModel({ + userId: req.userOid, + }); + await eventStatus.save(); + + await contract.completeFirstLoginQuest(req.userOid); + + res.json({ result: true }); + } catch (err) { + logger.error(err); + res + .status(500) + .json({ error: "GlobalState/Create : internal server error" }); + } +}; + module.exports = { getUserGlobalStateHandler, + createUserGlobalStateHandler, }; diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 36b4bf10..1eb1cd49 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -120,6 +120,12 @@ const listHandler = async (_, res) => { const purchaseHandler = async (req, res) => { try { + const eventStatus = await eventStatusModel.findOne({ userId: req.userOid }); + if (!eventStatus) + return res + .status(400) + .json({ error: "Items/Purchase : nonexistent eventStatus" }); + const now = Date.now(); if (now >= eventPeriod.end || now < eventPeriod.start) return res.status(400).json({ error: "Items/Purchase : out of date" }); @@ -129,12 +135,6 @@ const purchaseHandler = async (req, res) => { if (!item) return res.status(400).json({ error: "Items/Purchase : invalid Item" }); - const eventStatus = await eventStatusModel.find({ userId: req.userOid }); - if (!eventStatus) - return res - .status(400) - .json({ error: "Items/Purchase : invalid EventStatus" }); - // 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다. if (item.isDisabled) return res.status(400).json({ error: "Items/Purchase : disabled item" }); From b7c28d6e7915576dcfcff3c43089effbd30a7ff9 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 13:37:44 +0900 Subject: [PATCH 02/13] Docs: describe /events/2023fall/global-state/create endpoint --- src/lottery/routes/docs/globalState.js | 30 +++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 29173691..63825433 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -7,7 +7,7 @@ globalStateDocs[`${apiPrefix}/`] = { tags: [`${apiPrefix}`], summary: "Frontend에서 Global state로 관리하는 정보 반환", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다. 유저에 대한 EventStatus Document가 없을 경우 새롭게 생성합니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다.", responses: { 200: { description: "", @@ -116,5 +116,33 @@ globalStateDocs[`${apiPrefix}/`] = { }, }, }; +globalStateDocs[`${apiPrefix}/create`] = { + get: { + tags: [`${apiPrefix}`], + summary: "Frontend에서 Global state로 관리하는 정보 생성", + description: + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 생성합니다.", + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: { + type: "object", + required: ["result"], + properties: { + result: { + type: "boolean", + description: "성공 여부. 항상 true입니다.", + example: true, + }, + }, + }, + }, + }, + }, + }, + }, +}; module.exports = globalStateDocs; From 2a8adf08f10d024324736a47e94d35331b0d8e0d Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 15:28:27 +0900 Subject: [PATCH 03/13] Add: agreement field in the response of global-state endpoint --- src/lottery/modules/contracts/2023fall.js | 1 + src/lottery/routes/docs/globalState.js | 6 +++++ src/lottery/services/globalState.js | 30 ++++++++++++++--------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js index f5994d51..a15caf90 100644 --- a/src/lottery/modules/contracts/2023fall.js +++ b/src/lottery/modules/contracts/2023fall.js @@ -85,6 +85,7 @@ const eventPeriod = { * firstLogin 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @returns {Promise} + * @usage lottery/globalState/createUserGlobalStateHandler */ const completeFirstLoginQuest = async (userId) => { return await completeQuest(userId, eventPeriod, quests.firstLogin); diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 63825433..8a2afa77 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -16,6 +16,7 @@ globalStateDocs[`${apiPrefix}/`] = { schema: { type: "object", required: [ + "agreement", "creditAmount", "completedQuests", "ticket1Amount", @@ -23,6 +24,11 @@ globalStateDocs[`${apiPrefix}/`] = { "quests", ], properties: { + agreement: { + type: "boolean", + description: "유저의 이벤트 참여 동의 여부", + example: true, + }, creditAmount: { type: "number", description: "재화 개수. 0 이상입니다.", diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index f4caa970..bd31cb04 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -12,18 +12,24 @@ const getUserGlobalStateHandler = async (req, res) => { const eventStatus = await eventStatusModel .findOne({ userId: req.userOid }) .lean(); - if (!eventStatus) - return res - .status(400) - .json({ error: "GlobalState/ : nonexistent eventStatus" }); - - res.json({ - creditAmount: eventStatus.creditAmount, - completedQuests: eventStatus.completedQuests, - ticket1Amount: eventStatus.ticket1Amount, - ticket2Amount: eventStatus.ticket2Amount, - quests, - }); + if (eventStatus) + res.json({ + agreement: true, + creditAmount: eventStatus.creditAmount, + completedQuests: eventStatus.completedQuests, + ticket1Amount: eventStatus.ticket1Amount, + ticket2Amount: eventStatus.ticket2Amount, + quests, + }); + else + res.json({ + agreement: false, + creditAmount: 0, + completedQuests: [], + ticket1Amount: 0, + ticket2Amount: 0, + quests, + }); } catch (err) { logger.error(err); res.status(500).json({ error: "GlobalState/ : internal server error" }); From e21e4a04e4a41ff70faa8aaacd5e05a9cf4f8849 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 15:56:57 +0900 Subject: [PATCH 04/13] Refactor: use req.timestamp instead of Date.now() --- src/lottery/modules/quests.js | 3 +-- src/lottery/services/items.js | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index cd3a36c1..c25e4b7e 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -63,8 +63,7 @@ const completeQuest = async (userId, eventPeriod, quest) => { if (!eventStatus) return null; // 2단계: 이벤트 기간인지 확인합니다. - const now = Date.now(); - if (now >= eventPeriod.end || now < eventPeriod.start) { + if (req.timestamp >= eventPeriod.end || req.timestamp < eventPeriod.start) { logger.info( `User ${userId} failed to complete auto-disabled ${quest.id}Quest` ); diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 1eb1cd49..5e3a5a1c 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -126,8 +126,7 @@ const purchaseHandler = async (req, res) => { .status(400) .json({ error: "Items/Purchase : nonexistent eventStatus" }); - const now = Date.now(); - if (now >= eventPeriod.end || now < eventPeriod.start) + if (req.timestamp >= eventPeriod.end || req.timestamp < eventPeriod.start) return res.status(400).json({ error: "Items/Purchase : out of date" }); const { itemId } = req.params; From a8b685a3d68ece80b34999b8f41c72b4196b8982 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 17:31:28 +0900 Subject: [PATCH 05/13] Add: timestamp parameter in completeQuest function --- src/lottery/modules/contracts/2023fall.js | 49 +++++++++++++++++------ src/lottery/modules/quests.js | 5 ++- src/lottery/services/globalState.js | 2 +- src/services/rooms.js | 18 ++++++--- src/services/users.js | 11 ++++- 5 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js index a15caf90..ba656d4f 100644 --- a/src/lottery/modules/contracts/2023fall.js +++ b/src/lottery/modules/contracts/2023fall.js @@ -84,15 +84,17 @@ const eventPeriod = { /** * firstLogin 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @usage lottery/globalState/createUserGlobalStateHandler */ -const completeFirstLoginQuest = async (userId) => { - return await completeQuest(userId, eventPeriod, quests.firstLogin); +const completeFirstLoginQuest = async (userId, timestamp) => { + return await completeQuest(userId, timestamp, eventPeriod, quests.firstLogin); }; /** * payingAndSending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이거나, 모든 참가자가 정산 또는 송금을 완료하지 않았다면 요청하지 않습니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. * @param {number} roomObject.settlementTotal - 정산 또는 송금이 완료된 참여자 수입니다. @@ -100,7 +102,7 @@ const completeFirstLoginQuest = async (userId) => { * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. * @usage rooms/commitPaymentHandler, rooms/settlementHandler */ -const completePayingAndSendingQuest = async (roomObject) => { +const completePayingAndSendingQuest = async (timestamp, roomObject) => { if (roomObject.part.length < 2) return null; if (roomObject.part.length > roomObject.settlementTotal) return null; @@ -109,6 +111,7 @@ const completePayingAndSendingQuest = async (roomObject) => { async (participant) => await completeQuest( participant.user._id, + timestamp, eventPeriod, quests.payingAndSending ) @@ -119,12 +122,18 @@ const completePayingAndSendingQuest = async (roomObject) => { /** * firstRoomCreation 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 방을 만들 때마다 호출해 주세요. * @usage rooms/createHandler */ -const completeFirstRoomCreationQuest = async (userId) => { - return await completeQuest(userId, eventPeriod, quests.firstRoomCreation); +const completeFirstRoomCreationQuest = async (userId, timestamp) => { + return await completeQuest( + userId, + timestamp, + eventPeriod, + quests.firstRoomCreation + ); }; const completeRoomSharingQuest = async () => { @@ -134,56 +143,70 @@ const completeRoomSharingQuest = async () => { /** * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. * @returns {Promise} * @description 정산 요청이 이루어질 때마다 호출해 주세요. * @usage rooms/commitPaymentHandler */ -const completePayingQuest = async (userId, roomObject) => { +const completePayingQuest = async (userId, timestamp, roomObject) => { if (roomObject.part.length < 2) return null; - return await completeQuest(userId, eventPeriod, quests.paying); + return await completeQuest(userId, timestamp, eventPeriod, quests.paying); }; /** * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. * @returns {Promise} * @description 송금이 이루어질 때마다 호출해 주세요. * @usage rooms/settlementHandler */ -const completeSendingQuest = async (userId, roomObject) => { +const completeSendingQuest = async (userId, timestamp, roomObject) => { if (roomObject.part.length < 2) return null; - return await completeQuest(userId, eventPeriod, quests.sending); + return await completeQuest(userId, timestamp, eventPeriod, quests.sending); }; /** * nicknameChaning 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 닉네임을 변경할 때마다 호출해 주세요. * @usage users/editNicknameHandler */ -const completeNicknameChangingQuest = async (userId) => { - return await completeQuest(userId, eventPeriod, quests.nicknameChaning); +const completeNicknameChangingQuest = async (userId, timestamp) => { + return await completeQuest( + userId, + timestamp, + eventPeriod, + quests.nicknameChaning + ); }; /** * accountChanging 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {string} newAccount - 변경된 계좌입니다. * @returns {Promise} * @description 계좌를 변경할 때마다 호출해 주세요. * @usage users/editAccountHandler */ -const completeAccountChangingQuest = async (userId, newAccount) => { +const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { if (newAccount === "") return null; - return await completeQuest(userId, eventPeriod, quests.accountChanging); + return await completeQuest( + userId, + timestamp, + eventPeriod, + quests.accountChanging + ); }; const completeAdPushAgreementQuest = async () => { diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index c25e4b7e..4ec5c4f3 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -44,6 +44,7 @@ const buildQuests = (quests) => { /** * 퀘스트 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} eventPeriod - 이벤트의 기간입니다. * @param {Date} eventPeriod.start - 이벤트의 시작 시각(Inclusive)입니다. * @param {Date} eventPeriod.end - 이벤트의 종료 시각(Exclusive)입니다. @@ -56,14 +57,14 @@ const buildQuests = (quests) => { * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다. */ -const completeQuest = async (userId, eventPeriod, quest) => { +const completeQuest = async (userId, timestamp, eventPeriod, quest) => { try { // 1단계: 유저의 EventStatus를 가져옵니다. const eventStatus = await eventStatusModel.findOne({ userId }).lean(); if (!eventStatus) return null; // 2단계: 이벤트 기간인지 확인합니다. - if (req.timestamp >= eventPeriod.end || req.timestamp < eventPeriod.start) { + if (timestamp >= eventPeriod.end || timestamp < eventPeriod.start) { logger.info( `User ${userId} failed to complete auto-disabled ${quest.id}Quest` ); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index bd31cb04..425cb6d1 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -51,7 +51,7 @@ const createUserGlobalStateHandler = async (req, res) => { }); await eventStatus.save(); - await contract.completeFirstLoginQuest(req.userOid); + await contract.completeFirstLoginQuest(req.userOid, req.timestamp); res.json({ result: true }); } catch (err) { diff --git a/src/services/rooms.js b/src/services/rooms.js index eb158278..4ef35731 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -87,7 +87,7 @@ const createHandler = async (req, res) => { const roomObjectFormated = formatSettlement(roomObject); // 이벤트 코드입니다. - await contracts?.completeFirstRoomCreationQuest(user._id); + await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp); return res.send(roomObjectFormated); } catch (err) { @@ -492,8 +492,12 @@ const commitPaymentHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completePayingQuest(user._id, roomObject); - await contracts?.completePayingAndSendingQuest(roomObject); + await contracts?.completePayingQuest( + req.userOid, + req.timestamp, + roomObject + ); + await contracts?.completePayingAndSendingQuest(req.timestamp, roomObject); // 수정한 방 정보를 반환합니다. res.send(formatSettlement(roomObject, { isOver: true })); @@ -562,8 +566,12 @@ const settlementHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completeSendingQuest(user._id, roomObject); - await contracts?.completePayingAndSendingQuest(roomObject); + await contracts?.completeSendingQuest( + req.userOid, + req.timestamp, + roomObject + ); + await contracts?.completePayingAndSendingQuest(req.timestamp, roomObject); // 수정한 방 정보를 반환합니다. res.send(formatSettlement(roomObject, { isOver: true })); diff --git a/src/services/users.js b/src/services/users.js index b7b47e41..f4a00d6a 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -46,7 +46,10 @@ const editNicknameHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. - await contracts?.completeNicknameChangingQuest(req.userOid); + await contracts?.completeNicknameChangingQuest( + req.userOid, + req.timestamp + ); res.status(200).send("User/editNickname : edit user nickname successful"); } else { @@ -68,7 +71,11 @@ const editAccountHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. - await contracts?.completeAccountChangingQuest(req.userOid, newAccount); + await contracts?.completeAccountChangingQuest( + req.userOid, + req.timestamp, + newAccount + ); res.status(200).send("User/editAccount : edit user account successful"); } else { From 4a258ffebf333ffd81b63526c640a75d16dfbe23 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 19:37:22 +0900 Subject: [PATCH 06/13] Add: add validator to /events/2023fall/items/purchase/:itemId endpoint --- src/lottery/routes/items.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 539288bc..f4a39632 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -4,7 +4,16 @@ const router = express.Router(); const itemsHandlers = require("../services/items"); const auth = require("../../middlewares/auth"); +const { param } = require("express-validator"); +const validator = require("../../middlewares/validator"); + router.get("/list", itemsHandlers.listHandler); -router.post("/purchase/:itemId", auth, itemsHandlers.purchaseHandler); +router.post( + "/purchase/:itemId", + auth, + param("itemId").isMongoId(), + validator, + itemsHandlers.purchaseHandler +); module.exports = router; From e017c782055d6f33a93db493c267c5a8cfec50ae Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 21:45:18 +0900 Subject: [PATCH 07/13] Refactor: global-state endpoint can be accessed without session --- src/lottery/routes/docs/globalState.js | 4 +-- src/lottery/routes/globalState.js | 6 ++-- src/lottery/services/globalState.js | 43 +++++++++++++------------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 8a2afa77..12fd93bb 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -16,7 +16,7 @@ globalStateDocs[`${apiPrefix}/`] = { schema: { type: "object", required: [ - "agreement", + "isAgree", "creditAmount", "completedQuests", "ticket1Amount", @@ -24,7 +24,7 @@ globalStateDocs[`${apiPrefix}/`] = { "quests", ], properties: { - agreement: { + isAgree: { type: "boolean", description: "유저의 이벤트 참여 동의 여부", example: true, diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js index 4cb17a4a..e36828a1 100644 --- a/src/lottery/routes/globalState.js +++ b/src/lottery/routes/globalState.js @@ -2,11 +2,9 @@ const express = require("express"); const router = express.Router(); const globalStateHandlers = require("../services/globalState"); - -// 라우터 접근 시 로그인 필요 -router.use(require("../../middlewares/auth")); +const auth = require("../../middlewares/auth"); router.get("/", globalStateHandlers.getUserGlobalStateHandler); -router.post("/create", globalStateHandlers.createUserGlobalStateHandler); +router.post("/create", auth, globalStateHandlers.createUserGlobalStateHandler); module.exports = router; diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 425cb6d1..0b0a1cc1 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -1,5 +1,6 @@ const { eventStatusModel } = require("../modules/stores/mongo"); const logger = require("../../modules/logger"); +const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const { eventMode } = require("../../../loadenv"); const contract = eventMode @@ -9,27 +10,27 @@ const quests = contract ? Object.values(contract.quests) : undefined; const getUserGlobalStateHandler = async (req, res) => { try { - const eventStatus = await eventStatusModel - .findOne({ userId: req.userOid }) - .lean(); - if (eventStatus) - res.json({ - agreement: true, - creditAmount: eventStatus.creditAmount, - completedQuests: eventStatus.completedQuests, - ticket1Amount: eventStatus.ticket1Amount, - ticket2Amount: eventStatus.ticket2Amount, - quests, - }); - else - res.json({ - agreement: false, - creditAmount: 0, - completedQuests: [], - ticket1Amount: 0, - ticket2Amount: 0, - quests, - }); + const result = { + isAgree: false, + creditAmount: 0, + completedQuests: [], + ticket1Amount: 0, + ticket2Amount: 0, + quests, + }; + + const userId = isLogin(req) ? getLoginInfo(req).oid : null; + if (!userId) return res.json(result); + + const eventStatus = await eventStatusModel.findOne({ userId }).lean(); + if (eventStatus) { + result.isAgree = true; + result.creditAmount = eventStatus.creditAmount; + result.ticket1Amount = eventStatus.ticket1Amount; + result.ticket2Amount = eventStatus.ticket2Amount; + } + + res.json(result); } catch (err) { logger.error(err); res.status(500).json({ error: "GlobalState/ : internal server error" }); From 89c96cfb0879a187205e4afb056e43b0d0396c3f Mon Sep 17 00:00:00 2001 From: static Date: Mon, 18 Sep 2023 22:44:28 +0900 Subject: [PATCH 08/13] Refactor: use ajv instead of express-validator --- src/lottery/routes/docs/itemsSchema.js | 11 +++++++++++ src/lottery/routes/items.js | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/lottery/routes/docs/itemsSchema.js b/src/lottery/routes/docs/itemsSchema.js index 227f2b08..1601f237 100644 --- a/src/lottery/routes/docs/itemsSchema.js +++ b/src/lottery/routes/docs/itemsSchema.js @@ -75,6 +75,17 @@ const itemsSchema = { ...itemBase, description: "랜덤박스를 구입한 경우에만 포함됩니다.", }, + purchaseHandler: { + type: "object", + required: ["itemId"], + properties: { + itemId: { + type: "string", + pattern: "^[a-fA-F\\d]{24}$", + }, + }, + errorMessage: "validation: bad request", + }, }; module.exports = itemsSchema; diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index f4a39632..a87d0f78 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -4,15 +4,14 @@ const router = express.Router(); const itemsHandlers = require("../services/items"); const auth = require("../../middlewares/auth"); -const { param } = require("express-validator"); -const validator = require("../../middlewares/validator"); +const { validateParams } = require("../../middlewares/ajv"); +const itemsSchema = require("./docs/itemsSchema"); router.get("/list", itemsHandlers.listHandler); router.post( "/purchase/:itemId", auth, - param("itemId").isMongoId(), - validator, + validateParams(itemsSchema.purchaseHandler), itemsHandlers.purchaseHandler ); From 9632c46dc6f76e2f0fadfc26a902037f6b6ed4cf Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Sep 2023 00:11:36 +0900 Subject: [PATCH 09/13] Fix: typo of the id of nicknameChangingQuest --- src/lottery/modules/contracts/2023fall.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js index ba656d4f..6bbe0e43 100644 --- a/src/lottery/modules/contracts/2023fall.js +++ b/src/lottery/modules/contracts/2023fall.js @@ -44,7 +44,7 @@ const quests = buildQuests({ reward: 50, maxCount: 3, }, - nicknameChaning: { + nicknameChanging: { name: "닉네임 변경", description: "", imageUrl: "", @@ -173,7 +173,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => { }; /** - * nicknameChaning 퀘스트의 완료를 요청합니다. + * nicknameChanging 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} @@ -185,7 +185,7 @@ const completeNicknameChangingQuest = async (userId, timestamp) => { userId, timestamp, eventPeriod, - quests.nicknameChaning + quests.nicknameChanging ); }; From b40f2dcbf5ab11d3a41463eca9dd3d542c7f531f Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Sep 2023 01:27:11 +0900 Subject: [PATCH 10/13] Refactor: apply @14KGun 's suggestions --- src/lottery/routes/globalState.js | 7 ++++-- src/lottery/routes/items.js | 6 +++-- src/lottery/services/globalState.js | 38 ++++++++++++++--------------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js index e36828a1..d0461218 100644 --- a/src/lottery/routes/globalState.js +++ b/src/lottery/routes/globalState.js @@ -2,9 +2,12 @@ const express = require("express"); const router = express.Router(); const globalStateHandlers = require("../services/globalState"); -const auth = require("../../middlewares/auth"); router.get("/", globalStateHandlers.getUserGlobalStateHandler); -router.post("/create", auth, globalStateHandlers.createUserGlobalStateHandler); + +// 아래의 Endpoint 접근 시 로그인 필요 +router.use(require("../../middlewares/auth")); + +router.post("/create", globalStateHandlers.createUserGlobalStateHandler); module.exports = router; diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index a87d0f78..95136a5f 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -2,15 +2,17 @@ const express = require("express"); const router = express.Router(); const itemsHandlers = require("../services/items"); -const auth = require("../../middlewares/auth"); const { validateParams } = require("../../middlewares/ajv"); const itemsSchema = require("./docs/itemsSchema"); router.get("/list", itemsHandlers.listHandler); + +// 아래의 Endpoint 접근 시 로그인 필요 +router.use(require("../../middlewares/auth")); + router.post( "/purchase/:itemId", - auth, validateParams(itemsSchema.purchaseHandler), itemsHandlers.purchaseHandler ); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 0b0a1cc1..804cc160 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -10,27 +10,25 @@ const quests = contract ? Object.values(contract.quests) : undefined; const getUserGlobalStateHandler = async (req, res) => { try { - const result = { - isAgree: false, - creditAmount: 0, - completedQuests: [], - ticket1Amount: 0, - ticket2Amount: 0, - quests, - }; - const userId = isLogin(req) ? getLoginInfo(req).oid : null; - if (!userId) return res.json(result); - - const eventStatus = await eventStatusModel.findOne({ userId }).lean(); - if (eventStatus) { - result.isAgree = true; - result.creditAmount = eventStatus.creditAmount; - result.ticket1Amount = eventStatus.ticket1Amount; - result.ticket2Amount = eventStatus.ticket2Amount; - } - - res.json(result); + const eventStatus = + userId && + (await eventStatusModel.findOne({ userId }, "-_id -userId -__v").lean()); + if (eventStatus) + return res.json({ + isAgree: true, + ...eventStatus, + quests, + }); + else + return res.json({ + isAgree: false, + completedQuests: [], + creditAmount: 0, + ticket1Amount: 0, + ticket2Amount: 0, + quests, + }); } catch (err) { logger.error(err); res.status(500).json({ error: "GlobalState/ : internal server error" }); From 1c5850f179222da2aa68a903f904926abedd08aa Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Sep 2023 01:49:02 +0900 Subject: [PATCH 11/13] Add: detect adPushAgreementQuest --- src/lottery/modules/contracts/2023fall.js | 24 +++++++++++++++++++++-- src/services/notifications.js | 10 ++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js index 6bbe0e43..4b29041e 100644 --- a/src/lottery/modules/contracts/2023fall.js +++ b/src/lottery/modules/contracts/2023fall.js @@ -209,8 +209,28 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { ); }; -const completeAdPushAgreementQuest = async () => { - // TODO +/** + * adPushAgreementQuest 퀘스트의 완료를 요청합니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. + * @returns {Promise} + * @description 알림 옵션을 변경할 때마다 호출해 주세요. + * @usage notifications/editOptionsHandler + */ +const completeAdPushAgreementQuest = async ( + userId, + timestamp, + advertisement +) => { + if (!advertisement) return null; + + return await completeQuest( + userId, + timestamp, + eventPeriod, + quests.adPushAgreement + ); }; const completeEventSharingOnInstagramQuest = async () => { diff --git a/src/services/notifications.js b/src/services/notifications.js index 799c8bb7..633f6739 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -4,6 +4,9 @@ const logger = require("../modules/logger"); const { registerDeviceToken, validateDeviceToken } = require("../modules/fcm"); +// 이벤트 코드입니다. +const { contracts } = require("../lottery"); + const registerDeviceTokenHandler = async (req, res) => { try { // 해당 FCM device token이 유효한지 검사합니다. @@ -104,6 +107,13 @@ const editOptionsHandler = async (req, res) => { .send("Notification/editOptions: deviceToken not found"); } + // 이벤트 코드입니다. + await contracts?.completeAdPushAgreementQuest( + req.userOid, + req.timestamp, + options.advertisement + ); + res.status(200).json(updatedNotificationOptions); } catch (err) { logger.error(err); From cd6370a8e0d4c7c9bd6678072d07d58de908e856 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Sep 2023 02:29:30 +0900 Subject: [PATCH 12/13] Docs: update type of timestamp parameter of completeQuest function --- src/lottery/modules/contracts/2023fall.js | 16 ++++++++-------- src/lottery/modules/quests.js | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lottery/modules/contracts/2023fall.js b/src/lottery/modules/contracts/2023fall.js index 4b29041e..abe2f0d8 100644 --- a/src/lottery/modules/contracts/2023fall.js +++ b/src/lottery/modules/contracts/2023fall.js @@ -84,7 +84,7 @@ const eventPeriod = { /** * firstLogin 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @usage lottery/globalState/createUserGlobalStateHandler */ @@ -94,7 +94,7 @@ const completeFirstLoginQuest = async (userId, timestamp) => { /** * payingAndSending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이거나, 모든 참가자가 정산 또는 송금을 완료하지 않았다면 요청하지 않습니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. * @param {number} roomObject.settlementTotal - 정산 또는 송금이 완료된 참여자 수입니다. @@ -122,7 +122,7 @@ const completePayingAndSendingQuest = async (timestamp, roomObject) => { /** * firstRoomCreation 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 방을 만들 때마다 호출해 주세요. * @usage rooms/createHandler @@ -143,7 +143,7 @@ const completeRoomSharingQuest = async () => { /** * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. * @returns {Promise} @@ -159,7 +159,7 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { /** * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. * @returns {Promise} @@ -175,7 +175,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => { /** * nicknameChanging 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 닉네임을 변경할 때마다 호출해 주세요. * @usage users/editNicknameHandler @@ -192,7 +192,7 @@ const completeNicknameChangingQuest = async (userId, timestamp) => { /** * accountChanging 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {string} newAccount - 변경된 계좌입니다. * @returns {Promise} * @description 계좌를 변경할 때마다 호출해 주세요. @@ -212,7 +212,7 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { /** * adPushAgreementQuest 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. * @returns {Promise} * @description 알림 옵션을 변경할 때마다 호출해 주세요. diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 4ec5c4f3..ce2d22d4 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -44,7 +44,7 @@ const buildQuests = (quests) => { /** * 퀘스트 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} eventPeriod - 이벤트의 기간입니다. * @param {Date} eventPeriod.start - 이벤트의 시작 시각(Inclusive)입니다. * @param {Date} eventPeriod.end - 이벤트의 종료 시각(Exclusive)입니다. From 614a716ff088769f4db45e9980ec2825d1ddf446 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 19 Sep 2023 02:37:31 +0900 Subject: [PATCH 13/13] Refactor: rename isAgree to isAgreeOnTermsOfEvent --- src/lottery/routes/docs/globalState.js | 2 +- src/lottery/services/globalState.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 12fd93bb..6883ac33 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -24,7 +24,7 @@ globalStateDocs[`${apiPrefix}/`] = { "quests", ], properties: { - isAgree: { + isAgreeOnTermsOfEvent: { type: "boolean", description: "유저의 이벤트 참여 동의 여부", example: true, diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 804cc160..7c7f7c5f 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -16,13 +16,13 @@ const getUserGlobalStateHandler = async (req, res) => { (await eventStatusModel.findOne({ userId }, "-_id -userId -__v").lean()); if (eventStatus) return res.json({ - isAgree: true, + isAgreeOnTermsOfEvent: true, ...eventStatus, quests, }); else return res.json({ - isAgree: false, + isAgreeOnTermsOfEvent: false, completedQuests: [], creditAmount: 0, ticket1Amount: 0,