Skip to content

Commit

Permalink
Merge pull request #576 from sparcs-kaist/dev
Browse files Browse the repository at this point in the history
Main branch update from Dev branch
  • Loading branch information
kmc7468 authored Feb 20, 2025
2 parents fdff9ea + 20791ff commit bb6f1b4
Show file tree
Hide file tree
Showing 27 changed files with 788 additions and 146 deletions.
4 changes: 2 additions & 2 deletions src/loadenv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export const eventConfig = (process.env.EVENT_CONFIG &&
mode: "2025spring",
credit: { name: "넙죽코인", initialAmount: 0 },
period: {
startAt: "2025-02-19T00:00:00+09:00",
endAt: "2024-03-13T00:00:00+09:00",
startAt: "2025-02-21T00:00:00+09:00",
endAt: "2025-03-13T00:00:00+09:00",
},
};
export const naverMap = {
Expand Down
3 changes: 3 additions & 0 deletions src/lottery/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
questModel,
itemModel,
transactionModel,
quizModel,
} = require("./modules/stores/mongo");

const { buildResource } = require("../modules/adminResource");
Expand Down Expand Up @@ -33,6 +34,7 @@ lotteryRouter.use("/transactions", require("./routes/transactions"));
lotteryRouter.use("/items", require("./routes/items"));
// lotteryRouter.use("/publicNotice", require("./routes/publicNotice"));
lotteryRouter.use("/quests", require("./routes/quests"));
lotteryRouter.use("/quizzes", require("./routes/quizzes").default);

// [AdminJS] AdminJS에 표시할 Resource 생성
const resources =
Expand All @@ -41,6 +43,7 @@ const resources =
buildResource()(questModel),
buildResource([addOneItemStockAction, addFiveItemStockAction])(itemModel),
buildResource()(transactionModel),
buildResource()(quizModel),
]) ||
[];

Expand Down
2 changes: 1 addition & 1 deletion src/lottery/middlewares/checkBanned.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { eventStatusModel } = require("../modules/stores/mongo");
const logger = require("../../modules/logger");
const logger = require("@/modules/logger").default;

/**
* 사용자가 차단 되었는지 여부를 판단합니다.
Expand Down
84 changes: 63 additions & 21 deletions src/lottery/modules/contracts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { buildQuests, completeQuest } = require("./quests");
const mongoose = require("mongoose");
import logger from "../../modules/logger";
const logger = require("@/modules/logger").default;

const { eventConfig } = require("@/loadenv");
const eventPeriod = eventConfig && {
Expand All @@ -13,25 +13,25 @@ const quests = buildQuests({
firstLogin: {
name: "첫 발걸음",
description:
"이벤트 참여만 해도 송편코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 송편코인을 받아 보세요.",
"이벤트 참여만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 넙죽코인을 받아보세요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_firstLogin.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_firstLogin.png",
reward: 200,
},
firstRoomCreation: {
name: "첫 방 개설",
description:
"원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 <b>방 개설 페이지</b>에서 방을 직접 개설해 보세요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_firstRoomCreation.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_firstRoomCreation.png",
reward: 500,
},
roomSharing: {
name: "이 택시팟은 진짜 유명한 택시팟임",
name: "4명!",
description:
"방을 공유해 친구들을 택시팟에 초대해 보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 <b>공유하기 버튼</b>을 찾을 수 있어요.",
"방을 공유해 친구들을 택시팟에 초대해 보세요. 최대 4명까지 참여할 수 있어요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 <b>공유하기 버튼</b>을 찾을 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_roomSharing.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_roomSharing.png",
reward: 500,
isApiRequired: true,
},
Expand All @@ -40,7 +40,7 @@ const quests = buildQuests({
description:
"2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산을 요청해 보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 <b>+ 버튼</b>을 눌러 찾을 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_fareSettlement.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_fareSettlement.png",
reward: 2000,
maxCount: 0,
},
Expand All @@ -49,7 +49,7 @@ const quests = buildQuests({
description:
"2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해 주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 <b>+ 버튼</b>을 눌러 찾을 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_farePayment.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_farePayment.png",
reward: 2000,
maxCount: 0,
},
Expand All @@ -58,50 +58,68 @@ const quests = buildQuests({
description:
"닉네임을 변경하여 자신을 표현하세요. <b>마이 페이지</b>의 <b>수정하기</b> 버튼을 눌러 닉네임을 수정할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_nicknameChanging.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_nicknameChanging.png",
reward: 500,
},
accountChanging: {
name: "계좌 등록을 해야 능률이 올라갑니다",
description:
"정산하기 기능을 더욱 빠르게 이용할 수 있다고? 계좌 번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. <b>마이 페이지</b>의 <b>수정하기</b> 버튼을 눌러 계좌 번호를 등록 또는 수정할 수 있어요.",
"정산하기 기능을 더욱 빠르게 이용할 수 있다고? 계좌 번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. <b>마이 페이지</b>의 <b>수정하기</b> 버튼을 눌러 계좌 번호를 등록할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_accountChanging.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_accountChanging.png",
reward: 500,
},
adPushAgreement: {
name: "Taxi의 소울메이트",
name: "잊을만하면 찾아오는 Taxi",
description:
"Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 Taxi 앱 사용자가 있을 때 알려드릴 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_adPushAgreement.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_adPushAgreement.png",
reward: 500,
},
eventSharing: {
name: "Taxi를 아십니까",
description:
"내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요. 다른 사람의 초대를 받아 이벤트에 참여한 경우에도 이 퀘스트가 달성돼요.",
"내가 초대한 사람이 이벤트에 참여하면 넙죽코인을 드려요. 다른 사람의 초대를 받아 이벤트에 참여한 경우에도 이 퀘스트가 달성돼요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_eventSharing.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_eventSharing.png",
reward: 700,
maxCount: 0,
},
indirectEventSharing: {
name: "코인이 복사가 된다고?",
description:
"내가 초대한 사람이 다른 누군가를 이벤트에 초대하면 넙죽코인을 받아요. 그 사람이 또 다른 누군가를 초대하면 또 넙죽코인을 받아요. 그 사람이 또 …",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_indirectEventSharing.png",
reward: 300,
maxCount: 10,
},
dailyAttendance: {
name: "매일매일 출석 췤!",
description:
"매일 Taxi에 접속하면 하루 한 번 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요.",
"매일 Taxi에 접속해서 <b>밸런스 게임에 참여</b>하면 하루에 한 번씩 넙죽코인을 드려요! 매일 Taxi에서 택시팟 둘러보고 넙죽코인도 받아가세요. 밸런스 게임은 23시 55분까지만 참여할 수 있어요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_dailyAttendance.png",
reward: 700,
maxCount: 17,
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_dailyAttendance.png",
reward: 100,
maxCount: 20,
isApiRequired: true,
},
answerCorrectly: {
name: "이기는 편 우리 편",
description:
"전날의 밸런스 게임에서 응답자 수가 많은 선택지를 고른 경우 추가 넙죽코인을 드려요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_answerCorrectly.png",
reward: 600,
maxCount: 20,
},
itemPurchase: {
name: "Taxi에서 산 응모권",
description:
"응모권 교환소에서 아무 경품 응모권이나 구매해 보세요. Taxi에서 판매하는 응모권은 모두 정품이니 안심해도 좋아요.",
imageUrl:
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_itemPurchase.png",
"https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2025spring/quest_itemPurchase.png",
reward: 500,
},
});
Expand Down Expand Up @@ -242,6 +260,28 @@ const completeEventSharingQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.eventSharing);
};

/**
* indirectEventSharing 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @returns {Promise}
* @usage lottery/globalState - createUserGlobalStateHandler
*/
const completeIndirectEventSharingQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.indirectEventSharing);
};

/**
* completeAnswerCorrectlyQuest 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
* @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다.
* @returns {Promise}
* @usage lottery/schedules/dailyQuiz - determineQuizResult
*/
export const completeAnswerCorrectlyQuest = async (userId, timestamp) => {
return await completeQuest(userId, timestamp, quests.answerCorrectly);
};

/**
* itemPurchase 퀘스트의 완료를 요청합니다.
* @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다.
Expand All @@ -263,5 +303,7 @@ module.exports = {
completeAccountChangingQuest,
completeAdPushAgreementQuest,
completeEventSharingQuest,
completeIndirectEventSharingQuest,
completeAnswerCorrectlyQuest,
completeItemPurchaseQuest,
};
4 changes: 2 additions & 2 deletions src/lottery/modules/items.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { itemModel } = require("./stores/mongo");
const { buildRecordAction } = require("../../modules/adminResource");
const logger = require("../../modules/logger");
const { buildRecordAction } = require("@/modules/adminResource");
const logger = require("@/modules/logger").default;

const addItemStockActionHandler = (count) => async (req, res, context) => {
const itemId = context.record.params._id;
Expand Down
2 changes: 1 addition & 1 deletion src/lottery/modules/quests.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const {
itemModel,
transactionModel,
} = require("./stores/mongo");
const logger = require("../../modules/logger");
const logger = require("@/modules/logger").default;
const mongoose = require("mongoose");

const { eventConfig } = require("@/loadenv");
Expand Down
21 changes: 21 additions & 0 deletions src/lottery/modules/quizzes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { quizModel } from "../modules/stores/mongo";

/**
* 특정 날짜의 퀴즈를 조회하는 함수
* @param {Date} date - 조회할 날짜
* @returns {Promise<any>} - 조회된 퀴즈 객체 반환 (없으면 null)
*/
export const getQuizByDate = async (date: Date) => {
const todayMidnight = new Date(date);
todayMidnight.setHours(0, 0, 0, 0);

const tomorrowMidnight = new Date(todayMidnight);
tomorrowMidnight.setDate(todayMidnight.getDate() + 1);

return await quizModel.findOne({
quizDate: {
$gte: todayMidnight,
$lt: tomorrowMidnight,
},
});
};
2 changes: 1 addition & 1 deletion src/lottery/modules/slackNotification.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { sendTextToReportChannel } = require("../../modules/slackNotification");
const { sendTextToReportChannel } = require("@/modules/slackNotification");

const generateContent = (name, userIds, roomIds = []) => {
if (userIds.length === 0) return "";
Expand Down
35 changes: 34 additions & 1 deletion src/lottery/modules/stores/mongo.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ const itemSchema = Schema({
}, // 상품의 실제 재고
itemType: {
type: Number,
enum: [0, 1, 2, 3],
enum: [0, 1, 2, 3, 4], // 0: 일반 상품, 1: 일반 응모권, 2: 고급 응모권, 3: 랜덤 박스, 4: 쿠폰
required: true,
},
isRandomItem: {
Expand All @@ -128,6 +128,15 @@ const itemSchema = Schema({
min: 0,
validate: integerValidator,
},
couponCode: {
type: String,
unique: true,
},
couponReward: {
type: Number,
min: 0,
validate: integerValidator,
},
});

const transactionSchema = Schema({
Expand Down Expand Up @@ -169,6 +178,29 @@ transactionSchema.set("timestamps", {
updatedAt: false,
});

const quizSchema = Schema({
quizDate: { type: Date, required: true, unique: true },
title: { type: String, required: true },
content: { type: String, required: true },
image: { type: String, required: true },
// A와 B는 최종 답안, C와 D는 인원 수에 따라 dailyQuiz.ts 에서 A와 B로 결정됩니다.
answer: { type: String, enum: ["A", "B", "C", "D"], default: "C" },
countA: { type: Number, default: 0 },
countB: { type: Number, default: 0 },
answers: [
{
userId: { type: Schema.Types.ObjectId, ref: "User", required: true },
answer: { type: String, enum: ["A", "B"] },
submittedAt: { type: Date, required: true },
status: {
type: String,
enum: ["correct", "wrong", "unknown", "draw"],
default: "unknown",
},
},
],
});

module.exports = {
eventStatusModel: mongoose.model(
`${modelNamePrefix}EventStatus`,
Expand All @@ -180,4 +212,5 @@ module.exports = {
`${modelNamePrefix}Transaction`,
transactionSchema
),
quizModel: mongoose.model(`${modelNamePrefix}Quiz`, quizSchema),
};
42 changes: 41 additions & 1 deletion src/lottery/routes/docs/items.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ itemsDocs[`${apiPrefix}/purchase/{itemId}`] = {
in: "path",
name: "itemId",
required: true,
description: "리더보드를 조회할 상품의 ObjectId",
description: "구입할 상품의 ObjectId",
example: "ITEM ID",
},
],
Expand Down Expand Up @@ -316,5 +316,45 @@ itemsDocs[`${apiPrefix}/purchase/{itemId}`] = {
},
},
};
itemsDocs[`${apiPrefix}/useCoupon/{couponCode}`] = {
post: {
tags: [`${apiPrefix}`],
summary: "쿠폰 사용",
description: "쿠폰을 사용합니다.",
parameters: [
{
in: "path",
name: "couponCode",
required: true,
description: "사용할 쿠폰의 코드",
example: "COUPON CODE",
},
],
responses: {
200: {
content: {
"application/json": {
schema: {
type: "object",
required: ["result", "reward"],
properties: {
result: {
type: "boolean",
description: "성공 여부. 항상 true입니다.",
example: true,
},
reward: {
type: "number",
description: "쿠폰의 사용 보상. 0 이상의 정수입니다.",
example: 500,
},
},
},
},
},
},
},
},
};

module.exports = itemsDocs;
3 changes: 3 additions & 0 deletions src/lottery/routes/docs/schemas/itemsSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const itemsZod = {
purchaseItemHandlerBody: z.object({
amount: z.number().int().positive(),
}),
useCouponHandlerParams: z.object({
couponCode: z.string().regex(/^[a-zA-Z0-9]+$/),
}),
};

const itemsSchema = zodToSchemaObject(itemsZod);
Expand Down
Loading

0 comments on commit bb6f1b4

Please sign in to comment.