Skip to content

Commit

Permalink
[BSVR-196] 리뷰 공감 기능 (#146)
Browse files Browse the repository at this point in the history
* feat: reviewLike 도메인, 엔티티 추가

* feat: reviewLikeEntity에 컬럼명 추가

* feat: 공감 관련 repository 추가

* feat: 리뷰 공감 & 공감 취소 서비스 코드 구현

* feat: Review 공감 수 디폴트 값 추가

* fix: 저장된 리뷰와 관련된 builder에 좋아요 수 추가

* feat: 공감 후 리뷰 공감 수 갱신 로직 추가

* feat: 리뷰 공감 controller 추가

* test: review 공감수 테스트 추가

* refactor: 불필요한 코드 삭제

* feat: data.sql update

* test: 리뷰 공감수 차감 테스트 추가

* test: 리뷰 공감수 증가/차감 테스트 추가

* feat: default 공감수 상수 추가

* feat: 공감수 update 쿼리 추가
  • Loading branch information
EunjiShin authored Aug 19, 2024
1 parent 016f677 commit 1b3b4e1
Show file tree
Hide file tree
Showing 21 changed files with 359 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.depromeet.spot.application.review.like;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;

import org.depromeet.spot.application.common.annotation.CurrentMember;
import org.depromeet.spot.usecase.port.in.review.like.ReviewLikeUsecase;
import org.springframework.http.HttpStatus;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;

@RestController
@Tag(name = "리뷰 공감")
@RequiredArgsConstructor
@RequestMapping("/api/v1/reviews")
public class ReviewLikeController {

private final ReviewLikeUsecase reviewLikeUsecase;

@CurrentMember
@ResponseStatus(HttpStatus.OK)
@Operation(summary = "특정 리뷰에 공감한다. 만약 이전에 공감했던 리뷰라면, 공감을 취소한다.")
@PostMapping("/{reviewId}/like")
public void toggleLike(
@PathVariable @Positive @NotNull final Long reviewId,
@Parameter(hidden = true) Long memberId) {
reviewLikeUsecase.toggleLike(memberId, reviewId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum ReviewErrorCode implements ErrorCode {
INVALID_REVIEW_KEYWORDS(
HttpStatus.BAD_REQUEST, "RV004", "리뷰의 'good' 또는 'bad' 중 적어도 하나는 제공되어야 합니다."),
UNAUTHORIZED_REVIEW_MODIFICATION_EXCEPTION(
HttpStatus.BAD_REQUEST, "RV005", "이 리뷰를 수정할 권한이 없습니다.");
HttpStatus.BAD_REQUEST, "RV005", "이 리뷰를 수정할 권한이 없습니다."),
INVALID_REVIEW_LIKES(HttpStatus.INTERNAL_SERVER_ERROR, "RV006", "리뷰 공감 수는 음수일 수 없습니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ public InvalidReviewKeywordsException(String str) {
super(ReviewErrorCode.INVALID_REVIEW_KEYWORDS.appended(str));
}
}

public static class InvalidReviewLikesException extends ReviewException {
public InvalidReviewLikesException() {
super(ReviewErrorCode.INVALID_REVIEW_LIKES);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.List;
import java.util.Map;

import org.depromeet.spot.common.exception.review.ReviewException.InvalidReviewLikesException;
import org.depromeet.spot.domain.block.Block;
import org.depromeet.spot.domain.block.BlockRow;
import org.depromeet.spot.domain.member.Member;
Expand All @@ -20,6 +21,7 @@

@Getter
public class Review {

private final Long id;
private final Member member;
private final Stadium stadium;
Expand All @@ -33,6 +35,9 @@ public class Review {
private List<ReviewImage> images;
private List<ReviewKeyword> keywords;
private transient Map<Long, Keyword> keywordMap;
private int likesCount;

public static final int DEFAULT_LIKE_COUNT = 0;

@Builder
public Review(
Expand All @@ -47,7 +52,12 @@ public Review(
String content,
LocalDateTime deletedAt,
List<ReviewImage> images,
List<ReviewKeyword> keywords) {
List<ReviewKeyword> keywords,
int likesCount) {
if (likesCount < 0) {
throw new InvalidReviewLikesException();
}

this.id = id;
this.member = member;
this.stadium = stadium;
Expand All @@ -60,6 +70,7 @@ public Review(
this.deletedAt = deletedAt;
this.images = images != null ? images : new ArrayList<>();
this.keywords = keywords != null ? keywords : new ArrayList<>();
this.likesCount = likesCount;
}

public void addKeyword(ReviewKeyword keyword) {
Expand Down Expand Up @@ -89,6 +100,16 @@ public Keyword getKeywordById(Long keywordId) {
return keywordMap != null ? keywordMap.get(keywordId) : null;
}

public void addLike() {
this.likesCount++;
}

public void cancelLike() {
if (this.likesCount > 0) {
this.likesCount--;
}
}

public void setDeletedAt(LocalDateTime now) {
this.deletedAt = now;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.depromeet.spot.domain.review.like;

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

@Getter
@Builder
@AllArgsConstructor
public class ReviewLike {

private final Long id;
private final Long memberId;
private final Long reviewId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.depromeet.spot.domain.review;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.depromeet.spot.common.exception.review.ReviewException.InvalidReviewLikesException;
import org.junit.jupiter.api.Test;

class ReviewTest {

@Test
void review_builder에_공감수를_지정하지_않으면_0으로_할당된다() {
// given
// when
Review review = Review.builder().build();

// then
assertEquals(review.getLikesCount(), 0);
}

@Test
void review_builder에_공감수를_음수로_지정하면_에러를_반환한다() {
// given
int likesCount = -99;

// when
// then
assertThatThrownBy(() -> Review.builder().likesCount(likesCount).build())
.isInstanceOf(InvalidReviewLikesException.class);
}

@Test
void review_공감수를_증가할_수_있다() {
// given
Review review = Review.builder().likesCount(0).build();

// when
review.addLike();

// then
assertEquals(review.getLikesCount(), 1);
}

@Test
void review_공감수를_감소할_수_있다() {
// given
Review review = Review.builder().likesCount(10).build();

// when
review.cancelLike();

// then
assertEquals(review.getLikesCount(), 9);
}

@Test
void review_공감수가_0이라면_공감수를_감소하지_않는다() {
// given
Review review = Review.builder().likesCount(0).build();

// when
review.cancelLike();

// then
assertEquals(review.getLikesCount(), 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.depromeet.spot.infrastructure.jpa.section.entity.SectionEntity;
import org.depromeet.spot.infrastructure.jpa.stadium.entity.StadiumEntity;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.ColumnDefault;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down Expand Up @@ -92,6 +93,10 @@ public class ReviewEntity extends BaseEntity {
@BatchSize(size = 30)
private List<ReviewKeywordEntity> keywords;

@ColumnDefault("0")
@Column(name = "likes_count")
private Integer likesCount;

public static ReviewEntity from(Review review) {
SeatEntity seatEntity;
if (review.getSeat() == null) {
Expand All @@ -110,7 +115,8 @@ public static ReviewEntity from(Review review) {
review.getDateTime(),
review.getContent(),
new ArrayList<>(),
new ArrayList<>());
new ArrayList<>(),
review.getLikesCount());

entity.setId(review.getId()); // ID 설정 추가

Expand Down Expand Up @@ -139,6 +145,7 @@ public Review toDomain() {
.seat((this.seat == null) ? null : this.seat.toDomain())
.dateTime(this.dateTime)
.content(this.content)
.likesCount(likesCount)
.build();

review.setImages(
Expand Down Expand Up @@ -166,5 +173,6 @@ public ReviewEntity(Review review) {
seat = SeatEntity.withSeat(review.getSeat());
dateTime = review.getDateTime();
content = review.getContent();
likesCount = review.getLikesCount();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.depromeet.spot.infrastructure.jpa.review.entity.like;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

import org.depromeet.spot.domain.review.like.ReviewLike;
import org.depromeet.spot.infrastructure.jpa.common.entity.BaseEntity;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "review_likes")
public class ReviewLikeEntity extends BaseEntity {

@Column(name = "member_id", nullable = false)
private Long memberId;

@Column(name = "review_id", nullable = false)
private Long reviewId;

public static ReviewLikeEntity from(ReviewLike like) {
return new ReviewLikeEntity(like.getMemberId(), like.getReviewId());
}

public ReviewLike toDomain() {
return ReviewLike.builder().id(this.getId()).memberId(memberId).reviewId(reviewId).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,8 @@ int softDeleteByIdAndMemberId(
"SELECT count(r) FROM ReviewEntity r WHERE r.member.id = :memberId "
+ "AND r.deletedAt IS NULL")
long countByIdByMemberId(@Param("memberId") Long memberId);

@Modifying
@Query("update ReviewEntity r set r.likesCount = :likesCount where r.id = :id")
void updateLikesCount(@Param("id") long reviewId, @Param("likesCount") int likesCount);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import org.depromeet.spot.common.exception.review.ReviewException.ReviewNotFoundException;
import org.depromeet.spot.domain.review.Review;
import org.depromeet.spot.domain.review.Review.SortCriteria;
import org.depromeet.spot.domain.review.ReviewYearMonth;
Expand All @@ -23,6 +23,11 @@ public class ReviewRepositoryImpl implements ReviewRepository {
private final ReviewJpaRepository reviewJpaRepository;
private final ReviewCustomRepository reviewCustomRepository;

@Override
public void updateLikesCount(Long reviewId, int likesCount) {
reviewJpaRepository.updateLikesCount(reviewId, likesCount);
}

@Override
public Review save(Review review) {
ReviewEntity entity = ReviewEntity.from(review);
Expand All @@ -31,8 +36,12 @@ public Review save(Review review) {
}

@Override
public Optional<Review> findById(Long id) {
return reviewJpaRepository.findById(id).map(ReviewEntity::toDomain);
public Review findById(Long id) {
ReviewEntity entity =
reviewJpaRepository
.findById(id)
.orElseThrow(() -> new ReviewNotFoundException("요청한 리뷰를 찾을 수 없습니다." + id));
return entity.toDomain();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.depromeet.spot.infrastructure.jpa.review.repository.like;

import org.depromeet.spot.infrastructure.jpa.review.entity.like.ReviewLikeEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;

public interface ReviewLikeJpaRepository extends JpaRepository<ReviewLikeEntity, Long> {

long countByReviewId(long reviewId);

boolean existsByMemberIdAndReviewId(long memberId, long reviewId);

@Modifying
void deleteByMemberIdAndReviewId(long memberId, long reviewId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.depromeet.spot.infrastructure.jpa.review.repository.like;

import org.depromeet.spot.domain.review.like.ReviewLike;
import org.depromeet.spot.infrastructure.jpa.review.entity.like.ReviewLikeEntity;
import org.depromeet.spot.usecase.port.out.review.ReviewLikeRepository;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class ReviewLikeRepositoryImpl implements ReviewLikeRepository {

private final ReviewLikeJpaRepository reviewLikeJpaRepository;

@Override
public boolean existsBy(final long memberId, final long reviewId) {
return reviewLikeJpaRepository.existsByMemberIdAndReviewId(memberId, reviewId);
}

@Override
public long countByReview(final long reviewId) {
return reviewLikeJpaRepository.countByReviewId(reviewId);
}

@Override
public void deleteBy(final long memberId, final long reviewId) {
reviewLikeJpaRepository.deleteByMemberIdAndReviewId(memberId, reviewId);
}

@Override
public void save(ReviewLike like) {
ReviewLikeEntity entity = ReviewLikeEntity.from(like);
reviewLikeJpaRepository.save(entity);
}
}
Loading

0 comments on commit 1b3b4e1

Please sign in to comment.