본문 바로가기
[Project] CafeHub

[CafeHub] 코드 분석 및 이슈 기록 - 리뷰 상세 / 리뷰 찜 / 리뷰 추천

by heosj 2024. 1. 5.

✔️ 주요 코드 중심 분석 ✔️


리뷰 상세 보기

Front

useEffect(() => {
  let getDetailURL = `${url}/review/${reviewNo}`; // 1
  if (isLogin) { getDetailURL += `?memNo=${memNo}`; }
  axios.get(getDetailURL)
  .then((res) => {
    setReview(res.data.review); // 2
    setLike(res.data.isLike); // 3
    setWish(res.data.isWish); // 4
    setLikeCount(res.data.review.likeCount); // 5
  })
  .catch((error) => {
    console.error("에러:" + error);
  });
}, [reviewNo]);
  1.  review/{reviewNo} 엔드포인트로 리뷰 번호와 (회원 번호)를 담아 GET 요청 전달
  2. 응답 데이터로 받은 리뷰 정보 저장
  3. 응답 데이터로 받은 추천  여부 저장
  4. 응답 데이터로 받은 찜 여부 저장
  5. 응답 데이터로 받은 추천수 저장
관련 이슈 1
리뷰 상세 정보를 가져올 때,
로그인 여부에 따라 찜 / 추천 여부를 함께 가져와야 함
(찜 / 추 현재 상태 표시를 위해)

해결 방법
let getDetailURL = `${url}/review/${reviewNo}`;
if (isLogin) { getDetailURL += `?memNo=${memNo}`; }

로그인 하지 않은 방문자의 경우 기본 리뷰 상세 정보만 요청

로그인 한 회원의 경우 해당 회원 번호를 파라미터로 전달 해 해당 리뷰에 대한 찜 / 추 여부를 boolean 값으로 받음
관련 이슈 2
리뷰 상세 정보를 요청하는 페이지가 많았음
1. 사용자 마이페이지 - 찜한 리뷰 리스트, 리뷰 관리, 댓글 관리
2. 사장님 마이페이지 - 특정 카페에 작성된 리뷰 리스트
3. 지도 - 특정 카페에 작성된 리뷰 리스트
4. 리뷰 목록
특히, 찜한 리뷰 리스트에서 요청하는 경우 모달로 띄움

해결 방법
const ReviewDetail = ({ modalDetail, wishReviewNo }) => {
  const location = useLocation();
  const listReviewNo = location.state?.reviewNo;
  const reviewNo = wishReviewNo || listReviewNo;
// 생략

 useLocation() hook을 사용하여 현재 페이지의 상태에서 reviewNo 값을 가져옴
 만약 상태가 존재하지 않는 경우 props 값을 선택하고, 존재하는 경우 해당 값을 선택하도록 함
 이때, 옵셔널 체이닝 연산자(?)를 사용해 속성이 없는 경우 발생하는 TypeError를 방지

navigate('/reviewDetail/' + reviewNo, { state: { reviewNo: reviewNo } });
// 또는
<Link to={`/reviewDetail/${review.reviewNo}`} state={{ reviewNo: `${review.reviewNo}` }} >

 1. 리뷰 상세 정보를 요청하며 페이지 이동을 처리하는 경우, useNavigate()에 상태를 담아 전달

<ReviewDetail modalDetail wishReviewNo = {wishReviewNo}/>

 2. 찜한 리뷰 리스트에서 모달로 띄우려고 하는 경우, props를 전달

 

Back

@GetMapping("/review/{reviewNo}")
public ResponseEntity<Object> getReviewDetail(@PathVariable Integer reviewNo,
                                          @RequestParam(required = false) Integer memNo) { // 1
    try {
        Map<String, Object> res = new HashMap<>();
        ReviewDetailDto review = reviewService.reviewDetail(reviewNo);
        res.put("review", review);
        boolean isLike = reviewService.isLikeReview(memNo, reviewNo);
        res.put("isLike", isLike);
        boolean isWish = reviewService.isWishReview(memNo, reviewNo);
        res.put("isWish", isWish);
        Integer modDate = review.getRegDate()
        		.plusDays(3).getDayOfYear() - LocalDateTime.now().getDayOfYear(); // 2
        res.put("modDate", modDate);
        return new ResponseEntity<>(res, HttpStatus.OK);
    } catch (Exception e) {
        e.printStackTrace();
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}
public ReviewDetailDto reviewDetail(Integer reviewNo) throws Exception {
    return detailRepository.findReviewByReviewNo(reviewNo);
}

public boolean isLikeReview(Integer memNo, Integer reviewNo) throws Exception {
    return likeRepository.existsByMember_memNoAndReview_reviewNo(memNo, reviewNo); // 3
}

public boolean isWishReview(Integer memNo, Integer reviewNo) throws Exception {
    return wishRepository.existsByMember_memNoAndReview_reviewNo(memNo, reviewNo); // 3
}
  1. 클라이언트에서 전달 받은 데이터를 통해 서비스 레이어로 해당 회원 정보 요청
  2. 우리 서비스는 리뷰 수정이 작성일로부터 3일간만 가능하기 때문에, 수정 가능 일자와 찜 / 추 정보를 함께 저장
  3. 데이터베이스에서 해당 회원이 리뷰에 찜 / 추천한 데이터를 찾아 반환

리뷰 추천하기

Front

const toggleLike = () => {
    if (isLogin) { // 1
      axios.post(`${url}/member/like/${memNo}/${reviewNo}`, null, // 2
        {
          headers: {
            Authorization: accessToken,
            Refresh: getCookie("refreshToken")
          }
        })
        .then((res) => {
          tokenCreate(dispatch, setCookie, res.headers)
            .then(() => {
              setLike(res.data.toggleLike); // 3
              setLikeCount(res.data.likeCount); // 4
            })
        })
// 이하 생략
  1. 로그인  한 회원만 사용할 수 있는 기능이므로 로그인 여부 먼저 체크
  2. member/like/{memNo}/{reviewNo} 엔드포인트로  POST 요청 전달
  3. 응답 데이터를 통해 리뷰 추천 / 취소
  4. 응답 데이터를 통해 리뷰 추천  개수 카운트 (선택 / 취소 즉시 추천 수 변화)

Back

@PostMapping("member/like/{memNo}/{reviewNo}")
public ResponseEntity<Object> isLikeReview(@PathVariable Integer memNo, 
						@PathVariable Integer reviewNo) {
    try {
        Map<String, Object> res = new HashMap<>();
        boolean toggleLike = reviewService.toggleLikeReview(memNo, reviewNo); // 1
        res.put("toggleLike", toggleLike); // 2
        Integer likeCount = reviewService.reviewDetail(reviewNo).getLikeCount(); // 1
        res.put("likeCount", likeCount); // 2
        return new ResponseEntity<>(res, HttpStatus.OK);
    } catch (Exception e) {
        e.printStackTrace();
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}
@Transactional // 3
public boolean toggleLikeReview(Integer memNo, Integer reviewNo) throws Exception {
    Review review = reviewRepository.findByReviewNo(reviewNo);
    Member member = memberRepository.findByMemNo(memNo);
    boolean isLike = likeRepository.existsByMember_memNoAndReview_reviewNo(memNo, reviewNo);
    if (isLike) {
        likeRepository.deleteByMember_memNoAndReview_reviewNo(memNo, reviewNo);
        review.setLikeCount(review.getLikeCount() - 1); // 추천 수 감소
        return false; // 추천 취소
    } else {
        likeRepository.save(LikeReview.builder().member(member).review(review).build());
        review.setLikeCount(review.getLikeCount() + 1); // 추천 수 증가
        return true; // 추천
    }
}
  1. 클라이언트에서 전달 받은 데이터를 통해 서비스 레이어로 해당 정보 요청
  2. 추천  선택 / 취소와 추천수를 담아 결과 반환
  3. 추천  선택 / 취소와 추천수를 업데이트 하는 작업을 하나의 트랜잭션으로 처리하여 데이터 일관성 유지

리뷰 찜하기

추천수 카운트하는 부분을 제외하면, 리뷰 추천과 동일한 로직을 가지고 있음