diff --git a/apps/backend/apps/admin/src/admin.module.ts b/apps/backend/apps/admin/src/admin.module.ts index 355dfec8d9..35b87db812 100644 --- a/apps/backend/apps/admin/src/admin.module.ts +++ b/apps/backend/apps/admin/src/admin.module.ts @@ -25,6 +25,7 @@ import { ContestModule } from './contest/contest.module' import { GroupModule } from './group/group.module' import { ProblemModule } from './problem/problem.module' import { StorageModule } from './storage/storage.module' +import { SubmissionModule } from './submission/submission.module' import { UserModule } from './user/user.module' @Module({ @@ -55,6 +56,7 @@ import { UserModule } from './user/user.module' UserModule, AnnouncementModule, NoticeModule, + SubmissionModule, LoggerModule.forRoot(pinoLoggerModuleOption) ], controllers: [AdminController], diff --git a/apps/backend/apps/admin/src/submission/model/contest-submission.model.ts b/apps/backend/apps/admin/src/submission/model/contest-submission.model.ts new file mode 100644 index 0000000000..e3e7eb9afd --- /dev/null +++ b/apps/backend/apps/admin/src/submission/model/contest-submission.model.ts @@ -0,0 +1,32 @@ +import { Field, Int, ObjectType } from '@nestjs/graphql' +import { Language, ResultStatus } from '@admin/@generated' + +@ObjectType({ description: 'contestSubmissionOverall' }) +export class ContestSubmission { + @Field(() => String, { nullable: false }) + title!: string // 문제 title + + @Field(() => String, { nullable: false }) + studentId!: string // 학번 + + @Field(() => String, { nullable: true }) + realname?: string // 실명 + + @Field(() => String, { nullable: false }) + username!: string + + @Field(() => ResultStatus, { nullable: false }) + result!: ResultStatus // Accepted, WrongAnswer ... + + @Field(() => Language, { nullable: false }) + language!: Language + + @Field(() => String, { nullable: false }) + submissionTime!: Date // 제출 시각 + + @Field(() => Int, { nullable: true }) + codeSize: number | null + + @Field(() => String, { nullable: true }) + ip: string | null +} diff --git a/apps/backend/apps/admin/src/submission/model/get-contest-submission.input.ts b/apps/backend/apps/admin/src/submission/model/get-contest-submission.input.ts new file mode 100644 index 0000000000..7f6a8f9b9f --- /dev/null +++ b/apps/backend/apps/admin/src/submission/model/get-contest-submission.input.ts @@ -0,0 +1,10 @@ +import { Field, InputType, Int } from '@nestjs/graphql' + +@InputType() +export class GetContestSubmissionsInput { + @Field(() => Int, { nullable: false }) + contestId!: number + + @Field(() => Int, { nullable: true }) + problemId?: number +} diff --git a/apps/backend/apps/admin/src/submission/submission.module.ts b/apps/backend/apps/admin/src/submission/submission.module.ts new file mode 100644 index 0000000000..77c7e02949 --- /dev/null +++ b/apps/backend/apps/admin/src/submission/submission.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common' +import { SubmissionResolver } from './submission.resolver' +import { SubmissionService } from './submission.service' + +@Module({ + providers: [SubmissionResolver, SubmissionService] +}) +export class SubmissionModule {} diff --git a/apps/backend/apps/admin/src/submission/submission.resolver.ts b/apps/backend/apps/admin/src/submission/submission.resolver.ts new file mode 100644 index 0000000000..26a67b72de --- /dev/null +++ b/apps/backend/apps/admin/src/submission/submission.resolver.ts @@ -0,0 +1,37 @@ +import { InternalServerErrorException, Logger } from '@nestjs/common' +import { Args, Int, Query, Resolver } from '@nestjs/graphql' +import { CursorValidationPipe } from '@libs/pipe' +import { Submission } from '@admin/@generated' +import { ContestSubmission } from './model/contest-submission.model' +import { GetContestSubmissionsInput } from './model/get-contest-submission.input' +import { SubmissionService } from './submission.service' + +@Resolver(() => Submission) +export class SubmissionResolver { + private readonly logger = new Logger(SubmissionResolver.name) + constructor(private readonly submissionService: SubmissionService) {} + + @Query(() => [ContestSubmission]) + async getContestSubmissions( + @Args('input', { + nullable: false, + type: () => GetContestSubmissionsInput + }) + input: GetContestSubmissionsInput, + @Args('cursor', { nullable: true, type: () => Int }, CursorValidationPipe) + cursor: number | null, + @Args('take', { nullable: true, defaultValue: 10, type: () => Int }) + take: number + ): Promise { + try { + return await this.submissionService.getContestSubmissions( + input, + take, + cursor + ) + } catch (error) { + this.logger.error(error.error) + throw new InternalServerErrorException() + } + } +} diff --git a/apps/backend/apps/admin/src/submission/submission.service.ts b/apps/backend/apps/admin/src/submission/submission.service.ts new file mode 100644 index 0000000000..06ed277b91 --- /dev/null +++ b/apps/backend/apps/admin/src/submission/submission.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from '@nestjs/common' +import { PrismaService } from '@libs/prisma' +import type { Language, ResultStatus } from '@admin/@generated' +import type { GetContestSubmissionsInput } from './model/get-contest-submission.input' + +@Injectable() +export class SubmissionService { + constructor(private readonly prisma: PrismaService) {} + + async getContestSubmissions( + input: GetContestSubmissionsInput, + take: number, + cursor: number | null + ) { + const paginator = this.prisma.getPaginator(cursor) + + const { contestId, problemId } = input + const contestSubmissions = await this.prisma.submission.findMany({ + ...paginator, + take, + where: { + contestId, + problemId + }, + include: { + user: { + select: { + id: true, + username: true, + studentId: true, + userProfile: { + select: { + realName: true + } + } + } + }, + problem: { + select: { + title: true + } + } + } + }) + + const results = contestSubmissions.map((c) => { + return { + title: c.problem.title, + studentId: c.user?.studentId ?? 'Unknown', + realname: c.user?.userProfile?.realName ?? 'Unknown', + username: c.user?.username ?? 'Unknown', + result: c.result as ResultStatus, + language: c.language as Language, + submissionTime: c.createTime, + codeSize: c.codeSize ?? null, + ip: c.userIp ?? 'Unknown' + } + }) + + return results + } +} diff --git a/collection/admin/Submission/Get Contest Submission Summaries/Succeed.bru b/collection/admin/Submission/Get Contest Submission Summaries/Succeed.bru new file mode 100644 index 0000000000..07e7ec3ca5 --- /dev/null +++ b/collection/admin/Submission/Get Contest Submission Summaries/Succeed.bru @@ -0,0 +1,44 @@ +meta { + name: Succeed + type: graphql + seq: 1 +} + +post { + url: {{gqlUrl}} + body: graphql + auth: none +} + +body:graphql { + query GetContestSubmissions( + $input: GetContestSubmissionsInput!, + $cursor: Int, + $take: Int + ) { + getContestSubmissions( + input: $input, + cursor: $cursor, + take: $take + ) { + title + studentId + realname + username + result + language + submissionTime + codeSize + ip + } + } +} + +body:graphql:vars { + { + "input": { + "contestId": 1 + }, + "take": 10 + } +}