Skip to content

Commit

Permalink
feat(be): add logic for contest leaderboard (#2331)
Browse files Browse the repository at this point in the history
* feat(be): add leaderboard

* fix(be): change user schema

* chore(be): migrate

* fix(be): edit db schema

* feat(be): record update logic for contest leaderboard

* feat(be): leaderboard service logic

* test(be): fix tests for update contest records

* test(be): add test code for service

* feat(be): add controller

* fix(be): access permission check

* fix(be): change fit for specification

* fix(be): type fix

* fix(be): test

* feat(be): add first solver
  • Loading branch information
mnseok authored Jan 28, 2025
1 parent 4f5130c commit 5e582ad
Show file tree
Hide file tree
Showing 19 changed files with 827 additions and 139 deletions.
7 changes: 7 additions & 0 deletions apps/backend/apps/admin/src/contest/contest.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const contest: Contest = {
groupId,
title: 'title',
description: 'description',
penalty: 20,
lastPenalty: false,
startTime,
endTime,
isVisible: true,
Expand All @@ -63,6 +65,8 @@ const contestWithCount = {
groupId,
title: 'title',
description: 'description',
penalty: 20,
lastPenalty: false,
startTime,
endTime,
isVisible: true,
Expand Down Expand Up @@ -90,6 +94,8 @@ const contestWithParticipants: ContestWithParticipants = {
groupId,
title: 'title',
description: 'description',
penalty: 20,
lastPenalty: false,
startTime,
endTime,
isVisible: true,
Expand Down Expand Up @@ -151,6 +157,7 @@ const problem: Problem = {
}

const contestProblem: ContestProblem = {
id: 1,
order: 0,
contestId,
problemId,
Expand Down
22 changes: 22 additions & 0 deletions apps/backend/apps/admin/src/problem/mock/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,8 @@ export const exampleContest: Contest = {
id: 1,
title: 'example',
description: 'example',
penalty: 20,
lastPenalty: false,
groupId: 1,
createdById: 1,
isVisible: true,
Expand All @@ -391,6 +393,7 @@ export const exampleContest: Contest = {
}
export const exampleContestProblems: ContestProblem[] = [
{
id: 1,
order: 1,
contestId: 1,
problemId: 1,
Expand All @@ -399,6 +402,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 2,
order: 2,
contestId: 1,
problemId: 2,
Expand All @@ -407,6 +411,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 3,
order: 3,
contestId: 1,
problemId: 3,
Expand All @@ -415,6 +420,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 4,
order: 4,
contestId: 1,
problemId: 4,
Expand All @@ -423,6 +429,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 5,
order: 5,
contestId: 1,
problemId: 5,
Expand All @@ -431,6 +438,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 6,
order: 6,
contestId: 1,
problemId: 6,
Expand All @@ -439,6 +447,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 7,
order: 7,
contestId: 1,
problemId: 7,
Expand All @@ -447,6 +456,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 8,
order: 8,
contestId: 1,
problemId: 8,
Expand All @@ -455,6 +465,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 9,
order: 9,
contestId: 1,
problemId: 9,
Expand All @@ -463,6 +474,7 @@ export const exampleContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 10,
order: 10,
contestId: 1,
problemId: 10,
Expand All @@ -474,6 +486,7 @@ export const exampleContestProblems: ContestProblem[] = [

export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
{
id: 1,
order: 1,
contestId: 1,
problemId: 2,
Expand All @@ -482,6 +495,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 2,
order: 2,
contestId: 1,
problemId: 3,
Expand All @@ -490,6 +504,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 3,
order: 3,
contestId: 1,
problemId: 4,
Expand All @@ -498,6 +513,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 4,
order: 4,
contestId: 1,
problemId: 5,
Expand All @@ -506,6 +522,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 5,
order: 5,
contestId: 1,
problemId: 6,
Expand All @@ -514,6 +531,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 6,
order: 6,
contestId: 1,
problemId: 7,
Expand All @@ -522,6 +540,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 7,
order: 7,
contestId: 1,
problemId: 8,
Expand All @@ -530,6 +549,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 8,
order: 8,
contestId: 1,
problemId: 9,
Expand All @@ -538,6 +558,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 9,
order: 9,
contestId: 1,
problemId: 10,
Expand All @@ -546,6 +567,7 @@ export const exampleOrderUpdatedContestProblems: ContestProblem[] = [
updateTime: new Date()
},
{
id: 10,
order: 10,
contestId: 1,
problemId: 1,
Expand Down
11 changes: 11 additions & 0 deletions apps/backend/apps/client/src/contest/contest.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,15 @@ export class ContestController {
groupId
)
}

@Get(':id/leaderboard')
async getLeaderboard(
@Req() req: AuthenticatedRequest,
@Param('id', IDValidationPipe) contestId: number
) {
return await this.contestService.getContestLeaderboard(
req.user.id,
contestId
)
}
}
12 changes: 12 additions & 0 deletions apps/backend/apps/client/src/contest/contest.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const contest = {
groupId,
title: 'title',
description: 'description',
penalty: 100,
lastPenalty: false,
startTime: now.add(-1, 'day').toDate(),
endTime: now.add(1, 'day').toDate(),
isVisible: true,
Expand Down Expand Up @@ -512,4 +514,14 @@ describe('ContestService', () => {
).to.be.rejectedWith(ForbiddenAccessException)
})
})

describe('getContestLeaderboard', () => {
it('should return leaderboard of the contest', async () => {
const leaderboard = await service.getContestLeaderboard(
user01Id,
contestId
)
expect(leaderboard).to.be.ok
})
})
})
137 changes: 136 additions & 1 deletion apps/backend/apps/client/src/contest/contest.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'
import { Prisma, type Contest } from '@prisma/client'
import { Prisma, Role, type Contest } from '@prisma/client'
import { OPEN_SPACE_ID } from '@libs/constants'
import {
ConflictFoundException,
Expand Down Expand Up @@ -541,4 +541,139 @@ export class ContestService {
where: { contestId_userId: { contestId, userId } }
})
}

async getContestLeaderboard(userId: number, contestId: number) {
const isRegistered =
(await this.prisma.contestRecord.findFirst({
where: {
userId,
contestId
}
})) != null

const user = await this.prisma.user.findUnique({
where: {
id: userId
},
select: {
id: true,
role: true
}
})

if (
!isRegistered &&
user?.role !== Role.Admin &&
user?.role !== Role.SuperAdmin
) {
throw new ForbiddenAccessException('Not registered in this contest')
}

const sum = await this.prisma.contestProblem.aggregate({
where: {
contestId
},
// eslint-disable-next-line @typescript-eslint/naming-convention
_sum: {
score: true
}
})
const maxScore = sum._sum?.score ?? 0

const contestRecords = await this.prisma.contestRecord.findMany({
where: {
contestId
},
select: {
userId: true,
user: {
select: {
username: true
}
},
score: true,
totalPenalty: true,
contestProblemRecord: {
select: {
score: true,
timePenalty: true,
submitCountPenalty: true,
contestProblem: {
select: {
order: true,
problem: {
select: {
id: true
}
}
}
}
}
}
},
orderBy: [
{
score: 'desc'
},
{
totalPenalty: 'asc'
},
{
lastAcceptedTime: 'asc'
}
]
})

// 문제별 제출 횟수 데이터 가져오기
const submissionCounts = await this.prisma.submission.groupBy({
by: ['userId', 'problemId'], // userId와 problemId로 그룹화
// eslint-disable-next-line @typescript-eslint/naming-convention
_count: {
id: true // 제출 횟수를 세기 위해 id를 카운트
},
where: {
contestId
}
})

// 유저별 문제별 제출 횟수를 매핑
const submissionCountMap: {
[userId: number]: { [problemId: number]: number }
} = {}
submissionCounts.forEach((submission) => {
const { userId, problemId, _count } = submission
if (!userId || !problemId || !_count) return
if (!submissionCountMap[userId]) {
submissionCountMap[userId] = {}
}
submissionCountMap[userId][problemId] = _count.id // 문제별 제출 횟수 저장
})

let rank = 1
const leaderboard = contestRecords.map((contestRecord) => {
const { contestProblemRecord, userId, ...rest } = contestRecord
const problemRecords = contestProblemRecord.map((record) => {
const { contestProblem, submitCountPenalty, timePenalty, ...rest } =
record
return {
...rest,
order: contestProblem.order,
problemId: contestProblem.problem.id,
penalty: submitCountPenalty + timePenalty,
submissionCount:
submissionCountMap[userId!]?.[contestProblem.problem.id] ?? 0 // 기본값 0 설정
}
})
return {
...rest,
problemRecords,
rank: rank++
}
})

return {
maxScore,
leaderboard
}
}
}
Loading

0 comments on commit 5e582ad

Please sign in to comment.