댓글 기능을 만들도록 하겠습니다. 댓글의 경우 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.
For anyone looking to incorporate engaging features, I recommend exploring the block blast game as a fun project alongside this API. This can help you sharpen your skills in handling states, improving user interactions, and learning about game mechanics. Combining backend development with a game project can provide a holistic learning experience.
https://block-blast.online/
I have learned a lot from this, thank you! Learning NestJS makes me curious and fascinated, just like when I use a Morse code translator.
One of the greatest and most dependable Roblox exploits, created by Ice Bear, is Krnl executor https://krnl.vip/ https://thekrnl.com/
Because it enables them to win with little effort, gamers love this application. To win these quick Free Fire matches, download the FFH4X Injector. https://ffh4x.vip/ https://fisch-macro.com/
chicken jockey clicker features a blocky environment and pixelated textures that perfectly capture the iconic Minecraft aesthetic. You’ll find yourself in a quirk
You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife
You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife
You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife
You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife
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.