From eb8c18022fa288b3d1b05efc77d6e5a091e4bc54 Mon Sep 17 00:00:00 2001 From: mnseok kang Date: Tue, 12 Nov 2024 05:51:34 +0000 Subject: [PATCH] feat(be): merge client problem repository into service --- .../admin/src/problem/model/problem.input.ts | 2 +- .../apps/admin/src/problem/problem.service.ts | 37 +- .../problem/dto/code-draft.response.dto.ts | 12 +- .../src/problem/dto/problem.response.dto.ts | 80 ++- .../src/problem/dto/problems.response.dto.ts | 28 +- .../dto/related-problem.response.dto.ts | 11 +- .../dto/related-problems.response.dto.ts | 85 +-- .../client/src/problem/problem.service.ts | 525 +++++++++++++++--- 8 files changed, 589 insertions(+), 191 deletions(-) diff --git a/apps/backend/apps/admin/src/problem/model/problem.input.ts b/apps/backend/apps/admin/src/problem/model/problem.input.ts index c4007228b5..54a0dc9017 100644 --- a/apps/backend/apps/admin/src/problem/model/problem.input.ts +++ b/apps/backend/apps/admin/src/problem/model/problem.input.ts @@ -1,5 +1,5 @@ import { Field, InputType, Int } from '@nestjs/graphql' -import { Language, Level } from '@generated' +import { Language, Level } from '@prisma/client' import { ValidatePromise } from 'class-validator' import { GraphQLUpload } from 'graphql-upload' import type { FileUploadDto } from '../dto/file-upload.dto' diff --git a/apps/backend/apps/admin/src/problem/problem.service.ts b/apps/backend/apps/admin/src/problem/problem.service.ts index 16dfe025d7..bcb5b1de19 100644 --- a/apps/backend/apps/admin/src/problem/problem.service.ts +++ b/apps/backend/apps/admin/src/problem/problem.service.ts @@ -1,16 +1,16 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager' import { Cache } from '@nestjs/cache-manager' -import { - Inject, - Injectable, - InternalServerErrorException -} from '@nestjs/common' +import { Inject, Injectable } from '@nestjs/common' import { ConfigService } from '@nestjs/config' -import { Language } from '@generated' -import type { ContestProblem, Problem, Tag, WorkbookProblem } from '@generated' -import { Level } from '@generated' -import type { ProblemWhereInput } from '@generated' -import { Prisma } from '@prisma/client' +import { + Language, + Level, + Prisma, + type ContestProblem, + type Problem, + type Tag, + type WorkbookProblem +} from '@prisma/client' import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library' import { randomUUID } from 'crypto' import { Workbook } from 'exceljs' @@ -23,6 +23,7 @@ import { UnprocessableFileDataException } from '@libs/exception' import { PrismaService } from '@libs/prisma' +import type { ProblemWhereInput } from '@admin/@generated' import { StorageService } from '@admin/storage/storage.service' import { ImportedProblemHeader } from './model/problem.constants' import type { @@ -56,10 +57,12 @@ export class ProblemService { 'A problem should support at least one language' ) } - template.forEach((template) => { - if (!languages.includes(template.language)) { + + // Check if the problem supports the language in the template + template.forEach((template: Template) => { + if (!languages.includes(template.language as Language)) { throw new UnprocessableDataException( - `This problem does not support ${template.language}` + `This problem does not support ${template.language as Language}` ) } }) @@ -397,9 +400,9 @@ export class ProblemService { } const supportedLangs = languages ?? problem.languages template?.forEach((template) => { - if (!supportedLangs.includes(template.language)) { + if (!supportedLangs.includes(template.language as Language)) { throw new UnprocessableDataException( - `This problem does not support ${template.language}` + `This problem does not support ${template.language as Language}` ) } }) @@ -646,7 +649,7 @@ export class ProblemService { * @throws DuplicateFoundException - 이미 존재하는 태그일 경우 */ async createTag(tagName: string): Promise { - // throw error if tag already exists + // 존재하는 태그일 경우 에러를 throw합니다 try { return await this.prisma.tag.create({ data: { @@ -660,7 +663,7 @@ export class ProblemService { ) throw new DuplicateFoundException('tag') - throw new InternalServerErrorException(error) + throw error } } diff --git a/apps/backend/apps/client/src/problem/dto/code-draft.response.dto.ts b/apps/backend/apps/client/src/problem/dto/code-draft.response.dto.ts index 7d98b29883..3ecdd507d8 100644 --- a/apps/backend/apps/client/src/problem/dto/code-draft.response.dto.ts +++ b/apps/backend/apps/client/src/problem/dto/code-draft.response.dto.ts @@ -1,9 +1,9 @@ -import type { CreateTemplateDto } from './create-code-draft.dto' +import type { JsonValue } from '@prisma/client/runtime/library' export class CodeDraftResponseDto { - userId: string - problemId: string - template: CreateTemplateDto - createTime: string - updateTime: string + userId: number + problemId: number + template: JsonValue + createTime: Date + updateTime: Date } diff --git a/apps/backend/apps/client/src/problem/dto/problem.response.dto.ts b/apps/backend/apps/client/src/problem/dto/problem.response.dto.ts index 2eb2a6edf9..b47ddb323d 100644 --- a/apps/backend/apps/client/src/problem/dto/problem.response.dto.ts +++ b/apps/backend/apps/client/src/problem/dto/problem.response.dto.ts @@ -1,33 +1,53 @@ -import { - type Language, - Level, - type ProblemTestcase, - type Tag -} from '@prisma/client' -import { Exclude, Expose } from 'class-transformer' +import type { Language, Level, ProblemTestcase, Tag } from '@prisma/client' +import type { JsonValue } from '@prisma/client/runtime/library' -@Exclude() export class ProblemResponseDto { - @Expose() id: number - @Expose() title: string - @Expose() description: string - @Expose() inputDescription: string - @Expose() outputDescription: string - @Expose() hint: string - @Expose() engTitle: string - @Expose() engDescription: string - @Expose() engInputDescription: string - @Expose() engOutputDescription: string - @Expose() engHint: string - @Expose() languages: Language[] - @Expose() timeLimit: number - @Expose() memoryLimit: number - @Expose() difficulty: Level - @Expose() source: string[] - @Expose() submissionCount: number - @Expose() acceptedCount: number - @Expose() acceptedRate: number - @Expose() tags: Partial[] - @Expose() template: JSON[] - @Expose() problemTestcase: Pick[] + id: number + title: string + description: string + inputDescription: string + outputDescription: string + hint: string + engTitle: string | null + engDescription: string | null + engInputDescription: string | null + engOutputDescription: string | null + engHint: string | null + languages: Language[] + timeLimit: number + memoryLimit: number + difficulty: Level + source: string + submissionCount: number + acceptedCount: number + acceptedRate: number + tags: Partial[] + template: JsonValue[] + problemTestcase: Pick[] } + +// @Exclude() +// export class ProblemResponseDto { +// @Expose() id: number +// @Expose() title: string +// @Expose() description: string +// @Expose() inputDescription: string +// @Expose() outputDescription: string +// @Expose() hint: string +// @Expose() engTitle: string +// @Expose() engDescription: string +// @Expose() engInputDescription: string +// @Expose() engOutputDescription: string +// @Expose() engHint: string +// @Expose() languages: Language[] +// @Expose() timeLimit: number +// @Expose() memoryLimit: number +// @Expose() difficulty: Level +// @Expose() source: string[] +// @Expose() submissionCount: number +// @Expose() acceptedCount: number +// @Expose() acceptedRate: number +// @Expose() tags: Partial[] +// @Expose() template: JSON[] +// @Expose() problemTestcase: Pick[] +// } diff --git a/apps/backend/apps/client/src/problem/dto/problems.response.dto.ts b/apps/backend/apps/client/src/problem/dto/problems.response.dto.ts index 6251c6f8b6..f908340ab6 100644 --- a/apps/backend/apps/client/src/problem/dto/problems.response.dto.ts +++ b/apps/backend/apps/client/src/problem/dto/problems.response.dto.ts @@ -1,42 +1,18 @@ -import { Level, type Language, type Tag } from '@prisma/client' -import { Exclude, Expose, Type } from 'class-transformer' +import type { Language, Level, Tag } from '@prisma/client' -@Exclude() export class ProblemsResponseDto { - @Expose() - @Type(() => Problem) data: Problem[] - - @Expose() total: number } -@Exclude() class Problem { - @Expose() id: number - - @Expose() title: string - - @Expose() - engTitle: string - - @Expose() + engTitle: string | null difficulty: Level - - @Expose() submissionCount: number - - @Expose() acceptedRate: number - - @Expose() tags: Partial[] - - @Expose() languages: Language[] - - @Expose() hasPassed: boolean | null } diff --git a/apps/backend/apps/client/src/problem/dto/related-problem.response.dto.ts b/apps/backend/apps/client/src/problem/dto/related-problem.response.dto.ts index dbe2fc72ea..8db852affb 100644 --- a/apps/backend/apps/client/src/problem/dto/related-problem.response.dto.ts +++ b/apps/backend/apps/client/src/problem/dto/related-problem.response.dto.ts @@ -1,13 +1,6 @@ -import { Problem } from '@prisma/client' -import { Exclude, Expose, Type } from 'class-transformer' -import { ProblemResponseDto } from './problem.response.dto' +import type { ProblemResponseDto } from './problem.response.dto' -@Exclude() export class RelatedProblemResponseDto { - @Expose() order: number - - @Expose() - @Type(() => ProblemResponseDto) - problem: Problem + problem: ProblemResponseDto } diff --git a/apps/backend/apps/client/src/problem/dto/related-problems.response.dto.ts b/apps/backend/apps/client/src/problem/dto/related-problems.response.dto.ts index c517f5bbcc..dbb39f5ad3 100644 --- a/apps/backend/apps/client/src/problem/dto/related-problems.response.dto.ts +++ b/apps/backend/apps/client/src/problem/dto/related-problems.response.dto.ts @@ -1,51 +1,66 @@ -import { Level } from '@prisma/client' -import { Exclude, Expose, Transform, Type } from 'class-transformer' -import { IsOptional } from 'class-validator' +import type { Level } from '@prisma/client' -@Exclude() export class RelatedProblemsResponseDto { - @Expose() - @Type(() => Problem) data: Problem[] - - @Expose() total: number } -@Exclude() +// @Exclude() +// export class RelatedProblemsResponseDto { +// @Expose() +// @Type(() => Problem) +// data: Problem[] + +// @Expose() +// total: number +// } + class Problem { - @Expose() order: number - - @Expose() - @Transform(({ obj }) => obj.problem.id, { toClassOnly: true }) id: number - - @Expose() - @Transform(({ obj }) => obj.problem.title, { toClassOnly: true }) - title: number - - @Expose() - @Transform(({ obj }) => obj.problem.difficulty, { toClassOnly: true }) + title: string difficulty: Level - - @Expose() - @Transform(({ obj }) => obj.problem.submissionCount, { toClassOnly: true }) submissionCount: number - - @Expose() - @Transform(({ obj }) => obj.problem.acceptedRate, { toClassOnly: true }) acceptedRate: number - - @Expose() - @IsOptional() maxScore: number | null - - @Expose() - @IsOptional() score: string | null - - @Expose() - @IsOptional() submissionTime: Date | null } + +// @Exclude() +// class Problem { +// @Expose() +// order: number + +// @Expose() +// @Transform(({ obj }) => obj.problem.id, { toClassOnly: true }) +// id: number + +// @Expose() +// @Transform(({ obj }) => obj.problem.title, { toClassOnly: true }) +// title: number + +// @Expose() +// @Transform(({ obj }) => obj.problem.difficulty, { toClassOnly: true }) +// difficulty: Level + +// @Expose() +// @Transform(({ obj }) => obj.problem.submissionCount, { toClassOnly: true }) +// submissionCount: number + +// @Expose() +// @Transform(({ obj }) => obj.problem.acceptedRate, { toClassOnly: true }) +// acceptedRate: number + +// @Expose() +// @IsOptional() +// maxScore: number | null + +// @Expose() +// @IsOptional() +// score: string | null + +// @Expose() +// @IsOptional() +// submissionTime: Date | null +// } diff --git a/apps/backend/apps/client/src/problem/problem.service.ts b/apps/backend/apps/client/src/problem/problem.service.ts index 461d47ff48..05ce11ab83 100644 --- a/apps/backend/apps/client/src/problem/problem.service.ts +++ b/apps/backend/apps/client/src/problem/problem.service.ts @@ -1,8 +1,17 @@ import { Injectable } from '@nestjs/common' -import { plainToInstance } from 'class-transformer' -import { OPEN_SPACE_ID } from '@libs/constants' -import { ForbiddenAccessException } from '@libs/exception' +import { Prisma, ResultStatus } from '@prisma/client' +import { MIN_DATE, OPEN_SPACE_ID } from '@libs/constants' +import { + ConflictFoundException, + EntityNotExistException, + ForbiddenAccessException, + UnprocessableDataException +} from '@libs/exception' import { PrismaService } from '@libs/prisma' +import type { + CodeDraftCreateInput, + CodeDraftUpdateInput +} from '@admin/@generated' import { ContestService } from '@client/contest/contest.service' import { WorkbookService } from '@client/workbook/workbook.service' import { CodeDraftResponseDto } from './dto/code-draft.response.dto' @@ -12,11 +21,54 @@ import { ProblemsResponseDto } from './dto/problems.response.dto' import { RelatedProblemResponseDto } from './dto/related-problem.response.dto' import { RelatedProblemsResponseDto } from './dto/related-problems.response.dto' import { ProblemOrder } from './enum/problem-order.enum' -import { ProblemRepository } from './problem.repository' +const problemsSelectOption: Prisma.ProblemSelect = { + id: true, + title: true, + engTitle: true, + difficulty: true, + acceptedRate: true, + submissionCount: true, + languages: true +} + +const problemSelectOption: Prisma.ProblemSelect = { + ...problemsSelectOption, + description: true, + inputDescription: true, + outputDescription: true, + hint: true, + engDescription: true, + engInputDescription: true, + engOutputDescription: true, + engHint: true, + timeLimit: true, + memoryLimit: true, + source: true, + acceptedCount: true, + template: true, + problemTestcase: { + where: { + isHidden: false + }, + select: { + id: true, + input: true, + output: true + } + } +} + +const codeDraftSelectOption = { + userId: true, + problemId: true, + template: true, + createTime: true, + updateTime: true +} @Injectable() export class ProblemService { - constructor(private readonly problemRepository: ProblemRepository) {} + constructor(private readonly prisma: PrismaService) {} async getProblems(options: { userId: number | null @@ -25,9 +77,58 @@ export class ProblemService { groupId: number order?: ProblemOrder search?: string - }) { + }): Promise { + const { cursor, take, order, groupId, search } = options + const paginator = this.prisma.getPaginator(cursor) + + /* eslint-disable @typescript-eslint/naming-convention */ + const orderByMapper: Record< + ProblemOrder, + Prisma.ProblemOrderByWithRelationInput[] + > = { + 'id-asc': [{ id: 'asc' }], + 'id-desc': [{ id: 'desc' }], + 'title-asc': [{ title: 'asc' }, { id: 'asc' }], + 'title-desc': [{ title: 'desc' }, { id: 'asc' }], + 'level-asc': [{ difficulty: 'asc' }, { id: 'asc' }], + 'level-desc': [{ difficulty: 'desc' }, { id: 'asc' }], + 'acrate-asc': [{ acceptedRate: 'asc' }, { id: 'asc' }], + 'acrate-desc': [{ acceptedRate: 'desc' }, { id: 'asc' }], + 'submit-asc': [{ submissionCount: 'asc' }, { id: 'asc' }], + 'submit-desc': [{ submissionCount: 'desc' }, { id: 'asc' }] + } + /* eslint-enable @typescript-eslint/naming-convention */ + + const orderBy = orderByMapper[order ?? 'id-asc'] + const unprocessedProblems = - await this.problemRepository.getProblems(options) + // await this.problemRepository.getProblems(options) + await this.prisma.problem.findMany({ + ...paginator, + take, + orderBy, + where: { + groupId, + title: { + // TODO/FIXME: postgreSQL의 full text search를 사용하여 검색하려 했으나 + // 그럴 경우 띄어쓰기를 기준으로 나눠진 단어 단위로만 검색이 가능하다 + // ex) "hello world"를 검색하면 "hello"와 "world"로 검색이 된다. + // 글자 단위로 검색하기 위해서, 성능을 희생하더라도 contains를 사용하여 구현했다. + // 추후에 검색 성능을 개선할 수 있는 방법을 찾아보자 + // 아니면 텍스트가 많은 field에서는 full-text search를 사용하고, 텍스트가 적은 field에서는 contains를 사용하는 방법도 고려해보자. + contains: search + }, + visibleLockTime: MIN_DATE + }, + select: { + ...problemsSelectOption, + problemTag: { + select: { + tagId: true + } + } + } + }) const uniqueTagIds = new Set( unprocessedProblems.flatMap((item) => { @@ -35,7 +136,17 @@ export class ProblemService { }) ) const tagIds = [...uniqueTagIds] - const tagList = await this.problemRepository.getProblemsTags(tagIds) + const tagList = await this.prisma.tag.findMany({ + where: { + id: { + in: tagIds + } + }, + select: { + id: true, + name: true + } + }) const problems = unprocessedProblems.map(async (problem) => { let hasPassed: boolean | null = null @@ -43,10 +154,24 @@ export class ProblemService { const problemTags = problemTag.map((tag) => tag.tagId) const tags = tagList.filter((tagItem) => problemTags.includes(tagItem.id)) if (options.userId) { - hasPassed = await this.problemRepository.hasPassedProblem( - options.userId, - { problemId: problem.id } - ) + // hasPassed = await this.problemRepository.hasPassedProblem( + // options.userId, + // { problemId: problem.id } + // ) + + const submissions = await this.prisma.submission.findMany({ + where: { + problemId: problem.id, + userId: options.userId + }, + select: { result: true } + }) + + hasPassed = submissions.length + ? submissions.some( + (submission) => submission.result === ResultStatus.Accepted + ) + : null } return { ...data, @@ -55,21 +180,68 @@ export class ProblemService { } }) - const total = await this.problemRepository.getProblemTotalCount( - options.groupId, - options.search - ) + // const total = await this.problemRepository.getProblemTotalCount( + // options.groupId, + // options.search + // ) - return plainToInstance(ProblemsResponseDto, { + const total = await this.prisma.problem.count({ + where: { + groupId, + title: { + // TODO: 검색 방식 변경 시 함께 변경 요함 + contains: search + }, + visibleLockTime: MIN_DATE + } + }) + + // return plainToInstance(ProblemsResponseDto, { + // data: await Promise.all(problems), + // total + // }) + return { data: await Promise.all(problems), total - }) + } } - async getProblem(problemId: number, groupId = OPEN_SPACE_ID) { - const data = await this.problemRepository.getProblem(problemId, groupId) - const tags = await this.problemRepository.getProblemTags(problemId) - return plainToInstance(ProblemResponseDto, { ...data, tags }) + async getProblem( + problemId: number, + groupId = OPEN_SPACE_ID + ): Promise { + // const data = await this.problemRepository.getProblem(problemId, groupId) + const data = await this.prisma.problem.findUniqueOrThrow({ + where: { + id: problemId, + groupId, + visibleLockTime: MIN_DATE + }, + select: problemSelectOption + }) + + // const tags = await this.problemRepository.getProblemTags(problemId) + const tags = ( + await this.prisma.problemTag.findMany({ + where: { + problemId + }, + select: { + tag: { + select: { + id: true, + name: true + } + } + } + }) + ).map((tag) => tag.tag) + + // return plainToInstance(ProblemResponseDto, { ...data, tags }) + return { + ...data, + tags + } } } @@ -77,7 +249,6 @@ export class ProblemService { export class ContestProblemService { constructor( private readonly prisma: PrismaService, - private readonly problemRepository: ProblemRepository, private readonly contestService: ContestService ) {} @@ -87,7 +258,7 @@ export class ContestProblemService { cursor: number | null, take: number, groupId = OPEN_SPACE_ID - ) { + ): Promise { const contest = await this.contestService.getContest( contestId, groupId, @@ -104,8 +275,32 @@ export class ContestProblemService { ) } + const paginator = this.prisma.getPaginator(cursor, (value) => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + contestId_problemId: { + contestId, + problemId: value + } + })) + const [contestProblems, submissions] = await Promise.all([ - this.problemRepository.getContestProblems(contestId, cursor, take), + // this.problemRepository.getContestProblems(contestId, cursor, take), + this.prisma.contestProblem.findMany({ + ...paginator, + take, + orderBy: { order: 'asc' }, + where: { + contestId + }, + select: { + order: true, + problem: { + select: problemsSelectOption + }, + problemId: true, + score: true + } + }), this.prisma.submission.findMany({ where: { userId, @@ -130,17 +325,29 @@ export class ContestProblemService { } const contestProblemsWithScore = contestProblems.map((contestProblem) => { - const submission = submissionMap.get(contestProblem.problemId) + const { problemId, problem, order } = contestProblem + const submission = submissionMap.get(problemId) if (!submission) { return { - ...contestProblem, + order, + id: problem.id, + title: problem.title, + difficulty: problem.difficulty, + submissionCount: problem.submissionCount, + acceptedRate: problem.acceptedRate, maxScore: contest.isJudgeResultVisible ? contestProblem.score : null, score: null, submissionTime: null } } return { - ...contestProblem, + // ...contestProblem, + order, + id: contestProblem.problem.id, + title: contestProblem.problem.title, + difficulty: contestProblem.problem.difficulty, + submissionCount: contestProblem.problem.submissionCount, + acceptedRate: contestProblem.problem.acceptedRate, maxScore: contest.isJudgeResultVisible ? contestProblem.score : null, score: contest.isJudgeResultVisible ? ((submission.score * contestProblem.score) / 100).toFixed(0) @@ -149,13 +356,22 @@ export class ContestProblemService { } }) - const total = - await this.problemRepository.getContestProblemTotalCount(contestId) + // const total = + // await this.problemRepository.getContestProblemTotalCount(contestId) + const total = await this.prisma.contestProblem.count({ + where: { + contestId + } + }) - return plainToInstance(RelatedProblemsResponseDto, { + // return plainToInstance(RelatedProblemsResponseDto, { + // data: contestProblemsWithScore, + // total + // }) + return { data: contestProblemsWithScore, total - }) + } } async getContestProblem( @@ -163,7 +379,7 @@ export class ContestProblemService { problemId: number, userId: number, groupId = OPEN_SPACE_ID - ) { + ): Promise { const contest = await this.contestService.getContest( contestId, groupId, @@ -178,18 +394,58 @@ export class ContestProblemService { throw new ForbiddenAccessException('Register to access this problem.') } - const data = await this.problemRepository.getContestProblem( - contestId, - problemId - ) - return plainToInstance(RelatedProblemResponseDto, data) + // const data = await this.problemRepository.getContestProblem( + // contestId, + // problemId + // ) + + const data = await this.prisma.contestProblem.findUniqueOrThrow({ + where: { + // eslint-disable-next-line @typescript-eslint/naming-convention + contestId_problemId: { + contestId, + problemId + } + }, + select: { + order: true, + problem: { + select: problemSelectOption + } + } + }) + + const tags = ( + await this.prisma.problemTag.findMany({ + where: { + problemId + }, + select: { + tag: { + select: { + id: true, + name: true + } + } + } + }) + ).map((tag) => tag.tag) + + // return plainToInstance(RelatedProblemResponseDto, data) + return { + order: data.order, + problem: { + ...data.problem, + tags + } + } } } @Injectable() export class WorkbookProblemService { constructor( - private readonly problemRepository: ProblemRepository, + private readonly prisma: PrismaService, private readonly workbookService: WorkbookService ) {} @@ -198,66 +454,201 @@ export class WorkbookProblemService { cursor: number | null, take: number, groupId = OPEN_SPACE_ID - ) { + ): Promise { const isVisible = await this.workbookService.isVisible(workbookId, groupId) if (!isVisible) { throw new ForbiddenAccessException( 'You do not have access to this workbook.' ) } - const data = await this.problemRepository.getWorkbookProblems( - workbookId, - cursor, - take - ) + // const data = await this.problemRepository.getWorkbookProblems( + // workbookId, + // cursor, + // take + // ) - const total = - await this.problemRepository.getWorkbookProblemTotalCount(workbookId) + const paginator = this.prisma.getPaginator(cursor, (value) => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + workbookId_problemId: { + workbookId, + problemId: value + } + })) - return plainToInstance(RelatedProblemsResponseDto, { - data, - total + const data = await this.prisma.workbookProblem.findMany({ + ...paginator, + take, + where: { + workbookId, + problem: { + visibleLockTime: MIN_DATE + } + }, + select: { + order: true, + problem: { + select: problemsSelectOption + } + } + }) + + // const total = + // await this.problemRepository.getWorkbookProblemTotalCount(workbookId) + + const total = await this.prisma.workbookProblem.count({ + where: { + workbookId, + problem: { + visibleLockTime: MIN_DATE + } + } }) + + // return plainToInstance(RelatedProblemsResponseDto, { + // data, + // total + // }) + + return { + data: data.map((item) => ({ + order: item.order, + maxScore: null, + score: null, + submissionTime: null, + ...item.problem + })), + total + } } async getWorkbookProblem( workbookId: number, problemId: number, groupId = OPEN_SPACE_ID - ) { + ): Promise { const isVisible = await this.workbookService.isVisible(workbookId, groupId) if (!isVisible) { throw new ForbiddenAccessException( 'You do not have access to this workbook.' ) } - const data = await this.problemRepository.getWorkbookProblem( - workbookId, - problemId - ) - return plainToInstance(RelatedProblemResponseDto, data) + // const data = await this.problemRepository.getWorkbookProblem( + // workbookId, + // problemId + // ) + + const data = await this.prisma.workbookProblem.findUniqueOrThrow({ + where: { + // eslint-disable-next-line @typescript-eslint/naming-convention + workbookId_problemId: { + workbookId, + problemId + }, + problem: { + visibleLockTime: MIN_DATE + } + }, + select: { + order: true, + problem: { + select: problemSelectOption + } + } + }) + + const tags = ( + await this.prisma.problemTag.findMany({ + where: { + problemId + }, + select: { + tag: { + select: { + id: true, + name: true + } + } + } + }) + ).map((tag) => tag.tag) + + // return plainToInstance(RelatedProblemResponseDto, data) + return { + order: data.order, + problem: { + ...data.problem, + tags + } + } } } @Injectable() export class CodeDraftService { - constructor(private readonly problemRepository: ProblemRepository) {} + constructor(private readonly prisma: PrismaService) {} - async getCodeDraft(userId: number, problemId: number) { - const data = await this.problemRepository.getCodeDraft(userId, problemId) - return plainToInstance(CodeDraftResponseDto, data) + async getCodeDraft( + userId: number, + problemId: number + ): Promise { + // const data = await this.problemRepository.getCodeDraft(userId, problemId) + const data = await this.prisma.codeDraft.findUniqueOrThrow({ + where: { + codeDraftId: { + userId, + problemId + } + }, + select: codeDraftSelectOption + }) + // return plainToInstance(CodeDraftResponseDto, data) + return data } async upsertCodeDraft( userId: number, problemId: number, createTemplateDto: CreateTemplateDto - ) { - const data = await this.problemRepository.upsertCodeDraft( - userId, - problemId, - createTemplateDto - ) - return plainToInstance(CodeDraftResponseDto, data) + ): Promise { + // const data = await this.problemRepository.upsertCodeDraft( + // userId, + // problemId, + // createTemplateDto + // ) + let data + try { + data = await this.prisma.codeDraft.upsert({ + where: { + codeDraftId: { + userId, + problemId + } + }, + update: { + template: + createTemplateDto.template as CodeDraftUpdateInput['template'] + }, + create: { + userId, + problemId, + template: + createTemplateDto.template as CodeDraftCreateInput['template'] + }, + select: codeDraftSelectOption + }) + } catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === 'P2002') { + throw new ConflictFoundException('CodeDraft already exists.') + } else if (error.code === 'P2003') { + throw new EntityNotExistException('User or Problem') + } else { + throw new UnprocessableDataException('Invalid data provided.') + } + } + throw error + } + // return plainToInstance(CodeDraftResponseDto, data) + return data } }