From 437c64f5343d91802ffe99bf9efd79855a417dbb Mon Sep 17 00:00:00 2001 From: Hong-Mu Date: Sun, 5 May 2024 19:42:15 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20API=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../seoultech/sanEseo/SanEseoApplication.java | 2 + .../config/jwt/JwtAuthenticationFilter.java | 1 + .../global/config/jwt/JwtExceptionFilter.java | 4 ++ .../sanEseo/global/exception/ErrorType.java | 1 + .../exception/GlobalExceptionHandler.java | 9 ++++ .../UnauthorizedAccessException.java | 9 ++++ .../sanEseo/review/adapter/ReviewAdapter.java | 27 +++++------ .../review/adapter/ReviewController.java | 18 +++---- .../review/adapter/ReviewRepository.java | 2 +- .../review/application/port/ReviewPort.java | 6 +-- .../service/CreateReviewRequest.java | 14 +++--- .../service/GetReviewResponse.java | 48 +++++++++++++------ .../application/service/ReviewService.java | 41 ++++++++++------ .../sanEseo/review/domain/Review.java | 18 +++++-- 14 files changed, 129 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/seoultech/sanEseo/global/exception/UnauthorizedAccessException.java diff --git a/src/main/java/com/seoultech/sanEseo/SanEseoApplication.java b/src/main/java/com/seoultech/sanEseo/SanEseoApplication.java index a569fb9..9559a30 100644 --- a/src/main/java/com/seoultech/sanEseo/SanEseoApplication.java +++ b/src/main/java/com/seoultech/sanEseo/SanEseoApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class SanEseoApplication { diff --git a/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtAuthenticationFilter.java b/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtAuthenticationFilter.java index 0378b17..f39f488 100644 --- a/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtAuthenticationFilter.java @@ -54,6 +54,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) { skipPathList.add(new AntPathRequestMatcher("/api/posts/*", HttpMethod.GET.name())); skipPathList.add(new AntPathRequestMatcher("/api/posts/*/likes", HttpMethod.GET.name())); + skipPathList.add(new AntPathRequestMatcher("/api/posts/*/reviews", HttpMethod.GET.name())); skipPathList.add(new AntPathRequestMatcher("/api/districts/**", HttpMethod.GET.name())); diff --git a/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtExceptionFilter.java b/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtExceptionFilter.java index 6924614..7fc4165 100644 --- a/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtExceptionFilter.java +++ b/src/main/java/com/seoultech/sanEseo/global/config/jwt/JwtExceptionFilter.java @@ -38,6 +38,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse errorLoggerHelper.log(wrapper, e.getErrorType(), e.getMessage()); e.printStackTrace(); setErrorResponse(e.getErrorType(), e.getMessage(), response); + } catch (IllegalArgumentException e) { + errorLoggerHelper.log(wrapper, ErrorType.INVALID_INPUT_VALUE, e.getMessage()); + e.printStackTrace(); + setErrorResponse(ErrorType.INVALID_INPUT_VALUE, e.getMessage(), response); } catch (Exception e) { errorLoggerHelper.log(wrapper, ErrorType.INTERNAL_ERROR, e.getMessage()); e.printStackTrace(); diff --git a/src/main/java/com/seoultech/sanEseo/global/exception/ErrorType.java b/src/main/java/com/seoultech/sanEseo/global/exception/ErrorType.java index 248019f..d5e44bf 100644 --- a/src/main/java/com/seoultech/sanEseo/global/exception/ErrorType.java +++ b/src/main/java/com/seoultech/sanEseo/global/exception/ErrorType.java @@ -15,6 +15,7 @@ public enum ErrorType { DUPLICATE_LIKES("E009", 400), MEMBER_NOT_LIKED("E010", 400), AUTHOR_MISMATCH("E011", 400), + UNAUTHORIZED_ACCESS("E012", 401), ENTITY_NOT_FOUND("E404", 404), INVALID_INPUT_VALUE("E403", 403), INTERNAL_ERROR("E999", 500); diff --git a/src/main/java/com/seoultech/sanEseo/global/exception/GlobalExceptionHandler.java b/src/main/java/com/seoultech/sanEseo/global/exception/GlobalExceptionHandler.java index 2b5e017..7f35c0a 100644 --- a/src/main/java/com/seoultech/sanEseo/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/seoultech/sanEseo/global/exception/GlobalExceptionHandler.java @@ -40,6 +40,13 @@ public ResponseEntity handleConstraintViolationException(ConstraintViolationE return ApiResponse.fail(ErrorType.INVALID_INPUT_VALUE, "잘못된 값입니다."); } + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e, RequestWrapper request) throws IOException { + errorLoggerHelper.log(request, ErrorType.INVALID_INPUT_VALUE, e.getMessage()); + e.printStackTrace(); + return ApiResponse.fail(ErrorType.INVALID_INPUT_VALUE, e.getMessage()); + } + @ExceptionHandler(EntityNotFoundException.class) public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e, RequestWrapper request) throws IOException { errorLoggerHelper.log(request, ErrorType.ENTITY_NOT_FOUND, e.getMessage()); @@ -47,6 +54,8 @@ public ResponseEntity handleEntityNotFoundException(EntityNotFoundException e return ApiResponse.fail(ErrorType.ENTITY_NOT_FOUND, e.getMessage()); } + + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleIllegalArgumentException(MethodArgumentNotValidException e, RequestWrapper request) throws IOException { errorLoggerHelper.log(request, ErrorType.INVALID_INPUT_VALUE, e.getBindingResult().getAllErrors().get(0).getDefaultMessage()); diff --git a/src/main/java/com/seoultech/sanEseo/global/exception/UnauthorizedAccessException.java b/src/main/java/com/seoultech/sanEseo/global/exception/UnauthorizedAccessException.java new file mode 100644 index 0000000..4d2c817 --- /dev/null +++ b/src/main/java/com/seoultech/sanEseo/global/exception/UnauthorizedAccessException.java @@ -0,0 +1,9 @@ +package com.seoultech.sanEseo.global.exception; + +public class UnauthorizedAccessException extends BusinessException{ + + public UnauthorizedAccessException(String message) { + super(ErrorType.UNAUTHORIZED_ACCESS, message); + } +} + diff --git a/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewAdapter.java b/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewAdapter.java index fffbd23..13cbbd3 100644 --- a/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewAdapter.java +++ b/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewAdapter.java @@ -5,6 +5,7 @@ import com.seoultech.sanEseo.review.application.port.ReviewPort; import com.seoultech.sanEseo.review.application.service.GetReviewResponse; import com.seoultech.sanEseo.review.domain.Review; +import jakarta.persistence.EntityNotFoundException; import org.springframework.stereotype.Component; import java.util.List; @@ -25,28 +26,22 @@ public void createReview(Review review) { } @Override - public void deleteReview(Post post, Member member) { - reviewRepository.deleteByPostAndMember(post, member); - } - - @Override - public void updateReview(Post post, Member member, Review review) { - reviewRepository.save(review); + public void deleteReview(Long reviewId) { + reviewRepository.deleteById(reviewId); } @Override public List getReviewList(Long postId) { - List reviewResponses = reviewRepository.findByPostId(postId).stream() - .map(review -> GetReviewResponse.builder() - .reviewId(review.getId()) - .memberId(review.getMember().getId()) - .postId(review.getPost().getId()) - .content(review.getContent()) - .createDate(review.getCreateDate()) - .build()) + return reviewRepository.findByPostId(postId).stream() + .map(GetReviewResponse::fromEntity) .toList(); + } - return reviewResponses; + @Override + public Review findById(Long reviewId) { + return reviewRepository.findById(reviewId).orElseThrow( + () -> new EntityNotFoundException("해당 리뷰가 존재하지 않습니다.") + ); } } diff --git a/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewController.java b/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewController.java index 669b1c9..ad6dd64 100644 --- a/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewController.java +++ b/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewController.java @@ -24,21 +24,21 @@ public ReviewController(ReviewService reviewService) { this.reviewService = reviewService; } - @PostMapping("/reviews") - public ResponseEntity createReview(@LoginMember AuthMember authMember, @RequestBody @Valid CreateReviewRequest request) { - reviewService.createReview(authMember.getId(), request); + @PostMapping("/posts/{postId}/reviews") + public ResponseEntity createReview(@LoginMember AuthMember authMember, @PathVariable Long postId, @RequestBody CreateReviewRequest request) { + reviewService.createReview(authMember.getId(), postId, request); return ApiResponse.ok("리뷰가 성공적으로 생성되었습니다."); } - @DeleteMapping("/posts/{postId}/members/reviews") - public ResponseEntity deleteReview(@LoginMember AuthMember authMember, @PathVariable Long postId) { - reviewService.deleteReview(postId, authMember.getId()); + @DeleteMapping("/posts/{postId}/reviews/{reviewId}") + public ResponseEntity deleteReview(@LoginMember AuthMember authMember, @PathVariable Long postId, @PathVariable Long reviewId) { + reviewService.deleteReview(authMember.getId(), postId, reviewId); return ApiResponse.ok("리뷰가 성공적으로 삭제되었습니다."); } - @PutMapping("/posts/{postId}/members/reviews") - public ResponseEntity updateReview(@LoginMember AuthMember authMember, @PathVariable Long postId, @RequestBody UpdateReviewRequest request) { - reviewService.updateReview(postId, authMember.getId(), request); + @PutMapping("/posts/{postId}/reviews/{reviewId}") + public ResponseEntity updateReview(@LoginMember AuthMember authMember, @PathVariable Long postId, @PathVariable Long reviewId, @RequestBody UpdateReviewRequest request) { + reviewService.updateReview(authMember.getId(), postId, reviewId, request); return ApiResponse.ok("리뷰가 성공적으로 업데이트되었습니다."); } diff --git a/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewRepository.java b/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewRepository.java index 10f943d..9b16f74 100644 --- a/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewRepository.java +++ b/src/main/java/com/seoultech/sanEseo/review/adapter/ReviewRepository.java @@ -12,5 +12,5 @@ public interface ReviewRepository extends JpaRepository { List findByPostId(Long postId); - void deleteByPostAndMember(Post post, Member member); + void deleteById(Long reviewId); } diff --git a/src/main/java/com/seoultech/sanEseo/review/application/port/ReviewPort.java b/src/main/java/com/seoultech/sanEseo/review/application/port/ReviewPort.java index 74799ec..4db952f 100644 --- a/src/main/java/com/seoultech/sanEseo/review/application/port/ReviewPort.java +++ b/src/main/java/com/seoultech/sanEseo/review/application/port/ReviewPort.java @@ -10,9 +10,9 @@ public interface ReviewPort { void createReview(Review review); - void deleteReview(Post post, Member member); - - void updateReview(Post post, Member member, Review review); + void deleteReview(Long reviewId); List getReviewList(Long postId); + + Review findById(Long reviewId); } diff --git a/src/main/java/com/seoultech/sanEseo/review/application/service/CreateReviewRequest.java b/src/main/java/com/seoultech/sanEseo/review/application/service/CreateReviewRequest.java index 90b7570..cc12df4 100644 --- a/src/main/java/com/seoultech/sanEseo/review/application/service/CreateReviewRequest.java +++ b/src/main/java/com/seoultech/sanEseo/review/application/service/CreateReviewRequest.java @@ -3,23 +3,21 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Getter; +import lombok.NoArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.util.Assert; import java.time.LocalDateTime; +@NoArgsConstructor @Getter public class CreateReviewRequest { - @NotNull(message = "게시글 ID는 필수입니다.") - Long postId; - @NotBlank(message = "리뷰 내용은 필수입니다.") + + @NotBlank(message = "리뷰 내용은 필수 입력 값입니다.") String content; - @NotNull(message = "리뷰 작성일은 필수입니다.") - @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime createDate; - public CreateReviewRequest(Long postId, String content, LocalDateTime createDate) { - this.postId = postId; + public CreateReviewRequest(String content) { this.content = content; - this.createDate = createDate; } + } diff --git a/src/main/java/com/seoultech/sanEseo/review/application/service/GetReviewResponse.java b/src/main/java/com/seoultech/sanEseo/review/application/service/GetReviewResponse.java index a86b9f0..00479fb 100644 --- a/src/main/java/com/seoultech/sanEseo/review/application/service/GetReviewResponse.java +++ b/src/main/java/com/seoultech/sanEseo/review/application/service/GetReviewResponse.java @@ -1,26 +1,44 @@ package com.seoultech.sanEseo.review.application.service; import com.fasterxml.jackson.annotation.JsonFormat; +import com.seoultech.sanEseo.member.domain.Member; +import com.seoultech.sanEseo.review.domain.Review; +import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.util.Assert; import java.time.LocalDateTime; -public record GetReviewResponse( - Long reviewId, - Long memberId, - Long postId, - String content, - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - LocalDateTime createDate -) { - @Builder - public GetReviewResponse { - Assert.notNull(reviewId, "리뷰 ID는 필수입니다."); - Assert.notNull(memberId, "사용자 ID는 필수입니다."); - Assert.notNull(postId, "게시글 ID는 필수입니다."); - Assert.hasText(content, "리뷰 내용은 필수입니다."); - Assert.notNull(createDate, "리뷰 작성일은 필수입니다."); +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GetReviewResponse { + Long reviewId; + Long authorId; + String authorName; + String authorProfileImageUrl; + Long postId; + String content; + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime createAt; + @DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + LocalDateTime updateAt; + + public static GetReviewResponse fromEntity(Review review) { + Member author = review.getMember(); + return GetReviewResponse.builder() + .reviewId(review.getId()) + .authorId(author.getId()) + .authorName(author.getName()) + .authorProfileImageUrl(author.getProfile()) + .postId(review.getPost().getId()) + .content(review.getContent()) + .createAt(review.getCreateAt()) + .updateAt(review.getUpdateAt()) + .build(); } } \ No newline at end of file diff --git a/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java b/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java index b878959..115ae8f 100644 --- a/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java +++ b/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java @@ -1,5 +1,6 @@ package com.seoultech.sanEseo.review.application.service; +import com.seoultech.sanEseo.global.exception.UnauthorizedAccessException; import com.seoultech.sanEseo.member.application.port.out.MemberPort; import com.seoultech.sanEseo.member.domain.Member; import com.seoultech.sanEseo.post.application.port.PostPort; @@ -25,40 +26,50 @@ class ReviewService { } @Transactional - public void createReview(Long memberId, CreateReviewRequest request) { + public void createReview(Long memberId, Long postId, CreateReviewRequest request) { + Post post = postPort.getPost(postId); Member member = memberPort.loadById(memberId); - Post post = postPort.getPost(request.getPostId()); Review review = Review.builder() .member(member) .post(post) .content(request.getContent()) - .createDate(request.getCreateDate()) .build(); + reviewPort.createReview(review); } @Transactional - public void deleteReview(Long postId, Long memberId) { - Member member = memberPort.loadById(memberId); + public void deleteReview(Long memberId, Long postId, Long reviewId) { Post post = postPort.getPost(postId); + Review review = reviewPort.findById(reviewId); + + if(!post.getId().equals(review.getPost().getId())) { + throw new IllegalArgumentException("해당 게시글에 존재하지 않는 리뷰입니다."); + } - reviewPort.deleteReview(post, member); + if(!review.getMember().getId().equals(memberId)) { + throw new UnauthorizedAccessException("해당 리뷰를 삭제할 권한이 없습니다."); + } + + reviewPort.deleteReview(reviewId); } @Transactional - public void updateReview(Long postId, Long memberId, UpdateReviewRequest request) { - Member member = memberPort.loadById(memberId); + public void updateReview(Long memberId, Long postId, Long reviewId, UpdateReviewRequest request) { Post post = postPort.getPost(postId); + Review review = reviewPort.findById(reviewId); - Review review = Review.builder() - .member(member) - .post(post) - .content(request.getContent()) - .createDate(request.getCreateDate()) - .build(); - reviewPort.updateReview(post, member, review); + if(!post.getId().equals(review.getPost().getId())) { + throw new IllegalArgumentException("해당 게시글에 존재하지 않는 리뷰입니다."); + } + + if(!review.getMember().getId().equals(memberId)) { + throw new UnauthorizedAccessException("해당 리뷰를 수정할 권한이 없습니다."); + } + + review.updateContent(request.getContent()); } public List getReviewList(Long postId) { diff --git a/src/main/java/com/seoultech/sanEseo/review/domain/Review.java b/src/main/java/com/seoultech/sanEseo/review/domain/Review.java index 46ee6f1..3946c66 100644 --- a/src/main/java/com/seoultech/sanEseo/review/domain/Review.java +++ b/src/main/java/com/seoultech/sanEseo/review/domain/Review.java @@ -6,9 +6,13 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +@EntityListeners(AuditingEntityListener.class) @Entity @Getter @NoArgsConstructor @@ -28,14 +32,20 @@ public class Review { private String content; - @Column(name = "create_date", nullable = false) - private LocalDateTime createDate; + @CreatedDate + private LocalDateTime createAt; + + @LastModifiedDate + private LocalDateTime updateAt; @Builder - public Review(Member member, Post post, String content, LocalDateTime createDate) { + public Review(Member member, Post post, String content) { this.member = member; this.post = post; this.content = content; - this.createDate = createDate; + } + + public void updateContent(String content) { + this.content = content; } } From 5974ebba17cf3c8f69ce83adf267682b4c9b14d9 Mon Sep 17 00:00:00 2001 From: Hong-Mu Date: Sun, 5 May 2024 19:52:03 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=B6=88=EB=9F=AC=EC=98=AC=20=EB=95=8C=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=A1=B4=EC=9E=AC=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sanEseo/review/application/service/ReviewService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java b/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java index 115ae8f..844aaa9 100644 --- a/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java +++ b/src/main/java/com/seoultech/sanEseo/review/application/service/ReviewService.java @@ -74,6 +74,8 @@ public void updateReview(Long memberId, Long postId, Long reviewId, UpdateReview public List getReviewList(Long postId) { + postPort.getPost(postId); // 게시글이 존재하는지 확인 + return reviewPort.getReviewList(postId); } }