From abf6e0217ba33bcb9d51478d41b3455edb52c06f Mon Sep 17 00:00:00 2001 From: Sori Lim Date: Thu, 29 Feb 2024 01:16:30 +0900 Subject: [PATCH] fix: show submission list when not logged in (#1521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(be): seperate contest submission controller GET /submission에 로그인하지 않아도 접근할 수 있도록 합니다. 로그인이 필요한 contest submisison은 컨트롤러를 분리하였습니다. * fix(be): seperate contest problem controller 사용자 정보가 필요한 contest problem 컨트롤러를 분리합니다. * docs(be): re-write bruno for get problems & submissions * chore(fe): rename hook files * chore(be): allow local frontend server to request * feat(fe): handle when request is not successful * feat(fe): redesign locked page when submission cannot be accessed * chore(be): declare user id in problem spec --- .../client/src/contest/contest.service.ts | 3 +- backend/apps/client/src/main.ts | 7 + .../src/problem/problem.controller.spec.ts | 26 +++- .../client/src/problem/problem.controller.ts | 94 ++++++++---- .../apps/client/src/problem/problem.module.ts | 11 +- .../client/src/problem/problem.repository.ts | 10 -- .../src/problem/problem.service.spec.ts | 134 +++++++++++++----- .../client/src/problem/problem.service.ts | 42 ++++-- .../submission/submission.controller.spec.ts | 27 +++- .../src/submission/submission.controller.ts | 57 +++++--- .../src/submission/submission.module.ts | 7 +- .../src/submission/submission.service.ts | 4 +- .../Get Contest Problem by ID/Succeed.bru | 42 ++++++ .../[403] Registered but not started.bru | 24 ++++ .../[403] Unregistered and not finished.bru | 24 ++++ .../[404] Nonexistent Contest Problem.bru | 37 +++++ .../Problem/Get Contest Problems/Succeed.bru | 53 +++++++ .../[403] Registered but Not Started.bru | 26 ++++ .../[403] Unregistered and Not Finished.bru | 26 ++++ .../[404] Nonexistent Contest.bru | 26 ++++ .../Problem/Get problem by ID/Succeed.bru | 7 +- .../client/Problem/Get problems/Succeed.bru | 7 +- .../Get Contest Submissions/Succeed.bru | 47 ++++++ .../[404] Nonexistent Problem.bru | 27 ++++ .../Get Submission by ID/Succeed.bru | 2 +- .../[404] Nonexistent Problem.bru | 4 + .../[404] Nonexistent Submission.bru | 4 + .../Submission/Get Submissions/Succeed.bru | 6 - collection/client/login.js | 8 ++ .../[contestId]/@tabs/problem/page.tsx | 35 +++-- .../submission/[submissionId]/page.tsx | 2 +- .../_components/SubmissionDetail.tsx | 20 ++- .../submission/_components/dataIfError.ts | 29 ++++ .../problem/[problemId]/submission/page.tsx | 2 +- .../components/EditorResizablePanel.tsx | 2 +- .../lib/{usePagination.ts => pagination.ts} | 12 +- frontend-client/lib/{hooks.ts => storage.ts} | 0 frontend-client/types/type.ts | 6 +- 38 files changed, 747 insertions(+), 153 deletions(-) create mode 100644 collection/client/Problem/Get Contest Problem by ID/Succeed.bru create mode 100644 collection/client/Problem/Get Contest Problem by ID/[403] Registered but not started.bru create mode 100644 collection/client/Problem/Get Contest Problem by ID/[403] Unregistered and not finished.bru create mode 100644 collection/client/Problem/Get Contest Problem by ID/[404] Nonexistent Contest Problem.bru create mode 100644 collection/client/Problem/Get Contest Problems/Succeed.bru create mode 100644 collection/client/Problem/Get Contest Problems/[403] Registered but Not Started.bru create mode 100644 collection/client/Problem/Get Contest Problems/[403] Unregistered and Not Finished.bru create mode 100644 collection/client/Problem/Get Contest Problems/[404] Nonexistent Contest.bru create mode 100644 collection/client/Submission/Get Contest Submissions/Succeed.bru create mode 100644 collection/client/Submission/Get Contest Submissions/[404] Nonexistent Problem.bru create mode 100644 frontend-client/app/problem/[problemId]/submission/_components/dataIfError.ts rename frontend-client/lib/{usePagination.ts => pagination.ts} (94%) rename frontend-client/lib/{hooks.ts => storage.ts} (100%) diff --git a/backend/apps/client/src/contest/contest.service.ts b/backend/apps/client/src/contest/contest.service.ts index 9d80ed4f75..85579020d2 100644 --- a/backend/apps/client/src/contest/contest.service.ts +++ b/backend/apps/client/src/contest/contest.service.ts @@ -316,6 +316,7 @@ export class ContestService { } throw error } + /* HACK: standings 업데이트 로직 수정 후 삭제 // get contest participants ranking using ContestRecord const sortedContestRecordsWithUserDetail = await this.prisma.contestRecord.findMany({ @@ -348,10 +349,10 @@ export class ContestService { standing: index + 1 }) ) + */ // combine contest and sortedContestRecordsWithUserDetail return { ...contest, - standings: UsersWithStandingDetail, canRegister } } diff --git a/backend/apps/client/src/main.ts b/backend/apps/client/src/main.ts index b4600ab101..a9004cc4cd 100644 --- a/backend/apps/client/src/main.ts +++ b/backend/apps/client/src/main.ts @@ -15,6 +15,13 @@ const bootstrap = async () => { app.useGlobalPipes(new ValidationPipe({ whitelist: true })) app.use(cookieParser()) if (process.env.NODE_ENV !== 'production') { + app.enableCors({ + origin: 'http://localhost:5525', + credentials: true, + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + allowedHeaders: ['*'], + exposedHeaders: ['Content-Type', 'Authorization', 'Email-Auth'] + }) const config = new DocumentBuilder() .setTitle('SKKU coding platform') .setDescription('API description') diff --git a/backend/apps/client/src/problem/problem.controller.spec.ts b/backend/apps/client/src/problem/problem.controller.spec.ts index 8f36e267f4..a5f20ad265 100644 --- a/backend/apps/client/src/problem/problem.controller.spec.ts +++ b/backend/apps/client/src/problem/problem.controller.spec.ts @@ -1,7 +1,10 @@ import { Test, type TestingModule } from '@nestjs/testing' import { expect } from 'chai' import { RolesService } from '@libs/auth' -import { ProblemController } from './problem.controller' +import { + ProblemController, + ContestProblemController +} from './problem.controller' import { ContestProblemService, ProblemService, @@ -16,7 +19,6 @@ describe('ProblemController', () => { controllers: [ProblemController], providers: [ { provide: ProblemService, useValue: {} }, - { provide: ContestProblemService, useValue: {} }, { provide: WorkbookProblemService, useValue: {} }, { provide: RolesService, useValue: {} } ] @@ -29,3 +31,23 @@ describe('ProblemController', () => { expect(controller).to.be.ok }) }) + +describe('ContestProblemController', () => { + let controller: ContestProblemController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ContestProblemController], + providers: [ + { provide: ContestProblemService, useValue: {} }, + { provide: RolesService, useValue: {} } + ] + }).compile() + + controller = module.get(ContestProblemController) + }) + + it('should be defined', () => { + expect(controller).to.be.ok + }) +}) diff --git a/backend/apps/client/src/problem/problem.controller.ts b/backend/apps/client/src/problem/problem.controller.ts index 5d0eb74a50..1268ec9a48 100644 --- a/backend/apps/client/src/problem/problem.controller.ts +++ b/backend/apps/client/src/problem/problem.controller.ts @@ -6,10 +6,11 @@ import { Logger, NotFoundException, Param, - Query + Query, + Req } from '@nestjs/common' import { Prisma } from '@prisma/client' -import { AuthNotNeededIfOpenSpace } from '@libs/auth' +import { AuthNotNeededIfOpenSpace, type AuthenticatedRequest } from '@libs/auth' import { EntityNotExistException, ForbiddenAccessException @@ -35,14 +36,12 @@ export class ProblemController { constructor( private readonly problemService: ProblemService, - private readonly contestProblemService: ContestProblemService, private readonly workbookProblemService: WorkbookProblemService ) {} @Get() async getProblems( @Query('groupId', GroupIDPipe) groupId: number, - @Query('contestId', IDValidationPipe) contestId: number | null, @Query('workbookId', IDValidationPipe) workbookId: number | null, @Query('cursor', CursorValidationPipe) cursor: number | null, @Query('take', new DefaultValuePipe(10), new RequiredIntPipe('take')) @@ -52,7 +51,7 @@ export class ProblemController { @Query('search') search?: string ) { try { - if (!contestId && !workbookId) { + if (!workbookId) { return await this.problemService.getProblems({ cursor, take, @@ -60,13 +59,6 @@ export class ProblemController { order, search }) - } else if (contestId) { - return await this.contestProblemService.getContestProblems( - contestId, - cursor, - take, - groupId - ) } return await this.workbookProblemService.getWorkbookProblems( workbookId!, @@ -76,13 +68,10 @@ export class ProblemController { ) } catch (error) { if ( - (error instanceof Prisma.PrismaClientKnownRequestError && - error.name === 'NotFoundError') || - error instanceof EntityNotExistException + error instanceof Prisma.PrismaClientKnownRequestError && + error.name === 'NotFoundError' ) { throw new NotFoundException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -92,25 +81,80 @@ export class ProblemController { @Get(':problemId') async getProblem( @Query('groupId', GroupIDPipe) groupId: number, - @Query('contestId', IDValidationPipe) contestId: number | null, @Query('workbookId', IDValidationPipe) workbookId: number | null, @Param('problemId', new RequiredIntPipe('problemId')) problemId: number ) { try { - if (!contestId && !workbookId) { + if (!workbookId) { return await this.problemService.getProblem(problemId, groupId) - } else if (contestId) { - return await this.contestProblemService.getContestProblem( - contestId, - problemId, - groupId - ) } return await this.workbookProblemService.getWorkbookProblem( workbookId!, problemId, groupId ) + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.name === 'NotFoundError' + ) { + throw new NotFoundException(error.message) + } + this.logger.error(error) + throw new InternalServerErrorException() + } + } +} + +@Controller('contest/:contestId/problem') +export class ContestProblemController { + private readonly logger = new Logger(ContestProblemController.name) + + constructor(private readonly contestProblemService: ContestProblemService) {} + + @Get() + async getContestProblems( + @Req() req: AuthenticatedRequest, + @Param('contestId', IDValidationPipe) contestId: number, + @Query('groupId', GroupIDPipe) groupId: number, + @Query('cursor', CursorValidationPipe) cursor: number | null, + @Query('take', new DefaultValuePipe(10), new RequiredIntPipe('take')) + take: number + ) { + try { + return await this.contestProblemService.getContestProblems( + contestId, + req.user.id, + cursor, + take, + groupId + ) + } catch (error) { + if ( + error instanceof EntityNotExistException || + error instanceof ForbiddenAccessException + ) { + throw error.convert2HTTPException() + } + this.logger.error(error) + throw new InternalServerErrorException() + } + } + + @Get(':problemId') + async getContestProblem( + @Req() req: AuthenticatedRequest, + @Param('contestId', IDValidationPipe) contestId: number, + @Param('problemId', new RequiredIntPipe('problemId')) problemId: number, + @Query('groupId', GroupIDPipe) groupId: number + ) { + try { + return await this.contestProblemService.getContestProblem( + contestId, + problemId, + req.user.id, + groupId + ) } catch (error) { if ( (error instanceof Prisma.PrismaClientKnownRequestError && diff --git a/backend/apps/client/src/problem/problem.module.ts b/backend/apps/client/src/problem/problem.module.ts index f469917587..5bd019e82c 100644 --- a/backend/apps/client/src/problem/problem.module.ts +++ b/backend/apps/client/src/problem/problem.module.ts @@ -4,7 +4,10 @@ import { GroupMemberGuard, RolesModule } from '@libs/auth' import { ContestModule } from '@client/contest/contest.module' import { WorkbookModule } from '@client/workbook/workbook.module' import { CodeDraftController } from './code-draft.controller' -import { ProblemController } from './problem.controller' +import { + ContestProblemController, + ProblemController +} from './problem.controller' import { ProblemRepository } from './problem.repository' import { ContestProblemService, @@ -15,7 +18,11 @@ import { @Module({ imports: [RolesModule, ContestModule, WorkbookModule], - controllers: [ProblemController, CodeDraftController], + controllers: [ + ProblemController, + ContestProblemController, + CodeDraftController + ], providers: [ ProblemService, ContestProblemService, diff --git a/backend/apps/client/src/problem/problem.repository.ts b/backend/apps/client/src/problem/problem.repository.ts index 8463690b10..d2ef329ca1 100644 --- a/backend/apps/client/src/problem/problem.repository.ts +++ b/backend/apps/client/src/problem/problem.repository.ts @@ -208,11 +208,6 @@ export class ProblemRepository { order: true, problem: { select: this.problemsSelectOption - }, - contest: { - select: { - startTime: true - } } } }) @@ -245,11 +240,6 @@ export class ProblemRepository { order: true, problem: { select: this.problemSelectOption - }, - contest: { - select: { - startTime: true - } } } }) diff --git a/backend/apps/client/src/problem/problem.service.spec.ts b/backend/apps/client/src/problem/problem.service.spec.ts index 0eb86b5ff3..6ac5e142ed 100644 --- a/backend/apps/client/src/problem/problem.service.spec.ts +++ b/backend/apps/client/src/problem/problem.service.spec.ts @@ -1,10 +1,10 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager' import { Test, type TestingModule } from '@nestjs/testing' +import { faker } from '@faker-js/faker' import { Prisma, ResultStatus } from '@prisma/client' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { expect } from 'chai' import { plainToInstance } from 'class-transformer' -import * as dayjs from 'dayjs' import { stub } from 'sinon' import { OPEN_SPACE_ID } from '@libs/constants' import { @@ -63,6 +63,9 @@ const db = { problemTag: { findMany: stub() }, + contest: { + findUniqueOrThrow: stub() + }, user: { findUniqueOrThrow: stub() }, @@ -80,6 +83,7 @@ const db = { const ARBITRARY_VAL = 1 const problemId = ARBITRARY_VAL const groupId = ARBITRARY_VAL +const userId = ARBITRARY_VAL const contestId = ARBITRARY_VAL const workbookId = ARBITRARY_VAL const mockProblem = Object.assign({}, problems[0]) @@ -257,11 +261,16 @@ describe('ContestProblemService', () => { describe('getContestProblems', () => { it('should return public contest problems', async () => { // given - stub(contestService, 'isVisible').resolves(true) + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.past(), + endTime: faker.date.future(), + canRegister: false + }) db.contestProblem.findMany.resolves(mockContestProblems) // when - const result = await service.getContestProblems(contestId, 1, 1) + const result = await service.getContestProblems(contestId, userId, 1, 1) // then expect(result).to.deep.equal( @@ -274,11 +283,22 @@ describe('ContestProblemService', () => { it('should return group contest problems', async () => { // given - stub(contestService, 'isVisible').resolves(true) + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.past(), + endTime: faker.date.future(), + canRegister: false + }) db.contestProblem.findMany.resolves(mockContestProblems) // when - const result = await service.getContestProblems(contestId, 1, 1, groupId) + const result = await service.getContestProblems( + contestId, + userId, + 1, + 1, + groupId + ) // then expect(result).to.deep.equal( @@ -291,26 +311,40 @@ describe('ContestProblemService', () => { it('should throw error when the contest is not visible', async () => { // given - stub(contestService, 'isVisible').resolves(false) - db.contestProblem.findMany.resolves(mockContestProblems) + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.rejects(new EntityNotExistException('Contest')) // then await expect( - service.getContestProblems(contestId, 1, 1) + service.getContestProblems(contestId, userId, 1, 1) ).to.be.rejectedWith(EntityNotExistException) }) - it('should throw error when the contest is not started yet', async () => { - stub(contestService, 'isVisible').resolves(true) - const notStartedContestProblems = mockContestProblems.map((x) => ({ - ...x, - contest: { - startTime: dayjs().add(1, 'day') - } - })) - db.contestProblem.findMany.resolves(notStartedContestProblems) + it('should throw error when the user is registered but contest is not started', async () => { + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.future(), + endTime: faker.date.future(), + canRegister: false + }) + db.contestProblem.findMany.resolves(mockContestProblems) + + await expect( + service.getContestProblems(contestId, userId, 1, 1) + ).to.be.rejectedWith(ForbiddenAccessException) + }) + + it('should throw error when the user is not registered and contest is not ended', async () => { + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.past(), + endTime: faker.date.future(), + canRegister: true + }) + db.contestProblem.findMany.resolves(mockContestProblems) + await expect( - service.getContestProblems(contestId, 1, 1) + service.getContestProblems(contestId, 0, 1, 1) ).to.be.rejectedWith(ForbiddenAccessException) }) }) @@ -318,11 +352,20 @@ describe('ContestProblemService', () => { describe('getContestProblem', () => { it('should return the public contest problem', async () => { // given - stub(contestService, 'isVisible').resolves(true) + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.past(), + endTime: faker.date.future(), + canRegister: false + }) db.contestProblem.findUniqueOrThrow.resolves(mockContestProblem) // when - const result = await service.getContestProblem(contestId, problemId) + const result = await service.getContestProblem( + contestId, + problemId, + userId + ) // then expect(result).to.be.deep.equal( @@ -332,7 +375,12 @@ describe('ContestProblemService', () => { it('should return the group contest problem', async () => { // given - stub(contestService, 'isVisible').resolves(true) + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.past(), + endTime: faker.date.future(), + canRegister: false + }) db.contestProblem.findUniqueOrThrow.resolves(mockContestProblem) // when @@ -350,28 +398,40 @@ describe('ContestProblemService', () => { it('should throw error when the contest is not visible', async () => { // given - stub(contestService, 'isVisible').resolves(false) - db.contestProblem.findUnique.resolves(mockContestProblem) + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.rejects(new EntityNotExistException('Contest')) // then await expect( - service.getContestProblem(contestId, problemId) + service.getContestProblem(contestId, problemId, userId) ).to.be.rejectedWith(EntityNotExistException) }) - }) - it('should throw error when the contest is not started yet', async () => { - stub(contestService, 'isVisible').resolves(true) - const notStartedContestProblem = { - ...mockContestProblem, - contest: { - startTime: dayjs().add(1, 'day') - } - } - db.contestProblem.findUniqueOrThrow.resolves(notStartedContestProblem) - await expect( - service.getContestProblem(contestId, problemId) - ).to.be.rejectedWith(ForbiddenAccessException) + it('should throw error when the user is registered but contest is not started', async () => { + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.future(), + endTime: faker.date.future(), + canRegister: false + }) + db.contestProblem.findUniqueOrThrow.resolves(mockContestProblem) + await expect( + service.getContestProblem(contestId, problemId, userId) + ).to.be.rejectedWith(ForbiddenAccessException) + }) + + it('should throw error when the user is not registered and contest is not ended', async () => { + const getContestSpy = stub(contestService, 'getContest') + getContestSpy.resolves({ + startTime: faker.date.past(), + endTime: faker.date.future(), + canRegister: true + }) + db.contestProblem.findUniqueOrThrow.resolves(mockContestProblem) + await expect( + service.getContestProblem(contestId, problemId, userId) + ).to.be.rejectedWith(ForbiddenAccessException) + }) }) }) diff --git a/backend/apps/client/src/problem/problem.service.ts b/backend/apps/client/src/problem/problem.service.ts index a2bf02210c..77ddd101c5 100644 --- a/backend/apps/client/src/problem/problem.service.ts +++ b/backend/apps/client/src/problem/problem.service.ts @@ -78,23 +78,32 @@ export class ContestProblemService { async getContestProblems( contestId: number, + userId: number, cursor: number | null, take: number, groupId = OPEN_SPACE_ID ) { - if (!(await this.contestService.isVisible(contestId, groupId))) { - throw new EntityNotExistException('Contest') + const contest = await this.contestService.getContest( + contestId, + groupId, + userId + ) + const now = new Date() + if (!contest.canRegister && contest.startTime > now) { + throw new ForbiddenAccessException( + 'Cannot access to problems before the contest starts.' + ) + } else if (contest.canRegister && contest.endTime > now) { + throw new ForbiddenAccessException( + 'Register to access the problems of this contest.' + ) } + const data = await this.problemRepository.getContestProblems( contestId, cursor, take ) - - if (data.length > 0 && data[0].contest.startTime > new Date()) { - throw new ForbiddenAccessException('Contest is not started yet.') - } - const total = await this.problemRepository.getContestProblemTotalCount(contestId) @@ -107,18 +116,27 @@ export class ContestProblemService { async getContestProblem( contestId: number, problemId: number, + userId: number, groupId = OPEN_SPACE_ID ) { - if (!(await this.contestService.isVisible(contestId, groupId))) { - throw new EntityNotExistException('Contest') + const contest = await this.contestService.getContest( + contestId, + groupId, + userId + ) + const now = new Date() + if (!contest.canRegister && contest.startTime > now) { + throw new ForbiddenAccessException( + 'Cannot access to problems before the contest starts.' + ) + } else if (contest.canRegister && contest.endTime > now) { + throw new ForbiddenAccessException('Register to access this problem.') } + const data = await this.problemRepository.getContestProblem( contestId, problemId ) - if (data.contest.startTime > new Date()) { - throw new ForbiddenAccessException('Contest is not started yet.') - } return plainToInstance(RelatedProblemResponseDto, data) } } diff --git a/backend/apps/client/src/submission/submission.controller.spec.ts b/backend/apps/client/src/submission/submission.controller.spec.ts index 461f829236..baf292aa37 100644 --- a/backend/apps/client/src/submission/submission.controller.spec.ts +++ b/backend/apps/client/src/submission/submission.controller.spec.ts @@ -1,7 +1,10 @@ import { Test, type TestingModule } from '@nestjs/testing' import { expect } from 'chai' import { RolesService } from '@libs/auth' -import { SubmissionController } from './submission.controller' +import { + SubmissionController, + ContestSubmissionController +} from './submission.controller' import { SubmissionService } from './submission.service' describe('SubmissionController', () => { @@ -23,3 +26,25 @@ describe('SubmissionController', () => { expect(controller).to.be.ok }) }) + +describe('ContestSubmissionController', () => { + let controller: ContestSubmissionController + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ContestSubmissionController], + providers: [ + { provide: SubmissionService, useValue: {} }, + { provide: RolesService, useValue: {} } + ] + }).compile() + + controller = module.get( + ContestSubmissionController + ) + }) + + it('should be defined', () => { + expect(controller).to.be.ok + }) +}) diff --git a/backend/apps/client/src/submission/submission.controller.ts b/backend/apps/client/src/submission/submission.controller.ts index eb158b5c06..1af3580214 100644 --- a/backend/apps/client/src/submission/submission.controller.ts +++ b/backend/apps/client/src/submission/submission.controller.ts @@ -12,7 +12,7 @@ import { DefaultValuePipe } from '@nestjs/common' import { Prisma } from '@prisma/client' -import { AuthenticatedRequest } from '@libs/auth' +import { AuthNotNeededIfOpenSpace, AuthenticatedRequest } from '@libs/auth' import { ConflictFoundException, EntityNotExistException, @@ -89,26 +89,15 @@ export class SubmissionController { } @Get() + @AuthNotNeededIfOpenSpace() async getSubmissions( - @Req() req: AuthenticatedRequest, @Query('cursor', CursorValidationPipe) cursor: number | null, @Query('take', new DefaultValuePipe(10), new RequiredIntPipe('take')) take: number, @Query('problemId', new RequiredIntPipe('problemId')) problemId: number, - @Query('groupId', GroupIDPipe) groupId: number, - @Query('contestId', IDValidationPipe) contestId: number | null + @Query('groupId', GroupIDPipe) groupId: number ) { try { - if (contestId) { - return await this.submissionService.getContestSubmissions({ - cursor, - take, - problemId, - contestId, - userId: req.user.id, - groupId - }) - } return await this.submissionService.getSubmissions({ cursor, take, @@ -122,8 +111,6 @@ export class SubmissionController { error instanceof EntityNotExistException ) { throw new NotFoundException(error.message) - } else if (error instanceof ForbiddenAccessException) { - throw error.convert2HTTPException() } this.logger.error(error) throw new InternalServerErrorException() @@ -169,3 +156,41 @@ export class SubmissionController { } } } + +@Controller('contest/:contestId/submission') +export class ContestSubmissionController { + private readonly logger = new Logger(ContestSubmissionController.name) + + constructor(private readonly submissionService: SubmissionService) {} + + @Get() + async getSubmissions( + @Req() req: AuthenticatedRequest, + @Param('contestId', IDValidationPipe) contestId: number, + @Query('cursor', CursorValidationPipe) cursor: number | null, + @Query('take', new DefaultValuePipe(10), new RequiredIntPipe('take')) + take: number, + @Query('problemId', new RequiredIntPipe('problemId')) problemId: number, + @Query('groupId', GroupIDPipe) groupId: number + ) { + try { + return await this.submissionService.getContestSubmissions({ + cursor, + take, + problemId, + contestId, + userId: req.user.id, + groupId + }) + } catch (error) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.name == 'NotFoundError' + ) { + throw new NotFoundException(error.message) + } + this.logger.error(error) + throw new InternalServerErrorException() + } + } +} diff --git a/backend/apps/client/src/submission/submission.module.ts b/backend/apps/client/src/submission/submission.module.ts index 60fce74ba0..99ad0e2fbc 100644 --- a/backend/apps/client/src/submission/submission.module.ts +++ b/backend/apps/client/src/submission/submission.module.ts @@ -5,7 +5,10 @@ import { APP_GUARD } from '@nestjs/core' import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq' import { GroupMemberGuard, RolesModule } from '@libs/auth' import { CONSUME_CHANNEL, PUBLISH_CHANNEL } from '@libs/constants' -import { SubmissionController } from './submission.controller' +import { + ContestSubmissionController, + SubmissionController +} from './submission.controller' import { SubmissionService } from './submission.service' @Module({ @@ -47,7 +50,7 @@ import { SubmissionService } from './submission.service' HttpModule, RolesModule ], - controllers: [SubmissionController], + controllers: [SubmissionController, ContestSubmissionController], providers: [ SubmissionService, { provide: APP_GUARD, useClass: GroupMemberGuard } diff --git a/backend/apps/client/src/submission/submission.service.ts b/backend/apps/client/src/submission/submission.service.ts index adbe000de2..4d8ac3d747 100644 --- a/backend/apps/client/src/submission/submission.service.ts +++ b/backend/apps/client/src/submission/submission.service.ts @@ -568,7 +568,7 @@ export class SubmissionService implements OnModuleInit { } }) - const submissions = await this.prisma.submission.findMany({ + return await this.prisma.submission.findMany({ ...paginator, take, where: { @@ -589,8 +589,6 @@ export class SubmissionService implements OnModuleInit { }, orderBy: [{ id: 'desc' }, { createTime: 'desc' }] }) - - return submissions } async getContestSubmission( diff --git a/collection/client/Problem/Get Contest Problem by ID/Succeed.bru b/collection/client/Problem/Get Contest Problem by ID/Succeed.bru new file mode 100644 index 0000000000..8eb4afdfd0 --- /dev/null +++ b/collection/client/Problem/Get Contest Problem by ID/Succeed.bru @@ -0,0 +1,42 @@ +meta { + name: Succeed + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/contest/1/problem/1 + body: none + auth: none +} + +query { + ~groupId: 1 +} + +assert { + res.status: eq 200 + res.body.order: isNumber + res.body.problem: isJson +} + +script:pre-request { + await require("./login").loginUser(req); +} + +docs { + # Get Contest Problem by ID + + 하나의 문제 정보를 가져옵니다. + + ## Query + + | 이름 | 타입 | 설명 | + |-----|-----|-----| + |groupId |Integer|문제가 속한 Group ID| + + ## Constraints + + - 대회에 등록한 사용자는 대회 시작 전까지 문제를 열람할 수 없습니다. + - 대회에 등록하지 않은 사용자는 대회가 끝나기 전까지 문제를 열람할 수 없습니다. +} diff --git a/collection/client/Problem/Get Contest Problem by ID/[403] Registered but not started.bru b/collection/client/Problem/Get Contest Problem by ID/[403] Registered but not started.bru new file mode 100644 index 0000000000..4a13558ade --- /dev/null +++ b/collection/client/Problem/Get Contest Problem by ID/[403] Registered but not started.bru @@ -0,0 +1,24 @@ +meta { + name: [403] Registered but not started + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/contest/15/problem/1 + body: none + auth: none +} + +query { + ~groupId: 1 +} + +assert { + res.status: eq 404 + res.body.message: isDefined +} + +script:pre-request { + await require("./login").loginUser(req); +} diff --git a/collection/client/Problem/Get Contest Problem by ID/[403] Unregistered and not finished.bru b/collection/client/Problem/Get Contest Problem by ID/[403] Unregistered and not finished.bru new file mode 100644 index 0000000000..c40dcf6d10 --- /dev/null +++ b/collection/client/Problem/Get Contest Problem by ID/[403] Unregistered and not finished.bru @@ -0,0 +1,24 @@ +meta { + name: [403] Unregistered and not finished + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/contest/15/problem/1 + body: none + auth: none +} + +query { + ~groupId: 1 +} + +assert { + res.status: eq 404 + res.body.message: isDefined +} + +script:pre-request { + await require("./login").loginManager(req); +} diff --git a/collection/client/Problem/Get Contest Problem by ID/[404] Nonexistent Contest Problem.bru b/collection/client/Problem/Get Contest Problem by ID/[404] Nonexistent Contest Problem.bru new file mode 100644 index 0000000000..6b8426dcbd --- /dev/null +++ b/collection/client/Problem/Get Contest Problem by ID/[404] Nonexistent Contest Problem.bru @@ -0,0 +1,37 @@ +meta { + name: [404] Nonexistent Contest Problem + type: http + seq: 4 +} + +get { + url: {{baseUrl}}/contest/1/problem/999 + body: none + auth: none +} + +query { + ~groupId: 1 +} + +assert { + res.status: eq 200 + res.body.order: isNumber + res.body.problem: isJson +} + +script:pre-request { + await require("./login").loginUser(req); +} + +docs { + # Get Contest Problem by ID + + 하나의 문제 정보를 가져옵니다. + + ## Query + + | 이름 | 타입 | 설명 | + |-----|-----|-----| + |groupId |Integer|문제가 속한 Group ID| +} diff --git a/collection/client/Problem/Get Contest Problems/Succeed.bru b/collection/client/Problem/Get Contest Problems/Succeed.bru new file mode 100644 index 0000000000..a12c4d169f --- /dev/null +++ b/collection/client/Problem/Get Contest Problems/Succeed.bru @@ -0,0 +1,53 @@ +meta { + name: Succeed + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/contest/1/problem?take=5 + body: none + auth: none +} + +query { + take: 5 + ~cursor: 4 + ~groupId: 2 +} + +assert { + res.status: eq 200 + res.body.problems[0].id: isNumber + res.body.problems[0].title: isString + res.body.problems[0].difficulty: isString + res.body.problems[0].submissionCount: isNumber + res.body.problems[0].acceptedRate: isNumber + res.body.problems[0].order: isNumber + res.body.total: isNumber +} + +script:pre-request { + await require("./login").loginUser(req); +} + +docs { + # Get Contest Problems + + > 로그인이 필요한 API입니다. + + 대회의 문제 목록을 가져옵니다. + + ## Query + + | 이름 | 타입 | 설명 | + |-----|-----|-----| + |take |Integer|가져올 문제 개수 (default: 10)| + |cursor |Integer|cursor 값 다음의 ID를 가진 문제들을 반환| + |groupId |Integer|대회가 속한 Group ID (default: 1)| + + ## Constraints + + - 대회에 등록한 사용자는 대회 시작 전까지 문제를 열람할 수 없습니다. + - 대회에 등록하지 않은 사용자는 대회가 끝나기 전까지 문제를 열람할 수 없습니다. +} diff --git a/collection/client/Problem/Get Contest Problems/[403] Registered but Not Started.bru b/collection/client/Problem/Get Contest Problems/[403] Registered but Not Started.bru new file mode 100644 index 0000000000..f4df84506c --- /dev/null +++ b/collection/client/Problem/Get Contest Problems/[403] Registered but Not Started.bru @@ -0,0 +1,26 @@ +meta { + name: [403] Registered but Not Started + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/contest/15/problem?take=5 + body: none + auth: none +} + +query { + take: 5 + ~cursor: 4 + ~groupId: 1 +} + +assert { + res.status: eq 403 + res.body.message: isDefined +} + +script:pre-request { + await require("./login").loginManager(req); +} diff --git a/collection/client/Problem/Get Contest Problems/[403] Unregistered and Not Finished.bru b/collection/client/Problem/Get Contest Problems/[403] Unregistered and Not Finished.bru new file mode 100644 index 0000000000..b5e5d9197a --- /dev/null +++ b/collection/client/Problem/Get Contest Problems/[403] Unregistered and Not Finished.bru @@ -0,0 +1,26 @@ +meta { + name: [403] Unregistered and Not Finished + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/contest/15/problem?take=5 + body: none + auth: none +} + +query { + take: 5 + ~cursor: 4 + ~groupId: 1 +} + +assert { + res.status: eq 403 + res.body.message: isDefined +} + +script:pre-request { + await require("./login").loginManager(req); +} diff --git a/collection/client/Problem/Get Contest Problems/[404] Nonexistent Contest.bru b/collection/client/Problem/Get Contest Problems/[404] Nonexistent Contest.bru new file mode 100644 index 0000000000..0f3582b00a --- /dev/null +++ b/collection/client/Problem/Get Contest Problems/[404] Nonexistent Contest.bru @@ -0,0 +1,26 @@ +meta { + name: [404] Nonexistent Contest + type: http + seq: 4 +} + +get { + url: {{baseUrl}}/contest/999/problem?take=5 + body: none + auth: none +} + +query { + take: 5 + ~cursor: 4 + ~groupId: 2 +} + +assert { + res.status: eq 404 + res.body.message: isDefined +} + +script:pre-request { + await require("./login").loginUser(req); +} diff --git a/collection/client/Problem/Get problem by ID/Succeed.bru b/collection/client/Problem/Get problem by ID/Succeed.bru index 4b4d9eba94..05e52f9129 100644 --- a/collection/client/Problem/Get problem by ID/Succeed.bru +++ b/collection/client/Problem/Get problem by ID/Succeed.bru @@ -12,7 +12,6 @@ get { query { ~groupId: 1 - ~contestId: 1 ~workbookId: 1 } @@ -29,7 +28,7 @@ assert { res("memoryLimit"): isNumber res("difficulty"): isString res("source"): isString - res("tags"): isDefined + res("tags"): isDefined } docs { @@ -40,12 +39,10 @@ docs { ## Query - > contestId 또는 workbookId 중 하나 값은 필수로 주어져야 합니다. - > 두 값이 모두 주어진 경우, contestId 우선 적용되어 대회의 문제가 반환됩니다. + > workbookId가 주어진 경우, 워크북의 문제가 반환됩니다. | 이름 | 타입 | 설명 | |-----|-----|-----| |groupId |Integer|문제가 속한 Group ID| - |contestId |Integer|문제가 속한 대회 ID| |workbookId|Integer|문제가 속한 문제집 ID| } diff --git a/collection/client/Problem/Get problems/Succeed.bru b/collection/client/Problem/Get problems/Succeed.bru index 1d3c54c7ec..03692e2c9f 100644 --- a/collection/client/Problem/Get problems/Succeed.bru +++ b/collection/client/Problem/Get problems/Succeed.bru @@ -16,7 +16,6 @@ query { ~search: 정 ~order: level-desc ~groupId: 2 - ~contestId: 1 ~workbookId: 1 } @@ -28,7 +27,7 @@ assert { res.body.problems[0].submissionCount: isNumber res.body.problems[0].acceptedRate: isNumber res.body.problems[0].tags: isDefined - res.body.total: isNumber + res.body.total: isNumber } docs { @@ -38,8 +37,7 @@ docs { ## Query - > contestId 또는 workbookId 중 하나 값은 필수로 주어져야 합니다. - > 두 값이 모두 주어진 경우, contestId 우선 적용되어 대회의 문제들이 반환됩니다. + > workbookId가 주어진 경우 워크북의 문제들이 반환됩니다. | 이름 | 타입 | 설명 | |-----|-----|-----| @@ -48,7 +46,6 @@ docs { |search |String |검색 키워드| |order |String |정렬 기준 (아래 참고)| |groupId |Integer|문제가 속한 Group ID (default: 1)| - |contestId |Integer|문제가 속한 대회 ID| |workbookId|Integer|문제가 속한 문제집 ID| ### 정렬 기준 옵션 diff --git a/collection/client/Submission/Get Contest Submissions/Succeed.bru b/collection/client/Submission/Get Contest Submissions/Succeed.bru new file mode 100644 index 0000000000..1bb58852fa --- /dev/null +++ b/collection/client/Submission/Get Contest Submissions/Succeed.bru @@ -0,0 +1,47 @@ +meta { + name: Succeed + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/contest/1/submission?problemId=1 + body: none + auth: none +} + +query { + problemId: 1 + ~groupId: 1 + ~take: 10 + ~cursor: 5 +} + +assert { + res.status: eq 200 + res.body[0]: isJson +} + +script:pre-request { + await require("./login").loginUser(req); +} + +docs { + # Get Contest Submissions + + > 로그인이 필요한 API입니다. + + 대회 문제의 제출 내역을 가져옵니다. + + ## Query + + > 필수 query는 * 표시하였습니다. + + | 이름 | 타입 | 설명 | + |-----|-----|-----| + |problemId *|Integer|문제 ID| + |groupId|Integer|문제가 속한 Group ID (default: 1)| + |take|Integer|가져올 제출 내역 개수 (default: 10)| + |cursor|Integer|cursor 값 다음의 ID를 가진 제출 내역들을 반환| + +} diff --git a/collection/client/Submission/Get Contest Submissions/[404] Nonexistent Problem.bru b/collection/client/Submission/Get Contest Submissions/[404] Nonexistent Problem.bru new file mode 100644 index 0000000000..4710c957ce --- /dev/null +++ b/collection/client/Submission/Get Contest Submissions/[404] Nonexistent Problem.bru @@ -0,0 +1,27 @@ +meta { + name: [404] Nonexistent Problem + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/contest/1/submission?problemId=9999 + body: none + auth: none +} + +query { + problemId: 9999 + ~groupId: 2 + ~take: 10 + ~cursor: 5 +} + +assert { + res.status: eq 404 + res.body.message: isDefined No ContestProblem found +} + +script:pre-request { + await require("./login").loginUser(req); +} diff --git a/collection/client/Submission/Get Submission by ID/Succeed.bru b/collection/client/Submission/Get Submission by ID/Succeed.bru index 45cf476b85..8157d8952e 100644 --- a/collection/client/Submission/Get Submission by ID/Succeed.bru +++ b/collection/client/Submission/Get Submission by ID/Succeed.bru @@ -16,7 +16,7 @@ query { } script:pre-request { - require("./login").loginUser(req); + await require("./login").loginUser(req); } docs { diff --git a/collection/client/Submission/Get Submission by ID/[404] Nonexistent Problem.bru b/collection/client/Submission/Get Submission by ID/[404] Nonexistent Problem.bru index 50a16bc40f..381b12e521 100644 --- a/collection/client/Submission/Get Submission by ID/[404] Nonexistent Problem.bru +++ b/collection/client/Submission/Get Submission by ID/[404] Nonexistent Problem.bru @@ -13,3 +13,7 @@ get { query { problemId: 99999 } + +script:pre-request { + await require("./login").loginUser(req); +} diff --git a/collection/client/Submission/Get Submission by ID/[404] Nonexistent Submission.bru b/collection/client/Submission/Get Submission by ID/[404] Nonexistent Submission.bru index 38bd7cd7d9..2b13b729a6 100644 --- a/collection/client/Submission/Get Submission by ID/[404] Nonexistent Submission.bru +++ b/collection/client/Submission/Get Submission by ID/[404] Nonexistent Submission.bru @@ -13,3 +13,7 @@ get { query { problemId: 1 } + +script:pre-request { + await require("./login").loginUser(req); +} diff --git a/collection/client/Submission/Get Submissions/Succeed.bru b/collection/client/Submission/Get Submissions/Succeed.bru index fc057ca71b..2175c773f1 100644 --- a/collection/client/Submission/Get Submissions/Succeed.bru +++ b/collection/client/Submission/Get Submissions/Succeed.bru @@ -12,16 +12,11 @@ get { query { problemId: 1 - ~contestId: 1 ~groupId: 1 ~take: 10 ~cursor: 5 } -script:pre-request { - require("./login").loginUser(req); -} - docs { # Get Submissions @@ -34,7 +29,6 @@ docs { | 이름 | 타입 | 설명 | |-----|-----|-----| |problemId *|Integer|문제 ID| - |contestId|Integer|문제가 속한 대회 ID| |groupId|Integer|문제가 속한 Group ID (default: 1)| |take|Integer|가져올 제출 내역 개수 (default: 10)| |cursor|Integer|cursor 값 다음의 ID를 가진 제출 내역들을 반환| diff --git a/collection/client/login.js b/collection/client/login.js index c1bedf0f53..16b9647d5c 100644 --- a/collection/client/login.js +++ b/collection/client/login.js @@ -22,6 +22,13 @@ const loginAdmin = async (req) => { }) } +const loginManager = async (req) => { + await login(req, { + username: 'manager', + password: 'Managermanager' + }) +} + const loginUser = async (req) => { await login(req, { username: 'user01', @@ -38,6 +45,7 @@ const loginUser2nd = async (req) => { module.exports = { loginAdmin, + loginManager, loginUser, loginUser2nd } diff --git a/frontend-client/app/(main)/contest/[contestId]/@tabs/problem/page.tsx b/frontend-client/app/(main)/contest/[contestId]/@tabs/problem/page.tsx index 4be813c3a8..33c2e870bd 100644 --- a/frontend-client/app/(main)/contest/[contestId]/@tabs/problem/page.tsx +++ b/frontend-client/app/(main)/contest/[contestId]/@tabs/problem/page.tsx @@ -1,6 +1,7 @@ import DataTable from '@/components/DataTable' -import { fetcher } from '@/lib/utils' +import { fetcherWithAuth } from '@/lib/utils' import type { ContestProblem } from '@/types/type' +import { IoIosLock } from 'react-icons/io' import { columns } from './_components/Columns' interface ContestProblemProps { @@ -9,15 +10,31 @@ interface ContestProblemProps { export default async function ContestProblem({ params }: ContestProblemProps) { const { contestId } = params - const { problems }: { problems: ContestProblem[] } = await fetcher - .get('problem', { - searchParams: { - take: 10, - contestId - } - }) - .json() + const res = await fetcherWithAuth.get(`contest/${contestId}/problem`, { + searchParams: { + take: 10 + } + }) + if (!res.ok) { + const { statusCode, message }: { statusCode: number; message: string } = + await res.json() + return ( +
+ +
+

Access Denied

+

+ {statusCode === 401 + ? 'Log in first to check the problems.' + : message} +

+
+
+ ) + } + + const { problems }: { problems: ContestProblem[] } = await res.json() return ( -
+
diff --git a/frontend-client/app/problem/[problemId]/submission/_components/SubmissionDetail.tsx b/frontend-client/app/problem/[problemId]/submission/_components/SubmissionDetail.tsx index 35a71967e1..2383315013 100644 --- a/frontend-client/app/problem/[problemId]/submission/_components/SubmissionDetail.tsx +++ b/frontend-client/app/problem/[problemId]/submission/_components/SubmissionDetail.tsx @@ -1,4 +1,4 @@ -import Codeeditor from '@/components/CodeEditor' +import CodeEditor from '@/components/CodeEditor' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { Table, @@ -13,6 +13,7 @@ import type { SubmissionDetail } from '@/types/type' import dayjs from 'dayjs' import { revalidateTag } from 'next/cache' import { IoIosLock } from 'react-icons/io' +import dataIfError from './dataIfError' interface Props { problemId: number @@ -30,6 +31,7 @@ export default async function SubmissionDetail({ } }) + const submission: SubmissionDetail = res.ok ? await res.json() : dataIfError if (res.status == 403) { return (
@@ -41,8 +43,6 @@ export default async function SubmissionDetail({ ) } - const submission: SubmissionDetail = await res.json() - if (submission.result == 'Judging') { revalidateTag(`submission/${submissionId}`) } @@ -80,7 +80,7 @@ export default async function SubmissionDetail({

Source Code

-
)} + {res.ok ? ( + <> + ) : ( +
+ +

Access Denied

+

+ {`If you want to check other users' code, + please submit a correct answer of your own.`} +

+
+ )} ) } diff --git a/frontend-client/app/problem/[problemId]/submission/_components/dataIfError.ts b/frontend-client/app/problem/[problemId]/submission/_components/dataIfError.ts new file mode 100644 index 0000000000..78bad23bc4 --- /dev/null +++ b/frontend-client/app/problem/[problemId]/submission/_components/dataIfError.ts @@ -0,0 +1,29 @@ +import type { SubmissionDetail } from '@/types/type' + +const dataIfError: SubmissionDetail = { + problemId: 0, + username: 'skkuding', + code: `#include + +int main() { +char name[20]; +scanf("%s", name); +printf("Hello, %s!\\n", name); +return 0; +}`, + language: 'C', + createTime: new Date(), + result: 'Accepted', + testcaseResult: Array.from({ length: 5 }, (_, i) => ({ + id: i, + submissionId: 0, + problemTestCaseId: i, + result: 'Accepted', + cpuTime: '0', + memoryUsage: 12345, + createTime: new Date(), + updateTime: new Date() + })) +} + +export default dataIfError diff --git a/frontend-client/app/problem/[problemId]/submission/page.tsx b/frontend-client/app/problem/[problemId]/submission/page.tsx index 6ed2affb3d..1371fa1805 100644 --- a/frontend-client/app/problem/[problemId]/submission/page.tsx +++ b/frontend-client/app/problem/[problemId]/submission/page.tsx @@ -1,7 +1,7 @@ 'use client' import Paginator from '@/components/Paginator' -import { usePagination } from '@/lib/usePagination' +import { usePagination } from '@/lib/pagination' import type { SubmissionItem } from '@/types/type' import { columns } from './_components/Columns' import DataTable from './_components/DataTable' diff --git a/frontend-client/components/EditorResizablePanel.tsx b/frontend-client/components/EditorResizablePanel.tsx index 5cab7fd36d..dd19480020 100644 --- a/frontend-client/components/EditorResizablePanel.tsx +++ b/frontend-client/components/EditorResizablePanel.tsx @@ -8,7 +8,7 @@ import { } from '@/components/ui/resizable' import { ScrollArea } from '@/components/ui/scroll-area' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { useStorage } from '@/lib/hooks' +import { useStorage } from '@/lib/storage' import useEditorStore from '@/stores/editor' import type { Language, ProblemDetail } from '@/types/type' import type { Route } from 'next' diff --git a/frontend-client/lib/usePagination.ts b/frontend-client/lib/pagination.ts similarity index 94% rename from frontend-client/lib/usePagination.ts rename to frontend-client/lib/pagination.ts index 0568a85237..a0a4550b0b 100644 --- a/frontend-client/lib/usePagination.ts +++ b/frontend-client/lib/pagination.ts @@ -1,4 +1,4 @@ -import { fetcherWithAuth } from '@/lib/utils' +import { fetcher } from '@/lib/utils' import { useEffect, useState, useRef, useCallback } from 'react' interface Item { @@ -80,11 +80,11 @@ export const usePagination = ( useEffect(() => { ;(async () => { // TODO: fetcher <-> fetcherWithAuth 중 선택할 수 있도록 수정 - const data: T[] = await fetcherWithAuth - .get(path, { - searchParams: query - }) - .json() + const res = await fetcher.get(path, { + searchParams: query + }) + console.log(res.status) + const data: T[] = await res.json() const next = Number(query.get('take')) > 0 const full = data.length >= take diff --git a/frontend-client/lib/hooks.ts b/frontend-client/lib/storage.ts similarity index 100% rename from frontend-client/lib/hooks.ts rename to frontend-client/lib/storage.ts diff --git a/frontend-client/types/type.ts b/frontend-client/types/type.ts index 0bf8414b3a..2736689efb 100644 --- a/frontend-client/types/type.ts +++ b/frontend-client/types/type.ts @@ -121,7 +121,7 @@ export interface SubmissionDetail { username: string code: string language: Language - createTime: string + createTime: Date result: string testcaseResult: { id: number @@ -130,8 +130,8 @@ export interface SubmissionDetail { result: string cpuTime: string memoryUsage: number - createTime: string - updateTime: string + createTime: Date + updateTime: Date }[] }