๐ŸŽ‰ berenickt ๋ธ”๋กœ๊ทธ์— ์˜จ ๊ฑธ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. ๐ŸŽ‰
Back
NestJs
33-module-nesting

1. ์„น์…˜ ์†Œ๊ฐœ ๋ฐ ์ž‘์—… ์ธ์ŠคํŠธ๋Ÿญ์…˜

1
nest g resource
2
? comments
3
? REST API
4
? No

comments ํด๋”๊ฐ€ ์ƒ๊ธฐ๋ฉด, ์ด ํด๋” ํ†ต์จฐ๋กœ posts ํด๋” ์•ˆ์œผ๋กœ ์ด๋™์‹œ์ผœ ํ•˜์œ„ ๋ชจ๋“ˆ๋กœ ๋งŒ๋“ ๋‹ค.

comments ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •ํ•œ๋‹ค.

comments.controller.ts
1
/** 1) Entity ์ƒ์„ฑ
2
* author -> ์ž‘์„ฑ์ž
3
* post -> ๊ท€์†๋˜๋Š” ํฌ์ŠคํŠธ
4
* comment -> ์‹ค์ œ ๋Œ“๊ธ€ ๋‚ด์šฉ
5
* likeCount -> ์ข‹์•„์š” ๊ฐฏ์ˆ˜
6
*
7
* id -> PrimaryGeneratedColumn
8
* createdAt -> ์ƒ์„ฑ์ผ์ž
9
* updatedAt -> ์—…๋ฐ์ดํŠธ์ผ์ž
10
*
11
* 2) GET() pagination
12
* 3) GET(':commentId') ํŠน์ • comment๋งŒ ํ•˜๋‚˜ ๊ฐ€์ ธ์˜ค๋Š” ๊ธฐ๋Šฅ
13
* 4) POST() ์ฝ”๋ฉ˜ํŠธ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ
14
* 5) PATCH(':commentId') ํŠน์ • comment ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ๊ธฐ๋Šฅ
15
* 6) DELETE(':commentId') ํŠน์ • comment ์‚ญ์ œํ•˜๋Š” ๊ธฐ๋Šฅ
16
*/
17
@Controller('posts/:postId/comments')
18
export class CommentsController {
19
constructor(
20
private readonly commentsService: CommentsService, //
21
) {}
22
}

2. Comments Entity ์ƒ์„ฑ

posts/comments/entity/comments.entity.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

comments.entity.ts
1
import { IsNumber, IsString } from 'class-validator'
2
3
import { BaseModel } from 'src/common/entity/base.entity'
4
import { PostsModel } from 'src/posts/entity/posts.entity'
5
import { UsersModel } from 'src/users/entity/users.entity'
6
import { Column, Entity, ManyToOne } from 'typeorm'
7
8
@Entity()
9
export class CommentsModel extends BaseModel {
10
@ManyToOne(() => UsersModel, user => user.postComments)
11
author: UsersModel
12
13
@ManyToOne(() => PostsModel, post => post.comments)
14
post: PostsModel
15
16
@Column()
17
@IsString()
18
comment: string
19
20
@Column({
21
default: 0,
22
})
23
@IsNumber()
24
likeCount: number
25
}

users ๋ชจ๋ธ์— postComments ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

users.entity.ts
1
// users.entity.ts ์ƒ๋žต
2
@OneToMany(() => CommentsModel, comment => comment.author)
3
postComments: CommentsModel[]

post ๋ชจ๋ธ์— comments ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

posts.entity.ts
1
// posts.entity.ts ์ƒ๋žต
2
@OneToMany(() => CommentsModel, comment => comment.post)
3
comments: CommentsModel[]

app ๋ชจ๋“ˆ์—์„œ CommentsModel๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

app.module.ts
1
@Module({
2
imports: [
3
// ์ƒ๋žต
4
TypeOrmModule.forRoot({
5
// ์ƒ๋žต
6
entities: [
7
PostsModel, //
8
UsersModel,
9
ImageModel,
10
ChatsModel,
11
MessagesModel,
12
CommentsModel, // ์ถ”๊ฐ€
13
],
14
synchronize: true,
15
}),
16
// ์ƒ๋žต
17
],
18
// ์ƒ๋žต
19
})

๊ทธ๋ฆฌ๊ณ  comments ๋ชจ๋“ˆ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

comments.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([CommentsModel]), //
4
],
5
controllers: [CommentsController],
6
providers: [CommentsService],
7
})

3. Paginate Comments API

posts/comments/dto/paginate-comments.dto.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

posts/comments/dto/paginate-comments.dto.ts
1
import { BasePaginationDto } from 'src/common/dto/base-pagination.dto'
2
3
export class PaginateCommentsDto extends BasePaginationDto {}

comments.controller.ts์— ๋Œ“๊ธ€ ํŽ˜์ด์ง€๋„ค์ด์…˜ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.controller.ts
1
// **** (1) ๋Œ“๊ธ€ ํŽ˜์ด์ง€๋„ค์ด์…˜
2
@Get()
3
@IsPublic()
4
getComments(
5
@Param('postId', ParseIntPipe) postId: number, //
6
@Query() query: PaginateCommentsDto,
7
) {
8
return this.commentsService.paginteComments(query, postId)
9
}

comments ๋ชจ๋“ˆ์—์„œ CommonModule์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

comments.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([CommentsModel]), //
4
CommonModule,
5
],
6
controllers: [CommentsController],
7
providers: [CommentsService],
8
})

comments ์„œ๋น„์Šค์— ํŽ˜์ด์ง€๋„ค์ด์…˜ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.service.ts
1
@Injectable()
2
export class CommentsService {
3
constructor(
4
@InjectRepository(CommentsModel)
5
private readonly commentsRepository: Repository<CommentsModel>,
6
private readonly commonService: CommonService,
7
) {}
8
9
paginteComments(dto: PaginateCommentsDto, postId: number) {
10
return this.commonService.paginate(
11
dto,
12
this.commentsRepository,
13
{ where: { post: { id: postId } } },
14
`posts/${postId}/comments`,
15
)
16
}
17
}

4. ID ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋‚˜์˜ Comments ๊ฐ€์ ธ์˜ค๋Š” API ์ž‘์„ฑ

ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŠน์ • ๋Œ“๊ธ€ 1๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด comments ์ปจํŠธ๋กค๋Ÿฌ์— ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.controller.ts
1
// **** (2) ํŠน์ • ๋Œ“๊ธ€ 1๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
2
@Get(':commentId')
3
@IsPublic()
4
getComment(@Param('commentId', ParseIntPipe) commentId: number) {
5
return this.commentsService.getCommentById(commentId)
6
}

comments ์„œ๋น„์Šค์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.service.ts
1
// **** (2) ํŠน์ • ๋Œ“๊ธ€ 1๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
2
async getCommentById(id: number) {
3
const comment = await this.commentsRepository.findOne({
4
...DEFAULT_COMMENT_FIND_OPTIONS,
5
where: { id },
6
})
7
8
if (!comment) {
9
throw new BadRequestException(`id: ${id} Comment๋Š” ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.`)
10
}
11
12
return comment
13
}

5. Comment ์ƒ์„ฑ API

๋Œ“๊ธ€ ์ƒ์„ฑ DTO๋ฅผ ์œ„ํ•ด comments/dto/create-comments.dto.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

create-comments.dto.ts
1
import { PickType } from '@nestjs/mapped-types'
2
import { CommentsModel } from '../entity/comments.entity'
3
4
export class CreateCommentsDto extends PickType(CommentsModel, ['comment']) {}

๋Œ“๊ธ€ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด comments ์ปจํŠธ๋กค๋Ÿฌ์— ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.controller.ts
1
// **** (3) ๋Œ“๊ธ€ ์ƒ์„ฑ
2
@Post()
3
@UseInterceptors(TransactionInterceptor)
4
async postComment(
5
@Param('postId', ParseIntPipe) postId: number,
6
@Body() body: CreateCommentsDto,
7
@User() user: UsersModel,
8
@QueryRunner() qr: QR,
9
) {
10
const resp = await this.commentsService.createComment(body, postId, user, qr)
11
12
await this.postsService.incrementCommentCount(postId, qr)
13
14
return resp
15
}

comments ๋ชจ๋“ˆ์—์„œ AuthModule, UsersModule์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

comments.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([CommentsModel]), //
4
CommonModule,
5
AuthModule,
6
UsersModule,
7
],
8
controllers: [CommentsController],
9
providers: [CommentsService],
10
})

comments ์„œ๋น„์Šค์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.service.ts
1
// **** (3) ๋Œ“๊ธ€ ์ƒ์„ฑ
2
async createComment(
3
dto: CreateCommentsDto, //
4
postId: number,
5
author: UsersModel,
6
qr?: QueryRunner,
7
) {
8
const repository = this.getRepository(qr)
9
10
return repository.save({
11
...dto,
12
post: { id: postId },
13
author,
14
})
15
}

comments/const/default-comment-find-options.const.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

comments/const/default-comment-find-options.const.ts
1
import { FindManyOptions } from 'typeorm'
2
import { CommentsModel } from '../entity/comments.entity'
3
4
export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {
5
relations: {
6
author: true,
7
},
8
select: {
9
author: {
10
id: true,
11
nickname: true,
12
},
13
},
14
}

6. Patch Comment API

๋Œ“๊ธ€ ์ƒ์„ฑ DTO๋ฅผ ์œ„ํ•ด comments/dto/update-comments.dto.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

update-comments.dto.ts
1
import { PartialType } from '@nestjs/mapped-types'
2
import { CreateCommentsDto } from './create-comments.dto'
3
4
export class UpdateCommentsDto extends PartialType(CreateCommentsDto) {}

๋Œ“๊ธ€ ์ˆ˜์ •ํ•˜๊ธฐ ์œ„ํ•ด comments ์ปจํŠธ๋กค๋Ÿฌ์— ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.controller.ts
1
// **** (4) ๋Œ“๊ธ€ ์ˆ˜์ •
2
@Patch(':commentId')
3
@UseGuards(IsCommentMineOrAdminGuard)
4
async patchComment(
5
@Param('commentId', ParseIntPipe) commentId: number, //
6
@Body() body: UpdateCommentsDto,
7
) {
8
return this.commentsService.updateComment(body, commentId)
9
}

comments ์„œ๋น„์Šค์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.service.ts
1
// **** (4) ๋Œ“๊ธ€ ์ˆ˜์ •
2
async updateComment(dto: UpdateCommentsDto, commentId: number) {
3
const comment = await this.commentsRepository.findOne({
4
where: {
5
id: commentId,
6
},
7
})
8
9
if (!comment) {
10
throw new BadRequestException('์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค.')
11
}
12
13
const prevComment = await this.commentsRepository.preload({
14
id: commentId,
15
...dto,
16
})
17
18
const newComment = await this.commentsRepository.save(prevComment)
19
20
return newComment
21
}

7. Delete Comment API

๋Œ“๊ธ€ ์‚ญ์ œ๋ฅผ ์œ„ํ•ด comments ์ปจํŠธ๋กค๋Ÿฌ์— ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.controller.ts
1
// **** (5) ๋Œ“๊ธ€ ์‚ญ์ œ
2
@Delete(':commentId')
3
@UseGuards(IsCommentMineOrAdminGuard)
4
@UseInterceptors(TransactionInterceptor)
5
async deleteComment(
6
@Param('commentId', ParseIntPipe) commentId: number, //
7
@Param('postId', ParseIntPipe) postId: number,
8
@QueryRunner() qr: QR,
9
) {
10
const resp = await this.commentsService.deleteComment(commentId, qr)
11
12
await this.postsService.decrementCommentCount(postId, qr)
13
14
return resp
15
}

comments ์„œ๋น„์Šค์— ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

comments.service.ts
1
// **** (5) ๋Œ“๊ธ€ ์‚ญ์ œ
2
async deleteComment(id: number, qr?: QueryRunner) {
3
const repository = this.getRepository(qr)
4
5
const comment = await repository.findOne({
6
where: { id },
7
})
8
9
if (!comment) {
10
throw new BadRequestException('์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค.')
11
}
12
13
await repository.delete(id)
14
15
return id
16
}

8. Path Parameter ๊ฒ€์ฆํ•˜๋Š” Middleware ์ƒ์„ฑ

comment ์ปจํŠธ๋กค๋Ÿฌ ์•ˆ์— ์žˆ๋Š” :postIdํŒจ์Šค ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๋งŒ๋“ ๋‹ค.

posts.service์— id๋ฅผ ๊ฐ–๊ณ ์žˆ๋Š” post๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

posts.service.ts
1
// **** ์ด id๋ฅผ ๊ฐ€์ง„ post๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
2
async checkPostExistsById(id: number) {
3
return this.postsRepository.exist({
4
where: { id },
5
})
6
}

comments/middleware/post-exists.middleware.ts ํŒŒ์ผ์„ ๋งŒ๋“ ๋‹ค.

comments/middleware/post-exists.middleware.ts
1
import { BadRequestException, Injectable, NestMiddleware } from '@nestjs/common'
2
import { NextFunction, Request, Response } from 'express'
3
import { PostsService } from 'src/posts/posts.service'
4
5
@Injectable()
6
export class PostExistsMiddelware implements NestMiddleware {
7
constructor(private readonly postService: PostsService) {}
8
9
async use(req: Request, res: Response, next: NextFunction) {
10
const postId = req.params.postId
11
12
if (!postId) {
13
throw new BadRequestException('Post ID ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.')
14
}
15
16
const exists = await this.postService.checkPostExistsById(parseInt(postId))
17
18
if (!exists) {
19
throw new BadRequestException('Post๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.')
20
}
21
22
next() // ๋‹ค์Œ ๋กœ์ง์„ ์‹คํ–‰
23
}
24
}

9. PostExists Middleware CommentsController์— ์ ์šฉ

๋“ฑ๋กํ•˜๋ ค๋Š” ๋ชจ๋“ˆ(comments)์— PostExists Middleware๋ฅผ ์ ์šฉ์‹œํ‚ฌ ๊ฒƒ์ด๋‹ค.

comments.module.ts
1
@Module({
2
imports: [
3
TypeOrmModule.forFeature([CommentsModel]), //
4
CommonModule,
5
AuthModule,
6
UsersModule,
7
PostsModule,
8
],
9
controllers: [CommentsController],
10
providers: [CommentsService],
11
})
12
export class CommentsModule implements NestModule {
13
configure(consumer: MiddlewareConsumer) {
14
consumer.apply(PostExistsMiddelware).forRoutes(CommentsController)
15
}
16
}

๊ทธ๋ฆฌ๊ณ  posts ๋ชจ๋“ˆ์„ exportํ•œ๋‹ค.

posts.module.ts
1
@Module({
2
// ์ƒ๋žต
3
exports: [PostsService],
4
})
5
export class PostsModule {}