Skip to content

Commit

Permalink
Merge branch 'main' into renovate/codemirror
Browse files Browse the repository at this point in the history
  • Loading branch information
Kohminchae authored Jan 23, 2025
2 parents 788e949 + 9829d09 commit 03525e4
Show file tree
Hide file tree
Showing 23 changed files with 286 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export enum ContestSubmissionOrder {
studentIdASC = 'studentId-asc',
studentIdDESC = 'studentId-desc',
realNameASC = 'realName-asc',
realNameDESC = 'realName-desc',
usernameASC = 'username-asc',
usernameDESC = 'username-desc'
}
30 changes: 16 additions & 14 deletions apps/backend/apps/admin/src/submission/submission.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { InternalServerErrorException, Logger } from '@nestjs/common'
import { Args, Int, Query, Resolver } from '@nestjs/graphql'
import { CursorValidationPipe } from '@libs/pipe'
import { ContestSubmissionOrderPipe, CursorValidationPipe } from '@libs/pipe'
import { Submission } from '@admin/@generated'
import { ContestSubmissionOrder } from './enum/contest-submission-order.enum'
import { ContestSubmission } from './model/contest-submission.model'
import { GetContestSubmissionsInput } from './model/get-contest-submission.input'
import { SubmissionDetail } from './model/submission-detail.output'
import { SubmissionService } from './submission.service'

@Resolver(() => Submission)
export class SubmissionResolver {
private readonly logger = new Logger(SubmissionResolver.name)
constructor(private readonly submissionService: SubmissionService) {}

/**
Expand All @@ -28,19 +27,22 @@ export class SubmissionResolver {
@Args('cursor', { nullable: true, type: () => Int }, CursorValidationPipe)
cursor: number | null,
@Args('take', { nullable: true, defaultValue: 10, type: () => Int })
take: number
take: number,
@Args(
'order',
{ nullable: true, type: () => String },
ContestSubmissionOrderPipe
)
order: ContestSubmissionOrder | null
): Promise<ContestSubmission[]> {
try {
return await this.submissionService.getContestSubmissions(
input,
take,
cursor
)
} catch (error) {
this.logger.error(error.error)
throw new InternalServerErrorException()
}
return await this.submissionService.getContestSubmissions(
input,
take,
cursor,
order
)
}

/**
* 특정 Contest의 특정 제출 내역에 대한 상세 정보를 불러옵니다.
*/
Expand Down
35 changes: 33 additions & 2 deletions apps/backend/apps/admin/src/submission/submission.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common'
import type { Prisma } from '@prisma/client'
import { plainToInstance } from 'class-transformer'
import { EntityNotExistException } from '@libs/exception'
import { PrismaService } from '@libs/prisma'
import type { Language, ResultStatus } from '@admin/@generated'
import { Snippet } from '@admin/problem/model/template.input'
import { ContestSubmissionOrder } from './enum/contest-submission-order.enum'
import type { GetContestSubmissionsInput } from './model/get-contest-submission.input'

@Injectable()
Expand All @@ -13,7 +15,8 @@ export class SubmissionService {
async getContestSubmissions(
input: GetContestSubmissionsInput,
take: number,
cursor: number | null
cursor: number | null,
order: ContestSubmissionOrder | null
) {
const paginator = this.prisma.getPaginator(cursor)

Expand Down Expand Up @@ -59,7 +62,8 @@ export class SubmissionService {
}
}
}
}
},
orderBy: order ? this.getOrderBy(order) : undefined
})

const results = contestSubmissions.map((c) => {
Expand All @@ -84,6 +88,33 @@ export class SubmissionService {
return results
}

getOrderBy(
order: ContestSubmissionOrder
): Prisma.SubmissionOrderByWithRelationInput {
const [sortKey, sortOrder] = order.split('-')

switch (order) {
case ContestSubmissionOrder.studentIdASC:
case ContestSubmissionOrder.studentIdDESC:
case ContestSubmissionOrder.usernameASC:
case ContestSubmissionOrder.usernameDESC:
return {
user: {
[sortKey]: sortOrder
}
}
case ContestSubmissionOrder.realNameASC:
case ContestSubmissionOrder.realNameDESC:
return {
user: {
userProfile: {
[sortKey]: sortOrder
}
}
}
}
}

async getSubmission(id: number) {
const submission = await this.prisma.submission.findFirst({
where: {
Expand Down
44 changes: 44 additions & 0 deletions apps/backend/apps/admin/src/user/user.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import { UserService } from './user.service'
export class UserResolver {
constructor(private readonly userService: UserService) {}

/**
* 특정 그룹의 멤버를 페이지네이션과 필터링 조건에 따라 조회함.
*
* @param {number} groupId - 그룹의 ID. 기본 값은 OPEN_SPACE_ID.
* @param {number | null} cursor - 페이지네이션을 위한 커서. 선택적 매개변수임
* @param {number} take - 가져올 그룹 멤버의 수. 기본값은 10.
* @param {boolean} leaderOnly - 그룹의 리더만 필터링 해주는 플래그. 기본 값은 false
* @returns {Promise<GroupMember[]>} - 그룹 멤버 리스트를 리턴함.
*/
@Query(() => [GroupMember])
async getGroupMembers(
@Args(
Expand All @@ -36,6 +45,13 @@ export class UserResolver {
)
}

/**
* 특정 그룹에서 사용자 ID를 기반으로 그룹 멤버를 조회함.
*
* @param {number} groupId - 그룹의 ID. 기본 값은 OPEN_SPACE_ID.
* @param {number} userId - 조회할 사용자의 ID.
* @returns {Promise<GroupMember>} 그룹 멤버 객체를 리턴함.
*/
@Query(() => GroupMember)
async getGroupMember(
@Args(
Expand All @@ -50,6 +66,14 @@ export class UserResolver {
return await this.userService.getGroupMember(groupId, userId)
}

/**
* 특정 그룹 멤버를 리더로 업데이트함
*
* @param {number} userId - 역할을 업데이트 할 사용자의 ID.
* @param {number} groupId - 업데이트 대상 그룹의 ID.
* @param {boolean} toGroupLeader - 사용자를 리더로 설정할지에 대한 플래그
* @returns {Promise<UserGroup>} 업데이트된 그룹 멤버 객체를 리턴.
*/
@Mutation(() => UserGroup)
async updateGroupMember(
@Args('userId', { type: () => Int }, new RequiredIntPipe('userId'))
Expand All @@ -64,6 +88,13 @@ export class UserResolver {
)
}

/**
* 특정 사용자를 그룹에서 제거.
*
* @param {number} userId - 제거할 사용자의 id
* @param {number} groupId - 대상 그룹의 id
* @returns {Promise<UserGroup>} 삭제된 그룹 멤버 객체를 리턴.
*/
@Mutation(() => UserGroup)
async deleteGroupMember(
@Args('userId', { type: () => Int }, new RequiredIntPipe('userId'))
Expand All @@ -73,13 +104,26 @@ export class UserResolver {
return await this.userService.deleteGroupMember(userId, groupId)
}

/**
* 특정 그룹에 가입 요청한 유저 리스트를 조회.
* @param {number} groupId - 특정 그룹의 id.
* @returns {Promise<User[]>} 가입 요청한 유저 리스트를 리턴.
*/
@Query(() => [User])
async getJoinRequests(
@Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number
) {
return await this.userService.getJoinRequests(groupId)
}

/**
* 그룹 가입 요청을 처리.
*
* @param {number} groupId
* @param {number} userId
* @param {boolean} isAccept - 요청 승인에 대한 플래그.
* @returns {Promise<UserGroup>}
*/
@Mutation(() => UserGroup)
async handleJoinRequest(
@Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number,
Expand Down
21 changes: 21 additions & 0 deletions apps/backend/apps/client/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export class AuthController {
)
}

/**
* 유저가 로그인할 때 호출됨. JWT 토큰을 발급하고 리스폰스에 설정함.
* @param {LoginUserDto} loginUserDto - 로그인 유저의 정보
* @param {Response} res - 리스폰스 객체
*/
@AuthNotNeededIfOpenSpace()
@Post('login')
async login(
Expand All @@ -43,6 +48,12 @@ export class AuthController {
this.setJwtResponse(res, jwtTokens)
}

/**
* 유저가 로그아웃할 때 호출됨. refreshToken을 제거하고 쿠키를 삭제.
* @param {AuthenticatedRequest} req - 인증된 리퀘스트 객체
* @param {Response} res - 리스폰스 객체
* @returns {Promise<void>}
*/
@Post('logout')
async logout(
@Req() req: AuthenticatedRequest,
Expand All @@ -57,6 +68,13 @@ export class AuthController {
res.clearCookie('refresh_token', REFRESH_TOKEN_COOKIE_OPTIONS)
}

/**
* refreshToken을 기반으로 새로운 JWT 토큰을 재발급.
* @param {Request} req
* @param {Response} res
* @returns {Promise<void>}
* @throws {UnauthorizedException} refreshToken이 없거나 유효하지 않은 경우 예외 던짐.
*/
@AuthNotNeededIfOpenSpace()
@Get('reissue')
async reIssueJwtTokens(
Expand All @@ -70,6 +88,9 @@ export class AuthController {
this.setJwtResponse(res, newJwtTokens)
}

/**
* GitHub 로그인 페이지로 이동.
*/
@AuthNotNeededIfOpenSpace()
@Get('github')
@UseGuards(AuthGuard('github'))
Expand Down
53 changes: 48 additions & 5 deletions apps/backend/apps/client/src/submission/submission-pub.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { Span, TraceService } from 'nestjs-otel'
import {
EXCHANGE,
JUDGE_MESSAGE_TYPE,
MESSAGE_PRIORITY_HIGH,
MESSAGE_PRIORITY_LOW,
MESSAGE_PRIORITY_MIDDLE,
RUN_MESSAGE_TYPE,
SUBMISSION_KEY,
USER_TESTCASE_MESSAGE_TYPE
Expand Down Expand Up @@ -78,12 +81,52 @@ export class SubmissionPublicationService {
await this.amqpConnection.publish(EXCHANGE, SUBMISSION_KEY, judgeRequest, {
messageId: String(submission.id),
persistent: true,
type: isTest
? RUN_MESSAGE_TYPE
: isUserTest
? USER_TESTCASE_MESSAGE_TYPE
: JUDGE_MESSAGE_TYPE
type: this.calculateMessageType(isTest, isUserTest),
priority: this.calculateMessagePriority(isTest, isUserTest)
})
span.end()
}

/**
* 채점 요청 메세지의 타입을 계산하여 반환
*
* - RUN_MESSAGE_TYPE: 오픈 테스트 케이스 실행
* - USER_TESTCASE_MESSAGE_TYPE: 사용자 정의 테스트 케이스 실행
* - JUDGE_MESSAGE_TYPE: 제출
*
* @param isTest - 테스트 제출 여부
* @param isUserTest - 사용자 정의 테스트 케이스 제출 여부
*/
private calculateMessageType(isTest: boolean, isUserTest: boolean) {
if (isTest) return RUN_MESSAGE_TYPE
if (isUserTest) return USER_TESTCASE_MESSAGE_TYPE
return JUDGE_MESSAGE_TYPE
}

/**
* 채점 요청 메세지의 우선순위를 계산하여 반환
*
* 우선순위 (0 ~ 3, 클 수록 우선순위 높음)
*
* - JUDGE_MESSAGE_TYPE: 3
* - RUN_MESSAGE_TYPE: 2
* - USER_TESTCASE_MESSAGE_TYPE: 2
* - DEFAULT: 1
*
* @param isTest - 테스트 제출 여부
* @param isUserTest - 사용자 정의 테스트 케이스 제출 여부
*/
private calculateMessagePriority(isTest: boolean, isUserTest: boolean) {
const msgType = this.calculateMessageType(isTest, isUserTest)

switch (msgType) {
case JUDGE_MESSAGE_TYPE:
return MESSAGE_PRIORITY_HIGH
case RUN_MESSAGE_TYPE:
case USER_TESTCASE_MESSAGE_TYPE:
return MESSAGE_PRIORITY_MIDDLE
default:
return MESSAGE_PRIORITY_LOW
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import * as sinon from 'sinon'
import {
EXCHANGE,
JUDGE_MESSAGE_TYPE,
MESSAGE_PRIORITY_HIGH,
MESSAGE_PRIORITY_MIDDLE,
RUN_MESSAGE_TYPE,
SUBMISSION_KEY
} from '@libs/constants'
Expand Down Expand Up @@ -103,7 +105,8 @@ describe('SubmissionPublicationService', () => {
amqpSpy.calledOnceWith(EXCHANGE, SUBMISSION_KEY, judgeRequest, {
messageId: String(submission.id),
persistent: true,
type: JUDGE_MESSAGE_TYPE
type: JUDGE_MESSAGE_TYPE,
priority: MESSAGE_PRIORITY_HIGH
})
).to.be.true
})
Expand Down Expand Up @@ -143,7 +146,8 @@ describe('SubmissionPublicationService', () => {
amqpSpy.calledOnceWith(EXCHANGE, SUBMISSION_KEY, judgeRequest, {
messageId: String(submission.id),
persistent: true,
type: RUN_MESSAGE_TYPE
type: RUN_MESSAGE_TYPE,
priority: MESSAGE_PRIORITY_MIDDLE
})
).to.be.true
})
Expand Down
7 changes: 7 additions & 0 deletions apps/backend/libs/constants/src/rabbitmq.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ export const ORIGIN_HANDLER_NAME = 'codedang-handler'
export const JUDGE_MESSAGE_TYPE = 'judge'
export const RUN_MESSAGE_TYPE = 'run'
export const USER_TESTCASE_MESSAGE_TYPE = 'userTestCase'

/**
* 채점 요청 메세지 우선순위
*/
export const MESSAGE_PRIORITY_HIGH = 3
export const MESSAGE_PRIORITY_MIDDLE = 2
export const MESSAGE_PRIORITY_LOW = 1
24 changes: 24 additions & 0 deletions apps/backend/libs/pipe/src/contest-submission-order.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
BadRequestException,
Injectable,
type PipeTransform
} from '@nestjs/common'
import { ContestSubmissionOrder } from '@admin/submission/enum/contest-submission-order.enum'

@Injectable()
export class ContestSubmissionOrderPipe implements PipeTransform {
transform(value: unknown) {
if (!value) {
return null
} else if (
!Object.values(ContestSubmissionOrder).includes(
value as ContestSubmissionOrder
)
) {
throw new BadRequestException(
'Contest-submission-order validation failed'
)
}
return value
}
}
1 change: 1 addition & 0 deletions apps/backend/libs/pipe/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './id-validation.pipe'
export * from './group-id.pipe'
export * from './required-int.pipe'
export * from './problem-order.pipe'
export * from './contest-submission-order.pipe'
Loading

0 comments on commit 03525e4

Please sign in to comment.