diff --git a/.github/workflows/cd-prod.yml b/.github/workflows/cd-prod.yml
index 0dd837ffa2..960954f964 100644
--- a/.github/workflows/cd-prod.yml
+++ b/.github/workflows/cd-prod.yml
@@ -109,9 +109,13 @@ jobs:
- name: Create Terraform variable file
working-directory: ./infra/deploy
run: |
- echo "${{ secrets.TFVARS }}" >> terraform.tfvars
- echo "${{ secrets.OAUTH_GITHUB }}" >> terraform.tfvars
- echo "${{ secrets.OAUTH_KAKAO }}" >> terraform.tfvars
+ echo $TFVARS >> terraform.tfvars
+ echo $OAUTH_GITHUB >> terraform.tfvars
+ echo $OAUTH_KAKAO >> terraform.tfvars
+ env:
+ TFVARS: ${{ secrets.TFVARS }}
+ OAUTH_GITHUB: ${{ secrets.OAUTH_GITHUB }}
+ OAUTH_KAKAO: ${{ secrets.OAUTH_KAKAO }}
- name: Terraform Init
working-directory: ./infra/deploy
diff --git a/backend/apps/admin/src/contest/contest.resolver.ts b/backend/apps/admin/src/contest/contest.resolver.ts
index 071f25e421..6dbc9338ce 100644
--- a/backend/apps/admin/src/contest/contest.resolver.ts
+++ b/backend/apps/admin/src/contest/contest.resolver.ts
@@ -1,11 +1,13 @@
import {
InternalServerErrorException,
Logger,
+ NotFoundException,
ParseBoolPipe
} from '@nestjs/common'
import { Args, Context, Int, Mutation, Query, Resolver } from '@nestjs/graphql'
import { ContestProblem } from '@generated'
import { Contest } from '@generated'
+import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import { AuthenticatedRequest, UseRolesGuard } from '@libs/auth'
import { OPEN_SPACE_ID } from '@libs/constants'
import {
@@ -15,6 +17,7 @@ import {
} from '@libs/exception'
import { CursorValidationPipe, GroupIDPipe, RequiredIntPipe } from '@libs/pipe'
import { ContestService } from './contest.service'
+import { ContestWithParticipants } from './model/contest-with-participants.model'
import { CreateContestInput } from './model/contest.input'
import { UpdateContestInput } from './model/contest.input'
import { PublicizingRequest } from './model/publicizing-request.model'
@@ -25,7 +28,7 @@ export class ContestResolver {
private readonly logger = new Logger(ContestResolver.name)
constructor(private readonly contestService: ContestService) {}
- @Query(() => [Contest])
+ @Query(() => [ContestWithParticipants])
async getContests(
@Args(
'take',
@@ -45,6 +48,23 @@ export class ContestResolver {
return await this.contestService.getContests(take, groupId, cursor)
}
+ @Query(() => ContestWithParticipants)
+ async getContest(
+ @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId'))
+ contestId: number
+ ) {
+ try {
+ return await this.contestService.getContest(contestId)
+ } catch (error) {
+ if (
+ error instanceof PrismaClientKnownRequestError &&
+ error.code == 'P2025'
+ ) {
+ throw new NotFoundException(error.message)
+ }
+ }
+ }
+
@Mutation(() => Contest)
async createContest(
@Args('input') input: CreateContestInput,
@@ -96,7 +116,7 @@ export class ContestResolver {
@Mutation(() => Contest)
async deleteContest(
@Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number,
- @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId'))
+ @Args('contestId', { type: () => Int })
contestId: number
) {
try {
@@ -119,7 +139,7 @@ export class ContestResolver {
@Mutation(() => PublicizingRequest)
async createPublicizingRequest(
@Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number,
- @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId'))
+ @Args('contestId', { type: () => Int })
contestId: number
) {
try {
@@ -142,7 +162,7 @@ export class ContestResolver {
@Mutation(() => PublicizingResponse)
@UseRolesGuard()
async handlePublicizingRequest(
- @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId'))
+ @Args('contestId', { type: () => Int })
contestId: number,
@Args('isAccepted', ParseBoolPipe) isAccepted: boolean
) {
@@ -163,7 +183,7 @@ export class ContestResolver {
@Mutation(() => [ContestProblem])
async importProblemsToContest(
@Args('groupId', { type: () => Int }, GroupIDPipe) groupId: number,
- @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId'))
+ @Args('contestId', { type: () => Int })
contestId: number,
@Args('problemIds', { type: () => [Int] }) problemIds: number[]
) {
diff --git a/backend/apps/admin/src/contest/contest.service.spec.ts b/backend/apps/admin/src/contest/contest.service.spec.ts
index 2fca5c01cb..4b6f0f4ca9 100644
--- a/backend/apps/admin/src/contest/contest.service.spec.ts
+++ b/backend/apps/admin/src/contest/contest.service.spec.ts
@@ -11,6 +11,7 @@ import { stub } from 'sinon'
import { EntityNotExistException } from '@libs/exception'
import { PrismaService } from '@libs/prisma'
import { ContestService } from './contest.service'
+import type { ContestWithParticipants } from './model/contest-with-participants.model'
import type {
CreateContestInput,
UpdateContestInput
@@ -21,6 +22,10 @@ const contestId = 1
const userId = 1
const groupId = 1
const problemId = 2
+const startTime = faker.date.past()
+const endTime = faker.date.future()
+const createTime = faker.date.past()
+const updateTime = faker.date.past()
const contest: Contest = {
id: contestId,
@@ -28,14 +33,51 @@ const contest: Contest = {
groupId,
title: 'title',
description: 'description',
- startTime: faker.date.past(),
- endTime: faker.date.future(),
+ startTime,
+ endTime,
config: {
isVisible: true,
isRankVisible: true
},
- createTime: faker.date.past(),
- updateTime: faker.date.past()
+ createTime,
+ updateTime
+}
+
+const contestWithCount = {
+ id: contestId,
+ createdById: userId,
+ groupId,
+ title: 'title',
+ description: 'description',
+ startTime,
+ endTime,
+ config: {
+ isVisible: true,
+ isRankVisible: true
+ },
+ createTime,
+ updateTime,
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ _count: {
+ contestRecord: 10
+ }
+}
+
+const contestWithParticipants: ContestWithParticipants = {
+ id: contestId,
+ createdById: userId,
+ groupId,
+ title: 'title',
+ description: 'description',
+ startTime,
+ endTime,
+ config: {
+ isVisible: true,
+ isRankVisible: true
+ },
+ createTime,
+ updateTime,
+ participants: 10
}
const group: Group = {
@@ -61,6 +103,7 @@ const problem: Problem = {
inputDescription: 'inputdescription',
outputDescription: 'outputdescription',
hint: 'hint',
+ isVisible: true,
template: [],
languages: ['C'],
timeLimit: 10000,
@@ -175,10 +218,10 @@ describe('ContestService', () => {
describe('getContests', () => {
it('should return an array of contests', async () => {
- db.contest.findMany.resolves([contest])
+ db.contest.findMany.resolves([contestWithCount])
const res = await service.getContests(5, 2, 0)
- expect(res).to.deep.equal([contest])
+ expect(res).to.deep.equal([contestWithParticipants])
})
})
diff --git a/backend/apps/admin/src/contest/contest.service.ts b/backend/apps/admin/src/contest/contest.service.ts
index 956d806ad1..d0b1e86bab 100644
--- a/backend/apps/admin/src/contest/contest.service.ts
+++ b/backend/apps/admin/src/contest/contest.service.ts
@@ -33,11 +33,44 @@ export class ContestService {
async getContests(take: number, groupId: number, cursor: number | null) {
const paginator = this.prisma.getPaginator(cursor)
- return await this.prisma.contest.findMany({
+ const contests = await this.prisma.contest.findMany({
...paginator,
where: { groupId },
- take
+ take,
+ include: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ _count: {
+ select: { contestRecord: true }
+ }
+ }
+ })
+
+ return contests.map((contest) => {
+ const { _count, ...data } = contest
+ return {
+ ...data,
+ participants: _count.contestRecord
+ }
+ })
+ }
+
+ async getContest(contestId: number) {
+ const { _count, ...data } = await this.prisma.contest.findFirstOrThrow({
+ where: {
+ id: contestId
+ },
+ include: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ _count: {
+ select: { contestRecord: true }
+ }
+ }
})
+
+ return {
+ ...data,
+ participants: _count.contestRecord
+ }
}
async createContest(
diff --git a/backend/apps/admin/src/contest/model/contest-with-participants.model.ts b/backend/apps/admin/src/contest/model/contest-with-participants.model.ts
new file mode 100644
index 0000000000..9fcd0fd275
--- /dev/null
+++ b/backend/apps/admin/src/contest/model/contest-with-participants.model.ts
@@ -0,0 +1,8 @@
+import { Field, Int, ObjectType } from '@nestjs/graphql'
+import { Contest } from '@admin/@generated'
+
+@ObjectType({ description: 'contestWithParticipants' })
+export class ContestWithParticipants extends Contest {
+ @Field(() => Int)
+ participants: number
+}
diff --git a/backend/apps/admin/src/problem/mock/mock.ts b/backend/apps/admin/src/problem/mock/mock.ts
index e07ac5fbaf..12974ba5a4 100644
--- a/backend/apps/admin/src/problem/mock/mock.ts
+++ b/backend/apps/admin/src/problem/mock/mock.ts
@@ -42,7 +42,8 @@ export const problems: Problem[] = [
updateTime: faker.date.past(),
exposeTime: faker.date.anytime(),
inputExamples: [],
- outputExamples: []
+ outputExamples: [],
+ isVisible: true
},
{
id: 2,
@@ -66,7 +67,8 @@ export const problems: Problem[] = [
updateTime: faker.date.past(),
exposeTime: faker.date.anytime(),
inputExamples: [],
- outputExamples: []
+ outputExamples: [],
+ isVisible: true
}
]
@@ -119,7 +121,8 @@ export const importedProblems: Problem[] = [
updateTime: faker.date.past(),
exposeTime: faker.date.anytime(),
inputExamples: [],
- outputExamples: []
+ outputExamples: [],
+ isVisible: true
},
{
id: 33,
@@ -159,6 +162,7 @@ export const importedProblems: Problem[] = [
updateTime: faker.date.past(),
exposeTime: faker.date.anytime(),
inputExamples: [],
- outputExamples: []
+ outputExamples: [],
+ isVisible: true
}
]
diff --git a/backend/apps/admin/src/problem/model/problem.input.ts b/backend/apps/admin/src/problem/model/problem.input.ts
index fe5b34ea18..b227ed7340 100644
--- a/backend/apps/admin/src/problem/model/problem.input.ts
+++ b/backend/apps/admin/src/problem/model/problem.input.ts
@@ -23,6 +23,9 @@ export class CreateProblemInput {
@Field(() => String, { nullable: false })
hint!: string
+ @Field(() => Boolean, { defaultValue: true })
+ isVisible!: boolean
+
@Field(() => [Template], { nullable: false })
template!: Array
@@ -115,6 +118,9 @@ export class UpdateProblemInput {
@Field(() => String, { nullable: true })
hint?: string
+ @Field(() => Boolean, { nullable: true })
+ isVisible?: boolean
+
@Field(() => [Template], { nullable: true })
template?: Array
diff --git a/backend/apps/admin/src/problem/problem.service.spec.ts b/backend/apps/admin/src/problem/problem.service.spec.ts
index 0fc08fc702..7ee800308e 100644
--- a/backend/apps/admin/src/problem/problem.service.spec.ts
+++ b/backend/apps/admin/src/problem/problem.service.spec.ts
@@ -424,6 +424,7 @@ describe('ProblemService', () => {
inputDescription: problems[0].inputDescription,
outputDescription: problems[0].outputDescription,
hint: problems[0].hint,
+ isVisible: false,
template: problems[0].template,
languages: problems[0].languages,
timeLimit: problems[0].timeLimit,
diff --git a/backend/apps/admin/src/problem/problem.service.ts b/backend/apps/admin/src/problem/problem.service.ts
index cbdf995ca2..71feeafac5 100644
--- a/backend/apps/admin/src/problem/problem.service.ts
+++ b/backend/apps/admin/src/problem/problem.service.ts
@@ -550,20 +550,18 @@ export class ProblemService {
where: { id: contestId, groupId }
})
- const contestProblemsToBeUpdated =
- await this.prisma.contestProblem.findMany({
- where: { contestId }
- })
+ const contestProblems = await this.prisma.contestProblem.findMany({
+ where: { contestId }
+ })
- if (orders.length !== contestProblemsToBeUpdated.length) {
+ if (orders.length !== contestProblems.length) {
throw new UnprocessableDataException(
- 'the len of orders and the len of contestProblem are not equal.'
+ 'the length of orders and the length of contestProblem are not equal.'
)
}
- //problemId 기준으로 오름차순 정렬
- contestProblemsToBeUpdated.sort((a, b) => a.problemId - b.problemId)
- const queries = contestProblemsToBeUpdated.map((record) => {
- const newOrder = orders.indexOf(record.problemId) + 1
+
+ const queries = contestProblems.map((record) => {
+ const newOrder = orders.indexOf(record.problemId)
return this.prisma.contestProblem.update({
where: {
// eslint-disable-next-line @typescript-eslint/naming-convention
diff --git a/backend/apps/client/src/announcement/announcement.controller.spec.ts b/backend/apps/client/src/announcement/announcement.controller.spec.ts
new file mode 100644
index 0000000000..454dfc92c9
--- /dev/null
+++ b/backend/apps/client/src/announcement/announcement.controller.spec.ts
@@ -0,0 +1,25 @@
+import { Test, type TestingModule } from '@nestjs/testing'
+import { expect } from 'chai'
+import { RolesService } from '@libs/auth'
+import { AnnouncementController } from './announcement.controller'
+import { AnnouncementService } from './announcement.service'
+
+describe('AnnouncementController', () => {
+ let controller: AnnouncementController
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ controllers: [AnnouncementController],
+ providers: [
+ { provide: AnnouncementService, useValue: {} },
+ { provide: RolesService, useValue: {} }
+ ]
+ }).compile()
+
+ controller = module.get(AnnouncementController)
+ })
+
+ it('should be defined', () => {
+ expect(controller).to.be.ok
+ })
+})
diff --git a/backend/apps/client/src/announcement/announcement.controller.ts b/backend/apps/client/src/announcement/announcement.controller.ts
index cddaac8550..d53d21a0b9 100644
--- a/backend/apps/client/src/announcement/announcement.controller.ts
+++ b/backend/apps/client/src/announcement/announcement.controller.ts
@@ -3,12 +3,11 @@ import {
Get,
Logger,
InternalServerErrorException,
- Query,
- BadRequestException
+ Query
} from '@nestjs/common'
import { AuthNotNeededIfOpenSpace } from '@libs/auth'
import { EntityNotExistException } from '@libs/exception'
-import { GroupIDPipe, IDValidationPipe } from '@libs/pipe'
+import { GroupIDPipe, IDValidationPipe, RequiredIntPipe } from '@libs/pipe'
import { AnnouncementService } from './announcement.service'
@Controller('announcement')
@@ -21,16 +20,17 @@ export class AnnouncementController {
@Get()
async getAnnouncements(
@Query('problemId', IDValidationPipe) problemId: number | null,
- @Query('contestId', IDValidationPipe) contestId: number | null,
+ @Query('contestId', RequiredIntPipe) contestId: number,
@Query('groupId', GroupIDPipe) groupId: number
) {
try {
if (problemId) {
return await this.announcementService.getProblemAnnouncements(
problemId,
+ contestId,
groupId
)
- } else if (contestId) {
+ } else {
return await this.announcementService.getContestAnnouncements(
contestId,
groupId
@@ -43,8 +43,5 @@ export class AnnouncementController {
this.logger.error(error)
throw new InternalServerErrorException()
}
- throw new BadRequestException(
- 'Both problemId and contestId are not entered'
- )
}
}
diff --git a/backend/apps/client/src/announcement/announcement.service.spec.ts b/backend/apps/client/src/announcement/announcement.service.spec.ts
index cf01ea6746..572983e0f2 100644
--- a/backend/apps/client/src/announcement/announcement.service.spec.ts
+++ b/backend/apps/client/src/announcement/announcement.service.spec.ts
@@ -24,126 +24,36 @@ describe('AnnouncementService', () => {
describe('getProblemAnnouncements', () => {
it('should return problem announcements', async () => {
- const res = await service.getProblemAnnouncements(1, 1)
+ const res = await service.getProblemAnnouncements(1, 1, 1)
expect(res)
- .excluding(['createTime', 'updateTime'])
+ .excluding(['createTime', 'updateTime', 'content'])
.to.deep.equal([
{
id: 6,
- content: 'Announcement_1_0',
- problemId: 1,
- createTime: '2024-01-17T06:44:33.211Z',
- updateTime: '2024-01-17T06:44:33.211Z'
- },
- {
- id: 1,
- content: 'Announcement_0_0',
- problemId: 1,
- createTime: '2024-01-17T06:44:33.207Z',
- updateTime: '2024-01-17T06:44:33.207Z'
+ contestId: 1,
+ problemId: 1
}
])
})
-
- it('should return empty array when the problem announcement does not exist', async () => {
- const res = await service.getProblemAnnouncements(100, 2)
- expect(res).to.deep.equal([])
- })
})
describe('getContestAnnouncements', () => {
it('should return multiple contest announcements', async () => {
- /*
- pnpm test 실행 전 prisma migrate reset 실행하며 새롭게 seeding됨
- 이 과정에서 Announcement간 순서 섞이기 때문에 정렬 후 비교 필요
- */
- const res = (await service.getContestAnnouncements(1, 1)).sort((x, y) => {
- return x.problemId - y.problemId
- })
-
+ const res = await service.getContestAnnouncements(1, 1)
expect(res)
- .excluding(['createTime', 'updateTime', 'id', 'content'])
- .to.deep.equal(
- [
- {
- id: 10,
- content: 'Announcement_1_4',
- problemId: 5,
- createTime: '2024-01-26T09:39:30.978Z',
- updateTime: '2024-01-26T09:39:30.978Z'
- },
- {
- id: 6,
- content: 'Announcement_1_0',
- problemId: 1,
- createTime: '2024-01-26T09:39:30.977Z',
- updateTime: '2024-01-26T09:39:30.977Z'
- },
- {
- id: 7,
- content: 'Announcement_1_1',
- problemId: 2,
- createTime: '2024-01-26T09:39:30.977Z',
- updateTime: '2024-01-26T09:39:30.977Z'
- },
- {
- id: 8,
- content: 'Announcement_1_2',
- problemId: 3,
- createTime: '2024-01-26T09:39:30.977Z',
- updateTime: '2024-01-26T09:39:30.977Z'
- },
- {
- id: 9,
- content: 'Announcement_1_3',
- problemId: 4,
- createTime: '2024-01-26T09:39:30.977Z',
- updateTime: '2024-01-26T09:39:30.977Z'
- },
- {
- id: 3,
- content: 'Announcement_0_2',
- problemId: 3,
- createTime: '2024-01-26T09:39:30.976Z',
- updateTime: '2024-01-26T09:39:30.976Z'
- },
- {
- id: 4,
- content: 'Announcement_0_3',
- problemId: 4,
- createTime: '2024-01-26T09:39:30.976Z',
- updateTime: '2024-01-26T09:39:30.976Z'
- },
- {
- id: 5,
- content: 'Announcement_0_4',
- problemId: 5,
- createTime: '2024-01-26T09:39:30.976Z',
- updateTime: '2024-01-26T09:39:30.976Z'
- },
- {
- id: 1,
- content: 'Announcement_0_0',
- problemId: 1,
- createTime: '2024-01-26T09:39:30.975Z',
- updateTime: '2024-01-26T09:39:30.975Z'
- },
- {
- id: 2,
- content: 'Announcement_0_1',
- problemId: 2,
- createTime: '2024-01-26T09:39:30.975Z',
- updateTime: '2024-01-26T09:39:30.975Z'
- }
- ].sort((x, y) => {
- return x.problemId - y.problemId
- })
- )
- })
-
- it('should return empty array when the contest announcement does not exist', async () => {
- const res = await service.getContestAnnouncements(99999, 1)
- expect(res).to.deep.equal([])
+ .excluding(['createTime', 'updateTime', 'content'])
+ .to.deep.equal([
+ {
+ id: 6,
+ contestId: 1,
+ problemId: 0
+ },
+ {
+ id: 1,
+ contestId: 1,
+ problemId: null
+ }
+ ])
})
})
})
diff --git a/backend/apps/client/src/announcement/announcement.service.ts b/backend/apps/client/src/announcement/announcement.service.ts
index b37c5f886c..ec1e325dab 100644
--- a/backend/apps/client/src/announcement/announcement.service.ts
+++ b/backend/apps/client/src/announcement/announcement.service.ts
@@ -6,41 +6,48 @@ import { PrismaService } from '@libs/prisma'
export class AnnouncementService {
constructor(private readonly prisma: PrismaService) {}
- async getProblemAnnouncements(
- problemId: number,
+ async getContestAnnouncements(
+ contestId: number,
groupId: number
): Promise {
- const result = await this.prisma.announcement.findMany({
- where: {
- problem: {
- id: problemId,
+ const { contestProblem, announcement } =
+ await this.prisma.contest.findUniqueOrThrow({
+ where: {
+ id: contestId,
groupId
+ },
+ select: {
+ contestProblem: true,
+ announcement: {
+ orderBy: { updateTime: 'desc' }
+ }
}
- },
- orderBy: { updateTime: 'desc' }
- })
+ })
- return result
+ return announcement.map((announcement) => {
+ if (announcement.problemId !== null) {
+ announcement.problemId = contestProblem.find(
+ (problem) => announcement.problemId === problem.problemId
+ )!.order
+ }
+ return announcement
+ })
}
- async getContestAnnouncements(
+ async getProblemAnnouncements(
contestId: number,
+ problemId: number,
groupId: number
): Promise {
- const result = await this.prisma.announcement.findMany({
+ return await this.prisma.announcement.findMany({
where: {
- problem: {
- contestProblem: {
- some: {
- contestId
- }
- },
+ problemId,
+ contest: {
+ id: contestId,
groupId
}
},
orderBy: { updateTime: 'desc' }
})
-
- return result
}
}
diff --git a/backend/apps/client/src/contest/contest.controller.ts b/backend/apps/client/src/contest/contest.controller.ts
index 60031356f9..7ca3b7f7f4 100644
--- a/backend/apps/client/src/contest/contest.controller.ts
+++ b/backend/apps/client/src/contest/contest.controller.ts
@@ -36,33 +36,29 @@ export class ContestController {
constructor(private readonly contestService: ContestService) {}
- @Get()
+ @Get('ongoing-upcoming')
@AuthNotNeededIfOpenSpace()
- async getContests(
- @Req() req: AuthenticatedRequest,
+ async getOngoingUpcomingContests(
@Query('groupId', GroupIDPipe) groupId: number
) {
try {
- return await this.contestService.getContestsByGroupId(
- req.user?.id,
- groupId
- )
+ return await this.contestService.getContestsByGroupId(groupId)
} catch (error) {
- if (
- error instanceof Prisma.PrismaClientKnownRequestError &&
- error.name === 'NotFoundError'
- ) {
- throw new NotFoundException(error.message)
- }
this.logger.error(error)
throw new InternalServerErrorException()
}
}
- @Get('auth')
- async authGetContests(@Req() req: AuthenticatedRequest) {
+ @Get('ongoing-upcoming-with-registered')
+ async getOngoingUpcomingContestsWithRegistered(
+ @Req() req: AuthenticatedRequest,
+ @Query('groupId', GroupIDPipe) groupId: number
+ ) {
try {
- return await this.contestService.getContestsByGroupId(req.user.id)
+ return await this.contestService.getContestsByGroupId(
+ groupId,
+ req.user.id
+ )
} catch (error) {
this.logger.error(error)
throw new InternalServerErrorException()
@@ -75,13 +71,56 @@ export class ContestController {
@Query('groupId', GroupIDPipe) groupId: number,
@Query('cursor', CursorValidationPipe) cursor: number | null,
@Query('take', new DefaultValuePipe(10), new RequiredIntPipe('take'))
- take: number
+ take: number,
+ @Query('search') search?: string
) {
try {
return await this.contestService.getFinishedContestsByGroupId(
cursor,
take,
- groupId
+ groupId,
+ search
+ )
+ } catch (error) {
+ this.logger.error(error)
+ throw new InternalServerErrorException()
+ }
+ }
+
+ @Get('registered-finished')
+ async getRegisteredFinishedContests(
+ @Req() req: AuthenticatedRequest,
+ @Query('groupId', GroupIDPipe) groupId: number,
+ @Query('cursor', CursorValidationPipe) cursor: number | null,
+ @Query('take', new DefaultValuePipe(10), new RequiredIntPipe('take'))
+ take: number,
+ @Query('search') search?: string
+ ) {
+ try {
+ return await this.contestService.getRegisteredFinishedContests(
+ cursor,
+ take,
+ groupId,
+ req.user.id,
+ search
+ )
+ } catch (error) {
+ this.logger.error(error)
+ throw new InternalServerErrorException()
+ }
+ }
+
+ @Get('registered-ongoing-upcoming')
+ async getRegisteredOngoingUpcomingContests(
+ @Req() req: AuthenticatedRequest,
+ @Query('groupId', GroupIDPipe) groupId: number,
+ @Query('search') search?: string
+ ) {
+ try {
+ return await this.contestService.getRegisteredOngoingUpcomingContests(
+ groupId,
+ req.user.id,
+ search
)
} catch (error) {
this.logger.error(error)
diff --git a/backend/apps/client/src/contest/contest.service.spec.ts b/backend/apps/client/src/contest/contest.service.spec.ts
index e3eb4b0308..023c7104b8 100644
--- a/backend/apps/client/src/contest/contest.service.spec.ts
+++ b/backend/apps/client/src/contest/contest.service.spec.ts
@@ -1,32 +1,30 @@
+import { ConfigService } from '@nestjs/config'
import { Test, type TestingModule } from '@nestjs/testing'
import {
Prisma,
type Contest,
- type ContestRecord,
- type Group
+ type Group,
+ type ContestRecord
} from '@prisma/client'
-import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import { expect } from 'chai'
import * as dayjs from 'dayjs'
-import { stub } from 'sinon'
import {
ConflictFoundException,
EntityNotExistException,
ForbiddenAccessException
} from '@libs/exception'
import { PrismaService } from '@libs/prisma'
-import { type ContestSelectResult, ContestService } from './contest.service'
+import { ContestService, type ContestResult } from './contest.service'
const contestId = 1
-const userId = 1
+const user01Id = 4
const groupId = 1
-const undefinedUserId = undefined
const now = dayjs()
const contest = {
id: contestId,
- createdById: userId,
+ createdById: 1,
groupId,
title: 'title',
description: 'description',
@@ -45,24 +43,6 @@ const contest = {
} satisfies Contest & {
group: Partial
}
-const upcomingContest = {
- ...contest,
- startTime: now.add(1, 'day').toDate(),
- endTime: now.add(2, 'day').toDate()
-}
-
-const contestDetail = {
- id: contest.id,
- group: contest.group,
- title: contest.title,
- description: contest.description,
- startTime: contest.startTime,
- endTime: contest.endTime,
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _count: {
- contestRecord: 1
- }
-}
const ongoingContests = [
{
@@ -71,46 +51,9 @@ const ongoingContests = [
title: contest.title,
startTime: now.add(-1, 'day').toDate(),
endTime: now.add(1, 'day').toDate(),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _count: {
- contestRecord: 1
- }
- }
-] satisfies Partial[]
-const ongoingContestsWithParticipants = [
- {
- id: contest.id,
- group: contest.group,
- title: contest.title,
- startTime: now.add(-1, 'day').toDate(),
- endTime: now.add(1, 'day').toDate(),
- participants: 1
- }
-]
-
-const finishedContests = [
- {
- id: contest.id + 1,
- group: contest.group,
- title: contest.title,
- startTime: now.add(-2, 'day').toDate(),
- endTime: now.add(-1, 'day').toDate(),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _count: {
- contestRecord: 1
- }
- }
-] satisfies Partial[]
-const finishedContestsWithParticipants = [
- {
- id: contest.id + 1,
- group: contest.group,
- title: contest.title,
- startTime: now.add(-2, 'day').toDate(),
- endTime: now.add(-1, 'day').toDate(),
participants: 1
}
-]
+] satisfies Partial[]
const upcomingContests = [
{
@@ -119,365 +62,339 @@ const upcomingContests = [
title: contest.title,
startTime: now.add(1, 'day').toDate(),
endTime: now.add(2, 'day').toDate(),
- // eslint-disable-next-line @typescript-eslint/naming-convention
- _count: {
- contestRecord: 1
- }
- }
-] satisfies Partial[]
-const upcomingContestsWithParticipants = [
- {
- id: contest.id + 6,
- group: contest.group,
- title: contest.title,
- startTime: now.add(1, 'day').toDate(),
- endTime: now.add(2, 'day').toDate(),
- participants: 1
- }
-]
-
-const registeredOngoingContestsWithParticipants = [
- {
- id: contest.id,
- group: contest.group,
- title: contest.title,
- startTime: now.add(-1, 'day').toDate(),
- endTime: now.add(1, 'day').toDate(),
participants: 1
}
-]
+] satisfies Partial[]
-const registeredUpcomingContestsWithParticipants = [
+const finishedContests = [
{
- id: contest.id + 6,
+ id: contest.id + 1,
group: contest.group,
title: contest.title,
- startTime: now.add(1, 'day').toDate(),
- endTime: now.add(2, 'day').toDate(),
+ startTime: now.add(-2, 'day').toDate(),
+ endTime: now.add(-1, 'day').toDate(),
participants: 1
}
-]
+] satisfies Partial[]
const contests = [
...ongoingContests,
...finishedContests,
...upcomingContests
-] satisfies Partial[]
-
-const ongoingContest = ongoingContests[0]
-
-const earlierContest: Contest = {
- ...contest,
- id: contestId,
- startTime: new Date('2999-12-01T11:00:00.000+09:00'),
- endTime: new Date('2999-12-01T15:00:00.000+09:00'),
- config: {
- isVisible: false,
- isRankisVisible: true
- }
-}
-
-const laterContest: Contest = {
- ...contest,
- id: contestId,
- startTime: new Date('2999-12-01T12:00:00.000+09:00'),
- endTime: new Date('2999-12-01T15:00:00.000+09:00'),
- config: {
- isVisible: false,
- isRankisVisible: true
- }
-}
-
-const record: ContestRecord = {
- id: 1,
- contestId,
- userId,
- acceptedProblemNum: 0,
- score: 0,
- totalPenalty: 0,
- createTime: new Date(),
- updateTime: new Date()
-}
-const sortedContestRecordsWithUserDetail = [
- {
- user: {
- id: 13,
- username: 'user10'
- },
- score: 36,
- totalPenalty: 720
- },
- {
- user: {
- id: 12,
- username: 'user09'
- },
- score: 33,
- totalPenalty: 660
- },
- {
- user: {
- id: 11,
- username: 'user08'
- },
- score: 30,
- totalPenalty: 600
- }
-]
-
-const mockPrismaService = {
- contest: {
- findUnique: stub(),
- findUniqueOrThrow: stub(),
- findFirst: stub(),
- findFirstOrThrow: stub(),
- findMany: stub()
- },
- contestRecord: {
- findFirst: stub(),
- findFirstOrThrow: stub(),
- findMany: stub(),
- create: stub(),
- delete: stub()
- },
- userGroup: {
- findFirst: stub(),
- findMany: stub()
- },
- getPaginator: PrismaService.prototype.getPaginator
-}
+] satisfies Partial[]
describe('ContestService', () => {
let service: ContestService
+ let prisma: PrismaService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
- providers: [
- ContestService,
- { provide: PrismaService, useValue: mockPrismaService }
- ]
+ providers: [ContestService, PrismaService, ConfigService]
}).compile()
service = module.get(ContestService)
+ prisma = module.get(PrismaService)
})
it('should be defined', () => {
expect(service).to.be.ok
})
- describe('getContests', () => {
- beforeEach(() => {
- mockPrismaService.contest.findMany.resolves(contests)
- mockPrismaService.contestRecord.findMany.resolves([record])
+ describe('getContestsByGroupId', () => {
+ it('should return ongoing, upcoming contests when userId is undefined', async () => {
+ const contests = await service.getContestsByGroupId(groupId)
+ expect(contests.ongoing).to.have.lengthOf(4)
+ expect(contests.upcoming).to.have.lengthOf(2)
})
- afterEach(() => {
- mockPrismaService.contest.findMany.reset()
+
+ it('a contest should contain following fields when userId is undefined', async () => {
+ const contests = await service.getContestsByGroupId(groupId)
+ expect(contests.ongoing[0]).to.have.property('title')
+ expect(contests.ongoing[0]).to.have.property('startTime')
+ expect(contests.ongoing[0]).to.have.property('endTime')
+ expect(contests.ongoing[0]).to.have.property('participants')
+ expect(contests.ongoing[0].group).to.have.property('id')
+ expect(contests.ongoing[0].group).to.have.property('groupName')
+ expect(contests.upcoming[0]).to.have.property('title')
+ expect(contests.upcoming[0]).to.have.property('startTime')
+ expect(contests.upcoming[0]).to.have.property('endTime')
+ expect(contests.upcoming[0]).to.have.property('participants')
+ expect(contests.upcoming[0].group).to.have.property('id')
+ expect(contests.upcoming[0].group).to.have.property('groupName')
})
- it('should return ongoing, upcoming contests when userId is undefined', async () => {
- expect(
- await service.getContestsByGroupId(undefinedUserId, groupId)
- ).to.deep.equal({
- ongoing: ongoingContestsWithParticipants,
- upcoming: upcomingContestsWithParticipants
- })
+
+ it('should return ongoing, upcoming, registered ongoing, registered upcoming contests when userId is provided', async () => {
+ const contests = await service.getContestsByGroupId(groupId, user01Id)
+ expect(contests.ongoing).to.have.lengthOf(2)
+ expect(contests.upcoming).to.have.lengthOf(1)
+ expect(contests.registeredOngoing).to.have.lengthOf(2)
+ expect(contests.registeredUpcoming).to.have.lengthOf(2)
})
- it('should return registered ongoing, registered upcoming, ongoing, upcoming contests', async () => {
- expect(await service.getContestsByGroupId(userId, groupId)).to.deep.equal(
- {
- registeredOngoing: registeredOngoingContestsWithParticipants,
- registeredUpcoming: registeredUpcomingContestsWithParticipants,
- ongoing: ongoingContestsWithParticipants,
- upcoming: upcomingContestsWithParticipants
- }
- )
+ it('a contest should contain following fields when userId is provided', async () => {
+ const contests = await service.getContestsByGroupId(groupId, user01Id)
+ expect(contests.ongoing[0]).to.have.property('title')
+ expect(contests.ongoing[0]).to.have.property('startTime')
+ expect(contests.ongoing[0]).to.have.property('endTime')
+ expect(contests.ongoing[0]).to.have.property('participants')
+ expect(contests.ongoing[0].group).to.have.property('id')
+ expect(contests.ongoing[0].group).to.have.property('groupName')
+ expect(contests.upcoming[0]).to.have.property('title')
+ expect(contests.upcoming[0]).to.have.property('startTime')
+ expect(contests.upcoming[0]).to.have.property('endTime')
+ expect(contests.upcoming[0]).to.have.property('participants')
+ expect(contests.upcoming[0].group).to.have.property('id')
+ expect(contests.upcoming[0].group).to.have.property('groupName')
+ expect(contests.registeredOngoing[0]).to.have.property('title')
+ expect(contests.registeredOngoing[0]).to.have.property('startTime')
+ expect(contests.registeredOngoing[0]).to.have.property('endTime')
+ expect(contests.registeredOngoing[0]).to.have.property('participants')
+ expect(contests.registeredOngoing[0].group).to.have.property('id')
+ expect(contests.registeredOngoing[0].group).to.have.property('groupName')
+ expect(contests.registeredUpcoming[0]).to.have.property('title')
+ expect(contests.registeredUpcoming[0]).to.have.property('startTime')
+ expect(contests.registeredUpcoming[0]).to.have.property('endTime')
+ expect(contests.registeredUpcoming[0]).to.have.property('participants')
+ expect(contests.registeredUpcoming[0].group).to.have.property('id')
+ expect(contests.registeredUpcoming[0].group).to.have.property('groupName')
})
})
- describe('getFinishedContests', () => {
- after(() => {
- mockPrismaService.contest.findMany.reset()
+ describe('getRegisteredOngoingUpcomingContests', () => {
+ it('should return registeredOngoing, registeredUpcoming contests', async () => {
+ const contests = await service.getRegisteredOngoingUpcomingContests(
+ groupId,
+ user01Id
+ )
+ expect(contests.registeredOngoing).to.have.lengthOf(2)
+ expect(contests.registeredUpcoming).to.have.lengthOf(2)
})
- it('should return finished contests when cursor is 0', async () => {
- mockPrismaService.contest.findMany.resolves(finishedContests)
- expect(await service.getFinishedContestsByGroupId(null, 1)).to.deep.equal(
- {
- finished: finishedContestsWithParticipants
- }
+
+ it('a contest should contain following fields', async () => {
+ const contests = await service.getRegisteredOngoingUpcomingContests(
+ groupId,
+ user01Id
)
+ expect(contests.registeredOngoing[0]).to.have.property('title')
+ expect(contests.registeredOngoing[0]).to.have.property('startTime')
+ expect(contests.registeredOngoing[0]).to.have.property('endTime')
+ expect(contests.registeredOngoing[0]).to.have.property('participants')
+ expect(contests.registeredOngoing[0].group).to.have.property('id')
+ expect(contests.registeredOngoing[0].group).to.have.property('groupName')
+ expect(contests.registeredUpcoming[0]).to.have.property('title')
+ expect(contests.registeredUpcoming[0]).to.have.property('startTime')
+ expect(contests.registeredUpcoming[0]).to.have.property('endTime')
+ expect(contests.registeredUpcoming[0]).to.have.property('participants')
+ expect(contests.registeredUpcoming[0].group).to.have.property('id')
+ expect(contests.registeredUpcoming[0].group).to.have.property('groupName')
})
- })
- describe('startTimeCompare', () => {
- it('should return -1 when a is earlier than b', async () => {
+ it("shold return contests whose title contains '신입생'", async () => {
+ const keyword = '신입생'
+ const contests = await service.getRegisteredOngoingUpcomingContests(
+ groupId,
+ user01Id,
+ keyword
+ )
expect(
- service.startTimeCompare(earlierContest, laterContest)
- ).to.deep.equal(-1)
+ contests.registeredOngoing.map((contest) => contest.title)
+ ).to.deep.equals(['24년도 소프트웨어학과 신입생 입학 테스트2'])
})
+ })
- it('should return 1 when b is earlier than a', async () => {
- expect(
- service.startTimeCompare(laterContest, earlierContest)
- ).to.deep.equal(1)
+ describe('getRegisteredContestIds', async () => {
+ it("should return an array of contest's id user01 registered", async () => {
+ const contestIds = await service.getRegisteredContestIds(user01Id)
+ const registeredContestIds = [1, 3, 5, 7, 9, 11, 13, 15, 17]
+ contestIds.sort((a, b) => a - b)
+ expect(contestIds).to.deep.equal(registeredContestIds)
})
+ })
- it('should return 0 when a.startTime is equal b.startTime', async () => {
- expect(
- service.startTimeCompare(earlierContest, earlierContest)
- ).to.deep.equal(0)
+ describe('getRegisteredFinishedContests', async () => {
+ it('should return only 2 contests that user01 registered but finished', async () => {
+ const takeNum = 4
+ const contests = await service.getRegisteredFinishedContests(
+ null,
+ takeNum,
+ groupId,
+ user01Id
+ )
+ expect(contests).to.have.lengthOf(takeNum)
+ })
+
+ it('should return a contest array which starts with id 9', async () => {
+ const takeNum = 2
+ const prevCursor = 11
+ const contests = await service.getRegisteredFinishedContests(
+ prevCursor,
+ takeNum,
+ groupId,
+ user01Id
+ )
+ expect(contests[0].id).to.equals(9)
+ })
+
+ it('a contest should contain following fields', async () => {
+ const contests = await service.getRegisteredFinishedContests(
+ null,
+ 10,
+ groupId,
+ user01Id
+ )
+ expect(contests[0]).to.have.property('title')
+ expect(contests[0]).to.have.property('startTime')
+ expect(contests[0]).to.have.property('endTime')
+ expect(contests[0]).to.have.property('participants')
+ expect(contests[0].group).to.have.property('id')
+ expect(contests[0].group).to.have.property('groupName')
+ })
+
+ it("shold return contests whose title contains '낮'", async () => {
+ const keyword = '낮'
+ const contests = await service.getRegisteredFinishedContests(
+ null,
+ 10,
+ groupId,
+ user01Id,
+ keyword
+ )
+ expect(contests.map((contest) => contest.title)).to.deep.equals([
+ '소프트의 낮'
+ ])
+ })
+ })
+
+ describe('getFinishedContestsByGroupId', () => {
+ it('should return finished contests', async () => {
+ const contests = await service.getFinishedContestsByGroupId(
+ null,
+ 10,
+ groupId
+ )
+ const contestIds = contests.finished
+ .map((c) => c.id)
+ .sort((a, b) => a - b)
+ const finishedContestIds = [6, 7, 8, 9, 10, 11, 12, 13]
+ expect(contestIds).to.deep.equal(finishedContestIds)
})
})
describe('filterOngoing', () => {
- it('should return ongoing contests of the group', async () => {
+ it('should return ongoing contests of the group', () => {
expect(service.filterOngoing(contests)).to.deep.equal(ongoingContests)
})
})
describe('filterUpcoming', () => {
- it('should return upcoming contests of the group', async () => {
+ it('should return upcoming contests of the group', () => {
expect(service.filterUpcoming(contests)).to.deep.equal(upcomingContests)
})
})
- describe('getContestsByGroupId', () => {
- it('should return ongoing, upcoming, finished contests', async () => {
- mockPrismaService.contest.findMany.resolves(contests)
- expect(
- await service.getContestsByGroupId(undefinedUserId, groupId)
- ).to.deep.equal({
- ongoing: ongoingContestsWithParticipants,
- upcoming: upcomingContestsWithParticipants
- })
- mockPrismaService.contest.findMany.reset()
- })
-
- //TODO: test when userId is given
- })
-
describe('getContest', () => {
it('should throw error when contest does not exist', async () => {
- mockPrismaService.contest.findUniqueOrThrow.rejects(
- new Prisma.PrismaClientKnownRequestError('contest', {
- code: 'P2025',
- clientVersion: '5.8.1'
- })
- )
-
await expect(
- service.getContest(contestId + 999, groupId, 4)
+ service.getContest(999, groupId, user01Id)
).to.be.rejectedWith(EntityNotExistException)
})
it('should return contest', async () => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(contestDetail)
- mockPrismaService.contestRecord.findMany.resolves(
- sortedContestRecordsWithUserDetail
- )
-
- expect(await service.getContest(groupId, contestId, 4)).to.deep.equal({
- ...contestDetail,
- standings: sortedContestRecordsWithUserDetail.map((record, index) => ({
- ...record,
- standing: index + 1
- })),
- canRegister: true
- })
+ expect(await service.getContest(contestId, groupId, user01Id)).to.be.ok
})
})
describe('createContestRecord', () => {
- beforeEach(() => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(ongoingContest)
- mockPrismaService.contestRecord.findFirst.resolves(null)
- })
- afterEach(() => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(contest)
- mockPrismaService.contestRecord.findFirst.resolves(null)
+ let contestRecordId = -1
+
+ after(async () => {
+ await prisma.contestRecord.delete({
+ where: {
+ id: contestRecordId
+ }
+ })
})
it('should throw error when the contest does not exist', async () => {
- mockPrismaService.contest.findUniqueOrThrow.rejects(
- new Prisma.PrismaClientKnownRequestError('contest', {
- code: 'P2002',
- clientVersion: '5.1.1'
- })
- )
await expect(
- service.createContestRecord(contestId, userId)
+ service.createContestRecord(999, user01Id)
).to.be.rejectedWith(Prisma.PrismaClientKnownRequestError)
})
it('should throw error when user is participated in contest again', async () => {
- mockPrismaService.contestRecord.findFirst.resolves(record)
await expect(
- service.createContestRecord(contestId, userId)
+ service.createContestRecord(contestId, user01Id)
).to.be.rejectedWith(ConflictFoundException)
})
it('should throw error when contest is not ongoing', async () => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(finishedContests[0])
- await expect(
- service.createContestRecord(contestId, userId)
- ).to.be.rejectedWith(ConflictFoundException)
+ await expect(service.createContestRecord(8, user01Id)).to.be.rejectedWith(
+ ConflictFoundException
+ )
})
- it('should successfully create contestRankACM', async () => {
- mockPrismaService.contestRecord.create.reset()
- await service.createContestRecord(contestId, userId)
- expect(mockPrismaService.contestRecord.create.calledOnce).to.be.true
+ it('should register to a contest successfully', async () => {
+ const contestRecord = await service.createContestRecord(2, user01Id)
+ contestRecordId = contestRecord.id
+ expect(
+ await prisma.contestRecord.findUnique({
+ where: { id: contestRecordId }
+ })
+ ).to.deep.equals(contestRecord)
})
})
+
describe('deleteContestRecord', () => {
+ let contestRecord: ContestRecord | { id: number } = { id: -1 }
+
+ afterEach(async () => {
+ try {
+ await prisma.contestRecord.delete({
+ where: { id: contestRecord.id }
+ })
+ } catch (error) {
+ if (
+ !(
+ error instanceof Prisma.PrismaClientKnownRequestError &&
+ error.code === 'P2025'
+ )
+ ) {
+ throw error
+ }
+ }
+ })
+
it('should return deleted contest record', async () => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(upcomingContest)
- mockPrismaService.contestRecord.findFirstOrThrow.resolves(record)
- mockPrismaService.contestRecord.delete.resolves(record)
+ const newlyRegisteringContestId = 16
+ contestRecord = await prisma.contestRecord.create({
+ data: {
+ contestId: newlyRegisteringContestId,
+ userId: user01Id,
+ acceptedProblemNum: 0,
+ score: 0,
+ totalPenalty: 0
+ }
+ })
+
expect(
- await service.deleteContestRecord(contestId, userId)
- ).to.deep.equal(record)
+ await service.deleteContestRecord(newlyRegisteringContestId, user01Id)
+ ).to.deep.equal(contestRecord)
})
it('should throw error when contest does not exist', async () => {
- mockPrismaService.contest.findUniqueOrThrow.rejects(
- new PrismaClientKnownRequestError('contest', {
- code: 'P2025',
- clientVersion: '5.8.1'
- })
- )
await expect(
- service.deleteContestRecord(contestId, userId)
+ service.deleteContestRecord(999, user01Id)
).to.be.rejectedWith(EntityNotExistException)
})
+
it('should throw error when contest record does not exist', async () => {
- mockPrismaService.contestRecord.findFirstOrThrow.rejects(
- new PrismaClientKnownRequestError('contestRecord', {
- code: 'P2025',
- clientVersion: '5.8.1'
- })
- )
await expect(
- service.deleteContestRecord(contestId, userId)
+ service.deleteContestRecord(16, user01Id)
).to.be.rejectedWith(EntityNotExistException)
})
+
it('should throw error when contest is ongoing', async () => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(ongoingContest)
- mockPrismaService.contestRecord.findFirstOrThrow.resolves(record)
await expect(
- service.deleteContestRecord(contestId, userId)
+ service.deleteContestRecord(contestId, user01Id)
).to.be.rejectedWith(ForbiddenAccessException)
})
-
- it('should throw error when there is no record to delete', async () => {
- mockPrismaService.contest.findUniqueOrThrow.resolves(upcomingContest)
- mockPrismaService.contestRecord.findFirstOrThrow.resolves(record)
- mockPrismaService.contestRecord.delete.rejects(
- new PrismaClientKnownRequestError('contestRecord', {
- code: 'P2025',
- clientVersion: '5.8.1'
- })
- )
- await expect(
- service.deleteContestRecord(contestId, userId)
- ).to.be.rejectedWith(EntityNotExistException)
- })
})
})
diff --git a/backend/apps/client/src/contest/contest.service.ts b/backend/apps/client/src/contest/contest.service.ts
index f7a3cf1e8f..9d80ed4f75 100644
--- a/backend/apps/client/src/contest/contest.service.ts
+++ b/backend/apps/client/src/contest/contest.service.ts
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common'
-import { type Contest, Prisma } from '@prisma/client'
+import { Prisma } from '@prisma/client'
import { OPEN_SPACE_ID } from '@libs/constants'
import {
ConflictFoundException,
@@ -26,31 +26,31 @@ export type ContestSelectResult = Prisma.ContestGetPayload<{
select: typeof contestSelectOption
}>
+export type ContestResult = Omit & {
+ participants: number
+}
+
@Injectable()
export class ContestService {
constructor(private readonly prisma: PrismaService) {}
- async getContestsByGroupId(
- userId?: T,
- groupId?: number
+ async getContestsByGroupId(
+ groupId: number,
+ userId?: T
): Promise<
- T extends undefined
+ T extends undefined | null
? {
- ongoing: Partial[]
- upcoming: Partial[]
+ ongoing: ContestResult[]
+ upcoming: ContestResult[]
}
: {
- registeredOngoing: Partial[]
- registeredUpcoming: Partial[]
- ongoing: Partial[]
- upcoming: Partial[]
+ registeredOngoing: ContestResult[]
+ registeredUpcoming: ContestResult[]
+ ongoing: ContestResult[]
+ upcoming: ContestResult[]
}
>
-
- async getContestsByGroupId(
- userId: number | null = null,
- groupId = OPEN_SPACE_ID
- ) {
+ async getContestsByGroupId(groupId: number, userId: number | null = null) {
const now = new Date()
if (userId == null) {
const contests = await this.prisma.contest.findMany({
@@ -70,7 +70,8 @@ export class ContestService {
}
})
- const contestsWithParticipants = this.renameToParticipants(contests)
+ const contestsWithParticipants: ContestResult[] =
+ this.renameToParticipants(contests)
return {
ongoing: this.filterOngoing(contestsWithParticipants),
@@ -78,18 +79,7 @@ export class ContestService {
}
}
- const registeredContestRecords = await this.prisma.contestRecord.findMany({
- where: {
- userId
- },
- select: {
- contestId: true
- }
- })
-
- const registeredContestIds = registeredContestRecords.map(
- (obj) => obj.contestId
- )
+ const registeredContestIds = await this.getRegisteredContestIds(userId)
let registeredContests: ContestSelectResult[] = []
let restContests: ContestSelectResult[] = []
@@ -115,15 +105,15 @@ export class ContestService {
restContests = await this.prisma.contest.findMany({
where: {
groupId,
- endTime: {
- gt: now
- },
config: {
path: ['isVisible'],
equals: true
},
id: {
notIn: registeredContestIds
+ },
+ endTime: {
+ gt: now
}
},
select: contestSelectOption,
@@ -146,10 +136,98 @@ export class ContestService {
}
}
+ async getRegisteredOngoingUpcomingContests(
+ groupId: number,
+ userId: number,
+ search?: string
+ ) {
+ const now = new Date()
+ const registeredContestIds = await this.getRegisteredContestIds(userId)
+
+ const ongoingAndUpcomings = await this.prisma.contest.findMany({
+ where: {
+ groupId,
+ id: {
+ in: registeredContestIds
+ },
+ endTime: {
+ gt: now
+ },
+ title: {
+ contains: search
+ }
+ },
+ select: contestSelectOption
+ })
+
+ const ongoingAndUpcomingsWithParticipants =
+ this.renameToParticipants(ongoingAndUpcomings)
+
+ return {
+ registeredOngoing: this.filterOngoing(
+ ongoingAndUpcomingsWithParticipants
+ ),
+ registeredUpcoming: this.filterUpcoming(
+ ongoingAndUpcomingsWithParticipants
+ )
+ }
+ }
+
+ async getRegisteredContestIds(userId: number) {
+ const registeredContestRecords = await this.prisma.contestRecord.findMany({
+ where: {
+ userId
+ },
+ select: {
+ contestId: true
+ }
+ })
+
+ return registeredContestRecords.map((obj) => obj.contestId)
+ }
+
+ async getRegisteredFinishedContests(
+ cursor: number | null,
+ take: number,
+ groupId: number,
+ userId: number,
+ search?: string
+ ) {
+ const now = new Date()
+ const paginator = this.prisma.getPaginator(cursor)
+
+ const registeredContestIds = await this.getRegisteredContestIds(userId)
+ const contests = await this.prisma.contest.findMany({
+ ...paginator,
+ take,
+ where: {
+ groupId,
+ endTime: {
+ lte: now
+ },
+ id: {
+ in: registeredContestIds
+ },
+ title: {
+ contains: search
+ },
+ config: {
+ path: ['isVisible'],
+ equals: true
+ }
+ },
+ select: contestSelectOption,
+ orderBy: [{ endTime: 'desc' }, { id: 'desc' }]
+ })
+
+ return this.renameToParticipants(contests)
+ }
+
async getFinishedContestsByGroupId(
cursor: number | null,
take: number,
- groupId = OPEN_SPACE_ID
+ groupId: number,
+ search?: string
) {
const paginator = this.prisma.getPaginator(cursor)
const now = new Date()
@@ -165,12 +243,13 @@ export class ContestService {
config: {
path: ['isVisible'],
equals: true
+ },
+ title: {
+ contains: search
}
},
select: contestSelectOption,
- orderBy: {
- endTime: 'desc'
- }
+ orderBy: [{ endTime: 'desc' }, { id: 'desc' }]
})
return { finished: this.renameToParticipants(finished) }
}
@@ -184,37 +263,19 @@ export class ContestService {
}))
}
- startTimeCompare(
- a: Partial & Pick,
- b: Partial & Pick
- ) {
- if (a.startTime < b.startTime) {
- return -1
- }
- if (a.startTime > b.startTime) {
- return 1
- }
- return 0
- }
-
- filterOngoing(
- contests: Array & Pick>
- ) {
+ filterOngoing(contests: ContestResult[]) {
const now = new Date()
- const ongoingContest = contests.filter(
- (contest) => contest.startTime <= now && contest.endTime > now
- )
+ const ongoingContest = contests
+ .filter((contest) => contest.startTime <= now && contest.endTime > now)
+ .sort((a, b) => a.endTime.getTime() - b.endTime.getTime())
return ongoingContest
}
- filterUpcoming(
- contests: Array & Pick>
- ) {
+ filterUpcoming(contests: ContestResult[]) {
const now = new Date()
- const upcomingContest = contests.filter(
- (contest) => contest.startTime > now
- )
- upcomingContest.sort(this.startTimeCompare)
+ const upcomingContest = contests
+ .filter((contest) => contest.startTime > now)
+ .sort((a, b) => a.startTime.getTime() - b.startTime.getTime())
return upcomingContest
}
diff --git a/backend/apps/client/src/problem/mock/problem.mock.ts b/backend/apps/client/src/problem/mock/problem.mock.ts
index a51e35bfcc..ec88bd549c 100644
--- a/backend/apps/client/src/problem/mock/problem.mock.ts
+++ b/backend/apps/client/src/problem/mock/problem.mock.ts
@@ -31,7 +31,8 @@ export const problems: Problem[] = [
updateTime: faker.date.past(),
inputExamples: [],
outputExamples: [],
- template: []
+ template: [],
+ isVisible: true
},
{
id: 2,
@@ -55,7 +56,8 @@ export const problems: Problem[] = [
updateTime: faker.date.past(),
inputExamples: [],
outputExamples: [],
- template: []
+ template: [],
+ isVisible: true
}
]
diff --git a/backend/apps/client/src/problem/problem.repository.ts b/backend/apps/client/src/problem/problem.repository.ts
index 792183a386..a7b1c52c5a 100644
--- a/backend/apps/client/src/problem/problem.repository.ts
+++ b/backend/apps/client/src/problem/problem.repository.ts
@@ -98,7 +98,8 @@ export class ProblemRepository {
// 추후에 검색 성능을 개선할 수 있는 방법을 찾아보자
// 아니면 텍스트가 많은 field에서는 full-text search를 사용하고, 텍스트가 적은 field에서는 contains를 사용하는 방법도 고려해보자.
contains: search
- }
+ },
+ isVisible: true
},
select: {
...this.problemsSelectOption,
@@ -118,7 +119,8 @@ export class ProblemRepository {
title: {
// TODO: 검색 방식 변경 시 함께 변경 요함
contains: search
- }
+ },
+ isVisible: true
}
})
}
@@ -148,7 +150,8 @@ export class ProblemRepository {
return await this.prisma.problem.findUniqueOrThrow({
where: {
id: problemId,
- groupId
+ groupId,
+ isVisible: true
},
select: this.problemSelectOption
})
@@ -184,7 +187,13 @@ export class ProblemRepository {
return await this.prisma.contestProblem.findMany({
...paginator,
take,
- where: { contestId },
+ orderBy: { order: 'asc' },
+ where: {
+ contestId,
+ problem: {
+ isVisible: true
+ }
+ },
select: {
order: true,
problem: {
@@ -202,7 +211,10 @@ export class ProblemRepository {
async getContestProblemTotalCount(contestId: number) {
return await this.prisma.contestProblem.count({
where: {
- contestId
+ contestId,
+ problem: {
+ isVisible: true
+ }
}
})
}
@@ -214,6 +226,9 @@ export class ProblemRepository {
contestId_problemId: {
contestId,
problemId
+ },
+ problem: {
+ isVisible: true
}
},
select: {
@@ -246,7 +261,12 @@ export class ProblemRepository {
return await this.prisma.workbookProblem.findMany({
...paginator,
take,
- where: { workbookId },
+ where: {
+ workbookId,
+ problem: {
+ isVisible: true
+ }
+ },
select: {
order: true,
problem: {
@@ -259,7 +279,10 @@ export class ProblemRepository {
async getWorkbookProblemTotalCount(workbookId: number) {
return await this.prisma.workbookProblem.count({
where: {
- workbookId
+ workbookId,
+ problem: {
+ isVisible: true
+ }
}
})
}
@@ -271,6 +294,9 @@ export class ProblemRepository {
workbookId_problemId: {
workbookId,
problemId
+ },
+ problem: {
+ isVisible: true
}
},
select: {
diff --git a/backend/apps/client/src/submission/mock/problem.mock.ts b/backend/apps/client/src/submission/mock/problem.mock.ts
index ca778e0422..51c776a1a1 100644
--- a/backend/apps/client/src/submission/mock/problem.mock.ts
+++ b/backend/apps/client/src/submission/mock/problem.mock.ts
@@ -32,6 +32,7 @@ export const problems: Problem[] = [
createTime: faker.date.past(),
updateTime: faker.date.past(),
inputExamples: [],
- outputExamples: []
+ outputExamples: [],
+ isVisible: true
}
]
diff --git a/backend/package.json b/backend/package.json
index 48e656d8f0..0a74862ed9 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -18,22 +18,22 @@
},
"dependencies": {
"@apollo/server": "^4.10.0",
- "@aws-sdk/client-s3": "^3.504.0",
- "@aws-sdk/client-ses": "^3.504.0",
- "@aws-sdk/credential-provider-node": "^3.504.0",
+ "@aws-sdk/client-s3": "^3.514.0",
+ "@aws-sdk/client-ses": "^3.514.0",
+ "@aws-sdk/credential-provider-node": "^3.514.0",
"@golevelup/nestjs-rabbitmq": "^4.1.0",
"@nestjs-modules/mailer": "^1.10.3",
- "@nestjs/apollo": "^12.0.11",
- "@nestjs/axios": "^3.0.1",
- "@nestjs/cache-manager": "^2.2.0",
- "@nestjs/common": "^10.3.1",
- "@nestjs/config": "^3.1.1",
- "@nestjs/core": "^10.3.1",
- "@nestjs/graphql": "^12.0.11",
+ "@nestjs/apollo": "^12.1.0",
+ "@nestjs/axios": "^3.0.2",
+ "@nestjs/cache-manager": "^2.2.1",
+ "@nestjs/common": "^10.3.3",
+ "@nestjs/config": "^3.2.0",
+ "@nestjs/core": "^10.3.3",
+ "@nestjs/graphql": "^12.1.1",
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
- "@nestjs/platform-express": "^10.3.1",
- "@nestjs/swagger": "^7.2.0",
+ "@nestjs/platform-express": "^10.3.3",
+ "@nestjs/swagger": "^7.3.0",
"@prisma/client": "^5.9.1",
"argon2": "^0.31.2",
"axios": "^1.6.7",
@@ -65,24 +65,24 @@
"zod": "^3.22.4"
},
"devDependencies": {
- "@faker-js/faker": "^8.4.0",
- "@nestjs/cli": "^10.3.0",
- "@nestjs/schematics": "^10.1.0",
- "@nestjs/testing": "^10.3.1",
+ "@faker-js/faker": "^8.4.1",
+ "@nestjs/cli": "^10.3.2",
+ "@nestjs/schematics": "^10.1.1",
+ "@nestjs/testing": "^10.3.3",
"@types/cache-manager": "^4.0.6",
"@types/chai": "^4.3.11",
"@types/chai-as-promised": "^7.1.8",
"@types/express": "^4.17.21",
"@types/graphql-upload": "8.0.12",
"@types/mocha": "^10.0.6",
- "@types/node": "^20.11.16",
+ "@types/node": "^20.11.17",
"@types/nodemailer": "^6.4.14",
"@types/passport-jwt": "^4.0.1",
"@types/proxyquire": "^1.3.31",
"@types/sinon": "^17.0.3",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.1",
- "mocha": "^10.2.0",
+ "mocha": "^10.3.0",
"prisma": "^5.9.1",
"prisma-nestjs-graphql": "^20.0.2",
"proxyquire": "^2.1.3",
diff --git a/backend/prisma/migrations/20240207142636_connect_announcement_to_contest/migration.sql b/backend/prisma/migrations/20240207142636_connect_announcement_to_contest/migration.sql
new file mode 100644
index 0000000000..1112f5e7a4
--- /dev/null
+++ b/backend/prisma/migrations/20240207142636_connect_announcement_to_contest/migration.sql
@@ -0,0 +1,18 @@
+/*
+ Warnings:
+
+ - Added the required column `contest_id` to the `announcement` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "announcement" DROP CONSTRAINT "announcement_problem_id_fkey";
+
+-- AlterTable
+ALTER TABLE "announcement" ADD COLUMN "contest_id" INTEGER NOT NULL,
+ALTER COLUMN "problem_id" DROP NOT NULL;
+
+-- AddForeignKey
+ALTER TABLE "announcement" ADD CONSTRAINT "announcement_contest_id_fkey" FOREIGN KEY ("contest_id") REFERENCES "contest"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "announcement" ADD CONSTRAINT "announcement_problem_id_fkey" FOREIGN KEY ("problem_id") REFERENCES "problem"("id") ON DELETE SET NULL ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/20240213051227_add_is_visible_to_problem_model/migration.sql b/backend/prisma/migrations/20240213051227_add_is_visible_to_problem_model/migration.sql
new file mode 100644
index 0000000000..fcdd11007b
--- /dev/null
+++ b/backend/prisma/migrations/20240213051227_add_is_visible_to_problem_model/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "problem" ADD COLUMN "is_visible" BOOLEAN NOT NULL DEFAULT true;
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 8050f9404e..0bbcd72ae6 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -148,6 +148,7 @@ model Problem {
inputDescription String @map("input_description")
outputDescription String @map("output_description")
hint String
+ isVisible Boolean @default(true) @map("is_visible")
/// template code item structure
/// {
/// "lanaguage": Language,
@@ -178,7 +179,7 @@ model Problem {
workbookProblem WorkbookProblem[]
submission Submission[]
announcement Announcement[]
- CodeDraft CodeDraft[]
+ codeDraft CodeDraft[]
@@map("problem")
}
@@ -258,7 +259,7 @@ model Contest {
contestProblem ContestProblem[]
contestRecord ContestRecord[]
submission Submission[]
- // contestAnnouncement ContestAnnouncement[]
+ announcement Announcement[]
@@map("contest")
}
@@ -281,8 +282,10 @@ model ContestProblem {
model Announcement {
id Int @id @default(autoincrement())
content String
- problem Problem @relation(fields: [problemId], references: [id])
- problemId Int @map("problem_id")
+ contest Contest @relation(fields: [contestId], references: [id])
+ contestId Int @map("contest_id")
+ problem Problem? @relation(fields: [problemId], references: [id])
+ problemId Int? @map("problem_id")
createTime DateTime @default(now()) @map("create_time")
updateTime DateTime @updatedAt @map("update_time")
diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts
index bc2d3fc970..5a704b3e5e 100644
--- a/backend/prisma/seed.ts
+++ b/backend/prisma/seed.ts
@@ -30,6 +30,7 @@ let publicGroup: Group
let privateGroup: Group
const problems: Problem[] = []
const problemTestcases: ProblemTestcase[] = []
+const contests: Contest[] = []
const endedContests: Contest[] = []
const ongoingContests: Contest[] = []
const upcomingContests: Contest[] = []
@@ -1179,6 +1180,7 @@ const createContests = async () => {
const now = new Date()
for (const obj of contestData) {
const contest = await prisma.contest.create(obj)
+ contests.push(contest)
if (now < obj.data.startTime) {
upcomingContests.push(contest)
} else if (obj.data.endTime < now) {
@@ -1192,7 +1194,7 @@ const createContests = async () => {
for (const problem of problems) {
await prisma.contestProblem.create({
data: {
- order: problem.id,
+ order: problem.id - 1,
contestId: ongoingContests[0].id,
problemId: problem.id
}
@@ -1231,14 +1233,14 @@ const createWorkbooks = async () => {
for (const problem of problems) {
await prisma.workbookProblem.create({
data: {
- order: problem.id,
+ order: problem.id - 1,
workbookId: workbooks[0].id,
problemId: problem.id
}
})
await prisma.workbookProblem.create({
data: {
- order: problem.id,
+ order: problem.id - 1,
workbookId: privateWorkbooks[0].id,
problemId: problem.id
}
@@ -1512,7 +1514,7 @@ const createAnnouncements = async () => {
await prisma.announcement.create({
data: {
content: `Announcement_0_${i}`,
- problemId: problems[i].id
+ contestId: ongoingContests[i].id
}
})
)
@@ -1522,7 +1524,12 @@ const createAnnouncements = async () => {
announcements.push(
await prisma.announcement.create({
data: {
- content: `Announcement_1_${i}`,
+ content: `Announcement_1_${i}...
+아래 내용은 한글 Lorem Ipsum으로 생성된 내용입니다! 별 의미 없어요.
+모든 국민은 신속한 재판을 받을 권리를 가진다. 형사피고인은 상당한 이유가 없는 한 지체없이 공개재판을 받을 권리를 가진다.
+법관은 탄핵 또는 금고 이상의 형의 선고에 의하지 아니하고는 파면되지 아니하며, 징계처분에 의하지 아니하고는 정직·감봉 기타 불리한 처분을 받지 아니한다.
+일반사면을 명하려면 국회의 동의를 얻어야 한다. 연소자의 근로는 특별한 보호를 받는다.`,
+ contestId: ongoingContests[i].id,
problemId: problems[i].id
}
})
@@ -1607,21 +1614,23 @@ const createContestRecords = async () => {
contestRecords.push(contestRecord)
i++
}
- // User 1이 Future Contest에 참가한 record를 추가합니다.
- // 그래야 upcoming contest에 참가한 User 1의 contest register를 un-register할 수 있습니다.
- contestRecords.push(
- await prisma.contestRecord.create({
- data: {
- //User1
- userId: 4,
- //Future Contest
- contestId: 15,
- acceptedProblemNum: 0,
- score: 0,
- totalPenalty: 0
- }
- })
- )
+
+ // upcoming contest에 참가한 User 1의 contest register를 un-register하는 기능과,
+ // registered upcoming, ongoing, finished contest를 조회하는 기능을 확인하기 위함
+ const user01Id = 4
+ for (let contestId = 3; contestId <= contests.length; contestId += 2) {
+ contestRecords.push(
+ await prisma.contestRecord.create({
+ data: {
+ userId: user01Id,
+ contestId,
+ acceptedProblemNum: 0,
+ score: 0,
+ totalPenalty: 0
+ }
+ })
+ )
+ }
return contestRecords
}
diff --git a/collection/admin/Contest/Get Contest/NOT_FOUND.bru b/collection/admin/Contest/Get Contest/NOT_FOUND.bru
new file mode 100644
index 0000000000..b041e54bba
--- /dev/null
+++ b/collection/admin/Contest/Get Contest/NOT_FOUND.bru
@@ -0,0 +1,32 @@
+meta {
+ name: NOT_FOUND
+ type: graphql
+ seq: 2
+}
+
+post {
+ url: {{gqlUrl}}
+ body: graphql
+ auth: none
+}
+
+body:graphql {
+ query GetContest($contestId: Int!) {
+ getContest(contestId: $contestId) {
+ id
+ title
+ participants
+ }
+ }
+}
+
+body:graphql:vars {
+ {
+ "contestId": 99999
+ }
+}
+
+assert {
+ res.body.errors[0].message: eq No Contest found
+ res.body.errors[0].extensions.code: eq NOT_FOUND
+}
diff --git a/collection/admin/Contest/Get Contest/Succeed.bru b/collection/admin/Contest/Get Contest/Succeed.bru
new file mode 100644
index 0000000000..54780a3560
--- /dev/null
+++ b/collection/admin/Contest/Get Contest/Succeed.bru
@@ -0,0 +1,40 @@
+meta {
+ name: Succeed
+ type: graphql
+ seq: 1
+}
+
+post {
+ url: {{gqlUrl}}
+ body: graphql
+ auth: none
+}
+
+body:graphql {
+ query GetContest($contestId: Int!) {
+ getContest(contestId: $contestId) {
+ id
+ title
+ participants
+ }
+ }
+}
+
+body:graphql:vars {
+ {
+ "contestId": 1
+ }
+}
+
+assert {
+ res.body.data.getContest: isDefined
+}
+
+docs {
+ ## Get Problem
+ 참가자수 정보를 포함한 Problem 정보를 가져옵니다.
+
+ ### Error Cases
+ #### NOT_FOUND
+ 존재하는 contestId를 전달해야 합니다.
+}
diff --git a/collection/admin/Contest/Get Contests/Succeed.bru b/collection/admin/Contest/Get Contests/Succeed.bru
index 5c9188d623..97458d7f03 100644
--- a/collection/admin/Contest/Get Contests/Succeed.bru
+++ b/collection/admin/Contest/Get Contests/Succeed.bru
@@ -22,6 +22,7 @@ body:graphql {
description
startTime
endTime
+ participants
}
}
}
diff --git a/collection/admin/Problem/Create Problem/Succeed.bru b/collection/admin/Problem/Create Problem/Succeed.bru
index c5f37c0fa4..2b7f069b6b 100644
--- a/collection/admin/Problem/Create Problem/Succeed.bru
+++ b/collection/admin/Problem/Create Problem/Succeed.bru
@@ -21,6 +21,10 @@ body:graphql {
inputDescription
outputDescription
hint
+ isVisible
+ template
+ createTime
+ updateTime
}
}
}
@@ -34,6 +38,7 @@ body:graphql:vars {
"inputDescription": "input description",
"outputDescription": "output description",
"hint": "hint",
+ "isVisible": false,
"template": {
"language": "Cpp",
"code": [
diff --git a/collection/admin/Problem/Create Problem/UNPROCESSABLE (1).bru b/collection/admin/Problem/Create Problem/UNPROCESSABLE (1).bru
index 875fb7c5f8..ce2839ab11 100644
--- a/collection/admin/Problem/Create Problem/UNPROCESSABLE (1).bru
+++ b/collection/admin/Problem/Create Problem/UNPROCESSABLE (1).bru
@@ -34,6 +34,7 @@ body:graphql:vars {
"inputDescription": "input description",
"outputDescription": "output description",
"hint": "hint",
+ "isVisible": false,
"template": {
"language": "Cpp",
"code": [
diff --git a/collection/admin/Problem/Create Problem/UNPROCESSABLE (2).bru b/collection/admin/Problem/Create Problem/UNPROCESSABLE (2).bru
index e089b78765..3c66239c4e 100644
--- a/collection/admin/Problem/Create Problem/UNPROCESSABLE (2).bru
+++ b/collection/admin/Problem/Create Problem/UNPROCESSABLE (2).bru
@@ -34,6 +34,7 @@ body:graphql:vars {
"inputDescription": "input description",
"outputDescription": "output description",
"hint": "hint",
+ "isVisible": false,
"template": {
"language": "Cpp",
"code": [
diff --git a/collection/admin/Problem/Update ContestProblems Order/Succeed.bru b/collection/admin/Problem/Update ContestProblems Order/Succeed.bru
index 6424a35b55..71c5e3af01 100644
--- a/collection/admin/Problem/Update ContestProblems Order/Succeed.bru
+++ b/collection/admin/Problem/Update ContestProblems Order/Succeed.bru
@@ -21,7 +21,7 @@ body:graphql {
updateTime
}
}
-
+
}
body:graphql:vars {
@@ -39,10 +39,10 @@ assert {
docs {
## Update ContestProblems order
contest problems의 정렬 순서를 변경합니다.
-
+
order: [3, 4, 5, 6, 7, 8, 1, 2]와 같이 `problemId`의 순서를 변경할 수 있습니다.
- 위의 예시는 3번, 4번, 5번, 6번, 7번, 8번, 1번, 2번 `problemId` 순으로 (3번 problem이 1번 `order`, 2번 problem이 10번 `order`) `problemId`의 순서를 변경합니다.
-
+ 위의 예시는 3번, 4번, 5번, 6번, 7번, 8번, 1번, 2번 `problemId` 순으로 (3번 problem이 0번째, 2번 problem이 9번째) `problemId`의 순서를 변경합니다.
+
### Error Cases
#### UNPROCESSABLE
`orders`의 길이는 contest에 속한 문제들의 개수와 같아야 합니다.
diff --git a/collection/admin/Problem/Update a Problem/Succeed.bru b/collection/admin/Problem/Update a Problem/Succeed.bru
index 7e6657ad9d..d141268a73 100644
--- a/collection/admin/Problem/Update a Problem/Succeed.bru
+++ b/collection/admin/Problem/Update a Problem/Succeed.bru
@@ -21,6 +21,7 @@ body:graphql {
inputDescription
outputDescription
hint
+ isVisible
}
}
}
@@ -34,7 +35,8 @@ body:graphql:vars {
"description": "updated description",
"inputDescription": "updated input description",
"outputDescription": "updated output description",
- "hint": "updated hint"
+ "hint": "updated hint",
+ "isVisible": false
}
}
}
diff --git a/collection/client/Announcement/Get Announcements/Succeed.bru b/collection/client/Announcement/Get Announcements/Succeed.bru
index 7dc45eb380..06c93e41b4 100644
--- a/collection/client/Announcement/Get Announcements/Succeed.bru
+++ b/collection/client/Announcement/Get Announcements/Succeed.bru
@@ -5,14 +5,14 @@ meta {
}
get {
- url: {{baseUrl}}/announcement?problemId=1
+ url: {{baseUrl}}/announcement?contestId=1
body: none
auth: none
}
query {
- problemId: 1
- ~contestId: 1
+ contestId: 1
+ ~problemId: 1
~groupId: 1
}
@@ -32,12 +32,18 @@ docs {
## Query
- > problemId 또는 contestId 중 하나 값은 필수로 주어져야 합니다.
- > 두 값이 모두 주어진 경우, problemId가 우선 적용되어 problem announcements가 반환됩니다.
+ > 필수 query는 * 표시하였습니다.
| 이름 | 타입 | 설명 |
|-----|-----|-----|
- |problemId *|Integer|안내 사항이 속한 문제 ID|
|contestId *|Integer|안내 사항이 속한 대회 ID|
+ |problemId|Integer|안내 사항이 속한 문제 ID|
|groupId|Integer|안내 사항이 속한 Group ID (default: 1)|
+
+ problemId가 주어진 경우, 해당하는 contest problem에 대한 안내 사항을 반환합니다.
+
+ ## Response
+
+ - `problemId`는 안내 사항과 연관된 문제의 order입니다.
+ - `problemId`가 null이라면, 해당 안내 사항은 특정 문제에 국한된 것이 아니라 대회 자체에 대한 것입니다.
}
diff --git a/collection/client/Announcement/Get Announcements/[400] Related ID Not Given.bru b/collection/client/Announcement/Get Announcements/[400] Related ID Not Given.bru
index 3dce423dec..0f330f3a2c 100644
--- a/collection/client/Announcement/Get Announcements/[400] Related ID Not Given.bru
+++ b/collection/client/Announcement/Get Announcements/[400] Related ID Not Given.bru
@@ -19,19 +19,3 @@ query {
assert {
res.status: eq 400
}
-
-docs {
- # Get Announcements
-
- 대회 또는 문제에 대한 안내 사항을 가져옵니다.
-
- ## Query
-
- > problemId 또는 contestId 중 하나 값은 필수로 주어져야 합니다.
-
- | 이름 | 타입 | 설명 |
- |-----|-----|-----|
- |problemId *|Integer|안내 사항이 속한 문제 ID|
- |contestId *|Integer|안내 사항이 속한 대회 ID|
- |groupId|Integer|안내 사항이 속한 Group ID (default: 1)|
-}
diff --git a/collection/client/Contest/Get finished contests/Succeed.bru b/collection/client/Contest/Get finished contests/Succeed.bru
index 88e29a413c..3c5ffcb9d3 100644
--- a/collection/client/Contest/Get finished contests/Succeed.bru
+++ b/collection/client/Contest/Get finished contests/Succeed.bru
@@ -14,6 +14,7 @@ query {
~take: 10
~cursor: 1
~groupId: 1
+ ~search: 소
}
assert {
@@ -24,19 +25,23 @@ assert {
res("finished")[0].endTime: isString
res("finished")[0].group.id: isNumber
res("finished")[0].group.groupName: isString
+ res("finished")[0].participants: isNumber
}
docs {
- # Add Finished Contests
+ ## Add Finished Contests
종료된 대회들을 가져옵니다.
- ## Query
+ pagination이 가능하며, 제목 검색 기능을 포함합니다.
+
+ ### Query
| 이름 | 타입 | 설명 |
|-----|-----|-----|
|take |Integer|가져올 대회 개수 (default: 10)|
|cursor|Integer|cursor 값 다음의 ID를 가진 대회들을 반환|
|groupId |Integer|대회가 속한 Group ID (default: 1)|
+ |search|String|title을 기준으로 검색할 키워드. 포함하지 않으면 검색을 수행하지 않음|
}
diff --git a/collection/client/Contest/Get finished contests/[403] Not a Member of the Group.bru b/collection/client/Contest/Get finished contests/[403] Not a Member of the Group.bru
deleted file mode 100644
index 753ad57211..0000000000
--- a/collection/client/Contest/Get finished contests/[403] Not a Member of the Group.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: [403] Not a user of the group
- type: http
- seq: 2
-}
-
-get {
- url: {{baseUrl}}/contest/finished?groupId=99999
- body: none
- auth: none
-}
-
-query {
- groupId: 99999
-}
-
-assert {
- res.status: eq 403
-}
diff --git a/collection/client/Contest/Get ongoing & upcoming contests (logged in)/Succeed.bru b/collection/client/Contest/Get ongoing & upcoming contests (logged in)/Succeed.bru
index f06bbe9186..e9a09f5424 100644
--- a/collection/client/Contest/Get ongoing & upcoming contests (logged in)/Succeed.bru
+++ b/collection/client/Contest/Get ongoing & upcoming contests (logged in)/Succeed.bru
@@ -5,7 +5,7 @@ meta {
}
get {
- url: {{baseUrl}}/contest/auth
+ url: {{baseUrl}}/contest/ongoing-upcoming-with-registered
body: none
auth: none
}
@@ -18,12 +18,25 @@ assert {
res("ongoing")[0].group.id: isNumber
res("ongoing")[0].group.groupName: isString
res.status: eq 200
- res("ongoing")[0].participants: isNumber
+ res("ongoing")[0].participants: isNumber
+ res("registeredOngoing"): isDefined
+ res("registeredUpcoming"): isDefined
+}
+
+script:pre-request {
+ await require("./login").loginUser(req);
}
docs {
- # Get Ongoing & Upcoming contests (logged in)
- ---
- - 종료되지 않은 대회 중 현재 로그인한 유저가 등록한 대회와 등록하지 않은 대회를 구분하여 반환합니다.
- - 유저가 등록한 대회 정보는 `registeredOngoing`, `registeredUpcoming` 으로, 나머지는 `ongoing`, `upcoming` 으로 분류되어 반환됩니다.
+ ## Get Ongoing & Upcoming contests (logged in)
+
+ 종료되지 않은 대회 중 현재 로그인한 유저가 등록한 대회와 등록하지 않은 대회를 구분하여 반환합니다. 고로 로그인이 필요합니다.
+
+ 유저가 등록한 대회 정보는 `registeredOngoing`, `registeredUpcoming` 으로, 나머지는 `ongoing`, `upcoming` 으로 분류되어 반환됩니다.
+
+ ### Query
+
+ | 이름 | 타입 | 설명 |
+ |-----|-----|-----|
+ |groupId |Integer|대회가 속한 Group ID (default: 1)|
}
diff --git a/collection/client/Contest/Get ongoing & upcoming contests (logged in)/[401] Unauthorized.bru b/collection/client/Contest/Get ongoing & upcoming contests (logged in)/[401] Unauthorized.bru
deleted file mode 100644
index 2197fedb4d..0000000000
--- a/collection/client/Contest/Get ongoing & upcoming contests (logged in)/[401] Unauthorized.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: [401] Unauthorized
- type: http
- seq: 2
-}
-
-get {
- url: {{baseUrl}}/contest/auth
- body: none
- auth: none
-}
-
-headers {
- Authorization: Unauthorized!!
-}
-
-assert {
- res.status: eq 401
-}
diff --git a/collection/client/Contest/Get ongoing & upcoming contests/Succeed.bru b/collection/client/Contest/Get ongoing & upcoming contests/Succeed.bru
index e81fb67761..900493898c 100644
--- a/collection/client/Contest/Get ongoing & upcoming contests/Succeed.bru
+++ b/collection/client/Contest/Get ongoing & upcoming contests/Succeed.bru
@@ -5,13 +5,13 @@ meta {
}
get {
- url: {{baseUrl}}/contest
+ url: {{baseUrl}}/contest/ongoing-upcoming
body: none
auth: none
}
query {
- ~groupId: 2
+ ~groupId: 1
}
assert {
@@ -23,16 +23,19 @@ assert {
res("ongoing")[0].group.id: isNumber
res("ongoing")[0].group.groupName: isString
res("ongoing")[0].participants: isNumber
+ res("ongoing"): isDefined
+ res("upcoming"): isDefined
}
docs {
- # Get Ongoing & Upcoming Contests
+ ## Get Ongoing & Upcoming Contests
- 아직 종료되지 않은 대회들을 가져옵니다.
+ 아직 종료되지 않은 대회들을 가져옵니다. 로그인은 필요하지 않으며, groupId에 해당하는 ongoing 대회와 upcoming 대회를 배열로 가져옵니다. pagination은 수행하지 않습니다.
- ## Query
+ ### Query
| 이름 | 타입 | 설명 |
|-----|-----|-----|
|groupId |Integer|대회가 속한 Group ID (default: 1)|
+
}
diff --git a/collection/client/Contest/Get ongoing & upcoming contests/[403] Not a Member of the Group.bru b/collection/client/Contest/Get ongoing & upcoming contests/[403] Not a Member of the Group.bru
deleted file mode 100644
index 4dfee6dcc4..0000000000
--- a/collection/client/Contest/Get ongoing & upcoming contests/[403] Not a Member of the Group.bru
+++ /dev/null
@@ -1,19 +0,0 @@
-meta {
- name: [403] Not a user of the group
- type: http
- seq: 2
-}
-
-get {
- url: {{baseUrl}}/contest?groupId=9999
- body: none
- auth: none
-}
-
-query {
- groupId: 9999
-}
-
-assert {
- res.status: eq 403
-}
diff --git a/collection/client/Contest/Get registered finished contests/Succeed.bru b/collection/client/Contest/Get registered finished contests/Succeed.bru
new file mode 100644
index 0000000000..8e74a96888
--- /dev/null
+++ b/collection/client/Contest/Get registered finished contests/Succeed.bru
@@ -0,0 +1,50 @@
+meta {
+ name: Succeed
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/contest/registered-finished
+ body: none
+ auth: none
+}
+
+query {
+ ~groupId: 2
+ ~cursor: 1
+ ~take: 5
+ ~search: 밤
+}
+
+assert {
+ res.status: eq 200
+ res.body[0].id: isNumber
+ res.body[0].title: isString
+ res.body[0].startTime: isString
+ res.body[0].endTime: isString
+ res.body[0].group.id: isNumber
+ res.body[0].group.groupName: isString
+ res.body[0].participants: isNumber
+}
+
+script:pre-request {
+ await require("./login").loginUser(req);
+}
+
+docs {
+ ## Get Registered Finished Contests
+
+ 사용자가 참가 신청을 한 종료된 대회들을 가져옵니다.
+
+ pagination과 제목 검색이 가능합니다.
+
+ ### Query
+
+ | 이름 | 타입 | 설명 |
+ |-----|-----|-----|
+ |take |Integer|가져올 대회 개수 (default: 10)|
+ |cursor|Integer|cursor 값 다음의 ID를 가진 대회들을 반환|
+ |groupId |Integer|대회가 속한 Group ID (default: 1)|
+ |search|String|title을 기준으로 검색할 키워드. 포함하지 않으면 검색을 수행하지 않음|
+}
diff --git a/collection/client/Contest/Get registered ongoing & upcoming contests/Succeed.bru b/collection/client/Contest/Get registered ongoing & upcoming contests/Succeed.bru
new file mode 100644
index 0000000000..54b3eb52b3
--- /dev/null
+++ b/collection/client/Contest/Get registered ongoing & upcoming contests/Succeed.bru
@@ -0,0 +1,44 @@
+meta {
+ name: Succeed
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/contest/registered-ongoing-upcoming
+ body: none
+ auth: none
+}
+
+query {
+ ~groupId: 2
+ ~search: hello
+}
+
+assert {
+ res("registeredOngoing"): isDefined
+ res("registeredUpcoming"): isDefined
+}
+
+script:pre-request {
+ await require("./login").loginUser(req);
+}
+
+docs {
+ ## Get Registered Ongoing & Upcoming Contests
+
+ 사용자가 참가 신청을 한 Ongoing, Upcoming 대회들을 가져옵니다.
+
+ 로그인이 필요하며, pagination을 수행하지 않습니다.
+
+ 제목 검색 기능을 수행합니다.
+
+ Get ongoing&upcoming contests (logged in) API와 다른 점은 검색이 가능하다는 점입니다.
+
+ ### Query
+
+ | 이름 | 타입 | 설명 |
+ |-----|-----|-----|
+ |groupId |Integer|대회가 속한 Group ID (default: 1)|
+ |search|String|title을 기준으로 검색할 키워드. 포함하지 않으면 검색을 수행하지 않음|
+}
diff --git a/frontend-client/app/(main)/_components/Badge.tsx b/frontend-client/app/(main)/_components/Badge.tsx
index d22a3decfe..f7f8d843de 100644
--- a/frontend-client/app/(main)/_components/Badge.tsx
+++ b/frontend-client/app/(main)/_components/Badge.tsx
@@ -2,6 +2,8 @@ import { cn } from '@/lib/utils'
import React from 'react'
const variants = {
+ registeredOngoing: 'bg-rose-700',
+ registeredUpcoming: 'bg-rose-700',
ongoing: 'bg-emerald-700',
upcoming: 'bg-blue-700',
finished: 'bg-gray-700',
diff --git a/frontend-client/app/(main)/_components/ContestCard.tsx b/frontend-client/app/(main)/_components/ContestCard.tsx
index 2165d3a614..ab8dceb1e3 100644
--- a/frontend-client/app/(main)/_components/ContestCard.tsx
+++ b/frontend-client/app/(main)/_components/ContestCard.tsx
@@ -10,7 +10,9 @@ import TimeDiff from './TimeDiff'
const variants = {
ongoing: 'bg-gradient-to-br from-blue-500 to-blue-950',
upcoming: 'bg-gradient-to-br from-emerald-600 to-emerald-900',
- finished: 'bg-gray-500'
+ finished: 'bg-gray-500',
+ registeredOngoing: 'bg-gradient-to-br from-blue-500 to-blue-950',
+ registeredUpcoming: 'bg-gradient-to-br from-emerald-600 to-emerald-900'
}
interface Props {
@@ -36,7 +38,11 @@ export default function ContestCard({ contest }: Props) {
>
- {contest.status}
+
+ {contest.status.startsWith('registered')
+ ? 'registered'
+ : contest.status}
+
{contest.title}
diff --git a/frontend-client/app/(main)/_components/ContestCards.tsx b/frontend-client/app/(main)/_components/ContestCards.tsx
index e634c50f26..938a3d6abc 100644
--- a/frontend-client/app/(main)/_components/ContestCards.tsx
+++ b/frontend-client/app/(main)/_components/ContestCards.tsx
@@ -8,7 +8,7 @@ const getContests = async () => {
const data: {
ongoing: Contest[]
upcoming: Contest[]
- } = await fetcher.get('contest').json()
+ } = await fetcher.get('contest/ongoing-upcoming').json()
data.ongoing.forEach((contest) => {
contest.status = 'ongoing'
diff --git a/frontend-client/app/(main)/_components/HeaderAuthPanel.tsx b/frontend-client/app/(main)/_components/HeaderAuthPanel.tsx
index 14fa170a95..274a5e615d 100644
--- a/frontend-client/app/(main)/_components/HeaderAuthPanel.tsx
+++ b/frontend-client/app/(main)/_components/HeaderAuthPanel.tsx
@@ -43,7 +43,12 @@ export default function HeaderAuthPanel({ session }: HeaderAuthPanelProps) {
Sign Up
-
+ {
+ e.preventDefault()
+ }}
+ >
diff --git a/frontend-client/app/(main)/_components/ProblemCard.tsx b/frontend-client/app/(main)/_components/ProblemCard.tsx
index ed936592f8..077f87e5b6 100644
--- a/frontend-client/app/(main)/_components/ProblemCard.tsx
+++ b/frontend-client/app/(main)/_components/ProblemCard.tsx
@@ -15,7 +15,7 @@ export default function ProblemCard({ problem }: Props) {
type={problem.difficulty}
>{`Level ${problem.difficulty[5]}`}
- {`#${problem.problemId} ${problem.title}`}
+ {`#${problem.id} ${problem.title}`}
diff --git a/frontend-client/app/(main)/_components/ProblemCards.tsx b/frontend-client/app/(main)/_components/ProblemCards.tsx
index 344f18f143..bb7b275d28 100644
--- a/frontend-client/app/(main)/_components/ProblemCards.tsx
+++ b/frontend-client/app/(main)/_components/ProblemCards.tsx
@@ -5,7 +5,7 @@ import Link from 'next/link'
import ProblemCard from './ProblemCard'
const getProblems = async () => {
- const problems: WorkbookProblem[] = await fetcher
+ const { problems }: { problems: WorkbookProblem[] } = await fetcher
.get('problem', {
searchParams: {
take: 3,
@@ -25,8 +25,8 @@ export default async function ProblemCards() {
{problems.map((problem) => {
return (
diff --git a/frontend-client/app/(main)/_components/SignIn.tsx b/frontend-client/app/(main)/_components/SignIn.tsx
index dd15a39f5a..c02e90ba47 100644
--- a/frontend-client/app/(main)/_components/SignIn.tsx
+++ b/frontend-client/app/(main)/_components/SignIn.tsx
@@ -58,7 +58,7 @@ export default function SignIn() {
className="flex w-full flex-col gap-3"
onSubmit={handleSubmit(onSubmit)}
>
-
+
state)
const {
handleSubmit,
@@ -38,6 +43,28 @@ export default function SignUpEmailVerify() {
const [codeError, setCodeError] = useState('')
const [emailVerified, setEmailVerified] = useState(false)
const [emailAuthToken, setEmailAuthToken] = useState('')
+
+ useInterval(
+ () => {
+ if (timer > 0) {
+ setTimer((prevTimer) => prevTimer - 1)
+ }
+ },
+ sentEmail ? 1000 : null
+ )
+
+ useEffect(() => {
+ if (timer === 0) {
+ setExpired(true)
+ }
+ }, [timer])
+
+ const formatTimer = () => {
+ const minutes = Math.floor(timer / 60)
+ const seconds = timer % 60
+ return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
+ }
+
const onSubmit = (data: EmailVerifyInput) => {
setFormData({
...data,
@@ -99,6 +126,9 @@ export default function SignUpEmailVerify() {
return (
{sentEmail &&
+ !expired &&
!errors.verificationCode &&
codeError === '' &&
!emailVerified && (
We've sent an email!
)}
+ {expired && (
+
+ Verification code expired
+
+ Please resend an email and try again
+
+ )}
{!sentEmail ? (
- ) : (
+ ) : !expired ? (
+ ) : (
+
)}
)
diff --git a/frontend-client/app/(main)/contest/[id]/@tabs/announcement/_components/Columns.tsx b/frontend-client/app/(main)/contest/[id]/@tabs/announcement/_components/Columns.tsx
new file mode 100644
index 0000000000..b1d93e4464
--- /dev/null
+++ b/frontend-client/app/(main)/contest/[id]/@tabs/announcement/_components/Columns.tsx
@@ -0,0 +1,33 @@
+'use client'
+
+import { convertToLetter } from '@/lib/utils'
+import type { ContestAnnouncement } from '@/types/type'
+import type { ColumnDef } from '@tanstack/react-table'
+import dayjs from 'dayjs'
+import './styles.css'
+
+export const columns: ColumnDef[] = [
+ {
+ header: 'Problem',
+ accessorKey: 'problem',
+ cell: ({ row }) => (
+
+ {row.original.problemId !== null
+ ? convertToLetter(row.original.problemId)
+ : ''}
+
+ )
+ },
+ {
+ header: () => 'Description',
+ accessorKey: 'content',
+ cell: ({ row }) => (
+ {row.original.content}
+ )
+ },
+ {
+ header: () => 'Posted',
+ accessorKey: 'updateTime',
+ cell: ({ row }) => dayjs(row.original.updateTime).format('YYYY-MM-DD HH:mm')
+ }
+]
diff --git a/frontend-client/app/(main)/contest/[id]/@tabs/clarification/_components/styles.css b/frontend-client/app/(main)/contest/[id]/@tabs/announcement/_components/styles.css
similarity index 100%
rename from frontend-client/app/(main)/contest/[id]/@tabs/clarification/_components/styles.css
rename to frontend-client/app/(main)/contest/[id]/@tabs/announcement/_components/styles.css
diff --git a/frontend-client/app/(main)/contest/[id]/@tabs/clarification/page.tsx b/frontend-client/app/(main)/contest/[id]/@tabs/announcement/page.tsx
similarity index 52%
rename from frontend-client/app/(main)/contest/[id]/@tabs/clarification/page.tsx
rename to frontend-client/app/(main)/contest/[id]/@tabs/announcement/page.tsx
index 3e40d403f8..8c6098f408 100644
--- a/frontend-client/app/(main)/contest/[id]/@tabs/clarification/page.tsx
+++ b/frontend-client/app/(main)/contest/[id]/@tabs/announcement/page.tsx
@@ -1,17 +1,17 @@
import DataTable from '@/components/DataTable'
import { fetcher } from '@/lib/utils'
-import type { ContestClarification } from '@/types/type'
+import type { ContestAnnouncement } from '@/types/type'
import { columns } from './_components/Columns'
-interface ContestClarificationProps {
+interface ContestAnnouncementProps {
params: { id: string }
}
-export default async function ContestClarification({
+export default async function ContestAnnouncement({
params
-}: ContestClarificationProps) {
+}: ContestAnnouncementProps) {
const { id } = params
- const contestClarifications: ContestClarification[] = await fetcher
+ const contestAnnouncements: ContestAnnouncement[] = await fetcher
.get('announcement', {
searchParams: {
contestId: id
@@ -21,12 +21,12 @@ export default async function ContestClarification({
return (
diff --git a/frontend-client/app/(main)/contest/[id]/@tabs/clarification/_components/Columns.tsx b/frontend-client/app/(main)/contest/[id]/@tabs/clarification/_components/Columns.tsx
deleted file mode 100644
index bcbc9554d3..0000000000
--- a/frontend-client/app/(main)/contest/[id]/@tabs/clarification/_components/Columns.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-'use client'
-
-import type { ContestClarification } from '@/types/type'
-import type { ColumnDef } from '@tanstack/react-table'
-import dayjs from 'dayjs'
-import './styles.css'
-
-export const columns: ColumnDef[] = [
- {
- header: '#',
- accessorKey: 'id',
- cell: ({ row }) => {row.original.id}
- },
- {
- header: () => 'Description',
- accessorKey: 'content',
- cell: ({ row }) => (
- {row.original.content}
- )
- },
- {
- header: () => 'Time',
- accessorKey: 'createTime',
- cell: ({ row }) =>
- dayjs(row.original.createTime).format('YYYY-MM-DD HH:mm:ss')
- }
-]
diff --git a/frontend-client/app/(main)/contest/[id]/@tabs/problem/_components/Columns.tsx b/frontend-client/app/(main)/contest/[id]/@tabs/problem/_components/Columns.tsx
index 948ffb5f9f..9b3b6acaec 100644
--- a/frontend-client/app/(main)/contest/[id]/@tabs/problem/_components/Columns.tsx
+++ b/frontend-client/app/(main)/contest/[id]/@tabs/problem/_components/Columns.tsx
@@ -1,16 +1,24 @@
'use client'
import { Badge } from '@/components/ui/badge'
+import { convertToLetter } from '@/lib/utils'
import type { ContestProblem } from '@/types/type'
import type { ColumnDef } from '@tanstack/react-table'
export const columns: ColumnDef[] = [
+ {
+ header: '#',
+ accessorKey: 'order',
+ cell: ({ row }) => (
+ {convertToLetter(row.original.order)}
+ )
+ },
{
header: 'Title',
accessorKey: 'title',
cell: ({ row }) => {
return (
- {`${row.original.order}. ${row.original.title}`}
+ {`${row.original.title}`}
)
}
},
@@ -41,6 +49,6 @@ export const columns: ColumnDef[] = [
{
header: () => 'Solved',
accessorKey: 'acceptedRate',
- cell: ({ row }) => `${row.original.acceptedRate}%`
+ cell: ({ row }) => `${row.original.acceptedRate.toFixed(2)}%`
}
]
diff --git a/frontend-client/app/(main)/contest/[id]/@tabs/problem/page.tsx b/frontend-client/app/(main)/contest/[id]/@tabs/problem/page.tsx
index cd239e2f79..7113551a1f 100644
--- a/frontend-client/app/(main)/contest/[id]/@tabs/problem/page.tsx
+++ b/frontend-client/app/(main)/contest/[id]/@tabs/problem/page.tsx
@@ -9,7 +9,7 @@ interface ContestProblemProps {
export default async function ContestProblem({ params }: ContestProblemProps) {
const { id } = params
- const contestProblems: ContestProblem[] = await fetcher
+ const { problems }: { problems: ContestProblem[] } = await fetcher
.get('problem', {
searchParams: {
take: 10,
@@ -17,19 +17,21 @@ export default async function ContestProblem({ params }: ContestProblemProps) {
}
})
.json()
- contestProblems.forEach((problem) => {
+
+ problems.forEach((problem) => {
problem.id = problem.problemId
})
return (
diff --git a/frontend-client/app/(main)/contest/_components/Columns.tsx b/frontend-client/app/(main)/contest/_components/Columns.tsx
index baed725b28..a23fac9f4d 100644
--- a/frontend-client/app/(main)/contest/_components/Columns.tsx
+++ b/frontend-client/app/(main)/contest/_components/Columns.tsx
@@ -1,5 +1,6 @@
'use client'
+import Badge from '@/app/(main)/_components/Badge'
import type { Contest } from '@/types/type'
import type { ColumnDef } from '@tanstack/react-table'
import dayjs from 'dayjs'
@@ -14,6 +15,19 @@ export const columns: ColumnDef[] = [
)
},
+ {
+ header: 'Status',
+ accessorKey: 'status',
+ cell: ({ row }) => (
+
+
+ {row.original.status.startsWith('registered')
+ ? 'registered'
+ : row.original.status}
+
+
+ )
+ },
{
header: 'Starts at',
accessorKey: 'startTime',
@@ -27,6 +41,6 @@ export const columns: ColumnDef[] = [
{
header: 'Participants',
accessorKey: 'participants',
- cell: ({ row }) => row.original.id
+ cell: ({ row }) => row.original.participants
}
]
diff --git a/frontend-client/app/(main)/contest/_components/ContestCardList.tsx b/frontend-client/app/(main)/contest/_components/ContestCardList.tsx
index daf45c4756..9b760f687a 100644
--- a/frontend-client/app/(main)/contest/_components/ContestCardList.tsx
+++ b/frontend-client/app/(main)/contest/_components/ContestCardList.tsx
@@ -6,16 +6,17 @@ import {
CarouselNext,
CarouselPrevious
} from '@/components/ui/carousel'
-import { fetcher } from '@/lib/utils'
+import { fetcher, fetcherWithAuth } from '@/lib/utils'
import type { Contest } from '@/types/type'
import type { Route } from 'next'
+import type { Session } from 'next-auth'
import Link from 'next/link'
const getContests = async () => {
const data: {
ongoing: Contest[]
upcoming: Contest[]
- } = await fetcher.get('contest').json()
+ } = await fetcher.get('contest/ongoing-upcoming').json()
data.ongoing.forEach((contest) => {
contest.status = 'ongoing'
})
@@ -25,21 +26,56 @@ const getContests = async () => {
return data.ongoing.concat(data.upcoming)
}
+const getRegisteredContests = async () => {
+ const data: {
+ registeredOngoing: Contest[]
+ registeredUpcoming: Contest[]
+ ongoing: Contest[]
+ upcoming: Contest[]
+ } = await fetcherWithAuth
+ .get('contest/ongoing-upcoming-with-registered')
+ .json()
+ data.registeredOngoing.forEach((contest) => {
+ contest.status = 'registeredOngoing'
+ })
+ data.registeredUpcoming.forEach((contest) => {
+ contest.status = 'registeredUpcoming'
+ })
+ data.ongoing.forEach((contest) => {
+ contest.status = 'ongoing'
+ })
+ data.upcoming.forEach((contest) => {
+ contest.status = 'upcoming'
+ })
+ return data.ongoing.concat(
+ data.upcoming.concat(data.registeredOngoing.concat(data.registeredUpcoming))
+ )
+}
+
export default async function Contest({
title,
- type
+ type,
+ session
}: {
type: string
title: string
+ session?: Session | null
}) {
- const data = (await getContests()).filter(
- (contest) => contest.status.toLowerCase() === type.toLowerCase()
+ const data = (
+ session ? await getRegisteredContests() : await getContests()
+ ).filter(
+ (contest) =>
+ contest.status.toLowerCase() === 'registered' + type.toLowerCase() ||
+ contest.status.toLowerCase() === type.toLowerCase()
)
+
const contestChunks = []
for (let i = 0; i < data.length; i += 3)
contestChunks.push(data.slice(i, i + 3))
- return (
+ return data.length === 0 ? (
+ <>>
+ ) : (
{title}
@@ -48,14 +84,14 @@ export default async function Contest({
-
+
{contestChunks.map((contestChunk) => (
{contestChunk.map((contest) => (
diff --git a/frontend-client/app/(main)/contest/_components/ContestTabs.tsx b/frontend-client/app/(main)/contest/_components/ContestTabs.tsx
index 03e0445ff0..41bcf80c47 100644
--- a/frontend-client/app/(main)/contest/_components/ContestTabs.tsx
+++ b/frontend-client/app/(main)/contest/_components/ContestTabs.tsx
@@ -35,13 +35,13 @@ export default function ContestTabs({ contestId }: { contestId: string }) {
Problem
- Clarification
+ Announcement
diff --git a/frontend-client/app/(main)/contest/page.tsx b/frontend-client/app/(main)/contest/page.tsx
index a288c18a39..8f31b15227 100644
--- a/frontend-client/app/(main)/contest/page.tsx
+++ b/frontend-client/app/(main)/contest/page.tsx
@@ -1,4 +1,5 @@
import { Skeleton } from '@/components/ui/skeleton'
+import { auth } from '@/lib/auth'
import { Suspense } from 'react'
import ContestCardList from './_components/ContestCardList'
import FinishedContestTable from './_components/FinishedContestTable'
@@ -40,17 +41,23 @@ function FinishedContestTableFallback() {
)
}
-export default function Contest() {
+export default async function Contest() {
+ const session = await auth()
return (
<>
}>
-
+
}>
diff --git a/frontend-client/app/(main)/problem/_components/Columns.tsx b/frontend-client/app/(main)/problem/_components/Columns.tsx
index 8e7054e344..25c56601a5 100644
--- a/frontend-client/app/(main)/problem/_components/Columns.tsx
+++ b/frontend-client/app/(main)/problem/_components/Columns.tsx
@@ -42,7 +42,7 @@ export const columns: ColumnDef[] = [
{
header: () => Solved,
accessorKey: 'acceptedRate',
- cell: ({ row }) => `${parseFloat(row.original.acceptedRate.toFixed(4))}%`
+ cell: ({ row }) => `${row.original.acceptedRate.toFixed(2)}%`
},
{
header: 'Info',
diff --git a/frontend-client/app/(main)/problem/_components/ProblemTable.tsx b/frontend-client/app/(main)/problem/_components/ProblemTable.tsx
index 577309d9ad..a95f2a18d0 100644
--- a/frontend-client/app/(main)/problem/_components/ProblemTable.tsx
+++ b/frontend-client/app/(main)/problem/_components/ProblemTable.tsx
@@ -9,7 +9,7 @@ interface Props {
}
export default async function ProblemTable({ search, order }: Props) {
- const problems: Problem[] = await fetcher
+ const { problems }: { problems: Problem[] } = await fetcher
.get('problem', {
searchParams: {
take: 10,
diff --git a/frontend-client/app/admin/layout.tsx b/frontend-client/app/admin/layout.tsx
index 08c1cd7f2c..380a6bc47b 100644
--- a/frontend-client/app/admin/layout.tsx
+++ b/frontend-client/app/admin/layout.tsx
@@ -8,8 +8,8 @@ import SideBar from './_components/SideBar'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
-
-