Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge: 토익 문제 수정 및 삭제 적용 #43

Merged
merged 8 commits into from
Feb 25, 2024
119 changes: 119 additions & 0 deletions packages/backend/src/toeic/dto/patch-quesion.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
24 changes: 24 additions & 0 deletions packages/backend/src/toeic/dto/req-toeic-query.dto.ts
Original file line number Diff line number Diff line change
@@ -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),
};
}
}
72 changes: 66 additions & 6 deletions packages/backend/src/toeic/toeic.controller.ts
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
33 changes: 33 additions & 0 deletions packages/backend/src/toeic/toeic.interface.ts
Original file line number Diff line number Diff line change
@@ -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<ToeicReqBodyProps, 'questions'> {
questions: {
update: {
where: {
id: string;
};
data: {
question_number: number;
answer: string;
content: string;
choice: string;
translation: string;
};
}[];
};
}

export interface ToeicWhereInputProps {
is_public: boolean;
}
34 changes: 22 additions & 12 deletions packages/backend/src/toeic/toeic.service.ts
Original file line number Diff line number Diff line change
@@ -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,
},
Expand All @@ -33,7 +37,7 @@ export class ToeicService {
});
}

async deleteOne(id: number) {
async deleteToeicUnique(id: number) {
return this.prisma.toeic.update({
where: {
id,
Expand All @@ -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,
},
});
}
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/upload/dto/upload.dto.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/upload/upload.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/src/upload/upload.interface.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
Expand All @@ -17,5 +17,5 @@ export interface QuestionInFile {
size: number;
name: string;
questionAmount: number;
sheets: UploadedQuestionSheet[];
sheets: UploadedSheetProps[];
}
6 changes: 3 additions & 3 deletions packages/backend/src/upload/upload.service.ts
Original file line number Diff line number Diff line change
@@ -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]),
),
Expand Down