TIL

내일배움캠프 11주차 목요일 TIL

news0516 2025. 1. 9. 21:00

오늘 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);

아직 다른 팀원이 작업중인 인증 부분 코드가 완성되지 않았기 때문에, 테스트용 코드를 추가하여 진행하였다.



리뷰 생성 성공

 

 


추후 추가적인 메서드 작업이나 코드 변경 여지가 생긴다면 적용 예정.