[week_5] 댓글 설정

SH·2022년 5월 20일
0

서버 세미나

목록 보기
11/14

movie 페이지에 movie 댓글을 달 수 있도록 설정해보겠다

Movie 객체 안에 MovieComment가 배열 형태로 들어감


Movie 조회하기

MovieResponseDto.ts

import { MovieCommentInfo } from "./MovieInfo";

export interface MovieResponseDto {
    title: string;
    director: string;
    starDate?: Date;
    thumbnail?: string;
    story?: string;
    comments?: MovieCommentInfo[]; // 댓글 필드 추가
}

MovieController.ts

const getMovieById = async(req: Request, res: Response) => {
    
    const { movieId } = req.params;

    try {
        const data: null | MovieResponseDto = await MovieService.getMovieById(movieId);

        if (!data){
            return res.status(statusCode.NOT_FOUND).send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
        }

        res.status(statusCode.OK).send(util.success(statusCode.OK, message.READ_MOVIE_SUCCESS, data));
        
    } catch (error) {
        console.log(error)
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }

}

controller는 별거 없다


MovieService.ts

const getMovieById = async (movieId: string): Promise<MovieResponseDto | null> => {

    try{
        const data: MovieResponseDto | null = await Movie.findById(movieId).populate('comments.writer', 'name');
        
        if (!data){
            return null
        }

        return data;

    } catch (error) {
        console.log(error);
        throw error;
    }

}

populate로 찾은 movie 객체의 comments.writer 객체의 name만 가져온다 그럼 user의 name phone email 등등 많은 필드 중에서 name만 가져와진다


MovieComment 댓글 달기 (post)

MovieInfo.ts

import mongoose from "mongoose";

export interface MovieInfo{
    title: string;
    director: string;
    starDate: Date;
    thumbnail: string;
    story: string;
    comments: MovieCommentInfo[];
}

export interface MovieCommentInfo {
    writer: mongoose.Types.ObjectId | string;
    comment: string;
}

MovieInfo에 comments 필드를 추가해주고 MovieCommentInfo 인터페이스도 만들어준다
MovieComment는 writer과 comment 두 필드로 구성되어 있음


신기한게 MovieComment는 따로 model로 빼주지 않는다 그냥 Movie.ts 파일에

export default mongoose.model<MovieInfo & mongoose.Document>("Movie", MovieSchema);

이렇게 똑같이 해주기만 함 그냥 movie model 하위에서 해결 가능한 일이라 그런가봄


MovieCommentCreateDto.ts

export interface MovieCommentCreateDto {
    writer: string;
    comment: string;
}

댓글의 createDto는 다음과 같다


MovieController.ts

const createMovieComment = async (req: Request, res: Response) => {
    const error = validationResult(req);

    if (!error.isEmpty()){
        console.log(error);
        return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, message.BAD_REQUEST, res.json({ errors: error.array() })));
    } 

    const movieCommentCreateDto: MovieCommentCreateDto = req.body;
    const { movieId } = req.params;

    try{

        const data = await MovieService.createMovieComment(movieCommentCreateDto, movieId);
        if (!data) return res.status(statusCode.NOT_FOUND).send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));

        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_COMMENT_SUCCESS, data));

    }catch(error){
        console.log(error);
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

컨트롤러 단에서 MovieCommentCreateDto로 req의 body에서 데이터를 받아준다 이걸 service 단으로 넘김


MovieService.ts

const createMovieComment = async (movieCommentCreateDto: MovieCommentCreateDto, movieId: String) => {

    try{

        // 영화 정보 없는 경우 null 반환
        const movie = Movie.findById(movieId);
        if (!movie) return null

        // spread로 기존의 movie.comment 배열에다가 movieCommentCreateDto 추가한 newComments 배열 만들어줌
        // ...movie.comments 빨간줄 에러 - any로 해결
        const newComments: MovieCommentInfo[] = [...(movie as any).comments, movieCommentCreateDto];
        
        // _id가 movieId인 movie 객체를 찾아서 movie.comments를 아까 만든 newComments로 바꿔줌 
        // {new: true}를 지정해주면 업데이트하고 난 뒤의 값을 리턴함
        const updateMovie = await Movie.findOneAndUpdate({ _id: movieId }, { comments: newComments}, { new: true });
        if (!updateMovie) return null; 

        return updateMovie;
    
    } catch(error){
        console.log(error);
        throw(error);
    }

}

const newComments: MovieCommentInfo[] = [...movie.comments, movieCommentCreateDto];

movie.comments는 기존 movie 객체의 comments이다.
전개 연산자를 사용해서 newComments라는 배열에 일단 기존 comment 배열은 movie.comments를 전개해준다.
그리고 나서 movieCommentCreateDto를 넣어주면 newComments에는 기존movie.comment 배열 마지막에 movieCommentCreateDto가 추가된 형태가 된다


전개 연산자 추가 설명
https://pro-self-studier.tistory.com/13

https://velog.io/@recordboy/%EC%A0%84%EA%B0%9C-%EC%97%B0%EC%82%B0%EC%9E%90Spread-Operator


// _id가 movieId인 movie 객체를 찾아서 movie.comments를 아까 만든 newComments로 바꿔줌 
// {new: true}를 지정해주면 업데이트하고 난 뒤의 값을 리턴함
const updateMovie = await Movie.findOneAndUpdate({ _id: movieId }, { comments: newComments}, { new: true });

이후에 findOneAndUpdate() 메소드를 통해 전달받은 movieId를 갖는 Movie 객체를 찾는다. Movie 객체의 comments 배열을 아까 만들어놓았던 newComments로 업데이트해준다. {new: true}를 지정해주면 업데이트하고 난 뒤의 값을 리턴한다. 즉, newComments가 commtents 속성으로 들어간 버전이 반환된다.


findOneAndUpdate() 메소드 추가 설명

https://mongoosejs.com/docs/tutorials/findoneandupdate.html


MovieComment 수정하기 (put)

MovieRouter.ts

router.put('/:movieId/comments/:commentId', [
    body('comment').notEmpty()
], auth, MovieController.updateMovieComment);

댓글을 수정하는 건 해당 댓글을 작성한 사람만이 할 수 있어야 한다. 따라서 중간에 auth를 router에서 middleware로 넣어주었다.


MovieController.ts

/**
 *  @route PUT /movie/:movieId/comments/:commentId
 *  @desc Update Movie Comment
 *  @access Public
 */

const updateMovieComment = async (req: Request, res: Response) => {
    const error = validationResult(req);

    if (!error.isEmpty()){
        console.log(error);
        return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, message.BAD_REQUEST, res.json({ errors: error.array() })));
    } 
    
    const movieCommentUpdateDto: MovieCommentUpdateDto = req.body;
    const { movieId, commentId } = req.params;

    try{

        const data = await MovieService.updateMovieComment(movieId, commentId, req.body.user.id, movieCommentUpdateDto);
        
        if (!data) return res.status(statusCode.NOT_FOUND).send(util.fail(statusCode.NOT_FOUND, message.NOT_FOUND));
        res.status(statusCode.NO_CONTENT).send();

    } catch(error){

        console.log(error);
        res.status(statusCode.NO_CONTENT).send(util.success(statusCode.NO_CONTENT, message.DELETE_MOVIE_SUCCESS));

    }

}

넘겨주는 파라미터가 좀 많다,, movieId, commentId, req.body.user,id, movieCommentUpdateDto를 MovieService에 넘겨주어야 한다 많이 넘겨주는 이유는 url이 이렇기 때문

/movie/:movieId/comments/:commentId

req.body.user.id 로 해댱 url로 접속한 user의 id를 가져올 수 있는 미들웨어인 auth에서 token을 decode한 뒤에 그 토큰에 있는 payload(user Id 정보)를 request Body에 넣어주었기 때문이다. 이를 이해하기 위해서는 middleware/auth.ts 파일과 modules/jwtHandler.ts 파일을 보면 된다 5주차 다른 포스팅에 있음 참고하셈


MovieService.ts

const updateMovieComment = async(movieId: string, commentId: string, userId: string, movieCommentUpdateDto: MovieCommentUpdateDto ) => {

    try {
        const movie = await Movie.findById(movieId);
        if (!movie) return null;


        // 배열을 수정할 때 mongodb raw query를 작성해서 가져옴. $ 붙은 애들이 mongodb의 raw query 문법이라고 생각하면 됨
        const data = await Movie.findOneAndUpdate(

            // _id가 movieId인 Movie 객체를 찾고 그 객체의 comments 배열의 요소 중 comment id가 commentId고 comment writer가 userId인 comment 객체를 가져옴
            { _id: movieId, comments: { $elemMatch: { _id: commentId, writer: userId}}}, 
            {
                // 해당 movie 객체의 해당 comment의 wirter를 userId로, comment를 movieCommentUpdateDto로 수정해줌
                $set: {
                    'comments.$writer': userId,
                    'comments.$comment': movieCommentUpdateDto.comment
                }
            }, { new: true });
        
        return data;

    } catch (error) {
        console.log(error);
        throw error;
    }

}

findOneAndUpdate() 부분이 헬이다

먼저 여기에 쓰인 mongodb raw query 문법부터 보셈

$elemMatch : 배열에서 filter에 맞는 원소를 찾아줌
$set : update set 의 그 set 이라 생각하면 됨! 수정할 사항

일단 첫 번째 중괄호가 filter, 두 번째 중괄호가 바꿀 내용, 마지막 중괄호 {new: true}가 기타 설정(업데이트 하고 난 뒤의 값 반환)이라고 봐주면 된다.

여기서 $ 붙은 부분이 mongodb의 raw쿼리라고 봐주면 된다.

// filter 부분
{ _id: movieId, comments: { $elemMatch: { _id: commentId, writer: userId}}}, 

$elementMatch 란 mongodb에서 배열 안 요소에 대해 필터와 일치하는 요소를 반환하는 문법이다. _id가 movieId인 movie 객체를 찾고, 그 movie 객체의 comments 배열 안에서 _id가 commentId, writer가 userId인 comment 요소를 찾는다.


{
    // 해당 movie 객체의 해당 comment의 wirter를 userId로, comment를 movieCommentUpdateDto로 수정해줌
	$set: {
            'comments.$writer': userId,
            'comments.$comment': movieCommentUpdateDto.comment
          }
}, { new: true });

이후 $set 문법으로 해당 comment의 writer를 userId, comment를 movieCommentUpdateDto.comment로 수정해준다


mongodb query 연산자 추가 자료
https://velog.io/@tkddn2075/MongoDB-%EC%BF%BC%EB%A6%AC-%EC%97%B0%EC%82%B0%EC%9E%90
https://blog.naver.com/shino1025/221294664013

mongodb raw query
이거.. 정독하자
https://gmldbd94.tistory.com/24

profile
블로그 정리안하는 J개발자

0개의 댓글