찔끔찔끔씩😎

[Sopt] 4차 세미나(2) - Nodejs API 실습 본문

Server/Nodejs

[Sopt] 4차 세미나(2) - Nodejs API 실습

댕경 2022. 5. 18. 01:34
728x90


API 실습

🔎 필요한 Collection?

 

 이러한 뷰 형태를 위한 Collection은?

- 영화 정보를 저장할 Movie Collection

- 리뷰 정보를 저장할 Review Collection

 

그럼 이 뷰에서 만들 수 있는 API는?

- 영화 정보 저장 API

- 영화 정보 조회 API

- 리뷰 작성 API

- 리뷰 조회 API

등등

 

 


리뷰 생성

🔎 1. Model 만들기

Movie 모델 (src/models/Movie.ts)

import mongoose from "mongoose";
import { MovieInfo } from "../interfaces/movie/MovieInfo";

const MovieSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    director: {
        type: String,
        required: true
    },
    startDate: {
        type: Date
    },
    thumbnail: {
        type: String
    },
    story: {
        type: String
    }
});

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

Review 모델 (src/models/Review.ts)

- Review의 writer, movie는 기존에 만들어둔 넘들의 Object를 참조하도록 한다.

- 형식은 ref: "ModelName" 으로 참조한다.

import mongoose from "mongoose";
import { ReviewInfo } from "../interfaces/review/ReviewInfo";

const ReviewSchema = new mongoose.Schema({
    writer: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "User" 
        // id를 직접 참조하는 reference 방식임을 지정
    },
    movie: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "Movie"
        // id를 직접 참조하는 reference 방식임을 지정
    },
    title: {
        type: String,
        required: true
    },
    content: {
        type: String
    }
});

export default mongoose.model<ReviewInfo & mongoose.Document>("Review", ReviewSchema);

 

Interface 에 Info 만들기

MovieInfo.ts (src/interfaces/movie/MovieInfo.ts)

export interface MovieInfo{
    title:string;
    director:string;
    startDate:Date;
    thumbnail:string;
    story:string;
}

ReviewInfo.ts (src/interfaces/review/ReviewInfo.ts)

import mongoose from 'mongoose';

export interface ReviewInfo {
    writer: mongoose.Types.ObjectId;
    movie: mongoose.Types.ObjectId;
    title: string;
    content: string;
}

🔎 2. Collection 생성

src/loaders/db.ts

서버 실행과 동시에 빈 Collection을 생성하도록 코드를 추가한다.

    Movie.createCollection().then(function (collection) {
      console.log("Movie Collection is created!");
    });

    Review.createCollection().then(function (collection) {
      console.log("Review Collection is created!");
    });

📍 express-validator

express-validator 란 validation 을 위한 npm 라이브러리로 여러가지 validation 함수를 제공한다.

아래 명령어로 내려받기.

yarn add express-validator

🔎 3. Dto 만들기

리뷰 작성 및 조회 API를 위한 DTO

src/interfaces/review/ReviewCreateDto.ts

import mongoose from 'mongoose';

export interface ReviewCreateDto {
    writer: mongoose.Types.ObjectId;
    title: string;
    content: string;
}

src/interfaces/review/ReviewResponseDto.ts

import { MovieInfo } from "../movie/MovieInfo";

export interface ReviewResponseDto {
    writer: string;
    movie: MovieInfo;
    // movie는 movie에 대한 모든 정보 조회
    title: string;
    content: string;
}

🔎 4. Controller 만들기

src/controllers/ReviewController.ts

- express-validator에서 제공하는 validationResult를 사용할 것이기 때문에, import 해주기

- validationResult(req) : req 에 error 가 있는지 판단한 후에 변수에 넣어준다.

/**
 * @route POST /review/movies/:movieId
 * @desc Create Review
 * @access Public
 */

const createReview = async (req: Request, res: Response) => {
    // body 검사에 대한 result가 들어온다.
    const error = validationResult(req);
    if (!error.isEmpty()) {
        // error 가 있다면(비어있지 않다면)
        return res
            .status(statusCode.BAD_REQUEST)
            .send(util.fail(statusCode.BAD_REQUEST, message.NULL_VALUE));
    }

    const reviewCreateDto: ReviewCreateDto = req.body;
    const { movieId } = req.params;
    try {
        const data = await ReviewService.createReview(movieId, reviewCreateDto);

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

🔎 5. Router 만들기

src/routes/ReviewRouter.ts

- express-validator에서 제공하는 body 를 사용하여 validation을 적용한다.

- controller로 넘기기 전에 validator가 body를 검사한다.

- 해당 코드에서는 .notEmpty()를 사용하여 해당 필드가 비어있는지 검사한다.

import { body } from 'express-validator';

const router: Router = Router();

// validation 적용
router.post(
    '/movies/:movieId',
    [
        // controller로 넘어가기 전에 validator가 body를 검사함
        body('title').notEmpty(),
        body('writer').notEmpty(),
        body('content').notEmpty(),
    ],
    ReviewController.createReview,
);

src/routes/index.ts

- index에도 잊지 않고 ReviewRouter를 연결해준다.

🔎 6. Service 만들기

src/services/ReviewService.ts

- 리뷰 작성시에  Reference(Movie)는 ObjectId를 그대로 저장한다.

const createReview = async (
    movieId: string,
    reviewCreateDto: ReviewCreateDto,
): Promise<PostBaseResponseDto> => {
    try {
        const review = new Review({
            title: reviewCreateDto.title,
            content: reviewCreateDto.content,
            writer: reviewCreateDto.writer,
            movie: movieId, // reference는 ObjectId를 그대로 저장
        });

        await review.save();

        const data = {
            _id: review._id,
        };

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

 

📍 req.body 를 몇개 빠뜨린다면?

express-validator 에 의해 아까 정의한대로 validationResult 에서 error를 반환하기 때문에

400 BAD REQUEST를 반환한다!


리뷰 조회

🔎 1. Controller 만들기

src/controller/ReviewController.ts

/**
 *  @route GET /review/movies/:movieId
 *  @desc Get Review
 *  @access Public
 */
const getReviews = async (req: Request, res: Response) => {
    const { movieId } = req.params;

    try {
        const data = await ReviewService.getReviews(movieId); //서비스 로직 호출

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

🔎 2. Router 만들기

src/routes/ReviewRouter.ts

router.get('/movies/:movieId', ReviewController.getReviews);

🔎 3. Service 만들기

src/services/ReviewService.ts

const getReviews = async (movieId: string): Promise<ReviewResponseDto[]> => {
    try {
        // 걍 find를 하면 ObjectId가 반환될 것.
        // 해당 Id의 특정 정보를 받아오기 위해 ""populate"" 사용
        // 1) movie == movieId 인넘 find
        // 2) populate(reference되어있는 넘, 그넘중 불러올 넘)
        // ex) .populate('writer', 'name').populate('movie'); : writer의 이름, movie 전체 정보 가져오기
        const reviews = await Review.find({
            movie: movieId,
        })
            // populate(path: ref되어있는 필드명, select: 특정 필드)
            .populate('writer', 'name')
            .populate('movie');
        
        console.log(reviews); 
        // 확인해보면 객체까지 같이 반환하는데, 딱 name과 정보만 보내려한다!

        const data = await Promise.all(
            // map: 배열을 돌면서 작업함
            reviews.map(async (review: any) => {
                const result = {
                    writer: review.writer.name, // writer 이름정보만
                    movie: review.movie, // movie 전체정보
                    title: review.title,
                    content: review.content,
                };

                return result;
            }),
        );

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

 

📍위에서 쓴 Populate !!

document 의 경로를 다른 collection의 실제 document로 자동으로 바꾸는 방법이다.

즉, 다른 document 의 ObjectId를 실제 객체로 반환해준다.

사용방법

const reviews = await Review.find({ movie: movieId,}).populate('writer', 'name').populate('movie');

- 이때 그냥 find를 하면 ObjectId 가 반환될 것이다. 해당 Id의 특정 정보를 받아오기 위해 populate를 사용하는 것이다.

1) movie == movieId 인 넘을 find 한 뒤

2) populate(reference 되어 있는 넘, 그 넘 중 불러올 정보)

3) .populate('writer', 'name').populate('movie'); : writer의 이름, movie 전체 정보 가져오기

📍Populate  를 넘겨줄 때

Pomise.all을 사용하여 새로운 데이터 배열을 생성해준다.

 await Promise.all(reviews.map(async (review: any)

DTO에 맞게 데이터를 가공해 준다. 

ex) review.writer.name 으로 writer의 name만 가져오기

Comments