오늘 3계층 코드를 완성하고 API 테스트까지 완료하였다.
추후 변경될 여지가 있지만 일단 대부분의 유효성 검사를 서비스 계층에 몰아 작성되는 것을 기준으로 완성시켰다.
// src/repositories/reviews.repository.js
import { prisma } from '../utils/prisma/index.js';
class ReviewsRepository {
#orm;
constructor(orm) {
this.#orm = orm;
}
// 음식점 별 리뷰
findALLReviewByRestaurantId = async (restaurantId) => {
const resReviews = await this.#orm.review.findMany({
where: { restaurantId },
});
return resReviews;
};
// 내 리뷰 조회 (userId로)
findAllMyReviews = async (userId) => {
const myReviews = await this.#orm.review.findMany({
where: { userId },
});
return myReviews;
};
// 결제별로 하나만 있는 리뷰 조회 (결제 내역에 보이는 리뷰를 선택하여 상세하게 보려 한다면)
findReviewByPayId = async (paymentId) => {
const reviewByPay = await this.#orm.review.findUnique({
where: { paymentId },
});
return reviewByPay;
};
// reviewId로 리뷰 찾기 (업데이트, 삭제 비즈니스 로직에 사용)
findReviewByReviewId = async (reviewId) => {
const reviewByReviewId = await this.#orm.review.findUnique({
where: { reviewId },
});
return reviewByReviewId;
};
// 추가할만한 메서드
// 별점별 리뷰 조회 메서드 ex) 4점이상 리뷰, 5점 이상 리뷰
// 리뷰 정렬 기능 > 최신순 조회 메서드, 별점순 조회 메서드 등
// 음식점별 리뷰 통계 조회 메서드
createReview = async ({ restaurantId, paymentId, userId, content, star }) => {
const createdReview = await this.#orm.review.create({
data: {
restaurantId: restaurantId,
paymentId: paymentId,
userId: userId,
content: content,
star: star,
},
});
return createdReview;
};
// 내 리뷰 업데이트
updateReview = async ({ reviewId, content, star }) => {
const updatedReview = await this.#orm.review.update({
where: {
reviewId: reviewId,
},
data: {
...(content && { content }),
...(star && { star }),
},
});
return updatedReview;
};
deleteReview = async (reviewId) => {
const deletedReview = await this.#orm.review.delete({
where: {
reviewId,
},
});
return deletedReview;
};
}
export default new ReviewsRepository(prisma);
// src/services/reviews.service.js
import ReviewsRepository from '../repositories/reviews.repository.js';
class ReviewsService {
#repository;
constructor(repository) {
this.#repository = repository;
}
// 음식점별 리뷰
// data : restaurantId
findALLReviewByRestaurantId = async (data) => {
if (!Number.isInteger(data.restaurantId)) {
// 로그에 오류 기록
console.error('Validation Error: restaurantId는 정수여야 합니다.', {
restaurantId: data.restaurantId,
});
throw new Error('잘못된 요청입니다.');
}
// 저장소(Repository)에게 데이터를 요청합니다.
const reviews = await this.#repository.findALLReviewByRestaurantId(
data.restaurantId,
);
if (!reviews) {
throw new Error('매장에 리뷰가 없습니다.');
}
// 호출한 reviews를 가장 최신 게시글 부터 정렬합니다.
reviews.sort((a, b) => {
return b.createdAt - a.createdAt;
});
// 비즈니스 로직을 수행한 후 사용자에게 보여줄 데이터를 가공합니다.
// 서비스 계층에서는 return posts.map(post => {}); 와 같이 데이터를 가공하는 작업이 이루어짐
// 데이터를 그대로 전달하다 민감한 정보까지 노출되는 문제가 발생할 수 있기 때문에 가공
return reviews.map((review) => {
return {
// 클라이언트에서 어떻게 쓰일지 몰라 일단 다 보내는 중
restaurantId: review.restaurantId,
paymentId: review.paymentId,
userId: review.userId,
content: review.content,
createdAt: review.createdAt,
};
});
};
// 내 모든 리뷰
// data : userId
findAllMyReviews = async (data) => {
if (!Number.isInteger(data.userId)) {
console.error('Validation Error: userId 정수여야 합니다.', {
userId: data.userId,
});
throw new Error('잘못된 요청입니다.');
}
// 저장소(Repository)에게 데이터를 요청합니다.
const reviews = await this.#repository.findAllMyReviews(data.userId);
// 본인의 리뷰만 조회 가능
if (reviews.userId !== data.userId) throw new Error('잘못된 요청입니다.');
reviews.sort((a, b) => {
return b.createdAt - a.createdAt;
});
if (!reviews) {
throw new Error('작성한 리뷰가 없습니다.');
}
return reviews.map((review) => {
return {
restaurantId: review.restaurantId,
paymentId: review.paymentId,
userId: review.userId,
content: review.content,
createdAt: review.createdAt,
};
});
};
// 결제별 리뷰
// data : paymentId,userId
findReviewByPayId = async (data) => {
if (!Number.isInteger(data.userId) || !Number.isInteger(data.paymentId)) {
console.error('Validation Error: userId, paymentId는 정수여야 합니다.', {
userId: data.userId,
paymentId: data.paymentId,
});
throw new Error('잘못된 요청입니다.');
}
// 저장소(Repository)에게 특정 게시글 하나를 요청합니다.
const reviews = await this.#repository.findReviewByPayId(data.paymentId);
if (!reviews) {
throw new Error('작성한 리뷰가 없습니다.');
}
return {
postId: post.postId,
nickname: post.nickname,
title: post.title,
content: post.content,
createdAt: post.createdAt,
updatedAt: post.updatedAt,
};
};
// 리뷰 생성
// data : restaurantId: paymentId, userId, content, star
createReview = async (data) => {
if (
!data.restaurantId ||
!data.paymentId ||
!data.userId ||
!data.content ||
data.star === null
)
throw new Error('필수 필드가 누락되었습니다.');
const existingReview = await this.#repository.findReviewByPayId(
data.paymentId,
);
if (existingReview)
throw new Error('결제별로 하나의 리뷰만 작성할 수 있습니다.');
if (data.star < 1 || data.star > 5)
throw new Error('별점은 1에서 5 사이여야 합니다.');
if (data.content.length < 10 || data.content.length > 100)
throw new Error('리뷰 내용은 10자 이상 100자 이하이어야 합니다.');
if (
!Number.isInteger(data.userId) ||
!Number.isInteger(data.restaurantId) ||
!Number.isInteger(data.paymentId)
) {
// 로그에 오류 기록
console.error(
'Validation Error: userId, restaurantId, paymentId는 정수여야 합니다.',
{
userId: data.userId,
restaurantId: data.restaurantId,
paymentId: data.paymentId,
},
);
// 클라이언트에게는 일반적인 오류 메시지 반환
throw new Error('잘못된 요청입니다.');
}
// 저장소에 요청
return await this.#repository.createReview(data);
};
// 리뷰 수정
// data : reviewId, userId, content, star
updateReview = async (data) => {
// 저장소(Repository)에게 특정 게시글 하나를 요청합니다.
const review = await this.#repository.findReviewByReviewId(data.reviewId);
if (!review) throw new Error('존재하지 않는 리뷰입니다.');
if (review.userId !== data.userId)
throw new Error('본인의 리뷰만 수정할 수 있습니다.');
if (data.star < 1 || data.star > 5)
throw new Error('별점은 1에서 5 사이여야 합니다.');
if (data.content.length < 10 || data.content.length > 100)
throw new Error('리뷰 내용은 10자 이상 100자 이하이어야 합니다.');
// 저장소에 요청
await this.#repository.updateReview(data);
};
// 리뷰 삭제
// data : reviewId, userId
deleteReview = async (data) => {
// 저장소(Repository)에게 특정 게시글 하나를 요청합니다.
const review = await this.#repository.findReviewByReviewId(data.reviewId);
if (!review) throw new Error('이미 존재하지 않는 리뷰입니다.');
if (review.userId !== data.userId)
throw new Error('본인의 리뷰만 삭제할 수 있습니다.');
// 저장소에 요청
await this.#repository.deleteReview(data.reviewId);
};
}
export default new ReviewsService(ReviewsRepository);
// src/services/reviews.service.js
import ReviewsService from '../services/reviews.service.js';
// 데이터 타입 확인, 필수 필드확인은 보통 컨트롤러
class ReviewsController {
#service;
constructor(service) {
this.#service = service;
}
// 음식점 별 리뷰 (인증 X)
findALLReviewByRestaurantId = async (req, res) => {
const { restaurantId } = req.params;
try {
const AllReviewsByRestaurant =
await this.#service.findALLReviewByRestaurantId({
restaurantId: +restaurantId,
});
return res.status(201).json({ data: AllReviewsByRestaurant });
} catch (error) {
return res.status(400).json({ message: '잘못된 요청 입니다.' });
}
};
// 내 리뷰 전체 조회 (인증 O)
findAllMyReviews = async (req, res) => {
const userId = req.user;
try {
const AllMyReviews = await this.#service.findAllMyReviews({
userId: userId,
});
return res.status(201).json({ data: AllMyReviews });
} catch (error) {
return res.status(400).json({ message: '잘못된 요청 입니다.' });
}
};
// 결제id로 리뷰 조회 (인증 O)
findReviewByPayId = async (req, res) => {
const { paymentId } = req.params;
const userId = req.user;
try {
const ReviewByPayId = await this.#service.findReviewByPayId({
paymentId: +paymentId,
userId: userId,
});
return res.status(201).json({ data: ReviewByPayId });
} catch (error) {
return res.status(400).json({ message: '잘못된 요청 입니다.' });
}
};
// 리뷰 생성 (인증 O)
createReview = async (req, res) => {
// Client로 부터 받은 데이터를 가공
const { content, star } = req.body;
// 파라미터로 부터 받은 데이터
// const { restaurantId, paymentId } = req.params;
const { restaurantId } = req.params;
// 인증 미들웨어에서 받은 유저 정보
// const userId = req.user;
// 테스트용 //
const userId = 10;
const paymentId = 3;
// 테스틍용
try {
await this.#service.createReview({
restaurantId: +restaurantId,
paymentId: +paymentId,
userId,
content,
star,
});
return res.status(201).json({ message: '리뷰가 생성되었습니다.' });
} catch (error) {
console.error(error);
return res.status(400).json({ message: '리뷰 생성에 실패했습니다.' });
}
};
// 리뷰 업데이트 (인증 O)
updateReview = async (req, res) => {
// Client로 부터 받은 데이터를 가공
const { content, star } = req.body;
// 파라미터로 부터 받은 데이터
// const { reviewId } = req.params;
// 인증 미들웨어에서 받은 유저 정보
// const userId = req.user;
// 테스트 용
const reviewId = 4;
const userId = 10;
// 테스트 용
// ReviewsService를 이용하여 게시글 생성 요청
try {
await this.#service.updateReview({
reviewId: +reviewId,
userId,
content,
star,
});
return res.status(201).json({ message: '리뷰가 수정되었습니다.' });
} catch (error) {
console.error(error);
return res.status(400).json({ message: '리뷰 수정에 실패했습니다.' });
}
};
// 리뷰 삭제 (인증 O)
deleteReview = async (req, res) => {
// 파라미터로 부터 받은 데이터
// const { reviewId } = req.params;
// 인증 미들웨어에서 받은 유저 정보
// const userId = req.user;
// 테스트 용
const reviewId = 4;
const userId = 10;
// 테스트 용
// PostService를 이용하여 게시글 생성 요청
try {
await this.#service.deleteReview({
reviewId: +reviewId,
userId,
});
// PostService가 반환한 결과를 Client에게 전달
return res.status(201).json({ message: '리뷰가 삭제되었습니다.' });
} catch (error) {
console.error(error);
return res.status(400).json({ message: '리뷰 삭제에 실패했습니다.' });
}
};
}
export default new ReviewsController(ReviewsService);
아직 다른 팀원이 작업중인 인증 부분 코드가 완성되지 않았기 때문에, 테스트용 코드를 추가하여 진행하였다.
추후 추가적인 메서드 작업이나 코드 변경 여지가 생긴다면 적용 예정.
'TIL' 카테고리의 다른 글
내일배움캠프 11주차 WIL (0) | 2025.01.10 |
---|---|
내일배움캠프 11주차 금요일 TIL (0) | 2025.01.10 |
내일배움캠프 11주차 수요일 TIL (0) | 2025.01.08 |
내일배움캠프 11주차 화요일 TIL (0) | 2025.01.07 |
내일배움캠프 10주차 목요일 TIL (0) | 2025.01.02 |