1. ์น์ ์๊ฐ ๋ฐ ์์ ์ธ์คํธ๋ญ์
1nest g resource2? comments3? REST API4? No
comments ํด๋๊ฐ ์๊ธฐ๋ฉด, ์ด ํด๋ ํต์จฐ๋ก posts ํด๋ ์์ผ๋ก ์ด๋์์ผ ํ์ ๋ชจ๋๋ก ๋ง๋ ๋ค.
comments ์ปจํธ๋กค๋ฌ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
1/** 1) Entity ์์ฑ2* author -> ์์ฑ์3* post -> ๊ท์๋๋ ํฌ์คํธ4* comment -> ์ค์ ๋๊ธ ๋ด์ฉ5* likeCount -> ์ข์์ ๊ฐฏ์6*7* id -> PrimaryGeneratedColumn8* createdAt -> ์์ฑ์ผ์9* updatedAt -> ์ ๋ฐ์ดํธ์ผ์10*11* 2) GET() pagination12* 3) GET(':commentId') ํน์ comment๋ง ํ๋ ๊ฐ์ ธ์ค๋ ๊ธฐ๋ฅ13* 4) POST() ์ฝ๋ฉํธ ์์ฑํ๋ ๊ธฐ๋ฅ14* 5) PATCH(':commentId') ํน์ comment ์ ๋ฐ์ดํธ ํ๋ ๊ธฐ๋ฅ15* 6) DELETE(':commentId') ํน์ comment ์ญ์ ํ๋ ๊ธฐ๋ฅ16*/17@Controller('posts/:postId/comments')18export class CommentsController {19constructor(20private readonly commentsService: CommentsService, //21) {}22}
2. Comments Entity ์์ฑ
posts/comments/entity/comments.entity.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { IsNumber, IsString } from 'class-validator'23import { BaseModel } from 'src/common/entity/base.entity'4import { PostsModel } from 'src/posts/entity/posts.entity'5import { UsersModel } from 'src/users/entity/users.entity'6import { Column, Entity, ManyToOne } from 'typeorm'78@Entity()9export class CommentsModel extends BaseModel {10@ManyToOne(() => UsersModel, user => user.postComments)11author: UsersModel1213@ManyToOne(() => PostsModel, post => post.comments)14post: PostsModel1516@Column()17@IsString()18comment: string1920@Column({21default: 0,22})23@IsNumber()24likeCount: number25}
users ๋ชจ๋ธ์ postComments ์ปฌ๋ผ์ ์ถ๊ฐํ๋ค.
1// users.entity.ts ์๋ต2@OneToMany(() => CommentsModel, comment => comment.author)3postComments: CommentsModel[]
post ๋ชจ๋ธ์ comments ์ปฌ๋ผ์ ์ถ๊ฐํ๋ค.
1// posts.entity.ts ์๋ต2@OneToMany(() => CommentsModel, comment => comment.post)3comments: CommentsModel[]
app ๋ชจ๋์์ CommentsModel๋ฅผ ์ถ๊ฐํ๋ค.
1@Module({2imports: [3// ์๋ต4TypeOrmModule.forRoot({5// ์๋ต6entities: [7PostsModel, //8UsersModel,9ImageModel,10ChatsModel,11MessagesModel,12CommentsModel, // ์ถ๊ฐ13],14synchronize: true,15}),16// ์๋ต17],18// ์๋ต19})
๊ทธ๋ฆฌ๊ณ comments ๋ชจ๋์์๋ ์ฌ์ฉํ ์ ์๊ฒ ์ถ๊ฐํด์ค๋ค.
1@Module({2imports: [3TypeOrmModule.forFeature([CommentsModel]), //4],5controllers: [CommentsController],6providers: [CommentsService],7})
3. Paginate Comments API
posts/comments/dto/paginate-comments.dto.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { BasePaginationDto } from 'src/common/dto/base-pagination.dto'23export class PaginateCommentsDto extends BasePaginationDto {}
comments.controller.ts์ ๋๊ธ ํ์ด์ง๋ค์ด์
์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ๋ค.
1// **** (1) ๋๊ธ ํ์ด์ง๋ค์ด์ 2@Get()3@IsPublic()4getComments(5@Param('postId', ParseIntPipe) postId: number, //6@Query() query: PaginateCommentsDto,7) {8return this.commentsService.paginteComments(query, postId)9}
comments ๋ชจ๋์์ CommonModule์ ์ฌ์ฉํ ์ ์๊ฒ ์ถ๊ฐํด์ค๋ค.
1@Module({2imports: [3TypeOrmModule.forFeature([CommentsModel]), //4CommonModule,5],6controllers: [CommentsController],7providers: [CommentsService],8})
comments ์๋น์ค์ ํ์ด์ง๋ค์ด์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.
1@Injectable()2export class CommentsService {3constructor(4@InjectRepository(CommentsModel)5private readonly commentsRepository: Repository<CommentsModel>,6private readonly commonService: CommonService,7) {}89paginteComments(dto: PaginateCommentsDto, postId: number) {10return this.commonService.paginate(11dto,12this.commentsRepository,13{ where: { post: { id: postId } } },14`posts/${postId}/comments`,15)16}17}
4. ID ๊ธฐ๋ฐ์ผ๋ก ํ๋์ Comments ๊ฐ์ ธ์ค๋ API ์์ฑ
ID๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํน์ ๋๊ธ 1๊ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ ์ํด comments ์ปจํธ๋กค๋ฌ์ ์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ๋ค.
1// **** (2) ํน์ ๋๊ธ 1๊ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ2@Get(':commentId')3@IsPublic()4getComment(@Param('commentId', ParseIntPipe) commentId: number) {5return this.commentsService.getCommentById(commentId)6}
comments ์๋น์ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.
1// **** (2) ํน์ ๋๊ธ 1๊ฐ๋ง ๊ฐ์ ธ์ค๊ธฐ2async getCommentById(id: number) {3const comment = await this.commentsRepository.findOne({4...DEFAULT_COMMENT_FIND_OPTIONS,5where: { id },6})78if (!comment) {9throw new BadRequestException(`id: ${id} Comment๋ ์กด์ฌํ์ง ์์ต๋๋ค.`)10}1112return comment13}
5. Comment ์์ฑ API
๋๊ธ ์์ฑ DTO๋ฅผ ์ํด comments/dto/create-comments.dto.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { PickType } from '@nestjs/mapped-types'2import { CommentsModel } from '../entity/comments.entity'34export class CreateCommentsDto extends PickType(CommentsModel, ['comment']) {}
๋๊ธ ์์ฑํ๊ธฐ ์ํด comments ์ปจํธ๋กค๋ฌ์ ์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ๋ค.
1// **** (3) ๋๊ธ ์์ฑ2@Post()3@UseInterceptors(TransactionInterceptor)4async postComment(5@Param('postId', ParseIntPipe) postId: number,6@Body() body: CreateCommentsDto,7@User() user: UsersModel,8@QueryRunner() qr: QR,9) {10const resp = await this.commentsService.createComment(body, postId, user, qr)1112await this.postsService.incrementCommentCount(postId, qr)1314return resp15}
comments ๋ชจ๋์์ AuthModule, UsersModule์ ์ฌ์ฉํ ์ ์๊ฒ ์ถ๊ฐํด์ค๋ค.
1@Module({2imports: [3TypeOrmModule.forFeature([CommentsModel]), //4CommonModule,5AuthModule,6UsersModule,7],8controllers: [CommentsController],9providers: [CommentsService],10})
comments ์๋น์ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.
1// **** (3) ๋๊ธ ์์ฑ2async createComment(3dto: CreateCommentsDto, //4postId: number,5author: UsersModel,6qr?: QueryRunner,7) {8const repository = this.getRepository(qr)910return repository.save({11...dto,12post: { id: postId },13author,14})15}
comments/const/default-comment-find-options.const.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { FindManyOptions } from 'typeorm'2import { CommentsModel } from '../entity/comments.entity'34export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {5relations: {6author: true,7},8select: {9author: {10id: true,11nickname: true,12},13},14}
6. Patch Comment API
๋๊ธ ์์ฑ DTO๋ฅผ ์ํด comments/dto/update-comments.dto.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { PartialType } from '@nestjs/mapped-types'2import { CreateCommentsDto } from './create-comments.dto'34export class UpdateCommentsDto extends PartialType(CreateCommentsDto) {}
๋๊ธ ์์ ํ๊ธฐ ์ํด comments ์ปจํธ๋กค๋ฌ์ ์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ๋ค.
1// **** (4) ๋๊ธ ์์ 2@Patch(':commentId')3@UseGuards(IsCommentMineOrAdminGuard)4async patchComment(5@Param('commentId', ParseIntPipe) commentId: number, //6@Body() body: UpdateCommentsDto,7) {8return this.commentsService.updateComment(body, commentId)9}
comments ์๋น์ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.
1// **** (4) ๋๊ธ ์์ 2async updateComment(dto: UpdateCommentsDto, commentId: number) {3const comment = await this.commentsRepository.findOne({4where: {5id: commentId,6},7})89if (!comment) {10throw new BadRequestException('์กด์ฌํ์ง ์๋ ๋๊ธ์ ๋๋ค.')11}1213const prevComment = await this.commentsRepository.preload({14id: commentId,15...dto,16})1718const newComment = await this.commentsRepository.save(prevComment)1920return newComment21}
7. Delete Comment API
๋๊ธ ์ญ์ ๋ฅผ ์ํด comments ์ปจํธ๋กค๋ฌ์ ์๋ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ๋ค.
1// **** (5) ๋๊ธ ์ญ์ 2@Delete(':commentId')3@UseGuards(IsCommentMineOrAdminGuard)4@UseInterceptors(TransactionInterceptor)5async deleteComment(6@Param('commentId', ParseIntPipe) commentId: number, //7@Param('postId', ParseIntPipe) postId: number,8@QueryRunner() qr: QR,9) {10const resp = await this.commentsService.deleteComment(commentId, qr)1112await this.postsService.decrementCommentCount(postId, qr)1314return resp15}
comments ์๋น์ค์ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.
1// **** (5) ๋๊ธ ์ญ์ 2async deleteComment(id: number, qr?: QueryRunner) {3const repository = this.getRepository(qr)45const comment = await repository.findOne({6where: { id },7})89if (!comment) {10throw new BadRequestException('์กด์ฌํ์ง ์๋ ๋๊ธ์ ๋๋ค.')11}1213await repository.delete(id)1415return id16}
8. Path Parameter ๊ฒ์ฆํ๋ Middleware ์์ฑ
comment ์ปจํธ๋กค๋ฌ ์์ ์๋ :postIdํจ์ค ํ๋ผ๋ฏธํฐ๊ฐ ์ ํจํ์ง ํ์ธํ๋ ๋ฏธ๋ค์จ์ด๋ฅผ ๋ง๋ ๋ค.
posts.service์ id๋ฅผ ๊ฐ๊ณ ์๋ post๊ฐ ์กด์ฌํ๋์ง ํ์ธํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค.
1// **** ์ด id๋ฅผ ๊ฐ์ง post๊ฐ ์กด์ฌํ๋์ง ํ์ธ2async checkPostExistsById(id: number) {3return this.postsRepository.exist({4where: { id },5})6}
comments/middleware/post-exists.middleware.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common'2import { NextFunction, Request, Response } from 'express'3import { PostsService } from 'src/posts/posts.service'45@Injectable()6export class PostExistsMiddelware implements NestMiddleware {7constructor(private readonly postService: PostsService) {}89async use(req: Request, res: Response, next: NextFunction) {10const postId = req.params.postId1112if (!postId) {13throw new BadRequestException('Post ID ํ๋ผ๋ฏธํฐ๋ ํ์์ ๋๋ค.')14}1516const exists = await this.postService.checkPostExistsById(parseInt(postId))1718if (!exists) {19throw new BadRequestException('Post๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.')20}2122next() // ๋ค์ ๋ก์ง์ ์คํ23}24}
9. PostExists Middleware CommentsController์ ์ ์ฉ
๋ฑ๋กํ๋ ค๋ ๋ชจ๋(comments)์ PostExists Middleware๋ฅผ ์ ์ฉ์ํฌ ๊ฒ์ด๋ค.
1@Module({2imports: [3TypeOrmModule.forFeature([CommentsModel]), //4CommonModule,5AuthModule,6UsersModule,7PostsModule,8],9controllers: [CommentsController],10providers: [CommentsService],11})12export class CommentsModule implements NestModule {13configure(consumer: MiddlewareConsumer) {14consumer.apply(PostExistsMiddelware).forRoutes(CommentsController)15}16}
๊ทธ๋ฆฌ๊ณ posts ๋ชจ๋์ exportํ๋ค.
1@Module({2// ์๋ต3exports: [PostsService],4})5export class PostsModule {}