Skip to content

Commit

Permalink
feat: 리뷰 좋아요 등록/해제 구현 (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
punchdrunkard authored Aug 27, 2024
1 parent f365d1b commit 9882023
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ public ResponseEntity<BasicResponse<PageResponse<FestivalInfo>>> getFestivalsInT
@PatchMapping("/{festivalId}/bookmark")
public ResponseEntity<BasicResponse<FestivalBookmarkResponse>> updateFestivalBookmark(
@AuthUser User user,
@PathVariable Long festivalId
) {
@PathVariable Long festivalId) {
checkLogin(user);
FestivalBookmarkResponse bookmark = festivalBookmarkService.updateFestivalBookmark(user, festivalId);
return ResponseEntity.ok(BasicResponse.ok("페스티벌 북마크 등록/해제 성공", bookmark));
}
Expand All @@ -246,20 +246,14 @@ public ResponseEntity<BasicResponse<List<FestivalInfo>>> getRecommendFestival(
@AuthUser User user,
@RequestParam(required = false, defaultValue = "5") Long size
) {
checkLoginUser(user);
checkLogin(user);

List<FestivalInfo> recommendFestivals = festivalService.getRecommendFestivals(
isNull(user) ? null : user.getId(), size);

return ResponseEntity.ok(BasicResponse.ok("유형별 추천 페스티벌 조회 성공", recommendFestivals));
}

private static void checkLoginUser(User user) {
if (isNull(user)) {
throw new CustomException(UNAUTHENTICATED_USER);
}
}

private void validateFestivalDay(int year, int month, int day) {
YearMonth yearMonth = YearMonth.of(year, month);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -28,9 +30,11 @@
import com.odiga.fiesta.review.dto.request.ReviewCreateRequest;
import com.odiga.fiesta.review.dto.response.ReviewIdResponse;
import com.odiga.fiesta.review.dto.response.ReviewKeywordResponse;
import com.odiga.fiesta.review.dto.response.ReviewLikeResponse;
import com.odiga.fiesta.review.dto.response.ReviewResponse;
import com.odiga.fiesta.review.dto.response.ReviewSimpleResponse;
import com.odiga.fiesta.review.dto.response.TopReviewKeywordsResponse;
import com.odiga.fiesta.review.service.ReviewLikeService;
import com.odiga.fiesta.review.service.ReviewService;
import com.odiga.fiesta.user.domain.User;

Expand All @@ -51,6 +55,7 @@
public class ReviewController {

private final ReviewService reviewService;
private final ReviewLikeService reviewLikeService;

@PostMapping(consumes = MULTIPART_FORM_DATA_VALUE)
@Operation(summary = "리뷰 생성", description = "리뷰를 생성합니다.")
Expand All @@ -68,6 +73,15 @@ public ResponseEntity<BasicResponse<ReviewIdResponse>> createReview(
.body(BasicResponse.created("리뷰 생성 성공", review));
}

@PatchMapping("/{reviewId}/like")
@Operation(summary = "리뷰 좋아요 등록 / 해제", description = "리뷰 좋아요를 등록 또는 해제합니다.")
public ResponseEntity<BasicResponse<ReviewLikeResponse>> updateReviewLike(
@AuthUser User user,
@PathVariable Long reviewId) {
checkLogin(user);
ReviewLikeResponse reviewLike = reviewLikeService.updateReviewLike(user.getId(), reviewId);
return ResponseEntity.ok(BasicResponse.ok("리뷰 좋아요 등록 / 해제 성공", reviewLike));
}

@GetMapping("/keywords")
@Operation(summary = "모든 리뷰 키워드 조회", description = "리뷰 키워드 목록을 조회합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.odiga.fiesta.review.dto.response;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@AllArgsConstructor
@Builder
public class ReviewLikeResponse {

private Long reviewId;
private Long likeCount;
private Boolean isLiked;
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
package com.odiga.fiesta.review.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.odiga.fiesta.review.domain.ReviewLike;

public interface ReviewLikeRepository extends JpaRepository<ReviewLike, Long> {

List<ReviewLike> findByReviewId(Long reviewId);
Long countByReviewId(Long reviewId);

@Query("SELECT rl.reviewId, COUNT(rl.id) as likeCount " +
"FROM ReviewLike rl " +
"GROUP BY rl.reviewId " +
"ORDER BY likeCount DESC")
List<Object[]> findReviewsWithLikeCount();
Optional<ReviewLike> findByUserIdAndReviewId(Long userId, Long reviewId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.odiga.fiesta.review.service;

import static com.odiga.fiesta.common.error.ErrorCode.*;
import static java.util.Objects.*;

import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.odiga.fiesta.common.error.exception.CustomException;
import com.odiga.fiesta.review.domain.ReviewLike;
import com.odiga.fiesta.review.dto.response.ReviewLikeResponse;
import com.odiga.fiesta.review.repository.ReviewLikeRepository;
import com.odiga.fiesta.review.repository.ReviewRepository;
import com.odiga.fiesta.user.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReviewLikeService {

private final ReviewRepository reviewRepository;
private final ReviewLikeRepository reviewLikeRepository;
private final UserRepository userRepository;

@Transactional
public ReviewLikeResponse updateReviewLike(Long userId, Long reviewId) {
validateUser(userId);
validateReview(reviewId);

Optional<ReviewLike> optionalReviewLike = reviewLikeRepository.findByUserIdAndReviewId(userId, reviewId);
boolean isLiked = optionalReviewLike.isPresent();

optionalReviewLike.ifPresentOrElse(reviewLikeRepository::delete,
() -> reviewLikeRepository.save(ReviewLike.builder()
.userId(userId)
.reviewId(reviewId)
.build())
);

Long likeCount = reviewLikeRepository.countByReviewId(reviewId);

return ReviewLikeResponse.builder()
.reviewId(reviewId)
.isLiked(!isLiked)
.likeCount(likeCount)
.build();
}

private void validateReview(Long reviewId) {
if (!reviewRepository.existsById(reviewId)) {
throw new CustomException(REVIEW_NOT_FOUND);
}
}

private void validateUser(Long userId) {
if (isNull(userId)) {
throw new CustomException(UNAUTHENTICATED_USER);
}

userRepository.findById(userId).orElseThrow(() -> new CustomException(USER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
import static java.util.Objects.*;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
Expand Down Expand Up @@ -39,6 +36,7 @@
import com.odiga.fiesta.review.dto.response.TopReviewKeywordsResponse;
import com.odiga.fiesta.review.repository.ReviewImageRepository;
import com.odiga.fiesta.review.repository.ReviewKeywordRepository;
import com.odiga.fiesta.review.repository.ReviewLikeRepository;
import com.odiga.fiesta.review.repository.ReviewRepository;
import com.odiga.fiesta.user.repository.UserRepository;

Expand All @@ -50,6 +48,7 @@
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ReviewService {
private final ReviewLikeRepository reviewLikeRepository;

private static final String REVIEW_DIR_NAME = "review";

Expand Down
4 changes: 4 additions & 0 deletions src/test/java/com/odiga/fiesta/ControllerTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.odiga.fiesta.log.controller.LogController;
import com.odiga.fiesta.log.service.LogService;
import com.odiga.fiesta.review.controller.ReviewController;
import com.odiga.fiesta.review.service.ReviewLikeService;
import com.odiga.fiesta.review.service.ReviewService;
import com.odiga.fiesta.user.domain.User;

Expand Down Expand Up @@ -82,6 +83,9 @@ public abstract class ControllerTestSupport {
@MockBean
protected ReviewService reviewService;

@MockBean
protected ReviewLikeService reviewLikeService;

private User user;
private UserAccount userAccount;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ void updateFestivalBookmark_UserNotFound() {
.hasMessage(USER_NOT_FOUND.getMessage());
}

@DisplayName("페스티벌 북마크 등록/해제 - 존재하지 않는 페스티벌에 좋아요 등록 시 에러 발생")
@DisplayName("페스티벌 북마크 등록/해제 - 존재하지 않는 페스티벌에 북마크 등록 시 에러 발생")
@Test
void updateFestivalBookmark_FestivalNotFound() {
// given
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.odiga.fiesta.review.service;

import static com.odiga.fiesta.common.error.ErrorCode.*;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

import com.odiga.fiesta.IntegrationTestSupport;
import com.odiga.fiesta.review.domain.Review;
import com.odiga.fiesta.review.domain.ReviewLike;
import com.odiga.fiesta.review.dto.response.ReviewLikeResponse;
import com.odiga.fiesta.review.repository.ReviewLikeRepository;
import com.odiga.fiesta.review.repository.ReviewRepository;
import com.odiga.fiesta.user.domain.User;
import com.odiga.fiesta.user.repository.UserRepository;

class ReviewLikeServiceTest extends IntegrationTestSupport {

@Autowired
private ReviewLikeService reviewLikeService;

@Autowired
private ReviewLikeRepository reviewLikeRepository;

@Autowired
private ReviewRepository reviewRepository;

@Autowired
private UserRepository userRepository;

@DisplayName("리뷰 좋아요 등록/해제 - 좋아요 등록")
@Test
void updateReviewLike_AddLike() {
// given
Review review = reviewRepository.save(createReview());
User user = userRepository.save(createUser());

// when
ReviewLikeResponse reviewLikeResponse = reviewLikeService.updateReviewLike(user.getId(), review.getId());

// then
assertThat(reviewLikeResponse.getIsLiked()).isTrue();
assertEquals(1, reviewLikeResponse.getLikeCount());
}

@DisplayName("리뷰 좋아요 등록/해제 - 좋아요 해제")
@Test
void updateReviewLike_RemoveLike() {
// given
Review review = reviewRepository.save(createReview());
User user = userRepository.save(createUser());
ReviewLike reviewLike = createReviewLike(user, review);
reviewLikeRepository.save(reviewLike);

// when
ReviewLikeResponse reviewLikeResponse = reviewLikeService.updateReviewLike(user.getId(), review.getId());

// then
assertThat(reviewLikeResponse.getIsLiked()).isFalse();
assertEquals(0, reviewLikeResponse.getLikeCount());
}

@DisplayName("리뷰 좋아요 등록/해제 - 존재하지 않는 유저의 경우 에러 발생")
@Test
void updateReviewLike_UserNotFound() {
// given
Review review = reviewRepository.save(createReview());

User deletedUser = userRepository.save(createUser());
userRepository.delete(deletedUser);

// when // then
assertThatThrownBy(() -> reviewLikeService.updateReviewLike(deletedUser.getId(), review.getId()))
.hasMessage(USER_NOT_FOUND.getMessage());
}

@DisplayName("리뷰 좋아요 등록/해제 - 존재하지 않는 리뷰에 좋아요 등록 시 에러 발생")
@Test
void updateReviewLike_ReviewNotFound() {
// given
User user = userRepository.save(createUser());

// when // then
assertThatThrownBy(() -> reviewLikeService.updateReviewLike(user.getId(), -1L))
.hasMessage(REVIEW_NOT_FOUND.getMessage());
}

private static ReviewLike createReviewLike(User user, Review review) {
return ReviewLike.builder()
.userId(user.getId())
.reviewId(review.getId())
.build();
}

private static Review createReview() {
return Review.builder()
.userId(1L)
.festivalId(1L)
.rating(5)
.content("리뷰 내용")
.build();
}

private User createUser() {
return User.builder()
.userTypeId(1L)
.nickname("테스트 유저")
.statusMessage("상태 메시지")
.profileImage("프로필 이미지 링크")
.build();
}

}

0 comments on commit 9882023

Please sign in to comment.