From ce3eb8806e58c1f601062b0ae851baa59f67fb74 Mon Sep 17 00:00:00 2001 From: mnseok kang Date: Mon, 30 Sep 2024 07:38:59 +0000 Subject: [PATCH 01/85] refactor(be): improve admin problem module error handling --- .../admin/src/problem/problem.resolver.ts | 230 ++------------- .../apps/admin/src/problem/problem.service.ts | 261 ++++++++++++------ 2 files changed, 200 insertions(+), 291 deletions(-) diff --git a/apps/backend/apps/admin/src/problem/problem.resolver.ts b/apps/backend/apps/admin/src/problem/problem.resolver.ts index 0f3234b435..34ce3ecc9b 100644 --- a/apps/backend/apps/admin/src/problem/problem.resolver.ts +++ b/apps/backend/apps/admin/src/problem/problem.resolver.ts @@ -1,12 +1,4 @@ -import { - InternalServerErrorException, - Logger, - NotFoundException, - ParseArrayPipe, - UnprocessableEntityException, - UsePipes, - ValidationPipe -} from '@nestjs/common' +import { ParseArrayPipe, UsePipes, ValidationPipe } from '@nestjs/common' import { Args, Context, @@ -24,15 +16,8 @@ import { ProblemTestcase, WorkbookProblem } from '@generated' -import { Prisma } from '@prisma/client' -import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { AuthenticatedRequest } from '@libs/auth' import { OPEN_SPACE_ID } from '@libs/constants' -import { - ConflictFoundException, - ForbiddenAccessException, - UnprocessableDataException -} from '@libs/exception' import { CursorValidationPipe, GroupIDPipe, RequiredIntPipe } from '@libs/pipe' import { ImageSource } from './model/image.output' import { @@ -46,8 +31,6 @@ import { ProblemService } from './problem.service' @Resolver(() => ProblemWithIsVisible) export class ProblemResolver { - private readonly logger = new Logger(ProblemResolver.name) - constructor(private readonly problemService: ProblemService) {} @Mutation(() => ProblemWithIsVisible) @@ -61,24 +44,7 @@ export class ProblemResolver { groupId: number, @Args('input') input: CreateProblemInput ) { - try { - return await this.problemService.createProblem( - input, - req.user.id, - groupId - ) - } catch (error) { - if (error instanceof UnprocessableDataException) { - throw error.convert2HTTPException() - } else if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.code === 'P2003' - ) { - throw new UnprocessableEntityException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.createProblem(input, req.user.id, groupId) } @Mutation(() => [ProblemWithIsVisible]) @@ -93,19 +59,7 @@ export class ProblemResolver { groupId: number, @Args('input') input: UploadFileInput ) { - try { - return await this.problemService.uploadProblems( - input, - req.user.id, - groupId - ) - } catch (error) { - if (error instanceof UnprocessableDataException) { - throw error.convert2HTTPException() - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.uploadProblems(input, req.user.id, groupId) } @Mutation(() => ImageSource) @@ -113,15 +67,7 @@ export class ProblemResolver { @Args('input') input: UploadFileInput, @Context('req') req: AuthenticatedRequest ) { - try { - return await this.problemService.uploadImage(input, req.user.id) - } catch (error) { - if (error instanceof UnprocessableDataException) { - throw error.convert2HTTPException() - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.uploadImage(input, req.user.id) } @Mutation(() => Image) @@ -129,20 +75,7 @@ export class ProblemResolver { @Args('filename') filename: string, @Context('req') req: AuthenticatedRequest ) { - try { - return await this.problemService.deleteImage(filename, req.user.id) - } catch (error) { - if (error instanceof UnprocessableDataException) { - throw error.convert2HTTPException() - } else if ( - error instanceof PrismaClientKnownRequestError && - error.code == 'P2025' - ) { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.deleteImage(filename, req.user.id) } @Query(() => [ProblemWithIsVisible]) @@ -171,38 +104,17 @@ export class ProblemResolver { groupId: number, @Args('id', { type: () => Int }, new RequiredIntPipe('id')) id: number ) { - try { - return await this.problemService.getProblem(id, groupId) - } catch (error) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.name == 'NotFoundError' - ) { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.getProblem(id, groupId) } @ResolveField('tag', () => [ProblemTag]) async getProblemTags(@Parent() problem: ProblemWithIsVisible) { - try { - return await this.problemService.getProblemTags(problem.id) - } catch (error) { - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.getProblemTags(problem.id) } @ResolveField('testcase', () => [ProblemTestcase]) async getProblemTestCases(@Parent() problem: ProblemWithIsVisible) { - try { - return await this.problemService.getProblemTestcases(problem.id) - } catch (error) { - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.getProblemTestcases(problem.id) } @Mutation(() => ProblemWithIsVisible) @@ -215,24 +127,7 @@ export class ProblemResolver { groupId: number, @Args('input') input: UpdateProblemInput ) { - try { - return await this.problemService.updateProblem(input, groupId) - } catch (error) { - if ( - error instanceof UnprocessableDataException || - error instanceof ConflictFoundException - ) { - throw error.convert2HTTPException() - } else if (error instanceof Prisma.PrismaClientKnownRequestError) { - if (error.name == 'NotFoundError') { - throw new NotFoundException(error.message) - } else if (error.code === 'P2003') { - throw new UnprocessableEntityException(error.message) - } - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.updateProblem(input, groupId) } @Mutation(() => ProblemWithIsVisible) @@ -245,25 +140,12 @@ export class ProblemResolver { groupId: number, @Args('id', { type: () => Int }, new RequiredIntPipe('id')) id: number ) { - try { - return await this.problemService.deleteProblem(id, groupId) - } catch (error) { - if ( - error instanceof Prisma.PrismaClientKnownRequestError && - error.name == 'NotFoundError' - ) { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.deleteProblem(id, groupId) } } @Resolver(() => ContestProblem) export class ContestProblemResolver { - private readonly logger = new Logger(ProblemResolver.name) - constructor(private readonly problemService: ProblemService) {} @Query(() => [ContestProblem], { name: 'getContestProblems' }) @@ -277,20 +159,7 @@ export class ContestProblemResolver { @Args('contestId', { type: () => Int }, new RequiredIntPipe('contestId')) contestId: number ) { - try { - return await this.problemService.getContestProblems(groupId, contestId) - } catch (error) { - if ( - error instanceof UnprocessableDataException || - error instanceof ForbiddenAccessException - ) { - throw error.convert2HTTPException() - } else if (error.code == 'P2025') { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException(error.message) - } + return await this.problemService.getContestProblems(groupId, contestId) } @Mutation(() => [ContestProblem]) @@ -305,41 +174,21 @@ export class ContestProblemResolver { contestId: number, @Args('orders', { type: () => [Int] }, ParseArrayPipe) orders: number[] ) { - try { - return await this.problemService.updateContestProblemsOrder( - groupId, - contestId, - orders - ) - } catch (error) { - if ( - error instanceof UnprocessableDataException || - error instanceof ForbiddenAccessException - ) { - throw error.convert2HTTPException() - } else if (error.code == 'P2025') { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException(error.message) - } + return await this.problemService.updateContestProblemsOrder( + groupId, + contestId, + orders + ) } @ResolveField('problem', () => ProblemWithIsVisible) async getProblem(@Parent() contestProblem: ContestProblem) { - try { - return await this.problemService.getProblemById(contestProblem.problemId) - } catch (error) { - this.logger.error(error) - throw new InternalServerErrorException() - } + return await this.problemService.getProblemById(contestProblem.problemId) } } @Resolver(() => WorkbookProblem) export class WorkbookProblemResolver { - private readonly logger = new Logger(ProblemResolver.name) - constructor(private readonly problemService: ProblemService) {} @Query(() => [WorkbookProblem], { name: 'getWorkbookProblems' }) @@ -352,20 +201,7 @@ export class WorkbookProblemResolver { groupId: number, @Args('workbookId', { type: () => Int }) workbookId: number ) { - try { - return await this.problemService.getWorkbookProblems(groupId, workbookId) - } catch (error) { - if ( - error instanceof UnprocessableDataException || - error instanceof ForbiddenAccessException - ) { - throw error.convert2HTTPException() - } else if (error.code == 'P2025') { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException(error.message) - } + return await this.problemService.getWorkbookProblems(groupId, workbookId) } @Mutation(() => [WorkbookProblem]) @@ -380,33 +216,15 @@ export class WorkbookProblemResolver { // orders는 항상 workbookId에 해당하는 workbookProblems들이 모두 딸려 온다. @Args('orders', { type: () => [Int] }, ParseArrayPipe) orders: number[] ) { - try { - return await this.problemService.updateWorkbookProblemsOrder( - groupId, - workbookId, - orders - ) - } catch (error) { - if ( - error instanceof UnprocessableDataException || - error instanceof ForbiddenAccessException - ) { - throw error.convert2HTTPException() - } else if (error.code == 'P2025') { - throw new NotFoundException(error.message) - } - this.logger.error(error) - throw new InternalServerErrorException(error.message) - } + return await this.problemService.updateWorkbookProblemsOrder( + groupId, + workbookId, + orders + ) } @ResolveField('problem', () => ProblemWithIsVisible) async getProblem(@Parent() workbookProblem: WorkbookProblem) { - try { - return await this.problemService.getProblemById(workbookProblem.problemId) - } catch (error) { - console.log(error) - throw new InternalServerErrorException() - } + return await this.problemService.getProblemById(workbookProblem.problemId) } } diff --git a/apps/backend/apps/admin/src/problem/problem.service.ts b/apps/backend/apps/admin/src/problem/problem.service.ts index d74202b907..776efd854d 100644 --- a/apps/backend/apps/admin/src/problem/problem.service.ts +++ b/apps/backend/apps/admin/src/problem/problem.service.ts @@ -64,38 +64,54 @@ export class ProblemService { } }) - const problem = await this.prisma.problem.create({ - data: { - ...data, - visibleLockTime: isVisible ? MIN_DATE : MAX_DATE, - groupId, - createdById: userId, - languages, - template: [JSON.stringify(template)], - problemTag: { - create: tagIds.map((tagId) => { - return { tagId } - }) + try { + const problem = await this.prisma.problem.create({ + data: { + ...data, + visibleLockTime: isVisible ? MIN_DATE : MAX_DATE, + groupId, + createdById: userId, + languages, + template: [JSON.stringify(template)], + problemTag: { + create: tagIds.map((tagId) => { + return { tagId } + }) + } } + }) + await this.createTestcases(problem.id, testcases) + return this.changeVisibleLockTimeToIsVisible(problem) + } catch (error) { + if ( + error instanceof PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + throw new DuplicateFoundException('problem') } - }) - await this.createTestcases(problem.id, testcases) - return this.changeVisibleLockTimeToIsVisible(problem) + throw new InternalServerErrorException(error) + } } async createTestcases(problemId: number, testcases: Array) { await Promise.all( testcases.map(async (tc, index) => { - const problemTestcase = await this.prisma.problemTestcase.create({ - data: { - problemId, - input: tc.input, - output: tc.output, - scoreWeight: tc.scoreWeight, - isHidden: tc.isHidden - } - }) - return { index, id: problemTestcase.id } + try { + const problemTestcase = await this.prisma.problemTestcase.create({ + data: { + problemId, + input: tc.input, + output: tc.output, + scoreWeight: tc.scoreWeight, + isHidden: tc.isHidden + } + }) + return { index, id: problemTestcase.id } + } catch (error) { + throw new InternalServerErrorException( + error.message + ` at testcase ${index + 1}` + ) + } }) ) } @@ -340,46 +356,64 @@ export class ProblemService { whereOptions.languages = { hasSome: input.languages } } - const problems: Problem[] = await this.prisma.problem.findMany({ - ...paginator, - where: { - ...whereOptions, - groupId - }, - take - }) - return this.changeVisibleLockTimeToIsVisible(problems) + try { + const problems: Problem[] = await this.prisma.problem.findMany({ + ...paginator, + where: { + ...whereOptions, + groupId + }, + take + }) + return this.changeVisibleLockTimeToIsVisible(problems) + } catch (error) { + throw new InternalServerErrorException(error) + } } async getProblem(id: number, groupId: number) { - const problem = await this.prisma.problem.findFirstOrThrow({ + const problem = await this.prisma.problem.findFirst({ where: { id, groupId } }) + + if (!problem) { + throw new EntityNotExistException('problem') + } + return this.changeVisibleLockTimeToIsVisible(problem) } async getProblemById(id: number) { - const problem = await this.prisma.problem.findFirstOrThrow({ + const problem = await this.prisma.problem.findFirst({ where: { id } }) + + if (!problem) { + throw new EntityNotExistException('problem') + } + return this.changeVisibleLockTimeToIsVisible(problem) } async updateProblem(input: UpdateProblemInput, groupId: number) { const { id, languages, template, tags, testcases, isVisible, ...data } = input - const problem = await this.prisma.problem.findFirstOrThrow({ + const problem = await this.prisma.problem.findFirst({ where: { id, groupId } }) + if (!problem) { + throw new EntityNotExistException('problem') + } + if (languages && !languages.length) { throw new UnprocessableDataException( 'A problem should support at least one language' @@ -422,21 +456,33 @@ export class ProblemService { await this.updateTestcases(id, testcases) } - const updatedProblem = await this.prisma.problem.update({ - where: { id }, - data: { - ...data, - ...(isVisible != undefined && { - visibleLockTime: isVisible ? MIN_DATE : MAX_DATE - }), - ...(languages && { languages }), - ...(template && { template: [JSON.stringify(template)] }), - problemTag + try { + const updatedProblem = await this.prisma.problem.update({ + where: { id }, + data: { + ...data, + ...(isVisible != undefined && { + visibleLockTime: isVisible ? MIN_DATE : MAX_DATE + }), + ...(languages && { languages }), + ...(template && { template: [JSON.stringify(template)] }), + problemTag + } + }) + return this.changeVisibleLockTimeToIsVisible(updatedProblem) + } catch (error) { + if ( + error instanceof PrismaClientKnownRequestError && + error.code === 'P2002' + ) { + throw new UnprocessableDataException(error.message) } - }) - return this.changeVisibleLockTimeToIsVisible(updatedProblem) + } } + /** + * problemId에 해당하는 problem의 tag의 중복을 확인하고, prisma에 넣을 수 있도록 수정합니다. + */ async updateProblemTag( problemId: number, problemTags: UpdateProblemTagInput @@ -455,13 +501,18 @@ export class ProblemService { }) const deleteIds = problemTags.delete.map(async (tagId) => { - const check = await this.prisma.problemTag.findFirstOrThrow({ + const check = await this.prisma.problemTag.findFirst({ where: { tagId, problemId }, select: { id: true } }) + + if (!check) { + throw new EntityNotExistException(`${tagId} tag`) + } + return { id: check.id } }) @@ -472,45 +523,57 @@ export class ProblemService { } async updateTestcases(problemId: number, testcases: Array) { - await Promise.all([ - this.prisma.problemTestcase.deleteMany({ - where: { - problemId - } - }) - ]) + try { + await Promise.all([ + this.prisma.problemTestcase.deleteMany({ + where: { + problemId + } + }) + ]) - for (const tc of testcases) { - await this.prisma.problemTestcase.create({ - data: { - problemId, - input: tc.input, - output: tc.output, - scoreWeight: tc.scoreWeight, - isHidden: tc.isHidden - } - }) + for (const tc of testcases) { + await this.prisma.problemTestcase.create({ + data: { + problemId, + input: tc.input, + output: tc.output, + scoreWeight: tc.scoreWeight, + isHidden: tc.isHidden + } + }) + } + } catch (error) { + throw new InternalServerErrorException(error) } } async deleteProblem(id: number, groupId: number) { - const problem = await this.prisma.problem.findFirstOrThrow({ + const problem = await this.prisma.problem.findFirst({ where: { id, groupId } }) + if (!problem) { + throw new EntityNotExistException('problem') + } + // Problem description에 이미지가 포함되어 있다면 삭제 const uuidImageFileNames = this.extractUUIDs(problem.description) if (uuidImageFileNames) { - await this.prisma.image.deleteMany({ - where: { - filename: { - in: uuidImageFileNames + try { + await this.prisma.image.deleteMany({ + where: { + filename: { + in: uuidImageFileNames + } } - } - }) + }) + } catch (error) { + throw new InternalServerErrorException(error) + } const deleteFromS3Results = uuidImageFileNames.map((filename: string) => { return this.storageService.deleteImage(filename) @@ -519,9 +582,15 @@ export class ProblemService { await Promise.all(deleteFromS3Results) } - return await this.prisma.problem.delete({ - where: { id } - }) + try { + await this.prisma.problem.delete({ + where: { + id + } + }) + } catch (error) { + throw new InternalServerErrorException(error) + } } extractUUIDs(input: string) { @@ -540,14 +609,22 @@ export class ProblemService { ): Promise[]> { // id를 받은 workbook이 현재 접속된 group의 것인지 확인 // 아니면 에러 throw - await this.prisma.workbook.findFirstOrThrow({ + const workbook = await this.prisma.workbook.findFirst({ where: { id: workbookId, groupId } }) - const workbookProblems = await this.prisma.workbookProblem.findMany({ - where: { workbookId } - }) - return workbookProblems + if (!workbook) { + throw new EntityNotExistException('workbook') + } + + try { + const workbookProblems = await this.prisma.workbookProblem.findMany({ + where: { workbookId } + }) + return workbookProblems + } catch (error) { + throw new InternalServerErrorException(error) + } } async updateWorkbookProblemsOrder( @@ -557,9 +634,14 @@ export class ProblemService { orders: number[] ): Promise[]> { // id를 받은 workbook이 현재 접속된 group의 것인지 확인 - await this.prisma.workbook.findFirstOrThrow({ + const workbook = await this.prisma.workbook.findFirst({ where: { id: workbookId, groupId } }) + + if (!workbook) { + throw new EntityNotExistException('workbook') + } + // workbookId를 가지고 있는 workbookProblem을 모두 가져옴 const workbookProblemsToBeUpdated = await this.prisma.workbookProblem.findMany({ @@ -586,7 +668,12 @@ export class ProblemService { data: { order: newOrder } }) }) - return await this.prisma.$transaction(queries) + + try { + return await this.prisma.$transaction(queries) + } catch (error) { + throw new InternalServerErrorException(error) + } } async getContestProblems( @@ -635,7 +722,11 @@ export class ProblemService { }) }) - return await this.prisma.$transaction(queries) + try { + return await this.prisma.$transaction(queries) + } catch (error) { + throw new InternalServerErrorException(error) + } } /** From 1b503bd95218956f248aa64131c77f464df1825b Mon Sep 17 00:00:00 2001 From: mnseok kang Date: Mon, 30 Sep 2024 07:41:52 +0000 Subject: [PATCH 02/85] refactor(be): improve admin submission module error handling --- .../src/submission/submission.resolver.ts | 17 +--- .../src/submission/submission.service.ts | 98 ++++++++++--------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/apps/backend/apps/admin/src/submission/submission.resolver.ts b/apps/backend/apps/admin/src/submission/submission.resolver.ts index 91831bd8bb..ddb9cf8b95 100644 --- a/apps/backend/apps/admin/src/submission/submission.resolver.ts +++ b/apps/backend/apps/admin/src/submission/submission.resolver.ts @@ -1,4 +1,3 @@ -import { InternalServerErrorException, Logger } from '@nestjs/common' import { Args, Int, Query, Resolver } from '@nestjs/graphql' import { CursorValidationPipe } from '@libs/pipe' import { Submission } from '@admin/@generated' @@ -9,7 +8,6 @@ import { SubmissionService } from './submission.service' @Resolver(() => Submission) export class SubmissionResolver { - private readonly logger = new Logger(SubmissionResolver.name) constructor(private readonly submissionService: SubmissionService) {} /** @@ -30,16 +28,11 @@ export class SubmissionResolver { @Args('take', { nullable: true, defaultValue: 10, type: () => Int }) take: number ): Promise { - try { - return await this.submissionService.getContestSubmissions( - input, - take, - cursor - ) - } catch (error) { - this.logger.error(error.error) - throw new InternalServerErrorException() - } + return await this.submissionService.getContestSubmissions( + input, + take, + cursor + ) } /** * 특정 Contest의 특정 제출 내역에 대한 상세 정보를 불러옵니다. diff --git a/apps/backend/apps/admin/src/submission/submission.service.ts b/apps/backend/apps/admin/src/submission/submission.service.ts index 89170b9f29..9e23b5a9fd 100644 --- a/apps/backend/apps/admin/src/submission/submission.service.ts +++ b/apps/backend/apps/admin/src/submission/submission.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common' +import { Injectable, InternalServerErrorException } from '@nestjs/common' import { plainToInstance } from 'class-transformer' import { EntityNotExistException } from '@libs/exception' import { PrismaService } from '@libs/prisma' @@ -18,60 +18,64 @@ export class SubmissionService { const paginator = this.prisma.getPaginator(cursor) const { contestId, problemId } = input - const contestSubmissions = await this.prisma.submission.findMany({ - ...paginator, - take, - where: { - contestId, - problemId - }, - include: { - user: { - select: { - id: true, - username: true, - studentId: true, - userProfile: { - select: { - realName: true + try { + const contestSubmissions = await this.prisma.submission.findMany({ + ...paginator, + take, + where: { + contestId, + problemId + }, + include: { + user: { + select: { + id: true, + username: true, + studentId: true, + userProfile: { + select: { + realName: true + } } } - } - }, - problem: { - select: { - title: true, - contestProblem: { - where: { - contestId, - problemId: problemId ?? undefined + }, + problem: { + select: { + title: true, + contestProblem: { + where: { + contestId, + problemId: problemId ?? undefined + } } } } } - } - }) + }) - const results = contestSubmissions.map((c) => { - return { - title: c.problem.title, - studentId: c.user?.studentId ?? 'Unknown', - realname: c.user?.userProfile?.realName ?? 'Unknown', - username: c.user?.username ?? 'Unknown', - result: c.result as ResultStatus, - language: c.language as Language, - submissionTime: c.createTime, - codeSize: c.codeSize ?? null, - ip: c.userIp ?? 'Unknown', - id: c.id, - problemId: c.problemId, - order: c.problem.contestProblem.length - ? c.problem.contestProblem[0].order - : null - } - }) + const results = contestSubmissions.map((c) => { + return { + title: c.problem.title, + studentId: c.user?.studentId ?? 'Unknown', + realname: c.user?.userProfile?.realName ?? 'Unknown', + username: c.user?.username ?? 'Unknown', + result: c.result as ResultStatus, + language: c.language as Language, + submissionTime: c.createTime, + codeSize: c.codeSize ?? null, + ip: c.userIp ?? 'Unknown', + id: c.id, + problemId: c.problemId, + order: c.problem.contestProblem.length + ? c.problem.contestProblem[0].order + : null + } + }) - return results + return results + } catch (error) { + throw new InternalServerErrorException(error) + } } async getSubmission(id: number) { From 94a24d02f6c1a07dbb9bc89492a05ea116d09266 Mon Sep 17 00:00:00 2001 From: mnseok kang Date: Mon, 30 Sep 2024 07:45:54 +0000 Subject: [PATCH 03/85] refactor(be): improve admin problem module error handling in excluded part --- .../apps/admin/src/problem/problem.service.ts | 152 ++++++++++-------- 1 file changed, 88 insertions(+), 64 deletions(-) diff --git a/apps/backend/apps/admin/src/problem/problem.service.ts b/apps/backend/apps/admin/src/problem/problem.service.ts index 776efd854d..d5e2c2f2a4 100644 --- a/apps/backend/apps/admin/src/problem/problem.service.ts +++ b/apps/backend/apps/admin/src/problem/problem.service.ts @@ -642,34 +642,34 @@ export class ProblemService { throw new EntityNotExistException('workbook') } - // workbookId를 가지고 있는 workbookProblem을 모두 가져옴 - const workbookProblemsToBeUpdated = - await this.prisma.workbookProblem.findMany({ - where: { workbookId } - }) - // orders 길이와 찾은 workbookProblem 길이가 같은지 확인 - if (orders.length !== workbookProblemsToBeUpdated.length) { - throw new UnprocessableDataException( - 'the len of orders and the len of workbookProblem are not equal.' - ) - } - //problemId 기준으로 오름차순 정렬 - workbookProblemsToBeUpdated.sort((a, b) => a.problemId - b.problemId) - const queries = workbookProblemsToBeUpdated.map((record) => { - const newOrder = orders.indexOf(record.problemId) + 1 - return this.prisma.workbookProblem.update({ - where: { - // eslint-disable-next-line @typescript-eslint/naming-convention - workbookId_problemId: { - workbookId, - problemId: record.problemId - } - }, - data: { order: newOrder } + try { + // workbookId를 가지고 있는 workbookProblem을 모두 가져옴 + const workbookProblemsToBeUpdated = + await this.prisma.workbookProblem.findMany({ + where: { workbookId } + }) + // orders 길이와 찾은 workbookProblem 길이가 같은지 확인 + if (orders.length !== workbookProblemsToBeUpdated.length) { + throw new UnprocessableDataException( + 'the len of orders and the len of workbookProblem are not equal.' + ) + } + //problemId 기준으로 오름차순 정렬 + workbookProblemsToBeUpdated.sort((a, b) => a.problemId - b.problemId) + const queries = workbookProblemsToBeUpdated.map((record) => { + const newOrder = orders.indexOf(record.problemId) + 1 + return this.prisma.workbookProblem.update({ + where: { + // eslint-disable-next-line @typescript-eslint/naming-convention + workbookId_problemId: { + workbookId, + problemId: record.problemId + } + }, + data: { order: newOrder } + }) }) - }) - try { return await this.prisma.$transaction(queries) } catch (error) { throw new InternalServerErrorException(error) @@ -680,13 +680,22 @@ export class ProblemService { groupId: number, contestId: number ): Promise[]> { - await this.prisma.contest.findFirstOrThrow({ + await this.prisma.contest.findFirst({ where: { id: contestId, groupId } }) - const contestProblems = await this.prisma.contestProblem.findMany({ - where: { contestId } - }) - return contestProblems + + if (!contestId) { + throw new EntityNotExistException('contest') + } + + try { + const contestProblems = await this.prisma.contestProblem.findMany({ + where: { contestId } + }) + return contestProblems + } catch (error) { + throw new InternalServerErrorException(error) + } } async updateContestProblemsOrder( @@ -694,35 +703,38 @@ export class ProblemService { contestId: number, orders: number[] ): Promise[]> { - await this.prisma.contest.findFirstOrThrow({ + await this.prisma.contest.findFirst({ where: { id: contestId, groupId } }) - const contestProblems = await this.prisma.contestProblem.findMany({ - where: { contestId } - }) - - if (orders.length !== contestProblems.length) { - throw new UnprocessableDataException( - 'the length of orders and the length of contestProblem are not equal.' - ) + if (!contestId) { + throw new EntityNotExistException('contest') } - 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 - contestId_problemId: { - contestId, - problemId: record.problemId - } - }, - data: { order: newOrder } + try { + const contestProblems = await this.prisma.contestProblem.findMany({ + where: { contestId } }) - }) - try { + if (orders.length !== contestProblems.length) { + throw new UnprocessableDataException( + 'the length of orders and the length of contestProblem are not equal.' + ) + } + + 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 + contestId_problemId: { + contestId, + problemId: record.problemId + } + }, + data: { order: newOrder } + }) + }) return await this.prisma.$transaction(queries) } catch (error) { throw new InternalServerErrorException(error) @@ -770,7 +782,11 @@ export class ProblemService { }) } async getTags(): Promise[]> { - return await this.prisma.tag.findMany() + try { + return await this.prisma.tag.findMany() + } catch (error) { + throw new InternalServerErrorException(error) + } } async getTag(tagId: number) { @@ -786,19 +802,27 @@ export class ProblemService { } async getProblemTags(problemId: number) { - return await this.prisma.problemTag.findMany({ - where: { - problemId - } - }) + try { + return await this.prisma.problemTag.findMany({ + where: { + problemId + } + }) + } catch (error) { + throw new InternalServerErrorException(error) + } } async getProblemTestcases(problemId: number) { - return await this.prisma.problemTestcase.findMany({ - where: { - problemId - } - }) + try { + return await this.prisma.problemTestcase.findMany({ + where: { + problemId + } + }) + } catch (error) { + throw new InternalServerErrorException(error) + } } changeVisibleLockTimeToIsVisible( From aa72481a20f229ce0bceb30126a79b4592c6f360 Mon Sep 17 00:00:00 2001 From: Eunbi Kang Date: Fri, 20 Sep 2024 01:08:57 +0900 Subject: [PATCH 04/85] fix(fe): prevent horizontal scrollbar from appearing (#2097) * fix(fe): prevent horizontal scrollbar from appearing * chore: fix broken cover ui * chore: trigger github action --- apps/frontend/app/(main)/layout.tsx | 2 +- apps/frontend/components/Cover.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/frontend/app/(main)/layout.tsx b/apps/frontend/app/(main)/layout.tsx index fff7662ae6..848026a093 100644 --- a/apps/frontend/app/(main)/layout.tsx +++ b/apps/frontend/app/(main)/layout.tsx @@ -7,7 +7,7 @@ export default function MainLayout({ children: React.ReactNode }) { return ( -
+
{children} diff --git a/apps/frontend/components/Cover.tsx b/apps/frontend/components/Cover.tsx index 385753238c..4cf6fe992c 100644 --- a/apps/frontend/components/Cover.tsx +++ b/apps/frontend/components/Cover.tsx @@ -25,12 +25,12 @@ const icons: { [key: string]: string } = { */ export default function Cover({ title, description }: CoverProps) { return ( -
-
+
+
Date: Fri, 20 Sep 2024 16:44:12 +0900 Subject: [PATCH 05/85] fix(be): add image url for production env (#2101) --- .../apps/admin/src/problem/problem.service.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/backend/apps/admin/src/problem/problem.service.ts b/apps/backend/apps/admin/src/problem/problem.service.ts index d5e2c2f2a4..1309c39ee7 100644 --- a/apps/backend/apps/admin/src/problem/problem.service.ts +++ b/apps/backend/apps/admin/src/problem/problem.service.ts @@ -279,18 +279,19 @@ export class ProblemService { ) } - const baseUrlForImage = - this.config.get('APP_ENV') == 'stage' - ? 'https://stage.codedang.com/bucket' - : this.config.get('STORAGE_BUCKET_ENDPOINT_URL') + const APP_ENV = this.config.get('APP_ENV') + const MEDIA_BUCKET_NAME = this.config.get('MEDIA_BUCKET_NAME') + const STORAGE_BUCKET_ENDPOINT_URL = this.config.get( + 'STORAGE_BUCKET_ENDPOINT_URL' + ) return { src: - baseUrlForImage + - '/' + - this.config.get('MEDIA_BUCKET_NAME') + - '/' + - newFilename + APP_ENV === 'production' + ? `https://${MEDIA_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com/${newFilename}` + : APP_ENV === 'stage' + ? `https://stage.codedang.com/bucket/${MEDIA_BUCKET_NAME}/${newFilename}` + : `${STORAGE_BUCKET_ENDPOINT_URL}/${MEDIA_BUCKET_NAME}/${newFilename}` } } From 7c89f7c2420875671c84c918fd8936809cad3178 Mon Sep 17 00:00:00 2001 From: Eunbi Kang Date: Fri, 20 Sep 2024 21:08:11 +0900 Subject: [PATCH 06/85] refactor(fe): admin problem create page refactoring (#2100) * feat(fe): add create problem alert dialog component * feat(fe): add create problem form component * feat(fe): add confirm navigation component * chore(fe): add use client directive * refactor(fe): refactor admin problem create page * chore: use not operator * chore: trigger github action * chore(fe): use private folder --- .../admin/_components/ConfirmNavigation.tsx | 79 ++++++++- .../app/admin/_components/DescriptionForm.tsx | 2 + .../app/admin/_components/SwitchField.tsx | 2 + .../app/admin/_components/TitleForm.tsx | 2 + .../admin/problem/_components/InfoForm.tsx | 2 + .../admin/problem/_components/LimitForm.tsx | 2 + .../admin/problem/_components/VisibleForm.tsx | 2 + .../_components/CreateProblemAlertDialog.tsx | 84 +++++++++ .../create/_components/CreateProblemForm.tsx | 76 +++++++++ .../app/admin/problem/create/page.tsx | 159 ++---------------- 10 files changed, 267 insertions(+), 143 deletions(-) create mode 100644 apps/frontend/app/admin/problem/create/_components/CreateProblemAlertDialog.tsx create mode 100644 apps/frontend/app/admin/problem/create/_components/CreateProblemForm.tsx diff --git a/apps/frontend/app/admin/_components/ConfirmNavigation.tsx b/apps/frontend/app/admin/_components/ConfirmNavigation.tsx index 99bfbfe095..e005508a5f 100644 --- a/apps/frontend/app/admin/_components/ConfirmNavigation.tsx +++ b/apps/frontend/app/admin/_components/ConfirmNavigation.tsx @@ -1,6 +1,8 @@ +'use client' + import { useRouter } from 'next/navigation' -import type { MutableRefObject } from 'react' -import { useEffect } from 'react' +import type { MutableRefObject, ReactNode } from 'react' +import { createContext, useContext, useEffect, useMemo, useRef } from 'react' export const useConfirmNavigation = ( shouldSkipWarningRef: MutableRefObject @@ -35,3 +37,76 @@ export const useConfirmNavigation = ( } }, [router, shouldSkipWarningRef]) } + +interface ConfirmNavigationProps { + children: ReactNode +} + +interface Context { + setShouldSkipWarning: (value: boolean) => void +} + +const ConfirmNavigationContext = createContext(undefined) + +export default function ConfirmNavigation({ + children +}: ConfirmNavigationProps) { + const shouldSkipWarning = useRef(false) + + const router = useRouter() + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault() + event.returnValue = '' + } + + useEffect(() => { + window.addEventListener('beforeunload', handleBeforeUnload) + + const originalPush = router.push + + router.push = (href, ...args) => { + if (shouldSkipWarning.current) { + originalPush(href, ...args) + return + } + const shouldWarn = window.confirm( + 'Are you sure you want to leave this page? Changes you made may not be saved.' + ) + if (shouldWarn) { + originalPush(href, ...args) + } + } + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload) + router.push = originalPush + } + }, [router, shouldSkipWarning]) + + const contextValue = useMemo(() => { + const setShouldSkipWarning = (value: boolean) => { + shouldSkipWarning.current = value + } + return { + setShouldSkipWarning + } + }, []) + + return ( + + {children} + + ) +} + +export const useConfirmNavigationContext = () => { + const context = useContext(ConfirmNavigationContext) + + if (context === undefined) { + throw new Error( + 'useConfirmNavigationContext should used within the ConfirmNavigation component' + ) + } + + return context +} diff --git a/apps/frontend/app/admin/_components/DescriptionForm.tsx b/apps/frontend/app/admin/_components/DescriptionForm.tsx index 48ffd412bd..fd2333eedf 100644 --- a/apps/frontend/app/admin/_components/DescriptionForm.tsx +++ b/apps/frontend/app/admin/_components/DescriptionForm.tsx @@ -1,3 +1,5 @@ +'use client' + import TextEditor from '@/components/TextEditor' import { useController, useFormContext } from 'react-hook-form' import ErrorMessage from './ErrorMessage' diff --git a/apps/frontend/app/admin/_components/SwitchField.tsx b/apps/frontend/app/admin/_components/SwitchField.tsx index a8113653db..d6b229b2a6 100644 --- a/apps/frontend/app/admin/_components/SwitchField.tsx +++ b/apps/frontend/app/admin/_components/SwitchField.tsx @@ -1,3 +1,5 @@ +'use client' + import { Input } from '@/components/ui/input' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' diff --git a/apps/frontend/app/admin/_components/TitleForm.tsx b/apps/frontend/app/admin/_components/TitleForm.tsx index c5f008a073..d907ced70e 100644 --- a/apps/frontend/app/admin/_components/TitleForm.tsx +++ b/apps/frontend/app/admin/_components/TitleForm.tsx @@ -1,3 +1,5 @@ +'use client' + import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { useFormContext } from 'react-hook-form' diff --git a/apps/frontend/app/admin/problem/_components/InfoForm.tsx b/apps/frontend/app/admin/problem/_components/InfoForm.tsx index 7f8214cfcc..80e83bc594 100644 --- a/apps/frontend/app/admin/problem/_components/InfoForm.tsx +++ b/apps/frontend/app/admin/problem/_components/InfoForm.tsx @@ -1,3 +1,5 @@ +'use client' + import CheckboxSelect from '@/components/CheckboxSelect' import OptionSelect from '@/components/OptionSelect' import { languages, levels } from '@/lib/constants' diff --git a/apps/frontend/app/admin/problem/_components/LimitForm.tsx b/apps/frontend/app/admin/problem/_components/LimitForm.tsx index 434d2caa83..a850f06b1e 100644 --- a/apps/frontend/app/admin/problem/_components/LimitForm.tsx +++ b/apps/frontend/app/admin/problem/_components/LimitForm.tsx @@ -1,3 +1,5 @@ +'use client' + import { Input } from '@/components/ui/input' import { cn } from '@/lib/utils' import { useFormContext } from 'react-hook-form' diff --git a/apps/frontend/app/admin/problem/_components/VisibleForm.tsx b/apps/frontend/app/admin/problem/_components/VisibleForm.tsx index 35a7750449..b408099a48 100644 --- a/apps/frontend/app/admin/problem/_components/VisibleForm.tsx +++ b/apps/frontend/app/admin/problem/_components/VisibleForm.tsx @@ -1,3 +1,5 @@ +'use client' + import { useFormContext, useController } from 'react-hook-form' import { FaEye, FaEyeSlash } from 'react-icons/fa' import ErrorMessage from '../../_components/ErrorMessage' diff --git a/apps/frontend/app/admin/problem/create/_components/CreateProblemAlertDialog.tsx b/apps/frontend/app/admin/problem/create/_components/CreateProblemAlertDialog.tsx new file mode 100644 index 0000000000..75c8d6b37b --- /dev/null +++ b/apps/frontend/app/admin/problem/create/_components/CreateProblemAlertDialog.tsx @@ -0,0 +1,84 @@ +'use client' + +import { useConfirmNavigationContext } from '@/app/admin/_components/ConfirmNavigation' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' +import { CREATE_PROBLEM } from '@/graphql/problem/mutations' +import { useMutation } from '@apollo/client' +import type { CreateProblemInput } from '@generated/graphql' +import { useRouter } from 'next/navigation' +import { useFormContext } from 'react-hook-form' +import { toast } from 'sonner' + +interface CreateProblemAlertDialogProps { + open: boolean + onClose: () => void +} + +export default function CreateProblemAlertDialog({ + open, + onClose +}: CreateProblemAlertDialogProps) { + const { setShouldSkipWarning } = useConfirmNavigationContext() + const router = useRouter() + const methods = useFormContext() + + const [createProblem, { loading }] = useMutation(CREATE_PROBLEM, { + onError: () => { + toast.error('Failed to create problem') + }, + onCompleted: () => { + setShouldSkipWarning(true) + toast.success('Problem created successfully') + router.push('/admin/problem') + router.refresh() + } + }) + + const onSubmit = async () => { + const input = methods.getValues() + await createProblem({ + variables: { + groupId: 1, + input + } + }) + } + + return ( + + + + Create Problem? + + Once user submit any coding, the testcases{' '} + cannot be modified. + + + + + Cancel + + + + + + + + ) +} diff --git a/apps/frontend/app/admin/problem/create/_components/CreateProblemForm.tsx b/apps/frontend/app/admin/problem/create/_components/CreateProblemForm.tsx new file mode 100644 index 0000000000..56e5584744 --- /dev/null +++ b/apps/frontend/app/admin/problem/create/_components/CreateProblemForm.tsx @@ -0,0 +1,76 @@ +'use client' + +import { createSchema } from '@/app/admin/problem/utils' +import { Level, type CreateProblemInput } from '@generated/graphql' +import { zodResolver } from '@hookform/resolvers/zod' +import { useState, type ReactNode } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { CautionDialog } from '../../_components/CautionDialog' +import { validateScoreWeight } from '../../_libs/utils' +import CreateProblemAlertDialog from './CreateProblemAlertDialog' + +interface CreateProblemFormProps { + children: ReactNode +} + +export default function CreateProblemForm({ + children +}: CreateProblemFormProps) { + const methods = useForm({ + resolver: zodResolver(createSchema), + defaultValues: { + difficulty: Level.Level1, + tagIds: [], + testcases: [ + { input: '', output: '', isHidden: false, scoreWeight: null }, + { input: '', output: '', isHidden: true, scoreWeight: null } + ], + timeLimit: 2000, + memoryLimit: 512, + hint: '', + source: '', + template: [], + isVisible: true + } + }) + + const [message, setMessage] = useState('') + const [showCautionModal, setShowCautionModal] = useState(false) + const [showCreateModal, setShowCreateModal] = useState(false) + + const validate = () => { + const testcases = methods.getValues('testcases') + if (!validateScoreWeight(testcases)) { + setShowCautionModal(true) + setMessage( + 'The scoring ratios have not been specified correctly.\nPlease review and correct them.' + ) + return false + } + return true + } + + const onSubmit = methods.handleSubmit(() => { + if (!validate()) return + setShowCreateModal(true) + }) + + return ( + <> +
+ + {children} + setShowCreateModal(false)} + /> + +
+ setShowCautionModal(false)} + description={message} + /> + + ) +} diff --git a/apps/frontend/app/admin/problem/create/page.tsx b/apps/frontend/app/admin/problem/create/page.tsx index 3a4c502f7e..315bc5d7c1 100644 --- a/apps/frontend/app/admin/problem/create/page.tsx +++ b/apps/frontend/app/admin/problem/create/page.tsx @@ -1,122 +1,34 @@ -'use client' - -import { - AlertDialog, - AlertDialogContent, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogCancel, - AlertDialogAction -} from '@/components/ui/alert-dialog' import { Button } from '@/components/ui/button' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' -import { CREATE_PROBLEM } from '@/graphql/problem/mutations' -import { useMutation } from '@apollo/client' -import { Level, type CreateProblemInput } from '@generated/graphql' -import { zodResolver } from '@hookform/resolvers/zod' import Link from 'next/link' -import { useRouter } from 'next/navigation' -import { useRef, useState } from 'react' -import { useForm, FormProvider } from 'react-hook-form' import { FaAngleLeft } from 'react-icons/fa6' import { IoMdCheckmarkCircleOutline } from 'react-icons/io' -import { toast } from 'sonner' -import { useConfirmNavigation } from '../../_components/ConfirmNavigation' +import ConfirmNavigation from '../../_components/ConfirmNavigation' import DescriptionForm from '../../_components/DescriptionForm' import FormSection from '../../_components/FormSection' import SwitchField from '../../_components/SwitchField' import TitleForm from '../../_components/TitleForm' -import { CautionDialog } from '../_components/CautionDialog' import InfoForm from '../_components/InfoForm' import LimitForm from '../_components/LimitForm' import PopoverVisibleInfo from '../_components/PopoverVisibleInfo' import TemplateField from '../_components/TemplateField' import TestcaseField from '../_components/TestcaseField' import VisibleForm from '../_components/VisibleForm' -import { validateScoreWeight } from '../_libs/utils' -import { createSchema } from '../utils' +import CreateProblemForm from './_components/CreateProblemForm' export default function Page() { - const [isCreating, setIsCreating] = useState(false) - const [isDialogOpen, setDialogOpen] = useState(false) - const [dialogDescription, setDialogDescription] = useState('') - const [showCreateModal, setShowCreateModal] = useState(false) - - const shouldSkipWarning = useRef(false) - const router = useRouter() - - useConfirmNavigation(shouldSkipWarning) - - const methods = useForm({ - resolver: zodResolver(createSchema), - defaultValues: { - difficulty: Level.Level1, - tagIds: [], - testcases: [ - { input: '', output: '', isHidden: false, scoreWeight: null }, - { input: '', output: '', isHidden: true, scoreWeight: null } - ], - timeLimit: 2000, - memoryLimit: 512, - hint: '', - source: '', - template: [], - isVisible: true - } - }) - - const { handleSubmit, getValues } = methods - - const [createProblem, { error }] = useMutation(CREATE_PROBLEM) - const onSubmit = async () => { - const input = methods.getValues() - setIsCreating(true) - const testcases = getValues('testcases') - if (validateScoreWeight(testcases) === false) { - setDialogDescription( - 'The scoring ratios have not been specified correctly.\nPlease review and correct them.' - ) - setDialogOpen(true) - setIsCreating(false) - return - } - await createProblem({ - variables: { - groupId: 1, - input - } - }) - if (error) { - toast.error('Failed to create problem') - setIsCreating(false) - return - } - - shouldSkipWarning.current = true - toast.success('Problem created successfully') - router.push('/admin/problem') - router.refresh() - } - return ( - -
-
- - - - Create Problem -
- -
{ - setShowCreateModal(true) - })} - className="flex w-[760px] flex-col gap-6" - > - + + +
+
+ + + + Create Problem +
+ +
@@ -174,49 +86,14 @@ export default function Page() { - - - - Create Problem? - - Once user submit any coding, the testcases{' '} - cannot be modified. - - - - setShowCreateModal(false)} - > - Cancel - - - - - - - - - -
- - setDialogOpen(false)} - description={dialogDescription} - /> -
+ +
+ +
+ ) } From c196b163cdf3d33b6155a17b9c92d0d1f3936fc6 Mon Sep 17 00:00:00 2001 From: Jaehyeon Kim Date: Sat, 21 Sep 2024 00:24:14 +0900 Subject: [PATCH 07/85] fix(be): add condition for compile-error when run test (#2102) --- .../apps/client/src/submission/submission-sub.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/apps/client/src/submission/submission-sub.service.ts b/apps/backend/apps/client/src/submission/submission-sub.service.ts index f41e638f84..a7bf1ec79f 100644 --- a/apps/backend/apps/client/src/submission/submission-sub.service.ts +++ b/apps/backend/apps/client/src/submission/submission-sub.service.ts @@ -89,7 +89,7 @@ export class SubmissionSubscriptionService implements OnModuleInit { >(key)) ?? [] testcases.forEach((tc) => { - if (tc.id === testcaseId) { + if (!testcaseId || tc.id === testcaseId) { tc.result = status } }) From 921e9cf593eee92dbe76723c9e87cace68629d49 Mon Sep 17 00:00:00 2001 From: Kohminchae <72334086+Kohminchae@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:26:02 +0900 Subject: [PATCH 08/85] fix(fe): block testcase and limit edit when submission exist (#2084) * chore(fe): block testcase and limit edit when submission exist * chore(fe): change blockEdit as optional property --- apps/frontend/app/admin/problem/[id]/edit/page.tsx | 12 +++++++----- .../admin/problem/_components/ExampleTextarea.tsx | 6 +++++- .../app/admin/problem/_components/LimitForm.tsx | 4 +++- .../app/admin/problem/_components/TestcaseField.tsx | 4 +++- .../app/admin/problem/_components/TestcaseItem.tsx | 4 ++++ apps/frontend/app/admin/problem/page.tsx | 2 +- apps/frontend/graphql/problem/queries.ts | 1 + 7 files changed, 24 insertions(+), 9 deletions(-) diff --git a/apps/frontend/app/admin/problem/[id]/edit/page.tsx b/apps/frontend/app/admin/problem/[id]/edit/page.tsx index 3b670e930b..f7da9e2b99 100644 --- a/apps/frontend/app/admin/problem/[id]/edit/page.tsx +++ b/apps/frontend/app/admin/problem/[id]/edit/page.tsx @@ -38,13 +38,12 @@ export default function Page({ params }: { params: { id: string } }) { const methods = useForm({ resolver: zodResolver(editSchema), - defaultValues: { - template: [] - } + defaultValues: { template: [] } }) const { handleSubmit, setValue, getValues } = methods + const [blockEdit, setBlockEdit] = useState(false) const [showHint, setShowHint] = useState(false) const [showSource, setShowSource] = useState(false) const [isDialogOpen, setDialogOpen] = useState(false) @@ -57,6 +56,9 @@ export default function Page({ params }: { params: { id: string } }) { }, onCompleted: (problemData) => { const data = problemData.getProblem + + if (data.submissionCount > 0) setBlockEdit(true) + setValue('id', +id) setValue('title', data.title) setValue('isVisible', data.isVisible) @@ -193,10 +195,10 @@ export default function Page({ params }: { params: { id: string } }) {
- {getValues('testcases') && } + {getValues('testcases') && } - + onRemove()} />