댓글 기능을 만들도록 하겠습니다. 댓글의 경우 Post 내부에 존재하기 때문에 posts 내부에 생성을 하도록 하겠습니다.
nest g resource -> comments -> REST API -> n
@Controller('posts/:postId/comments')
export class CommentsController {
constructor(private readonly commentsService: CommentsService) {}
/**
* 1) Entity 생성
* author -> 작성자
* comment -> 실제 댓글 내용
* likeCount -> 좋아요 갯수
*
* id -> PrimaryGeneratedColumn
* createAt -> 생성일자
* updatedAt -> 업데이트일자
*
* 2) GET() pagination
* 3) GET(':commentId') 특정 comment만 하나 가져오는 기능
* 4) POST() 코멘트 생성하는 기능
* 5) PATCH(':commentId') 특정 comment 업데이트 하는 기능
* 6) DELETE(':commentId) 특정 comment 삭제하는 기능
*/
}
import { IsNumber, IsString } from "class-validator";
import { BaseModel } from "src/common/entity/base.entity";
import { PostsModel } from "src/posts/entity/posts.entity";
import { UsersModel } from "src/users/entity/users.entity";
import { Column, Entity, ManyToOne } from "typeorm";
@Entity()
export class CommentsModel extends BaseModel {
@ManyToOne(() => UsersModel, (user) => user.postComments)
author: UsersModel;
@ManyToOne(() => PostsModel, (post) => post.comments)
post: PostsModel;
@Column()
@IsString()
comment: string;
@Column({
default: 0
})
@IsNumber()
likeCount: number;
}
.
.
@OneToMany(() => CommentsModel, (comment) => comment.post)
comments: CommentsModel[];
.
.
@OneToMany(() => CommentsModel, (comment) => comment.author)
postComments: CommentsModel[];
app.module.ts에 등록을 하고 typeORM 사용을 위해 typeORM 등록을 하겠습니다.
entities: [
PostsModel,
UsersModel,
ImageModel,
ChatsModel,
MessagesModel,
CommentsModel, // 등록
],
import { Module } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CommentsController } from './comments.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommentsModel } from './entity/comments.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
CommentsModel,
])
],
controllers: [CommentsController],
providers: [CommentsService],
})
export class CommentsModule {}
import { BasePaginationDto } from "src/common/dto/base-pagination.dto";
export class PaginateCommentsDto extends BasePaginationDto {}
@Get()
getComments(
@Param('postId', ParseIntPipe) postId: number,
@Query() query: PaginateCommentsDto
) {
return this.commentsService.paginateComments(
query,
postId,
);
}
@Injectable()
export class CommentsService {
constructor(
@InjectRepository(CommentsModel)
private readonly commentsRepository: Repository<CommentsModel>,
private readonly commonService: CommonService,
) {}
paginateComments(
dto: PaginateCommentsDto,
postId: number,
) {
return this.commonService.paginate(
dto,
this.commentsRepository,
{
where: {
post: {
id: postId,
}
}
},
`posts/${postId}/comments`,
);
}
}
@Module({
imports: [
TypeOrmModule.forFeature([
CommentsModel,
]),
CommonModule, // 등록
],
controllers: [CommentsController],
providers: [CommentsService],
})
export class CommentsModule {}
@Get(':commentId')
getComment(
@Param('commentId', ParseIntPipe) commentId: number,
) {
return this.commentsService.getCommentById(commentId);
}
async getCommentById(id: number) {
const comment = await this.commentsRepository.findOne({
where: {
id,
}
});
if (!comment) throw new BadRequestException(`id: ${id} Comment는 존재하지 않습니다. `);
return comment;
}
포스트맨으로 테스트를 하겠습니다.
{
"message": "id: 1 Comment는 존재하지 않습니다. ",
"error": "Bad Request",
"statusCode": 400
}
import { PickType } from "@nestjs/mapped-types";
import { CommentsModel } from "../entity/comments.entity";
export class CreateCommentsDto extends PickType(CommentsModel, [
'comment' // comment 프로퍼티만 상속받기
]) {}
@Post()
@UseGuards(AccessTokenGuard)
postComment(
@Param('postId', ParseIntPipe) postId: number,
@Body() body: CreateCommentsDto,
@User() user: UsersModel
) {
return this.commentsService.createComment(
body,
postId,
user
)
}
async createComment(
dto: CreateCommentsDto,
postId: number,
author: UsersModel // AccessToken에서 넘겨주면 UsersModel이 들어있음
) {
return this.commentsRepository.save({
...dto,
post: {
id: postId
},
author,
})
}
@Module({
imports: [
TypeOrmModule.forFeature([
CommentsModel,
]),
CommonModule,
AuthModule,
UsersModule,
],
controllers: [CommentsController],
providers: [CommentsService],
})
export class CommentsModule {}
포스트맨으로 테스트를 해보겠습니다. 로그인 후 토큰 값을 넣은 뒤에 요청합니다.
{
"comment": "강의 너무 좋아요!!!",
"post": {
"id": 101
},
"author": {
"id": 4,
"updatedAt": "2024-02-18T02:33:34.030Z",
"createdAt": "2024-02-18T02:33:34.030Z",
"nickname": "codefactory123",
"email": "codefactory123@codefactory.ai",
"role": "USER"
},
"id": 1,
"updatedAt": "2024-02-21T17:43:27.716Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"likeCount": 0
}
101에 대한 댓글 페이지네이션을 요청하겠습니다.
{
"data": [
{
"id": 1,
"updatedAt": "2024-02-21T17:43:27.716Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "강의 너무 좋아요!!!",
"likeCount": 0
}
],
"cursor": {
"after": null
},
"count": 1,
"next": null
}
누가 작성했는지까지 포함하겠습니다.
paginateComments(
dto: PaginateCommentsDto,
postId: number,
) {
return this.commonService.paginate(
dto,
this.commentsRepository,
{
where: {
post: {
id: postId,
}
},
relations: { // 추가
author: true
}
},
`posts/${postId}/comments`,
);
}
{
"data": [
{
"id": 1,
"updatedAt": "2024-02-21T17:43:27.716Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "강의 너무 좋아요!!!",
"likeCount": 0,
"author": {
"id": 4,
"updatedAt": "2024-02-18T02:33:34.030Z",
"createdAt": "2024-02-18T02:33:34.030Z",
"nickname": "codefactory123",
"email": "codefactory123@codefactory.ai",
"role": "USER"
}
}
],
"cursor": {
"after": null
},
"count": 1,
"next": null
}
이번에는 특정 Post의 comment정보를 가져오는 요청을 해보겠습니다.
{
"id": 1,
"updatedAt": "2024-02-21T17:43:27.716Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "강의 너무 좋아요!!!",
"likeCount": 0
}
해당 요청에도 override 기능을 추가해보겠습니다.
async getCommentById(id: number) {
const comment = await this.commentsRepository.findOne({
where: {
id,
},
relations: { // 추가
author: true
}
});
if (!comment) throw new BadRequestException(`id: ${id} Comment는 존재하지 않습니다. `);
return comment;
}
그리고 override 부분에서 계속해서 author: true
가 반복됩니다. 따라서 묶어주도록 하겠습니다. FindManyOptions
기능을 이용하겠습니다.
import { FindManyOptions } from "typeorm";
import { CommentsModel } from "../entity/comments.entity";
export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {
relations: {
author: true
},
}
서비스 코드를 바꿔줍니다.
paginateComments(
dto: PaginateCommentsDto,
postId: number,
) {
return this.commonService.paginate(
dto,
this.commentsRepository,
{
...DEFAULT_COMMENT_FIND_OPTIONS,
where: {
post: {
id: postId,
}
},
},
`posts/${postId}/comments`,
);
}
async getCommentById(id: number) {
const comment = await this.commentsRepository.findOne({
...DEFAULT_COMMENT_FIND_OPTIONS,
where: {
id,
}
});
if (!comment) throw new BadRequestException(`id: ${id} Comment는 존재하지 않습니다. `);
return comment;
}
{
"data": [
{
"id": 1,
"updatedAt": "2024-02-21T17:43:27.716Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "강의 너무 좋아요!!!",
"likeCount": 0,
"author": {
"id": 4,
"updatedAt": "2024-02-18T02:33:34.030Z",
"createdAt": "2024-02-18T02:33:34.030Z",
"nickname": "codefactory123",
"email": "codefactory123@codefactory.ai",
"role": "USER"
}
}
],
"cursor": {
"after": null
},
"count": 1,
"next": null
}
응답 데이터에서 보면 author정보는 사실상 id와 nickname만 필요합니다. 따라서 default option을 바꿔보도록 하겠습니다.
import { FindManyOptions } from "typeorm";
import { CommentsModel } from "../entity/comments.entity";
export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {
relations: {
author: true
},
select: {
author: {
id: true,
nickname: true,
}
}
}
같은 요청을 보내보겠습니다.
{
"data": [
{
"id": 1,
"updatedAt": "2024-02-21T17:43:27.716Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "강의 너무 좋아요!!!",
"likeCount": 0,
"author": {
"id": 4,
"nickname": "codefactory123"
}
}
],
"cursor": {
"after": null
},
"count": 1,
"next": null
}
import { PartialType } from "@nestjs/mapped-types";
import { CreateCommentsDto } from "./create-comments.dto";
// CreateCommentsDto의 부분 상속
export class UpdateCommentsDto extends PartialType(CreateCommentsDto) {}
@Patch(':commentId')
@UseGuards(AccessTokenGuard)
async patchComment(
@Param('commentId', ParseIntPipe) commentId: number,
@Body() body: UpdateCommentsDto,
) {
return this.commentsService.updateComment(
body,
commentId
)
}
async updateComment(
dto: UpdateCommentsDto,
commentId: number,
) {
const comment = await this.commentsRepository.findOne({
where: {
id,
}
});
if (!comment) throw new BadRequestException(`존재하지 않는 댓글입니다. `);
// preload 기능
const prevComment = await this.commentsRepository.preload({
id: commentId, // id 기반의 commentId 들어오게 됨
...dto, // 나머지는 dto내용으로 변경
});
const newComment = await this.commentsRepository.save(
prevComment,
);
return newComment;
}
포스트맨으로 테스트를 하겠습니다.
{
"id": 1,
"updatedAt": "2024-02-21T18:17:05.264Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "NestJS 너무",
"likeCount": 0
}
GET 요청으로 바꿔졌는지 확인을 해보겠습니다.
{
"data": [
{
"id": 1,
"updatedAt": "2024-02-21T18:17:05.264Z",
"createdAt": "2024-02-21T17:43:27.716Z",
"comment": "NestJS 너무",
"likeCount": 0,
"author": {
"id": 4,
"nickname": "codefactory123"
}
}
],
"cursor": {
"after": null
},
"count": 1,
"next": null
}
@Delete(':commentId')
@UseGuards(AccessTokenGuard)
async deleteComment(
@Param('commentId', ParseIntPipe) commentId: number,
) {
return this.commentsService.deleteComment(commentId);
}
async deleteComment(
id: number
) {
const comment = await this.commentsRepository.findOne({
where: {
id,
}
});
if (!comment) throw new BadRequestException(`존재하지 않는 댓글입니다. `);
await this.commentsRepository.delete(id);
return id;
}
포스트맨으로 테스트를 해보겠습니다.
{
"raw": [],
"affected": 1
}
GET 요청으로 확인하면 4번이 사라진 것을 알 수 있습니다.
현재 endpoint 경로는 post가 존재하면 에러를 던지는 코드는 존재하지 않습니다. 이 부분은 Middleware로 적용을 해보겠습니다. Middleware는 가장 앞단에서 먼저 필터링을 시작합니다.
컨트롤러에서 전반적으로 post가 존재하지 않으면 전부 BadRequestException을 던지도록 하겠습니다. 먼저 middleware 코드를 작성하겠습니다.
import { BadRequestException, Injectable, NestMiddleware } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";
import { PostsService } from "src/posts/posts.service";
@Injectable()
export class PostExistsMiddleware implements NestMiddleware {
constructor(
private readonly postService: PostsService,
) {}
async use(req: Request, res: Response, next: NextFunction) {
const postId = req.params.postId; // path parameter 안의 postId를 가져올 수 있습니다.
if (!postId) throw new BadRequestException(`Post ID 파라미터는 필수입니다. `);
const exists = await this.postService.checkPostExistsById(
parseInt(postId),
);
if (!exists) throw new BadRequestException(`Post가 존재하지 않습니다. `);
next(); // next를 해줘야 다음단계로 이동
}
}
async checkPostExistsById(id: number) {
return this.postsRepository.exists({
where: {
id,
},
})
}
이제 PostExists Middleware
를 등록하겠습니다. Middleware를 등록하려면 등록할 module로 가서 implements를 합니다.
@Module({
imports: [
TypeOrmModule.forFeature([
CommentsModel,
]),
CommonModule,
AuthModule,
UsersModule,
PostsModule, // 등록
],
controllers: [CommentsController],
providers: [CommentsService],
})
export class CommentsModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(PostExistsMiddleware) // 적용할 Middleware
.forRoutes(CommentsController); // 적용할 Controller 전체
}
}
포스트맨으로 테스트를 하겠습니다. 존재하지 않는 Post를 조회하겠습니다. 만약 comment와 관련된 endpoint로 199번을 조회하면 전부 동일한 에러 메세지를 보내줄 것입니다.
{
"message": "Post가 존재하지 않습니다. ",
"error": "Bad Request",
"statusCode": 400
}
이런식으로 Middleware를 적용하는 것이 좋은 사례인 것을 알 수 있습니다.
Really interesting programs to learn more. Explore the space and enjoy a deeper understanding of the rules. Continuously update advanced knowledge basketbros
I really value module nesting for its ability to structure my application's features into layered modules. It’s a great way to boost code level devil reusability, simplify maintenance, and scale up effectively.
In NestJS, Skribbl IO module nesting is a powerful feature that helps you organize your application in a modular and maintainable way.
It's useful. Module nesting refers to the process of nesting other components or modules within a page component or layout component. This nesting can help you build complex page structures and improve code maintainability and reusability.
I appreciate module nesting. This technique allows me to organize my application's functionality into hierarchical modules, promoting Wordle Unlimited code reusability, maintainability, and scalability.