From 1dea168ada4c4d3b36ef6564316643e80e8bf8f0 Mon Sep 17 00:00:00 2001 From: static Date: Sat, 23 Sep 2023 20:48:39 +0900 Subject: [PATCH 1/7] Fix: initial creditAmount --- src/lottery/services/globalState.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index c36ac9f1..13ce9bb7 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -47,6 +47,7 @@ const createUserGlobalStateHandler = async (req, res) => { eventStatus = new eventStatusModel({ userId: req.userOid, + creditAmount: 100, // 초기 송편 개수는 0개가 아닌 100개입니다. }); await eventStatus.save(); From ae6f35f906ab4b195f7e26cf68ad96f175798b0e Mon Sep 17 00:00:00 2001 From: static Date: Sun, 24 Sep 2023 18:32:33 +0900 Subject: [PATCH 2/7] Add: new leaderboard probability model --- src/lottery/services/publicNotice.js | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 62421bbd..e6e1e6f9 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -35,6 +35,25 @@ const getRecentPurchaceItemListHandler = async (req, res) => { } }; +const calculateProbabilityV2 = (users, weightSum, base, weight) => { + // 유저 수가 상품 수보다 적거나 같으면 무조건 상품을 받게된다. + if (users.length <= 15) return 1; + + /** + * 경험적으로 발견한 사실 + * + * p를 에어팟 당첨 확률이라고 하자. + * 모든 유저의 p값이 1/15 미만일 경우, 실제 당첨 확률은 15p이다. + * 그렇지 않은 경우, 실제 당첨 확률은 1-a^p꼴의 지수함수를 따른다. + * + * 계산 과정 + * a는 유저 수, 전체 티켓 수, 티켓 분포에 의해 결정되는 값으로, 현실적으로 계산하기 어렵다. + * 따라서, 모든 유저가 같은 수의 티켓을 가지고 있다고 가정하고 a를 계산한 뒤, 이를 확률 계산에 사용한다. + */ + if (base !== null) return 1 - Math.pow(base, weight); + else return (weight / weightSum) * 15; +}; + const getTicketLeaderboardHandler = async (req, res) => { try { const users = await eventStatusModel @@ -60,6 +79,11 @@ const getTicketLeaderboardHandler = async (req, res) => { } return before + user.weight; }, 0); + const isExponential = + sortedUsers.find((user) => user.weight >= weightSum / 15) != undefined; + const base = isExponential + ? Math.pow(1 - 15 / users.length, users.length / weightSum) + : null; const leaderboard = await Promise.all( sortedUsers.slice(0, 20).map(async (user) => { @@ -74,6 +98,12 @@ const getTicketLeaderboardHandler = async (req, res) => { ticket1Amount: user.ticket1Amount, ticket2Amount: user.ticket2Amount, probability: user.weight / weightSum, + probabilityV2: calculateProbabilityV2( + users, + weightSum, + base, + user.weight + ), }; }) ); @@ -87,6 +117,12 @@ const getTicketLeaderboardHandler = async (req, res) => { leaderboard, rank: rank + 1, probability: sortedUsers[rank].weight / weightSum, + probabilityV2: calculateProbabilityV2( + users, + weightSum, + base, + sortedUsers[rank].weight + ), }); else res.json({ leaderboard }); } catch (err) { From 86ffd0729ee8abb7c794ca167658d0d6f6dcc70b Mon Sep 17 00:00:00 2001 From: static Date: Sun, 24 Sep 2023 21:04:00 +0900 Subject: [PATCH 3/7] Docs: describe probabilityV2 field --- src/lottery/routes/docs/publicNotice.js | 11 +++++++++++ src/lottery/services/publicNotice.js | 11 +++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index ca30cfb9..71e9e3a3 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -59,6 +59,7 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { "ticket1Amount", "ticket2Amount", "probability", + "probabilityV2", ], properties: { nickname: { @@ -86,6 +87,11 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { description: "1등 당첨 확률", example: 0.001, }, + probabilityV2: { + type: "number", + description: "근사적인 상품 당첨 확률", + example: 0.015, + }, }, }, }, @@ -99,6 +105,11 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { description: "1등 당첨 확률", example: 0.00003, }, + probabilityV2: { + type: "number", + description: "근사적인 상품 당첨 확률", + example: 0.00045, + }, }, }, }, diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index e6e1e6f9..b58e41be 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -42,13 +42,20 @@ const calculateProbabilityV2 = (users, weightSum, base, weight) => { /** * 경험적으로 발견한 사실 * - * p를 에어팟 당첨 확률이라고 하자. + * p를 에어팟 당첨 확률, M을 전체 티켓 수라고 하자. * 모든 유저의 p값이 1/15 미만일 경우, 실제 당첨 확률은 15p이다. - * 그렇지 않은 경우, 실제 당첨 확률은 1-a^p꼴의 지수함수를 따른다. + * 그렇지 않은 경우, 실제 당첨 확률은 1-a^Mp꼴의 지수함수를 따른다. (Note: Mp는 티켓 수이다.) * * 계산 과정 + * * a는 유저 수, 전체 티켓 수, 티켓 분포에 의해 결정되는 값으로, 현실적으로 계산하기 어렵다. * 따라서, 모든 유저가 같은 수의 티켓을 가지고 있다고 가정하고 a를 계산한 뒤, 이를 확률 계산에 사용한다. + * + * a값의 계산 과정 + * + * N을 유저 수라고 하자. 모든 유저가 같은 수의 티켓 M/N개를 가지고 있다고 하자. + * 이때 기대되는 당첨 확률은 직관적으로 15/N임을 알 수 있다. 즉, 1-a^(M/N) = 15/N이다. + * a에 대해 정리하면, a = (1-15/N)^(N/M)임을 알 수 있다. */ if (base !== null) return 1 - Math.pow(base, weight); else return (weight / weightSum) * 15; From c4a69c3fa5f745129862af08716a60e4b55400e9 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 24 Sep 2023 21:41:29 +0900 Subject: [PATCH 4/7] Add: totalTicketAmount and totalUserAmount field --- src/lottery/routes/docs/publicNotice.js | 12 +++++++++++- src/lottery/services/publicNotice.js | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index 71e9e3a3..eed3a652 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -46,7 +46,7 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { "application/json": { schema: { type: "object", - required: ["leaderboard"], + required: ["leaderboard", "totalTicketAmount", "totalUserAmount"], properties: { leaderboard: { type: "array", @@ -95,6 +95,16 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { }, }, }, + totalTicketAmount: { + type: "number", + description: "전체 티켓의 수", + example: 300, + }, + totalUserAmount: { + type: "number", + description: "리더보드에 포함된 유저의 수", + example: 100, + }, rank: { type: "number", description: "유저의 리더보드 순위. 1부터 시작합니다.", diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index b58e41be..ccd66aa0 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -122,6 +122,8 @@ const getTicketLeaderboardHandler = async (req, res) => { if (rank >= 0) res.json({ leaderboard, + totalTicketAmount: weightSum, + totalUserAmount: users.length, rank: rank + 1, probability: sortedUsers[rank].weight / weightSum, probabilityV2: calculateProbabilityV2( @@ -131,7 +133,12 @@ const getTicketLeaderboardHandler = async (req, res) => { sortedUsers[rank].weight ), }); - else res.json({ leaderboard }); + else + res.json({ + leaderboard, + totalTicketAmount: weightSum, + totalUserAmount: users.length, + }); } catch (err) { logger.error(err); res From a9ee2367d1d2ed50e45d4e5cb0157390f8188158 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 24 Sep 2023 23:12:32 +0900 Subject: [PATCH 5/7] Add: totalTicket1Amount and totalTicket2Amount field --- src/lottery/routes/docs/publicNotice.js | 16 +++++++++++++--- src/lottery/services/publicNotice.js | 19 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index eed3a652..e5ce2f55 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -46,7 +46,12 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { "application/json": { schema: { type: "object", - required: ["leaderboard", "totalTicketAmount", "totalUserAmount"], + required: [ + "leaderboard", + "totalTicket1Amount", + "totalTicket2Amount", + "totalUserAmount", + ], properties: { leaderboard: { type: "array", @@ -95,11 +100,16 @@ publicNoticeDocs[`${apiPrefix}/leaderboard`] = { }, }, }, - totalTicketAmount: { + totalTicket1Amount: { type: "number", - description: "전체 티켓의 수", + description: "전체 일반 티켓의 수", example: 300, }, + totalTicket2Amount: { + type: "number", + description: "전체 고급 티켓의 수", + example: 100, + }, totalUserAmount: { type: "number", description: "리더보드에 포함된 유저의 수", diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 94d25e8c..87b77428 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -110,12 +110,19 @@ const getTicketLeaderboardHandler = async (req, res) => { const userId = isLogin(req) ? getLoginInfo(req).oid : null; let rank = -1; - const weightSum = sortedUsers.reduce((before, user, index) => { + let weightSum = 0; + let totalTicket1Amount = 0; + let totalTicket2Amount = 0; + for (const user of sortedUsers) { + weightSum += user.weight; + totalTicket1Amount += user.ticket1Amount; + totalTicket2Amount += user.ticket2Amount; + if (rank < 0 && user.userId === userId) { rank = index; } - return before + user.weight; - }, 0); + } + const isExponential = sortedUsers.find((user) => user.weight >= weightSum / 15) != undefined; const base = isExponential @@ -152,7 +159,8 @@ const getTicketLeaderboardHandler = async (req, res) => { if (rank >= 0) res.json({ leaderboard, - totalTicketAmount: weightSum, + totalTicket1Amount, + totalTicket2Amount, totalUserAmount: users.length, rank: rank + 1, probability: sortedUsers[rank].weight / weightSum, @@ -166,7 +174,8 @@ const getTicketLeaderboardHandler = async (req, res) => { else res.json({ leaderboard, - totalTicketAmount: weightSum, + totalTicket1Amount, + totalTicket2Amount, totalUserAmount: users.length, }); } catch (err) { From fbf7cc945bf2c05f58feb7af0b12ed55ab28d517 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 24 Sep 2023 23:22:59 +0900 Subject: [PATCH 6/7] Refactor: use reduce function instead of for loop --- src/lottery/services/publicNotice.js | 30 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index 87b77428..cd78b84d 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -110,18 +110,24 @@ const getTicketLeaderboardHandler = async (req, res) => { const userId = isLogin(req) ? getLoginInfo(req).oid : null; let rank = -1; - let weightSum = 0; - let totalTicket1Amount = 0; - let totalTicket2Amount = 0; - for (const user of sortedUsers) { - weightSum += user.weight; - totalTicket1Amount += user.ticket1Amount; - totalTicket2Amount += user.ticket2Amount; - - if (rank < 0 && user.userId === userId) { - rank = index; - } - } + const [weightSum, totalTicket1Amount, totalTicket2Amount] = + sortedUsers.reduce( + ( + [_weightSum, _totalTicket1Amount, _totalTicket2Amount], + user, + index + ) => { + if (rank < 0 && user.userId === userId) { + rank = index; + } + return [ + _weightSum + user.weight, + _totalTicket1Amount + user.ticket1Amount, + _totalTicket2Amount + user.ticket2Amount, + ]; + }, + [0, 0, 0] + ); const isExponential = sortedUsers.find((user) => user.weight >= weightSum / 15) != undefined; From 560b1743fb9aaef488aeae4432893f0000444ede Mon Sep 17 00:00:00 2001 From: static Date: Sun, 24 Sep 2023 23:33:45 +0900 Subject: [PATCH 7/7] Refactor: resolve comment --- src/lottery/services/publicNotice.js | 42 +++++++++++++--------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/lottery/services/publicNotice.js b/src/lottery/services/publicNotice.js index cd78b84d..379e08da 100644 --- a/src/lottery/services/publicNotice.js +++ b/src/lottery/services/publicNotice.js @@ -130,7 +130,7 @@ const getTicketLeaderboardHandler = async (req, res) => { ); const isExponential = - sortedUsers.find((user) => user.weight >= weightSum / 15) != undefined; + sortedUsers.find((user) => user.weight >= weightSum / 15) !== undefined; const base = isExponential ? Math.pow(1 - 15 / users.length, users.length / weightSum) : null; @@ -162,28 +162,24 @@ const getTicketLeaderboardHandler = async (req, res) => { .status(500) .json({ error: "PublicNotice/Leaderboard : internal server error" }); - if (rank >= 0) - res.json({ - leaderboard, - totalTicket1Amount, - totalTicket2Amount, - totalUserAmount: users.length, - rank: rank + 1, - probability: sortedUsers[rank].weight / weightSum, - probabilityV2: calculateProbabilityV2( - users, - weightSum, - base, - sortedUsers[rank].weight - ), - }); - else - res.json({ - leaderboard, - totalTicket1Amount, - totalTicket2Amount, - totalUserAmount: users.length, - }); + res.json({ + leaderboard, + totalTicket1Amount, + totalTicket2Amount, + totalUserAmount: users.length, + ...(rank >= 0 + ? { + rank: rank + 1, + probability: sortedUsers[rank].weight / weightSum, + probabilityV2: calculateProbabilityV2( + users, + weightSum, + base, + sortedUsers[rank].weight + ), + } + : {}), + }); } catch (err) { logger.error(err); res