diff --git a/packages/backend/src/toeic/dto/patch-quesion.dto.ts b/packages/backend/src/toeic/dto/patch-quesion.dto.ts new file mode 100644 index 0000000..c443836 --- /dev/null +++ b/packages/backend/src/toeic/dto/patch-quesion.dto.ts @@ -0,0 +1,119 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUUID, + MaxLength, + MinLength, + ValidateNested, +} from 'class-validator'; +import { + PatchQuestionToEntity, + QuestionWithId, + ToeicReqBodyProps, +} from '../toeic.interface'; + +export class PatchToeicWithQuestion implements ToeicReqBodyProps { + @ApiProperty({ + description: '토익 문제 제목입니다.', + example: '해커스 토익 실전 1회', + }) + @IsString() + @MinLength(2) + @MaxLength(32) + @IsOptional() + @Type(() => String) + title: string; + + @ApiProperty({ + description: '토익 문제 공개 여부입니다.', + }) + @IsBoolean() + @Type(() => Boolean) + @IsOptional() + is_public: boolean; + + @ApiProperty({ + description: '토익 문제입니다.', + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => PatchQuestion) + questions: PatchQuestion[]; + + toEntity(): PatchQuestionToEntity { + return { + title: this.title, + is_public: this.is_public, + questions: { + update: this.questions.map((question) => ({ + where: { id: question.id }, + data: { + question_number: question.question_number, + answer: question.answer, + content: question.content, + choice: question.choice, + translation: question.translation, + }, + })), + }, + }; + } +} + +export class PatchQuestion implements QuestionWithId { + @ApiProperty({ + description: '문제 DB 고유 ID입니다.', + example: 'uuid', + }) + @IsString() + @IsNotEmpty() + @IsUUID() + id: string; + + @ApiProperty({ + description: '문제 번호입니다.', + example: 1, + }) + @Type(() => Number) + @IsNumber() + question_number: number; + + @ApiProperty({ + description: '정답입니다.', + example: 'A', + }) + @IsString() + @MaxLength(1) + @IsOptional() + answer: string; + + @ApiProperty({ + description: '문제입니다.', + example: '문제입니다.', + }) + @IsString() + @IsOptional() + content: string; + + @ApiProperty({ + description: '선택지입니다.', + example: '(A) --- (B) --- (C) --- (D) ---.', + }) + @IsString() + @IsOptional() + choice: string; + + @ApiProperty({ + description: '문제 한글 번역입니다.', + example: '문제 한글 번역입니다.', + }) + @IsString() + @IsOptional() + translation: string; +} diff --git a/packages/backend/src/toeic/dto/req-toeic-query.dto.ts b/packages/backend/src/toeic/dto/req-toeic-query.dto.ts new file mode 100644 index 0000000..1e2e426 --- /dev/null +++ b/packages/backend/src/toeic/dto/req-toeic-query.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional, Max, Min } from 'class-validator'; +import { ToeicWhereInputProps } from '../toeic.interface'; +import { Type } from 'class-transformer'; + +export class ToeicQueryParam { + @ApiProperty({ + description: '문제 공개 여부 (0: 비공개, 1: 공개)', + required: false, + default: 1, + }) + @Min(0) + @Max(1) + @IsOptional() + @Type(() => Number) + isPublic: number; + + getQueryProps(): ToeicWhereInputProps { + return { + is_public: + typeof this.isPublic === 'undefined' ? true : Boolean(this.isPublic), + }; + } +} diff --git a/packages/backend/src/toeic/toeic.controller.ts b/packages/backend/src/toeic/toeic.controller.ts index cf7e21c..8ca4411 100644 --- a/packages/backend/src/toeic/toeic.controller.ts +++ b/packages/backend/src/toeic/toeic.controller.ts @@ -1,20 +1,80 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + ParseUUIDPipe, + Patch, + Query, +} from '@nestjs/common'; import { ToeicService } from './toeic.service'; import { ApiSwagger } from '../common/swagger/api.decorator'; +import { ResponseEntity } from '../common/entity/response.entity'; +import { PatchToeicWithQuestion } from './dto/patch-quesion.dto'; +import { ApiTags } from '@nestjs/swagger'; +import { Role } from '../auth/constant/roles.enum'; +import { Roles } from '../auth/decorator/roles.decorator'; +import { ToeicQueryParam } from './dto/req-toeic-query.dto'; +@ApiTags('토익 문제') @Controller('toeic') export class ToeicController { constructor(private readonly toeicService: ToeicService) {} @Get() @ApiSwagger({ name: '토익 문제 조회' }) - findAll() { - return this.toeicService.findAll(); + async findAll(@Query() query: ToeicQueryParam) { + const result = await this.toeicService.findAllToeic(query.getQueryProps()); + return ResponseEntity.OK_WITH( + `Successfully find ${result.length} questsions`, + result, + ); + } + + @Get('question/:uuid') + @ApiSwagger({ name: '문제 아이디로 상세 조회' }) + async findQuestionUnique(@Param('uuid', ParseUUIDPipe) uuid: string) { + const result = await this.toeicService.findQuestionUnique(uuid); + return ResponseEntity.OK_WITH( + `Successfully find question number ${result.question_number} in toeic id ${result.toeic_id}.`, + result, + ); } @Get('/:id') - @ApiSwagger({ name: '토익 문제 조회' }) - findOne(@Param('id') id: number) { - return this.toeicService.findOne(+id); + @ApiSwagger({ name: '토익 문제 상세 조회' }) + async findToeicUnique(@Param('id', ParseIntPipe) id: number) { + const result = await this.toeicService.findToeicUnique(+id); + return ResponseEntity.OK_WITH( + `Successfully find question id: ${id}.`, + result, + ); + } + + @Patch('/:id') + @Roles([Role.MANAGER]) + @ApiSwagger({ name: '토익 문제 수정' }) + updateToeicUnique( + @Param('id', ParseIntPipe) id: number, + @Body() request: PatchToeicWithQuestion, + ) { + const result = this.toeicService.updateToeicUnique(+id, request.toEntity()); + return ResponseEntity.OK_WITH( + `Successfully update question id: ${id}.`, + result, + ); + } + + @Delete('/:id') + @Roles([Role.MANAGER]) + @ApiSwagger({ name: '토익 문제 삭제' }) + deleteToeicUnique(@Param('id', ParseIntPipe) id: number) { + const result = this.toeicService.deleteToeicUnique(+id); + return ResponseEntity.OK_WITH( + `Successfully delete question id: ${id}.`, + result, + ); } } diff --git a/packages/backend/src/toeic/toeic.interface.ts b/packages/backend/src/toeic/toeic.interface.ts new file mode 100644 index 0000000..dc52287 --- /dev/null +++ b/packages/backend/src/toeic/toeic.interface.ts @@ -0,0 +1,33 @@ +import { Question } from '../upload/upload.interface'; + +export interface ToeicReqBodyProps { + title: string; + is_public: boolean; + questions: QuestionWithId[]; +} + +export interface QuestionWithId extends Question { + id: string; +} + +export interface PatchQuestionToEntity + extends Omit { + questions: { + update: { + where: { + id: string; + }; + data: { + question_number: number; + answer: string; + content: string; + choice: string; + translation: string; + }; + }[]; + }; +} + +export interface ToeicWhereInputProps { + is_public: boolean; +} diff --git a/packages/backend/src/toeic/toeic.service.ts b/packages/backend/src/toeic/toeic.service.ts index 8effcdd..f38c60d 100644 --- a/packages/backend/src/toeic/toeic.service.ts +++ b/packages/backend/src/toeic/toeic.service.ts @@ -1,29 +1,33 @@ import { Injectable } from '@nestjs/common'; -import { UploadedSheetData } from '../upload/dto/upload.dto'; +import { UploadedQuestionInSheet } from '../upload/dto/upload.dto'; import { PrismaService } from '../prisma/prisma.service'; +import { PatchQuestionToEntity, ToeicWhereInputProps } from './toeic.interface'; @Injectable() export class ToeicService { constructor(private prisma: PrismaService) {} - async create(filename: string, sheetData: UploadedSheetData) { + async createToeic( + filename: string, + questionInSheet: UploadedQuestionInSheet, + ) { return this.prisma.toeic.create({ data: { filename, - title: sheetData.title, + title: questionInSheet.title, questions: { - create: sheetData.data, + create: questionInSheet.data, }, }, }); } - async findAll() { - return this.prisma.toeic.findMany(); + async findAllToeic(where: ToeicWhereInputProps) { + return this.prisma.toeic.findMany({ where }); } - async findOne(id: number) { - return this.prisma.toeic.findUnique({ + async findToeicUnique(id: number) { + return this.prisma.toeic.findUniqueOrThrow({ where: { id, }, @@ -33,7 +37,7 @@ export class ToeicService { }); } - async deleteOne(id: number) { + async deleteToeicUnique(id: number) { return this.prisma.toeic.update({ where: { id, @@ -44,13 +48,19 @@ export class ToeicService { }); } - async updateOne(id: number) { + async updateToeicUnique(id: number, data: PatchQuestionToEntity) { return this.prisma.toeic.update({ where: { id, }, - data: { - is_public: false, + data, + }); + } + + async findQuestionUnique(uuid: string) { + return this.prisma.question.findUniqueOrThrow({ + where: { + id: uuid, }, }); } diff --git a/packages/backend/src/upload/dto/upload.dto.ts b/packages/backend/src/upload/dto/upload.dto.ts index d11fbb9..d28a807 100644 --- a/packages/backend/src/upload/dto/upload.dto.ts +++ b/packages/backend/src/upload/dto/upload.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Question, UploadedQuestionSheet } from '../upload.interface'; +import { Question, UploadedSheetProps } from '../upload.interface'; -export class UploadedSheetData implements UploadedQuestionSheet { +export class UploadedQuestionInSheet implements UploadedSheetProps { @ApiProperty({ description: '엑셀파일 시트 이름입니다.', required: true, diff --git a/packages/backend/src/upload/upload.controller.ts b/packages/backend/src/upload/upload.controller.ts index 635240f..e6278cd 100644 --- a/packages/backend/src/upload/upload.controller.ts +++ b/packages/backend/src/upload/upload.controller.ts @@ -40,7 +40,7 @@ export class UploadController { file: QuestionInFile, ) { file.sheets.map(async (sheet) => { - await this.toeicService.create(file.name, sheet); + await this.toeicService.createToeic(file.name, sheet); }); return ResponseEntity.CREATED_WITH( diff --git a/packages/backend/src/upload/upload.interface.ts b/packages/backend/src/upload/upload.interface.ts index 0a61770..e489473 100644 --- a/packages/backend/src/upload/upload.interface.ts +++ b/packages/backend/src/upload/upload.interface.ts @@ -1,6 +1,6 @@ import { Database } from 'src/supabase/schema/database.schema'; -export interface UploadedQuestionSheet { +export interface UploadedSheetProps { title: Database['public']['Tables']['toeic']['Row']['title']; data: Question[]; } @@ -17,5 +17,5 @@ export interface QuestionInFile { size: number; name: string; questionAmount: number; - sheets: UploadedQuestionSheet[]; + sheets: UploadedSheetProps[]; } diff --git a/packages/backend/src/upload/upload.service.ts b/packages/backend/src/upload/upload.service.ts index 31dab50..2ae7dc1 100644 --- a/packages/backend/src/upload/upload.service.ts +++ b/packages/backend/src/upload/upload.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; import { WorkBook, utils } from 'xlsx'; -import { UploadedSheetData } from './dto/upload.dto'; +import { UploadedQuestionInSheet } from './dto/upload.dto'; @Injectable() export class UploadService { - sheetToQuestions(workbook: WorkBook): UploadedSheetData[] { + sheetToQuestions(workbook: WorkBook): UploadedQuestionInSheet[] { return workbook.SheetNames.map( (title) => - new UploadedSheetData( + new UploadedQuestionInSheet( title, utils.sheet_to_json(workbook.Sheets[title]), ),