diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index 06a6c118..063e5f45 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -82,6 +82,92 @@ itemsDocs[`${apiPrefix}/`] = { }, }, }; +itemsDocs[`${apiPrefix}/{itemId}`] = { + get: { + tags: [`${apiPrefix}`], + 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}`], @@ -114,6 +200,7 @@ itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { "profileImageUrl", "amount", "probability", + "rank", ], properties: { nickname: { @@ -136,6 +223,11 @@ itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { description: "유저가 상품에 당첨될 확률", example: 0.1, }, + rank: { + type: "number", + description: "순위", + example: 1, + }, }, }, }, diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index 7e570b5a..d224ba70 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -3,6 +3,9 @@ const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const { objectId } = require("../../../../modules/patterns"); const itemsZod = { + getItemHandler: z.object({ + itemId: z.string().regex(objectId), + }), getItemLeaderboardHandler: z.object({ itemId: z.string().regex(objectId), }), diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 135617b8..0de8be18 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -6,6 +6,11 @@ const { itemsZod } = require("./docs/schemas/itemsSchema"); const itemsHandlers = require("../services/items"); router.get("/", itemsHandlers.getItemsHandler); +router.get( + "/:itemId", + validateParams(itemsZod.getItemHandler), + itemsHandlers.getItemHandler +); router.get( "/leaderboard/:itemId", validateParams(itemsZod.getItemLeaderboardHandler), diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 9196ad53..eb00535d 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -25,6 +25,24 @@ const getItemsHandler = async (req, res) => { } }; +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; @@ -113,6 +131,7 @@ const getItemLeaderboardHandler = async (req, res) => { profileImageUrl: userInfo.profileImageUrl, amount: user.amount, probability: user.probability, + rank: user.rank, }; }) ); @@ -363,6 +382,7 @@ const purchaseItemHandler = async (req, res) => { module.exports = { getItemsHandler, + getItemHandler, getItemLeaderboardHandler, purchaseItemHandler, }; 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/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, };