웹 프로그래밍

[Yelpcamp 프로젝트] 리뷰 기능 추가하기

미안하다 강림이 좀 늦었다 2023. 11. 16. 00:01

 

 

스키마 정의

review 스키마를 정의합니다

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const ReviewSchema = new Schema({
    body: {
        type: String,
        required: true
    },
    rating: {
        type: String,
        required: true
    }
});

module.exports = mongoose.model('Review', ReviewSchema);

 

campground 스키마를 수정합니다. 스키마를 정의하는 부분에서 아래 부분을 추가하면 됩니다.

reviews: [
        {
            type: Schema.Types.ObjectId,
            ref: 'Review'
        }
    ]

 

 

리뷰 작성 폼 만들기

show.ejs에서 아래 코드를 추가합니다.

<div class="col-4">
            <div class="card">
                <div class="card-body">
                    <form action="/campgrounds/<%= camp.id %>/reviews" method="POST" class="was-validated">
                        <label for="rating" class="fw-medium form-label fs-5">Rating</label>
                        <input type="range" class="form-range" min="1" max="5" step="1" id="rating"
                            name="review[rating]" required>

                        <label for="body" class="form-label fw-medium fs-5">Review</label>
                        <br>
                        <textarea name="review[body]" id="body" class="form-control" cols="30" rows="3"
                            required></textarea>
                        <div class="invalid-feedback">
                            내용을 입력해주세요.
                        </div>

                        <div class="text-end mt-2">
                            <button class="btn btn-success">Submit</button>
                        </div>
                    </form>
                </div>
            </div>
</div>

별점이 아닌게 매우 킹받지만 나중에 수정해보겠습니다

 

app.js를 수정합니다.

app.post('/campgrounds/:id/reviews', validateReview, catchAsync(async (req, res) => {
    const review = new Review(req.body.review);
    const camp = await Campground.findById(req.params.id);
    await review.save();
    camp.reviews.push(review);
    await camp.save();
    res.redirect(`/campgrounds/${req.params.id}`);
}))

해당 캠핑장에 리뷰를 생성하는 코드입니다. 일단 validateReview는 무시합니다. catchAsync 함수는 비동기함수에서 발생하는 에러를 잡아줍니다.

 

 

유효성 검사 추가

클라이언트측에서 발생할 수 있는 유효성 검사는 다 막아뒀지만 postman같은 곳에서 들어오는 요청에도 유효성 검사를 해야합니다.

const validateReview = (req, res, next) => {
    const { error } = reviewSchema.validate(req.body);
    if (error) {
        const msg = error.details.map(el => el.message).join(',')
        throw new ExpressError(msg, 400)
    } else {
        next();
    }
}

이상한 요청이 들어오면 네가 잘못했다는 400 에러를 날려줍니다.

 

 

리뷰 표시하기

리뷰 작성 폼 밑에 작성된 리뷰들을 띄어보겠습니다.

먼저 show.ejs에 아래 코드를 추가합니다.

<% for(let review of camp.reviews) {%>
	<div class="card mt-3">
		<div class="card-body">
			<p class="card-title fs-5 fw-medium">
				Rating: <%= review.rating %>
			</p>
			<p>
				<%= review.body %>
			</p>
			<form action="/campgrounds/<%= camp.id %>/reviews/<%= review.id %>?_method=DELETE"
                            method="POST">
				<div class="text-end mt-2">
					<button class="btn btn-danger">Delete</button>
				</div>
			</form>
		</div>
	</div>

<% } %>

 

app.js에 아래 코드를 추가합니다.

app.get('/campgrounds/:id', catchAsync(async (req, res) => {
    const camp = await Campground.findById(req.params.id).populate('reviews');
    res.render('show', { camp });
}))

findById뒤에 populate를 추가하였습니다. 캠핑장의 리뷰 배열에는 리뷰의 id들 밖에 없었지만 populate를 써주어 리뷰 배열에 리뷰의 모든 정보가 다 들어가게 됩니다.

Rating이 아주 킹받네요

 

 

리뷰 삭제하기

리뷰를 지우기를 클릭하면 해당 라우트로 delete 요청을 합니다.

<form action="/campgrounds/<%= camp.id %>/reviews/<%= review.id %>?_method=DELETE"
                            method="POST">
	<div class="text-end mt-2">
		<button class="btn btn-danger">Delete</button>
	</div>
</form>

 

app.delete('/campgrounds/:id/reviews/:reviewId', catchAsync(async (req, res) => {
    const { id, reviewId } = req.params;
    await Campground.findByIdAndUpdate(id, { $pull: { reviews: reviewId } });
    await Review.findByIdAndDelete(reviewId);
    res.redirect(`/campgrounds/${id}`);
}))

$pull은 배열에 있는 특정 값을 지워줍니다. 캠핑장 리뷰 배열에서 삭제하려는 리뷰의  id를 먼저 삭제하고, 이후에 삭제하려는 리뷰를 review 집합에서 삭제합니다.

 

 

캠핑장 삭제하기

캠핑장을 삭제하면 그 캠핑장의 리뷰는 어떻게 될까요. 아무도 찾지 못하게 덩그러니 남아있으면 안되기때문에 미들웨어 post를 사용해줍니다.

캠핑장을 삭제할 때 사용한 몽구스 함수의 이름은 findByIdandDelete이지만 사용되는 미들웨어는 findOneandDelete입니다. 왜냐하면

몽구스 공식 문서에서 그렇다고했기 때문입니다.

CampgroundSchema.post('findOneAndDelete', async function (doc) {
    if (doc) {
        await Review.deleteMany({
            id: { $in: doc.reviews }
        })
    }
})

이제 캠핑장을 삭제하면 그 캠핑장에 작성되어있던 모든 리뷰도 함께 사라집니다.