Skip to content

Commit

Permalink
[BE - #38] 퀴즈 관련 도메인 구축 및 생성 API 구현 (#42)
Browse files Browse the repository at this point in the history
* feat: quiz controller, service 파일 초기 설정

* feat: class entity, requestDTO 구현

* feat: quiz entity, requestDTO 구현

* feat: quiz Controller, Service 구현

* refactor: Entity Class 네이밍 테이블 명으로 수정

* feat: quiz, class repository 구현

* feat: class, quiz, choice nested DTO 객체로 구성

* feat: choice entity 구현

* feat: choice repository 구현

* feat: repository 상속-> @InjectRepository 수정

* feat: app, quiz module 연결

* feat: quiz controller, service 구현

* feat: controller, service 퀴즈 생성 API 구현

* feat:  퀴즈 생성 API 관련 DTO 구현

* feat:  퀴즈 생성 API 관련 entity, repository 구현

* feat: 기본 responseDto 구현

* refactor: 클래스, 퀴즈 생성 api를 restful하게 수정

* refactor: Entity 저장 시에 객체 구조 분해 할당으로 가독성 증가
  • Loading branch information
glaxyt authored Nov 11, 2024
1 parent 1d7cebd commit 2575441
Show file tree
Hide file tree
Showing 15 changed files with 404 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/BE/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { MysqlConfigModule } from './config/database/mysql/configuration.module';
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { UserModule } from './module/user/user.module';
import { QuizModule } from './module/quiz/quiz.module';

@Module({
imports: [
UserModule,
QuizModule,
MysqlConfigModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
Expand Down
30 changes: 30 additions & 0 deletions packages/BE/src/module/quiz/quiz.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Quiz } from './quizzes/entities/quiz.entity';
import { Choice } from './quizzes/entities/choice.entity';
import { Class } from './quizzes/entities/class.entity';
import { QuizRepository } from './quizzes/repositories/quiz.repository';
import { ChoiceRepository } from './quizzes/repositories/choice.repository';
import { ClassRepository } from './quizzes/repositories/class.repository';
import { QuizService } from './quizzes/quiz.service';
import { QuizController } from './quizzes/quiz.controller';


@Module({
imports: [
TypeOrmModule.forFeature([Quiz, Choice, Class])
],
controllers: [QuizController],
providers: [
QuizService,
QuizRepository,
ChoiceRepository,
ClassRepository
],
exports: [
QuizRepository,
ChoiceRepository,
ClassRepository
]
})
export class QuizModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { IsNotEmpty, IsString, IsBoolean, IsNumber } from 'class-validator';

export class CreateChoiceRequestDto {
@IsNumber()
@IsNotEmpty()
position: number;

@IsString()
@IsNotEmpty()
content: string;

@IsBoolean()
@IsNotEmpty()
isCorrect: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class CreateClassRequestDto {
@IsString()
@IsNotEmpty()
title: string;

@IsString()
@IsNotEmpty()
description: string;
}
34 changes: 34 additions & 0 deletions packages/BE/src/module/quiz/quizzes/dto/create-quiz.request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { IsNotEmpty, IsString, IsNumber, IsArray, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
import { CreateChoiceRequestDto } from "./create-choice.request.dto";

export class CreateQuizRequestDto {
@IsNumber()
@IsNotEmpty()
position: number;

@IsString()
@IsNotEmpty()
content: string;

@IsNumber()
@IsNotEmpty()
timeLimit: number;

@IsNumber()
@IsNotEmpty()
point: number;

@IsString()
@IsNotEmpty()
questionType: string;

// @IsNumber()
// @IsNotEmpty()
// position: number;

@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateChoiceRequestDto)
choices: CreateChoiceRequestDto[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { IsNumber, IsNotEmpty, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { CreateQuizRequestDto } from './create-quiz.request.dto';

export class CreateQuizListRequestDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateQuizRequestDto)
quizzes: CreateQuizRequestDto[];
}
19 changes: 19 additions & 0 deletions packages/BE/src/module/quiz/quizzes/entities/choice.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Choice {
@PrimaryGeneratedColumn()
id: number;

@Column()
quiz_id: number;

@Column()
position: number;

@Column()
content: string;

@Column()
is_correct: boolean;
}
19 changes: 19 additions & 0 deletions packages/BE/src/module/quiz/quizzes/entities/class.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Class {
@PrimaryGeneratedColumn()
id: number;

@Column()
creator_id: number;

@Column()
title: string;

@Column()
description: string;

@Column()
created_at: Date;
}
28 changes: 28 additions & 0 deletions packages/BE/src/module/quiz/quizzes/entities/quiz.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Quiz {
@PrimaryGeneratedColumn()
id: number;

@Column()
class_id: number;

@Column()
position: number

@Column()
content: string;

@Column()
time_limit: number;

@Column()
point: number;

@Column()
question_type: string; // 퀴즈 타입에 따른 구분이 가능한 String Enum 혹은 정수로 해도 좋을 것 같음

// @Column()
// position: number;
}
30 changes: 30 additions & 0 deletions packages/BE/src/module/quiz/quizzes/quiz.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Body, Controller, Param, Post, UsePipes, ValidationPipe } from "@nestjs/common";
import { QuizService } from "./quiz.service";
// index.ts barrel file로 처리해도 좋을 듯.
import { CreateClassRequestDto } from "./dto/create-class.request.dto";
import { CreateQuizListRequestDto } from "./dto/create-quizlist.request.dto";

@Controller('api')
export class QuizController {
constructor( private readonly quizService : QuizService) {}

// class 생성
@Post('classes')
@UsePipes(ValidationPipe)
async createClass(@Body() dto : CreateClassRequestDto) {
return await this.quizService.createClass(dto);
}

@Post('classes/:classId/quizzes')
@UsePipes(ValidationPipe)
async createQuiz(@Param('classId') classId: number,
@Body() dto : CreateQuizListRequestDto) {
return await this.quizService.createQuiz(classId, dto);
}

// @Delete('delete-class')
// @UsePipes(ValidationPipe)
// async deleteQuiz(@Body() dto : CreateClassRequestDto) {
// return await this.quizService.deleteClass(dto);
// }
}
94 changes: 94 additions & 0 deletions packages/BE/src/module/quiz/quizzes/quiz.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Injectable, HttpException, HttpStatus, Param } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { QuizRepository } from './repositories/quiz.repository';
import { ChoiceRepository } from './repositories/choice.repository';
import { ClassRepository } from './repositories/class.repository';
import { CreateClassRequestDto } from './dto/create-class.request.dto';
import { CreateQuizListRequestDto } from './dto/create-quizlist.request.dto';
import { CreateQuizRequestDto } from './dto/create-quiz.request.dto';
import { CreateChoiceRequestDto } from './dto/create-choice.request.dto';
import { ResponseDto } from '../../utils/dto/response.dto';
import { Quiz } from './entities/quiz.entity';
import { Class } from './entities/class.entity';
import { Choice } from './entities/choice.entity';

@Injectable()
export class QuizService {
constructor(
private readonly quizRepository: QuizRepository,
private readonly choiceRepository: ChoiceRepository,
private readonly classRepository: ClassRepository,
private readonly dataSource: DataSource,
) {}

async createClass(createClassRequestDto: CreateClassRequestDto): Promise<void> {
try {
const classData = await this.classRepository.create({
title: createClassRequestDto.title,
description: createClassRequestDto.description,
});
} catch (error) {
console.error('error:', error);
}
}

// dto가 여러개라서 처리하기 좀 그러네 quiz, choice는 dto가 아니라 인터페이스로 구현하는게 좋지않을까라는 생각...?
async createQuiz(classId: number, quizData: CreateQuizListRequestDto): Promise<ResponseDto> {
// 그럼 이 컨트롤러에서 dto 구분이 힘들다
// 너무 이 메서드에 책임이 많은게 아닌가 라는 생각도 든다.
// const queryRunner = this.dataSource.createQueryRunner();
// await queryRunner.connect();
// await queryRunner.startTransaction();
try {
// class_id가 유효한지 확인
const is_valid_class = await this.classRepository.findClassById(classId);
if (!is_valid_class) {
throw new Error(`Class with ID ${classId} not found`);
}

await Promise.all(
quizData.quizzes.map(async (quiz) => {
const quizEntity = await this.quizRepository.create(classId, quiz);
quiz.choices.map(async (choice) => {
this.choiceRepository.create(quizEntity.id, choice);
});
}
));

// await queryRunner.commitTransaction();


return {
success: true,
message: 'Quiz created successfully',
};
} catch (error) {
// await queryRunner.rollbackTransaction();
throw new HttpException({
status: HttpStatus.FORBIDDEN,
error: `${error}`,
}, HttpStatus.FORBIDDEN, {
cause: error
});
}
// } finally {
// await queryRunner.release();
// }
}

async findAll(): Promise<Quiz[]> {
return this.quizRepository.findAll();
}

// async findById(id: number): Promise<Quiz> {
// const quiz = await this.quizRepository.findById(id);
// if (!quiz) {
// throw new NotFoundException(`Quiz with ID ${id} not found`);
// }
// return quiz;
// }
// async updateQuiz(id: number, updateQuizDto: UpdateQuizDto): Promise<Quiz> {}

// async deleteQuiz(id: number): Promise<void> {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from 'typeorm';
import { Choice } from '../entities/choice.entity';
import { CreateChoiceRequestDto } from "../dto/create-choice.request.dto";

@Injectable()
export class ChoiceRepository {
constructor(
@InjectRepository(Choice)
private readonly repository: Repository<Choice>
) {}

async create(quiz_id: number, choiceData: CreateChoiceRequestDto): Promise<Choice> {
const { position, content, isCorrect: is_correct } = choiceData;
const choiceEntity = this.repository.create({
quiz_id,
position,
content,
is_correct,
});
return await this.repository.save(choiceEntity);
}


async findById(id: number): Promise<Choice> {
return this.repository.findOne({ where: { id } });
}

async findAll(): Promise<Choice[]> {
return this.repository.find();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Class } from '../entities/class.entity';

@Injectable()
export class ClassRepository {
constructor(
@InjectRepository(Class)
private readonly repository: Repository<Class>
) {}

async create(classData: Partial<Class>): Promise<Class> {
return this.repository.save(classData);
}

// async delete(classData: Partial<Class>): Promise<void> {
// return this.repository.delete(classData);
// }

async findById(id: number): Promise<Class> {
return this.repository.findOne({ where: { id } });
}

async findClassById(id: number): Promise<Class> {
return this.repository.findOne({ where: { id } });
}

async findAll(): Promise<Class[]> {
return this.repository.find();
}
}
Loading

0 comments on commit 2575441

Please sign in to comment.