Skip to content

Commit

Permalink
Merge pull request #470 from sparcs-kaist/#457-add-invite-system
Browse files Browse the repository at this point in the history
#457 추천인 시스템 구현
  • Loading branch information
kmc7468 authored Feb 22, 2024
2 parents d5d302e + e08dc79 commit c6e617b
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 10 deletions.
1 change: 1 addition & 0 deletions src/lottery/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ lotteryRouter.use(require("../middlewares/originValidator"));

// [Router] APIs
lotteryRouter.use("/globalState", require("./routes/globalState"));
lotteryRouter.use("/invite", require("./routes/invite"));
lotteryRouter.use("/transactions", require("./routes/transactions"));
lotteryRouter.use("/items", require("./routes/items"));
lotteryRouter.use("/publicNotice", require("./routes/publicNotice"));
Expand Down
18 changes: 14 additions & 4 deletions src/lottery/modules/contracts.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,20 @@ const completeAdPushAgreementQuest = async (
* @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요.
*/
const completeEventSharingQuest = async (userId, timestamp) => {
return [
await completeQuest(userId, timestamp, quests.eventSharing),
await completeQuest(userId, timestamp, quests.eventSharing5),
];
const eventSharingResult = await completeQuest(
userId,
timestamp,
quests.eventSharing
);
if (!eventSharingResult || eventSharingResult.questCount % 5 !== 0)
return [eventSharingResult, null];

const eventSharing5Result = await completeQuest(
userId,
timestamp,
quests.eventSharing5
);
return [eventSharingResult, eventSharing5Result];
};

module.exports = {
Expand Down
1 change: 1 addition & 0 deletions src/lottery/modules/quests.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const completeQuest = async (userId, timestamp, quest) => {
logger.info(`User ${userId} successfully completed ${quest.id}Quest`);
return {
quest,
questCount: questCount + 1,
transactionsId,
};
} catch (err) {
Expand Down
4 changes: 4 additions & 0 deletions src/lottery/modules/stores/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ const eventStatusSchema = Schema({
type: Schema.Types.ObjectId,
ref: "User",
}, // 이 사용자를 초대한 사용자
isEnabledInviteUrl: {
type: Boolean,
default: false,
}, // 초대 링크 활성화 여부
});

const questSchema = Schema({
Expand Down
75 changes: 75 additions & 0 deletions src/lottery/routes/docs/invite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
const { eventConfig } = require("../../../../loadenv");
const apiPrefix = `/events/${eventConfig?.mode}/invite`;

const inviteDocs = {};
inviteDocs[`${apiPrefix}/search/:inviter`] = {
get: {
tags: [`${apiPrefix}`],
summary: "초대자 정보 조회",
description: "초대자의 정보를 조회합니다.",
requestBody: {
description: "",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/searchInviterHandler",
},
},
},
},
responses: {
200: {
description: "",
content: {
"application/json": {
schema: {
type: "object",
required: ["nickname", "profileImageUrl"],
properties: {
nickname: {
type: "string",
description: "초대자의 닉네임",
example: "asdf",
},
profileImageUrl: {
type: "string",
description: "초대자의 프로필 이미지 URL",
example: "IMAGE URL",
},
},
},
},
},
},
},
},
};
inviteDocs[`${apiPrefix}/create`] = {
post: {
tags: [`${apiPrefix}`],
summary: "초대 링크 생성",
description: "초대 링크를 생성합니다.",
responses: {
200: {
description: "",
content: {
"application/json": {
schema: {
type: "object",
required: ["inviteUrl"],
properties: {
inviteUrl: {
type: "string",
description: "초대 링크",
example: "INVITE URL",
},
},
},
},
},
},
},
},
};

module.exports = inviteDocs;
15 changes: 15 additions & 0 deletions src/lottery/routes/docs/inviteSchema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const inviteSchema = {
searchInviterHandler: {
type: "object",
required: ["inviter"],
properties: {
inviter: {
type: "string",
pattern: "^[a-fA-F\\d]{24}$",
},
},
errorMessage: "validation: bad request",
},
};

module.exports = inviteSchema;
6 changes: 3 additions & 3 deletions src/lottery/routes/docs/quests.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const { eventConfig } = require("../../../../loadenv");
const apiPrefix = `/events/${eventConfig?.mode}/quests`;

const eventsDocs = {};
eventsDocs[`${apiPrefix}/complete/:questId`] = {
const questsDocs = {};
questsDocs[`${apiPrefix}/complete/:questId`] = {
post: {
tags: [`${apiPrefix}`],
summary: "퀘스트 완료 요청",
Expand Down Expand Up @@ -59,4 +59,4 @@ eventsDocs[`${apiPrefix}/complete/:questId`] = {
},
};

module.exports = eventsDocs;
module.exports = questsDocs;
1 change: 1 addition & 0 deletions src/lottery/routes/docs/questsSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const questsSchema = {
enum: ["roomSharing"],
},
},
errorMessage: "validation: bad request",
},
};

Expand Down
10 changes: 9 additions & 1 deletion src/lottery/routes/docs/swaggerDocs.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const globalStateDocs = require("./globalState");
const inviteDocs = require("./invite");
const itemsDocs = require("./items");
const publicNoticeDocs = require("./publicNotice");
const questsDocs = require("./quests");
const transactionsDocs = require("./transactions");

const itemsSchema = require("./itemsSchema");
const globalStateSchema = require("./globalStateSchema");
const inviteSchema = require("./inviteSchema");
const itemsSchema = require("./itemsSchema");
const questsSchema = require("./questsSchema");

const { eventConfig } = require("../../../../loadenv");
Expand All @@ -17,6 +19,10 @@ const eventSwaggerDocs = {
name: `${apiPrefix}/globalState`,
description: "이벤트 - Global State 관련 API",
},
{
name: `${apiPrefix}/invite`,
description: "이벤트 - 초대 링크 관련 API",
},
// 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다.
//
// {
Expand All @@ -38,6 +44,7 @@ const eventSwaggerDocs = {
],
paths: {
...globalStateDocs,
...inviteDocs,
//...itemsDocs,
...publicNoticeDocs,
...questsDocs,
Expand All @@ -46,6 +53,7 @@ const eventSwaggerDocs = {
components: {
schemas: {
...globalStateSchema,
...inviteSchema,
//...itemsSchema,
...questsSchema,
},
Expand Down
22 changes: 22 additions & 0 deletions src/lottery/routes/invite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const express = require("express");

const router = express.Router();
const inviteHandlers = require("../services/invite");

const { validateParams } = require("../../middlewares/ajv");
const inviteSchema = require("./docs/inviteSchema");

router.get(
"/search/:inviter",
validateParams(inviteSchema.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;
22 changes: 20 additions & 2 deletions src/lottery/services/globalState.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,19 @@ const createUserGlobalStateHandler = async (req, res) => {
.status(400)
.json({ error: "GlobalState/Create : already created" });

/* Request의 inviter 필드가 설정되어 있는데,
1. 해당되는 유저가 이벤트에 참여하지 않았거나,
2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나,
3. 해당되는 유저의 초대 링크가 활성화되지 않았으면,
에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */
const inviterStatus =
req.body.inviter &&
(await eventStatusModel.findOne({ _id: req.body.inviter }).lean());
if (
req.body.inviter &&
(await eventStatusModel.findOne({ _id: req.body.inviter }).lean())
(!inviterStatus ||
inviterStatus.isBanned ||
!inviterStatus.isEnabledInviteUrl)
)
return res.status(400).json({
error: "GlobalState/Create : inviter did not participate in the event",
Expand Down Expand Up @@ -118,7 +128,15 @@ const createUserGlobalStateHandler = async (req, res) => {

await contracts.completeFirstLoginQuest(req.userOid, req.timestamp);

res.json({ result: true });
if (req.body.inviter) {
await contracts.completeEventSharingQuest(req.userOid, req.timestamp);
await contracts.completeEventSharingQuest(
inviterStatus.userId,
req.timestamp
);
}

return res.json({ result: true });
} catch (err) {
logger.error(err);
res
Expand Down
66 changes: 66 additions & 0 deletions src/lottery/services/invite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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,
};

0 comments on commit c6e617b

Please sign in to comment.