Skip to content

Commit

Permalink
[BSVR-172] 리뷰 수정 API 구현 (#98)
Browse files Browse the repository at this point in the history
* chore: 안쓰는 코드 정리

* fix: review 도메인 내의 member가 아닌 result의 member 객체를 사용하도록 변경

* refactor: usecase 반환형 레코드 ReviewResult->ReadReviewResult 네이밍 변경

* feat: update 로직을 위해 from 메서드에 id 세팅하는 부분 추가

* feat: 키워드가 수정됐을 때 blockTopKeyword 테이블의 데이터도 수정되도록 메서드 구현

* feat: 리뷰 업데이트 로직 구현

* feat: 필수 필드에  @NotNull 어노테이션 추가

* feat: ReviewResult->CreateReviewResult로 네이밍 롤백..

* feat: UpdateReviewResult 다시 부활

* feat: reviewId에 @NotNull 추가

* feat: list에 @NotNull 대신 @SiZe(minn1, max=3)추가

* feat: max값 10에서 3으로 변경

* feat: blocktopkeyword 를 업데이트하는 부분을 foreach가 아닌 batch 업데이트로 수정

* fix: 새로 들어온 keyword들에 대해서 blocktopkeyword 테이블에 새로운 레코드를 생성하도록 수정
  • Loading branch information
pminsung12 authored Aug 2, 2024
1 parent 16105c2 commit 8108efb
Show file tree
Hide file tree
Showing 18 changed files with 354 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.depromeet.spot.application.review.dto.request.CreateReviewRequest;
import org.depromeet.spot.application.review.dto.response.BaseReviewResponse;
import org.depromeet.spot.usecase.port.in.review.CreateReviewUsecase;
import org.depromeet.spot.usecase.port.in.review.CreateReviewUsecase.CreateReviewResult;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -38,7 +39,7 @@ public BaseReviewResponse create(
@PathVariable @Positive @NotNull final Integer seatNumber,
@Parameter(hidden = true) Long memberId,
@RequestBody @Valid CreateReviewRequest request) {
CreateReviewUsecase.CreateReviewResult result =
CreateReviewResult result =
createReviewUsecase.create(blockId, seatNumber, memberId, request.toCommand());
return BaseReviewResponse.from(result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.depromeet.spot.application.review.dto.response.ReviewMonthsResponse;
import org.depromeet.spot.domain.review.ReviewYearMonth;
import org.depromeet.spot.usecase.port.in.review.ReadReviewUsecase;
import org.depromeet.spot.usecase.port.in.review.ReadReviewUsecase.ReadReviewResult;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
Expand Down Expand Up @@ -122,7 +123,7 @@ public BaseReviewResponse findReviewByReviewId(
@Parameter(hidden = true) Long memberId,
@PathVariable("reviewId") @NotNull @Parameter(description = "리뷰 PK", required = true)
Long reviewId) {
ReadReviewUsecase.ReviewResult reviewResult = readReviewUsecase.findReviewById(reviewId);
return BaseReviewResponse.from(reviewResult.review());
ReadReviewResult readReviewResult = readReviewUsecase.findReviewById(reviewId);
return BaseReviewResponse.from(readReviewResult.review());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.depromeet.spot.application.review;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;

import org.depromeet.spot.application.common.annotation.CurrentMember;
import org.depromeet.spot.application.review.dto.request.UpdateReviewRequest;
import org.depromeet.spot.application.review.dto.response.BaseReviewResponse;
import org.depromeet.spot.usecase.port.in.review.UpdateReviewUsecase;
import org.depromeet.spot.usecase.port.in.review.UpdateReviewUsecase.UpdateReviewResult;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

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")
public class UpdateReviewController {

private final UpdateReviewUsecase updateReviewUsecase;

@CurrentMember
@ResponseStatus(HttpStatus.OK)
@PutMapping("/reviews/{reviewId}")
@Operation(summary = "특정 리뷰를 수정한다.")
public BaseReviewResponse updateReview(
@Parameter(hidden = true) Long memberId,
@PathVariable("reviewId") @NotNull @Parameter(description = "리뷰 PK", required = true)
Long reviewId,
@RequestBody @Valid UpdateReviewRequest request) {

UpdateReviewResult result =
updateReviewUsecase.updateReview(memberId, reviewId, request.toCommand());

return BaseReviewResponse.from(result.review());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import java.util.List;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import org.depromeet.spot.common.exception.review.ReviewException.InvalidReviewDateTimeFormatException;
import org.depromeet.spot.common.exception.review.ReviewException.InvalidReviewKeywordsException;
import org.depromeet.spot.usecase.port.in.review.CreateReviewUsecase.CreateReviewCommand;

public record CreateReviewRequest(
@NotNull List<String> images,
@Size(min = 1, max = 3) List<String> images,
List<String> good,
List<String> bad,
String content,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.depromeet.spot.application.review.dto.request;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import org.depromeet.spot.common.exception.review.ReviewException.InvalidReviewDateTimeFormatException;
import org.depromeet.spot.common.exception.review.ReviewException.InvalidReviewKeywordsException;
import org.depromeet.spot.usecase.port.in.review.UpdateReviewUsecase.UpdateReviewCommand;

public record UpdateReviewRequest(
@NotNull Long blockId,
@NotNull Integer seatNumber,
@Size(min = 1, max = 3) List<String> images,
List<String> good,
List<String> bad,
String content,
@NotNull String dateTime) {

public UpdateReviewCommand toCommand() {
validateGoodAndBad();
return UpdateReviewCommand.builder()
.blockId(blockId)
.seatNumber(seatNumber)
.images(images)
.good(good)
.bad(bad)
.content(content)
.dateTime(toLocalDateTime(dateTime))
.build();
}

private void validateGoodAndBad() {
if ((good == null || good.isEmpty()) && (bad == null || bad.isEmpty())) {
throw new InvalidReviewKeywordsException();
}
}

private LocalDateTime toLocalDateTime(String dateTimeStr) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
try {
return LocalDateTime.parse(dateTimeStr, formatter);
} catch (DateTimeParseException e) {
throw new InvalidReviewDateTimeFormatException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public enum ReviewErrorCode implements ErrorCode {
INVALID_REVIEW_DATETIME_FORMAT(
HttpStatus.BAD_REQUEST, "RV003", "리뷰 작성일시는 yyyy-MM-dd HH:mm 포맷이어야 합니다."),
INVALID_REVIEW_KEYWORDS(
HttpStatus.BAD_REQUEST, "RV004", "리뷰의 'good' 또는 'bad' 중 적어도 하나는 제공되어야 합니다.");
HttpStatus.BAD_REQUEST, "RV004", "리뷰의 'good' 또는 'bad' 중 적어도 하나는 제공되어야 합니다."),
UNAUTHORIZED_REVIEW_MODIFICATION_EXCEPTION(
HttpStatus.BAD_REQUEST, "RV005", "이 리뷰를 수정할 권한이 없습니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ public ReviewNotFoundException(String s) {
}
}

public static class UnauthorizedReviewModificationException extends ReviewException {
public UnauthorizedReviewModificationException() {
super(ReviewErrorCode.UNAUTHORIZED_REVIEW_MODIFICATION_EXCEPTION);
}

public UnauthorizedReviewModificationException(String str) {
super(ReviewErrorCode.UNAUTHORIZED_REVIEW_MODIFICATION_EXCEPTION.appended(str));
}
}

public static class InvalidReviewDataException extends ReviewException {
public InvalidReviewDataException() {
super(ReviewErrorCode.INVALID_REVIEW_DATA);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public static ReviewEntity from(Review review) {
new ArrayList<>(),
new ArrayList<>());

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

entity.images =
review.getImages().stream()
.map(image -> ReviewImageEntity.from(image, entity))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ public interface BlockTopKeywordJpaRepository extends JpaRepository<BlockTopKeyw
+ "WHERE b.block.id = :blockId AND b.keyword.id = :keywordId")
int incrementCount(Long blockId, Long keywordId);

@Modifying
@Query(
"UPDATE BlockTopKeywordEntity b SET b.count = CASE "
+ "WHEN b.keyword.id IN :incrementIds THEN b.count + 1 "
+ "WHEN b.keyword.id IN :decrementIds THEN GREATEST(0, b.count - 1) "
+ "ELSE b.count END, "
+ "b.updatedAt = CURRENT_TIMESTAMP "
+ "WHERE b.block.id = :blockId AND b.keyword.id IN :allIds")
int batchUpdateCounts(
@Param("blockId") Long blockId,
@Param("incrementIds") List<Long> incrementIds,
@Param("decrementIds") List<Long> decrementIds,
@Param("allIds") List<Long> allIds);

// JPA에서 ON Duplicate key update 구문을 지원하지 않음 -> native query 사용
@Modifying
@Query(
Expand All @@ -41,4 +55,18 @@ List<BlockTopKeywordDto> findTopKeywordsByStadiumIdAndBlockCode(
@Param("stadiumId") Long stadiumId,
@Param("blockCode") String blockCode,
Pageable pageable);

@Query(
"SELECT b.keyword.id FROM BlockTopKeywordEntity b WHERE b.block.id = :blockId AND b.keyword.id IN :keywordIds")
List<Long> findExistingKeywordIds(
@Param("blockId") Long blockId, @Param("keywordIds") List<Long> keywordIds);

@Modifying
@Query(
value =
"INSERT INTO block_top_keywords (block_id, keyword_id, count, created_at, updated_at) "
+ "VALUES (:blockId, :keywordId, 1, NOW(), NOW())",
nativeQuery = true)
void insertNewBlockTopKeyword(
@Param("blockId") Long blockId, @Param("keywordId") Long keywordId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.depromeet.spot.usecase.port.in.review.ReadReviewUsecase.BlockKeywordInfo;
import org.depromeet.spot.usecase.port.out.review.BlockTopKeywordRepository;
Expand All @@ -19,6 +20,37 @@ public class BlockTopKeywordRepositoryImpl implements BlockTopKeywordRepository

private final BlockTopKeywordJpaRepository blockTopKeywordJpaRepository;

@Override
@Transactional
public void batchUpdateCounts(Long blockId, List<Long> incrementIds, List<Long> decrementIds) {
List<Long> allIds =
Stream.concat(incrementIds.stream(), decrementIds.stream())
.distinct()
.collect(Collectors.toList());

if (!allIds.isEmpty()) {
// 1단계: 기존 키워드 업데이트
int updatedRows =
blockTopKeywordJpaRepository.batchUpdateCounts(
blockId, incrementIds, decrementIds, allIds);
log.debug("Batch update performed. Rows affected: {}", updatedRows);

// 2단계: 새 키워드 삽입
List<Long> existingKeywordIds =
blockTopKeywordJpaRepository.findExistingKeywordIds(blockId, incrementIds);
List<Long> newKeywordIds =
incrementIds.stream().filter(id -> !existingKeywordIds.contains(id)).toList();

for (Long keywordId : newKeywordIds) {
blockTopKeywordJpaRepository.insertNewBlockTopKeyword(blockId, keywordId);
log.debug(
"Inserted new BlockTopKeyword for blockId: {} and keywordId: {}",
blockId,
keywordId);
}
}
}

@Override
@Transactional
public void updateKeywordCount(Long blockId, Long keywordId) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ MyReviewListResult findMyReviewsByUserId(

MyRecentReviewResult findLastReviewByMemberId(Long memberId);

ReviewResult findReviewById(Long reviewId);
ReadReviewResult findReviewById(Long reviewId);

@Builder
record BlockReviewListResult(
Expand Down Expand Up @@ -68,5 +68,5 @@ record MemberInfoOnMyReviewResult(
record MyRecentReviewResult(Review review, Long reviewCount) {}

@Builder
record ReviewResult(Review review) {}
record ReadReviewResult(Review review) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.depromeet.spot.usecase.port.in.review;

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

import org.depromeet.spot.domain.review.Review;

import lombok.Builder;

public interface UpdateReviewUsecase {
UpdateReviewResult updateReview(Long memberId, Long reviewId, UpdateReviewCommand command);

@Builder
record UpdateReviewCommand(
Long blockId,
Integer seatNumber,
List<String> images,
List<String> good,
List<String> bad,
String content,
LocalDateTime dateTime) {}

record UpdateReviewResult(Review review) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public interface BlockTopKeywordRepository {

void updateKeywordCount(Long blockId, Long keywordId);

void batchUpdateCounts(Long blockId, List<Long> incrementIds, List<Long> decrementIds);

List<BlockKeywordInfo> findTopKeywordsByStadiumIdAndBlockCode(
Long stadiumId, String blockCode, int limit);
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public CreateReviewResult create(

// 저장 및 blockTopKeyword에도 count 업데이트
Review savedReview = reviewRepository.save(review);

// BlockTopKeyword 업데이트 및 생성
updateBlockTopKeywords(savedReview);

savedReview.setKeywordMap(keywordMap);
Expand Down
Loading

0 comments on commit 8108efb

Please sign in to comment.