게시글에서의 댓글을 저장하거나 삭제할 때의 데이터 관리는 다음과 같이 진행됩니다.
1) 댓글 저장: 댓글 스키마를 추가하여 독립적으로 comment document를 저장합니다.
2) 댓글 삭제: 댓글 id값을 받아와 해당 document를 삭제합니다.
3) 게시글 불러오기: 게시글을 불러올 시 게시글 id값에 대한 find query를 이용하여 관련 댓글들을 불러옵니다.
4) 게시글 삭제: 해당 게시글 id 값을 deleteMany 쿼리를 이용하여 관련 댓글들을 삭제하도록 합니다.
post-service에서 댓글 기능을 구성해보도록 하겠습니다. 우선 컨트롤러에서 댓글 endpoint를 만들어 보도록 하겠습니다.
...
@Controller('post-service')
export class AppController {
...
@Post('comment')
public async comment(@Body() requestComment: RequestComment) : Promise<any> {
try {
const dto: any = await this.postService.comment(Builder(CommentDto).postId(requestComment.postId)
.userId(requestComment.userId)
.writer(requestComment.writer)
.content(requestComment.content)
.build());
if(dto.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dto.message,
});
}
return await Object.assign({
status: HttpStatus.CREATED,
payload: statusConstants.SUCCESS,
message: "Successful save comment"
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
@Delete(':_id/comment')
public async deleteComment(@Param('_id') _id: string) : Promise<any> {
try {
const dto: any = await this.postService.deleteComment(_id);
if(dto.status === statusConstants.ERROR) {
return await Object.assign({
status: HttpStatus.INTERNAL_SERVER_ERROR,
payload: null,
message: "Error message: " + dto.message
});
}
return await Object.assign({
status: HttpStatus.NO_CONTENT,
payload: null,
message: "Successful delete comment"
});
} catch(err) {
return await Object.assign({
status: HttpStatus.BAD_REQUEST,
payload: null,
message: "Error message: " + err
});
}
}
}
1) comment: 댓글을 작성할 수 있는 rest 요청입니다. 게시글 id값을 param으로 전달받고, vo 객체로 RequestComment를 전달받습니다.
2) deleteComment: 댓글을 삭제할 수 있는 rest 요청입니다. 댓글의 id값을 받아 서비스 클래스로 삭제 트랜잭션을 요청합니다.
Comment를 위한 dto, vo, interface, schema 클래스를 만들어 보도록 하겠습니다.
export class CommentDto {
id: number;
postId: string;
userId: string;
writer: string;
content: string;
createdAt: string;
}
import { IsString } from "class-validator";
export class RequestComment {
@IsString()
postId: string;
@IsString()
userId: string;
@IsString()
writer: string;
@IsString()
content: string;
}
export class Comment {
userId: string;
writer: string;
content: string;
createdAt: string;
}
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type CommentDocument = Comment & Document;
@Schema()
export class Comment {
@Prop({ required: true })
postId: string;
@Prop({ required: true })
userId: string;
@Prop({ required: true })
writer: string;
@Prop({ required: true })
content: string;
@Prop({ required: true })
createdAt: string;
}
export const CommentSchema = SchemaFactory.createForClass(Comment);
댓글 dto ~ schema를 작성했습니다. 그러면 추가적으로 PostDto, PostSchema 객체에 속성으로써의 comment 객체를 추가하도록 하겠습니다.
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { Types } from "mongoose";
import { Comment } from "src/interfaces/comment.interface";
import { Rental } from "src/interfaces/rental.interface";
export type PostDocument = Post & Document;
@Schema()
export class Post {
@Prop({ required: true })
type: string;
@Prop({ required: true})
title: string;
@Prop({ required: true })
content: string;
@Prop({ required: true })
userId: string;
@Prop({ required: true })
writer: string;
@Prop({ required: true })
createdAt: string;
@Prop({ required: false })
rental: Rental;
@Prop({ required: false })
comments: Comment[];
}
export const PostSchema = SchemaFactory.createForClass(Post);
import { IsString } from "class-validator";
export class PostDto {
@IsString()
_id: string;
@IsString()
type: string;
@IsString()
title: string;
@IsString()
content: string;
@IsString()
userId: string;
@IsString()
writer: string;
@IsString()
createdAt: string;
rental: any;
comments: any;
}
PostModule에 Comment 스키마를 등록하도록 하겠습니다.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Comment, CommentSchema } from 'src/schema/comment.schema';
import { Post, PostSchema } from 'src/schema/post.schema';
import { PostService } from './post.service';
@Module({
imports: [
MongooseModule.forFeature([{
name: Post.name,
schema: PostSchema,
}]),
MongooseModule.forFeature([{
name: Comment.name,
schema: CommentSchema
}]),
],
providers: [PostService],
exports: [PostService]
})
export class PostModule {}
이를 바탕으로 하여 PostService를 수정하도록 하겠습니다.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Builder } from 'builder-pattern';
import { Model } from 'mongoose';
import { statusConstants } from 'src/constants/status.constants';
import { CommentDto } from 'src/dto/comment.dto';
import { PostDto } from 'src/dto/post.dto';
import { Comment, CommentDocument } from 'src/schema/comment.schema';
import { Post, PostDocument } from 'src/schema/post.schema';
@Injectable()
export class PostService {
constructor(
@InjectModel(Post.name) private postModel: Model<PostDocument>,
@InjectModel(Comment.name) private commentModel: Model<CommentDocument>
) {}
public async write(dto: PostDto): Promise<any> {
try {
const entity: any = await new this.postModel(Builder(Post).type(dto.type)
.title(dto.title)
.content(dto.content)
.userId(dto.userId)
.writer(dto.writer)
.createdAt(new Date().toString())
.rental(dto.rental ? dto.rental : null)
.build())
.save();
if(!entity) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: Database error!"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: Builder(PostDto).title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.build(),
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "post-service: " + err,
});
}
}
public async getAll(): Promise<any> {
try {
const entities: any = await this.postModel.find();
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist posts data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(Builder(PostDto)._id(entity._id)
.title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.comments(await this.commentModel.find({ postId: entity._id }))
.build());
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err
});
}
}
public async getOne(_id: string): Promise<any> {
try {
const post: any = await this.postModel.findOne({ _id: _id });
const comments: any = await this.commentModel.find({ postId: post._id });
if(!post) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: Builder(PostDto)._id(String(post._id))
.title(post.title)
.content(post.content)
.type(post.type)
.userId(post.userId)
.writer(post.writer)
.createdAt(post.createdAt)
.rental(post.rental)
.comments(comments)
.build(),
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async getPostsByType(type: string): Promise<any> {
try {
const entities: any = await this.postModel.find({ type: type });
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(Builder(PostDto)._id(entity._id)
.title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.comments(await this.commentModel.find({ postId: entity._id }))
.build());
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err
});
}
}
public async getPostsByUserId(userId: string): Promise<any> {
try {
const entities: any = await this.postModel.find({ userId: userId });
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(Builder(PostDto)._id(entity._id)
.title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.comments(await this.commentModel.find({ postId: entity._id }))
.build());
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async getPostsByKeyword(keyword: string): Promise<any> {
try {
const entities: any = await this.postModel.aggregate([{
$group: {
$or: [
{ title: { $regex: '.*' + keyword + '.*' }},
{ content: { $regex: '.*' + keyword + '.*' }}
]
}
}]);
if(!entities) {
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Not exist post data"
});
}
const dtos: Array<PostDto> = [];
for(const entity of entities) {
dtos.push(Builder(PostDto)._id(entity._id)
.title(entity.title)
.content(entity.content)
.type(entity.type)
.userId(entity.userId)
.writer(entity.writer)
.createdAt(entity.createdAt)
.rental(entity.rental)
.comments(await this.commentModel.find({ postId: entity._id }))
.build());
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: dtos,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async deletePost(_id: string): Promise<any> {
try {
const result: any = await this.postModel.deleteOne({ _id: _id });
await this.commentModel.deleteMany({ postId: _id });
if(!result) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "Not exist post data"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
});
}
}
public async comment(dto: CommentDto): Promise<any> {
try {
const entity: any = await new this.commentModel(Builder(Comment).postId(dto.postId)
.userId(dto.userId)
.writer(dto.writer)
.content(dto.content)
.createdAt(new Date().toString())
.build())
.save();
if(!entity) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: database error"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err,
})
}
}
public async deleteComment(_id: string): Promise<any> {
try {
const result: any = await this.commentModel.deleteOne({ _id: _id });
if(!result) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: database error"
});
}
return await Object.assign({
status: statusConstants.SUCCESS,
payload: null,
message: "Successful transaction"
});
} catch(err) {
return await Object.assign({
status: statusConstants.ERROR,
payload: null,
message: "post-service: " + err
});
}
}
}
주요 수정된 메서드들과 추가된 메서드를 살펴보겠습니다.
1) getAll:
for(const entity of entities) {
const comments: any = await this.commentModel.find({ postId: entity._id });
dtos.push(entity.push(comments));
}
해당 부분이 수정된 코드입니다. for 구문을 이용하여 가져온 게시글 객체의 id값을 가져옵니다. 그리고 이 id값을 commentModel의 find 쿼리에 넣어 전체 댓글리스트를 가져온 후 entity에 넣어줍니다. 마지막으로 dtos 배열 객체에 entity들을 담아 데이터를 반환합니다.
2) deletePost:
const result: any = await this.postModel.deleteOne({ _id: _id });
await this.commentModel.deleteMany({ postId: _id });
게시글의 id값을 가져와 deleteOne 쿼리를 이용하여 게시글을 삭제하고, 이어서 deleteMany 쿼리를 이용하여 관련 댓글들을 전부 삭제합니다.
3) comment: 댓글을 저장하는 메서드입니다. 댓글 entity에 값을 담아 save쿼리를 이용하여 댓글을 데이터베이스에 저장합니다.
4) deleteComment: 댓글을 삭제하는 메서드입니다. 댓글 id값을 가져와 deleteOne 메서드를 이용하여 해당 댓글을 삭제합니다.
테스트를 진행해보도록 하겠습니다.
1) 댓글 저장
2) 게시글 불러오기
{
"status": 200,
"payload": {
"_id": "61720baeb2731c19e165a175",
"type": "함께해요",
"title": "test-03",
"content": "test-03",
"userId": "78e834ff-f270-4a9e-9b30-1a44daff9469",
"writer": "asd",
"createdAt": "Fri Oct 22 2021 09:54:06 GMT+0900 (대한민국 표준시)",
"rental": null,
"comments": [
{
"_id": "61760b20d7e1941bf80e208d",
"createdAt": "Mon Oct 25 2021 10:40:48 GMT+0900 (대한민국 표준시)",
"content": "test-01",
"writer": "biuea",
"userId": "bb49cad9-1d76-41b2-abad-7a271c4394ca",
"postId": "61720baeb2731c19e165a175",
"__v": 0
},
{
"_id": "6176128e528e131ff92ea087",
"createdAt": "Mon Oct 25 2021 11:12:30 GMT+0900 (대한민국 표준시)",
"content": "test-01",
"writer": "biuea",
"userId": "bb49cad9-1d76-41b2-abad-7a271c4394ca",
"postId": "61720baeb2731c19e165a175",
"__v": 0
},
{
"_id": "61761588528e131ff92ea08b",
"createdAt": "Mon Oct 25 2021 11:25:12 GMT+0900 (대한민국 표준시)",
"content": "test-03",
"writer": "biuea",
"userId": "bb49cad9-1d76-41b2-abad-7a271c4394ca",
"postId": "61720baeb2731c19e165a175",
"__v": 0
}
]
},
"message": "Get post data"
}
3) 댓글 삭제
댓글이 정상적으로 삭제되는 모습을 볼 수 있습니다.
테스트가 잘 진행되었으니 다음 포스트에선 프론트에서 댓글 기능을 연동해보도록 하겠습니다.