From efd4c8131833e3e6549dd1cba01f31107387838d Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Mon, 18 Aug 2025 23:29:10 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=ED=83=80?= =?UTF-8?q?=EC=9E=84=EC=9C=A0=ED=8B=B8=20=EC=9D=B4=EB=8F=99=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8E=B8=EC=9D=98=EC=A0=90=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8B=9C=EA=B0=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/community/dto/response/CommentListResponse.java | 2 +- .../dto/response/CommunityPostInfoResponse.java | 2 +- .../dto/response/CommunityPostListResponse.java | 2 +- .../dto/response/ConveniencePostListResponse.java | 9 +++++++-- .../be/{community => global}/util/TimeUtil.java | 2 +- .../notification/dto/response/NotificationResponse.java | 2 +- .../user/dto/response/MyCommentGroupByPostResponse.java | 2 +- .../be/user/dto/response/MypageReviewResponse.java | 2 +- 8 files changed, 14 insertions(+), 9 deletions(-) rename src/main/java/com/kkinikong/be/{community => global}/util/TimeUtil.java (96%) diff --git a/src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java b/src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java index ab218b2e..a0e80da0 100644 --- a/src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java +++ b/src/main/java/com/kkinikong/be/community/dto/response/CommentListResponse.java @@ -5,7 +5,7 @@ import lombok.Builder; import com.kkinikong.be.community.domain.Comment; -import com.kkinikong.be.community.util.TimeUtil; +import com.kkinikong.be.global.util.TimeUtil; import com.kkinikong.be.user.utils.UserNicknameUtil; @Builder diff --git a/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java b/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java index f549e96b..a501d2fe 100644 --- a/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java +++ b/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostInfoResponse.java @@ -5,7 +5,7 @@ import lombok.Builder; import com.kkinikong.be.community.domain.CommunityPost; -import com.kkinikong.be.community.util.TimeUtil; +import com.kkinikong.be.global.util.TimeUtil; import com.kkinikong.be.user.utils.UserNicknameUtil; @Builder diff --git a/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java b/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java index 0dd06733..2307680a 100644 --- a/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java +++ b/src/main/java/com/kkinikong/be/community/dto/response/CommunityPostListResponse.java @@ -1,7 +1,7 @@ package com.kkinikong.be.community.dto.response; import com.kkinikong.be.community.domain.CommunityPost; -import com.kkinikong.be.community.util.TimeUtil; +import com.kkinikong.be.global.util.TimeUtil; public record CommunityPostListResponse( long communityPostId, diff --git a/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java b/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java index 6f94c609..3317e184 100644 --- a/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java +++ b/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java @@ -1,10 +1,15 @@ package com.kkinikong.be.convenience.dto.response; import com.kkinikong.be.convenience.domain.ConveniencePost; +import com.kkinikong.be.global.util.TimeUtil; -public record ConveniencePostListResponse(Long id, String name, boolean isAvailable) { +public record ConveniencePostListResponse( + Long id, String name, boolean isAvailable, String CreatedAt) { public static ConveniencePostListResponse from(ConveniencePost conveniencePost) { return new ConveniencePostListResponse( - conveniencePost.getId(), conveniencePost.getName(), conveniencePost.getIsAvailable()); + conveniencePost.getId(), + conveniencePost.getName(), + conveniencePost.getIsAvailable(), + TimeUtil.relativeTimeFormatter(conveniencePost.getCreatedDate())); } } diff --git a/src/main/java/com/kkinikong/be/community/util/TimeUtil.java b/src/main/java/com/kkinikong/be/global/util/TimeUtil.java similarity index 96% rename from src/main/java/com/kkinikong/be/community/util/TimeUtil.java rename to src/main/java/com/kkinikong/be/global/util/TimeUtil.java index 48a730ce..c69119cf 100644 --- a/src/main/java/com/kkinikong/be/community/util/TimeUtil.java +++ b/src/main/java/com/kkinikong/be/global/util/TimeUtil.java @@ -1,4 +1,4 @@ -package com.kkinikong.be.community.util; +package com.kkinikong.be.global.util; import java.time.Duration; import java.time.LocalDateTime; diff --git a/src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java b/src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java index daec5d58..6233765b 100644 --- a/src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java +++ b/src/main/java/com/kkinikong/be/notification/dto/response/NotificationResponse.java @@ -1,6 +1,6 @@ package com.kkinikong.be.notification.dto.response; -import com.kkinikong.be.community.util.TimeUtil; +import com.kkinikong.be.global.util.TimeUtil; import com.kkinikong.be.notification.domain.Notification; import com.kkinikong.be.notification.domain.type.NotificationType; diff --git a/src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java b/src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java index eccf4b50..e5deef97 100644 --- a/src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java +++ b/src/main/java/com/kkinikong/be/user/dto/response/MyCommentGroupByPostResponse.java @@ -4,7 +4,7 @@ import com.kkinikong.be.community.domain.Comment; import com.kkinikong.be.community.domain.CommunityPost; -import com.kkinikong.be.community.util.TimeUtil; +import com.kkinikong.be.global.util.TimeUtil; public record MyCommentGroupByPostResponse( Long postId, diff --git a/src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java b/src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java index 277642f9..df167d22 100644 --- a/src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java +++ b/src/main/java/com/kkinikong/be/user/dto/response/MypageReviewResponse.java @@ -4,7 +4,7 @@ import jakarta.annotation.Nullable; -import com.kkinikong.be.community.util.TimeUtil; +import com.kkinikong.be.global.util.TimeUtil; import com.kkinikong.be.review.domain.Review; import com.kkinikong.be.review.domain.type.Tag; From 562e9f519c470b5fa4fdce8e5cbe03455f662ea4 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Mon, 18 Aug 2025 23:38:18 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=EC=97=90=EC=84=9C=EB=A7=8C=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B0=80=EB=8A=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/auth/controller/AuthController.java | 11 ------ .../be/auth/controller/SignupController.java | 39 +++++++++++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/kkinikong/be/auth/controller/SignupController.java diff --git a/src/main/java/com/kkinikong/be/auth/controller/AuthController.java b/src/main/java/com/kkinikong/be/auth/controller/AuthController.java index 5c04588f..e18aa737 100644 --- a/src/main/java/com/kkinikong/be/auth/controller/AuthController.java +++ b/src/main/java/com/kkinikong/be/auth/controller/AuthController.java @@ -36,17 +36,6 @@ public ResponseEntity> login( return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.from(loginResponse)); } - @Operation( - summary = "기본 회원가입", - description = "(관리자) 이메일과 비밀번호로 회원가입합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.") - @PostMapping("/signup") - public ResponseEntity> signUp( - @Valid @RequestBody BasicLoginRequest basicLoginRequest) { - - authService.signup(basicLoginRequest); - return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); - } - @Operation( summary = "기본 로그인", description = "(관리자) 이메일과 비밀번호로 로그인합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.") diff --git a/src/main/java/com/kkinikong/be/auth/controller/SignupController.java b/src/main/java/com/kkinikong/be/auth/controller/SignupController.java new file mode 100644 index 00000000..289f9f46 --- /dev/null +++ b/src/main/java/com/kkinikong/be/auth/controller/SignupController.java @@ -0,0 +1,39 @@ +package com.kkinikong.be.auth.controller; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +import com.kkinikong.be.auth.dto.request.BasicLoginRequest; +import com.kkinikong.be.auth.service.AuthService; +import com.kkinikong.be.global.response.ApiResponse; + +@Profile({"local"}) +@RequestMapping("/api/v1/auth") +@Tag(name = "Auth 로컬", description = "인증 및 회원 관련 API") +@RestController +@RequiredArgsConstructor +public class SignupController { + + private final AuthService authService; + + @Operation( + summary = "기본 회원가입", + description = "(관리자) 이메일과 비밀번호로 회원가입합니다. '@'를 포함한 이메일과 비밀번호를 입력해주세요.") + @PostMapping("/signup") + public ResponseEntity> signUp( + @Valid @RequestBody BasicLoginRequest basicLoginRequest) { + + authService.signup(basicLoginRequest); + return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE); + } +} From cc9964930395c5e39590c6367afac006957f17e4 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Mon, 18 Aug 2025 23:59:21 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=EC=BB=A4?= =?UTF-8?q?=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=9D=BD=20=EB=82=99=EA=B4=80=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/community/domain/CommunityPost.java | 3 + .../CommunityPostRepository.java | 7 -- .../community/service/CommunityService.java | 72 ++++++++++++------- 3 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/kkinikong/be/community/domain/CommunityPost.java b/src/main/java/com/kkinikong/be/community/domain/CommunityPost.java index 6b237b81..57b225da 100644 --- a/src/main/java/com/kkinikong/be/community/domain/CommunityPost.java +++ b/src/main/java/com/kkinikong/be/community/domain/CommunityPost.java @@ -14,6 +14,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.Version; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -73,6 +74,8 @@ public class CommunityPost extends BaseEntity { @OneToMany(mappedBy = "communityPost", cascade = CascadeType.ALL, orphanRemoval = true) private List commentList = new ArrayList<>(); + @Version private Long version; + @Builder public CommunityPost(String title, String content, User user, Category category) { this.title = title; diff --git a/src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java b/src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java index c1b2e5b0..f8c00067 100644 --- a/src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java +++ b/src/main/java/com/kkinikong/be/community/repository/communityPost/CommunityPostRepository.java @@ -2,19 +2,16 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import io.lettuce.core.dynamic.annotation.Param; -import jakarta.persistence.LockModeType; import com.kkinikong.be.community.domain.CommunityPost; import com.kkinikong.be.community.domain.type.Category; @@ -42,10 +39,6 @@ ORDER BY COUNT(cpl.id) DESC, cp.viewCount DESC Page findAllByUserIdOrderByCreatedDateDesc(Long userId, Pageable pageable); - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT p FROM CommunityPost p WHERE p.id = :postId") - Optional findByIdForUpdate(@Param("postId") Long postId); - @Modifying(clearAutomatically = true) @Query("UPDATE CommunityPost p SET p.viewCount = p.viewCount + :count WHERE p.id = :postId") void incrementViews(@Param("postId") Long postId, @Param("count") Long count); diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityService.java b/src/main/java/com/kkinikong/be/community/service/CommunityService.java index 9e9914cd..d6928e5e 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityService.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityService.java @@ -15,6 +15,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -80,6 +81,8 @@ public class CommunityService { private final RedisTempleCacheService redisTempleCacheService; private final OpenSearchService openSearchService; + final int maxRetries = 3; + @Transactional public CommunityPostResponse postCommunityPost(CommunityPostRequest request, Long userId) { CommunityPost communityPost = @@ -191,34 +194,51 @@ public Page getCommunityPostList( @Transactional public LikeToggleResponse postCommunityPostLike(Long postId, Long userId) { - CommunityPost communityPost = - communityPostRepository - .findByIdForUpdate(postId) - .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND)); - - User user = getUserOrThrow(userId); - - Optional postLike = - communityPostLikeRepository.findByCommunityPostIdAndUserId(postId, userId); - - boolean isLiked; - if (postLike.isPresent()) { - communityPostLikeRepository.delete(postLike.get()); - communityPost.decrementLikeCount(); - isLiked = false; - } else { - communityPostLikeRepository.save( - CommunityPostLike.builder().communityPost(communityPost).user(user).build()); - communityPost.incrementLikeCount(); - isLiked = true; - - User receiver = communityPost.getUser(); - // 알림 이벤트 발행 - if (!communityPost.getUser().getId().equals(userId)) { - eventPublisher.publishEvent(new CommunityLikeEvent(receiver, user, communityPost)); + for (int attempt = 0; attempt < maxRetries; attempt++) { + try { + CommunityPost communityPost = + communityPostRepository + .findById(postId) + .orElseThrow( + () -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND)); + + User user = getUserOrThrow(userId); + + Optional postLike = + communityPostLikeRepository.findByCommunityPostIdAndUserId(postId, userId); + + boolean isLiked; + if (postLike.isPresent()) { + communityPostLikeRepository.delete(postLike.get()); + communityPost.decrementLikeCount(); + isLiked = false; + } else { + communityPostLikeRepository.save( + CommunityPostLike.builder().communityPost(communityPost).user(user).build()); + communityPost.incrementLikeCount(); + isLiked = true; + + User receiver = communityPost.getUser(); + // 알림 이벤트 발행 + if (!communityPost.getUser().getId().equals(userId)) { + eventPublisher.publishEvent(new CommunityLikeEvent(receiver, user, communityPost)); + } + } + + // 버전 충돌 조기 감지를 위해 flush + communityPostRepository.saveAndFlush(communityPost); + return LikeToggleResponse.from(isLiked, communityPost.getLikeCount()); + } catch (ObjectOptimisticLockingFailureException e) { + if (attempt == maxRetries - 1) { + throw e; + } + try { + Thread.sleep(30L * attempt); + } catch (InterruptedException ignored) { + } } } - return LikeToggleResponse.from(isLiked, communityPost.getLikeCount()); + throw new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND); } @Transactional From 003ccb9d9ab3c9fbc4dd748ad7084b472e8afb4f Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Tue, 19 Aug 2025 00:03:59 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=EC=BB=A4?= =?UTF-8?q?=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EB=9D=BD=20=EB=82=99=EA=B4=80=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/community/domain/Comment.java | 3 + .../repository/comment/CommentRepository.java | 12 ---- .../community/service/CommunityService.java | 64 ++++++++++++------- 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/kkinikong/be/community/domain/Comment.java b/src/main/java/com/kkinikong/be/community/domain/Comment.java index 3739a8bf..5c8eb69f 100644 --- a/src/main/java/com/kkinikong/be/community/domain/Comment.java +++ b/src/main/java/com/kkinikong/be/community/domain/Comment.java @@ -14,6 +14,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import jakarta.persistence.Version; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -68,6 +69,8 @@ public class Comment extends BaseEntity { @OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true) private List childComments = new ArrayList<>(); + @Version private Long version; + @Builder public Comment( String content, diff --git a/src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java b/src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java index 60934906..36bc96ea 100644 --- a/src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java +++ b/src/main/java/com/kkinikong/be/community/repository/comment/CommentRepository.java @@ -1,17 +1,11 @@ package com.kkinikong.be.community.repository.comment; import java.util.List; -import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import io.lettuce.core.dynamic.annotation.Param; -import jakarta.persistence.LockModeType; - import com.kkinikong.be.community.domain.Comment; @Repository @@ -19,10 +13,4 @@ public interface CommentRepository extends JpaRepository, Comment @EntityGraph(attributePaths = {"commentLikeList", "commentLikeList.user"}) List findAllByCommunityPostId(Long postId); - - @Lock(LockModeType.PESSIMISTIC_WRITE) - @Query("SELECT c FROM Comment c WHERE c.id = :commentId") - Optional findByIdForUpdate(@Param("commentId") Long commentId); - - void deleteAllByCommunityPostId(Long postId); } diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityService.java b/src/main/java/com/kkinikong/be/community/service/CommunityService.java index d6928e5e..316d50f1 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityService.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityService.java @@ -243,32 +243,48 @@ public LikeToggleResponse postCommunityPostLike(Long postId, Long userId) { @Transactional public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) { - Comment comment = - commentRepository - .findByIdForUpdate(commentId) - .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND)); - User user = getUserOrThrow(userId); - - Optional commentLike = - commentLikeRepository.findByCommentIdAndUserId(commentId, userId); - - boolean isLiked; - if (commentLike.isPresent()) { - commentLikeRepository.delete(commentLike.get()); - comment.decrementLikeCount(); - isLiked = false; - } else { - commentLikeRepository.save(CommentLike.builder().comment(comment).user(user).build()); - comment.incrementLikeCount(); - isLiked = true; - - User receiver = comment.getUser(); - // 알림 이벤트 발행 - if (!comment.getUser().getId().equals(userId)) { - eventPublisher.publishEvent(new CommentLikeEvent(receiver, user, comment)); + for (int attempt = 0; attempt < maxRetries; attempt++) { + try { + Comment comment = + commentRepository + .findById(commentId) + .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND)); + User user = getUserOrThrow(userId); + + Optional commentLike = + commentLikeRepository.findByCommentIdAndUserId(commentId, userId); + + boolean isLiked; + if (commentLike.isPresent()) { + commentLikeRepository.delete(commentLike.get()); + comment.decrementLikeCount(); + isLiked = false; + } else { + commentLikeRepository.save(CommentLike.builder().comment(comment).user(user).build()); + comment.incrementLikeCount(); + isLiked = true; + + User receiver = comment.getUser(); + // 알림 이벤트 발행 + if (!comment.getUser().getId().equals(userId)) { + eventPublisher.publishEvent(new CommentLikeEvent(receiver, user, comment)); + } + } + + // 버전 충돌 조기 감지를 위해 flush + commentRepository.saveAndFlush(comment); + return LikeToggleResponse.from(isLiked, comment.getLikeCount()); + } catch (ObjectOptimisticLockingFailureException e) { + if (attempt == maxRetries - 1) { + throw e; + } + try { + Thread.sleep(30L * attempt); + } catch (InterruptedException ignored) { + } } } - return LikeToggleResponse.from(isLiked, comment.getLikeCount()); + throw new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND); } public CommunityPostInfoResponse getCommunityPost(Long postId, Long userId) { From 2aac5c4ec242c610ff985723e2c26dda662871ca Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Tue, 19 Aug 2025 00:15:10 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[SCRUM-364]=20Rename:=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...heService.java => RedisTemplateCacheService.java} | 2 +- .../kkinikong/be/cache/service/ScheduledService.java | 10 +++++----- .../be/community/service/CommunityService.java | 12 ++++++------ .../exception/handler/GlobalExceptionHandler.java | 4 ++-- .../com/kkinikong/be/store/service/StoreService.java | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) rename src/main/java/com/kkinikong/be/cache/service/{RedisTempleCacheService.java => RedisTemplateCacheService.java} (97%) diff --git a/src/main/java/com/kkinikong/be/cache/service/RedisTempleCacheService.java b/src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java similarity index 97% rename from src/main/java/com/kkinikong/be/cache/service/RedisTempleCacheService.java rename to src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java index cbd10727..9fc9c172 100644 --- a/src/main/java/com/kkinikong/be/cache/service/RedisTempleCacheService.java +++ b/src/main/java/com/kkinikong/be/cache/service/RedisTemplateCacheService.java @@ -13,7 +13,7 @@ @Service @Slf4j -public class RedisTempleCacheService { +public class RedisTemplateCacheService { @Autowired private RedisTemplate redisTemplate; diff --git a/src/main/java/com/kkinikong/be/cache/service/ScheduledService.java b/src/main/java/com/kkinikong/be/cache/service/ScheduledService.java index 37cc0ff7..f1da559c 100644 --- a/src/main/java/com/kkinikong/be/cache/service/ScheduledService.java +++ b/src/main/java/com/kkinikong/be/cache/service/ScheduledService.java @@ -18,7 +18,7 @@ @RequiredArgsConstructor public class ScheduledService { - private final RedisTempleCacheService redisTempleCacheService; + private final RedisTemplateCacheService redisTemplateCacheService; private final StoreRepository storeRepository; private final CommunityPostRepository communityPostRepository; @@ -26,7 +26,7 @@ public class ScheduledService { @Transactional public void syncStoreViewCount() { Map storesViewCounts = - redisTempleCacheService.getViewCounts(RedisKey.STORE_VIEWS_KEY); + redisTemplateCacheService.getViewCounts(RedisKey.STORE_VIEWS_KEY); if (storesViewCounts == null || storesViewCounts.isEmpty()) { return; } @@ -38,14 +38,14 @@ public void syncStoreViewCount() { storeRepository.incrementViews(storeId, viewCount); }); - redisTempleCacheService.clearViewCounts(RedisKey.STORE_VIEWS_KEY); + redisTemplateCacheService.clearViewCounts(RedisKey.STORE_VIEWS_KEY); } @Scheduled(cron = "0 10 * * * *") // 매시간 10분에 실행 @Transactional public void syncCommunityPostViewCount() { Map communityPostsViewCounts = - redisTempleCacheService.getViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY); + redisTemplateCacheService.getViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY); if (communityPostsViewCounts == null || communityPostsViewCounts.isEmpty()) { return; } @@ -57,6 +57,6 @@ public void syncCommunityPostViewCount() { communityPostRepository.incrementViews(storeId, viewCount); }); - redisTempleCacheService.clearViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY); + redisTemplateCacheService.clearViewCounts(RedisKey.COMMUNITY_POST_VIEWS_KEY); } } diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityService.java b/src/main/java/com/kkinikong/be/community/service/CommunityService.java index 316d50f1..e8e6cf9e 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityService.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityService.java @@ -23,7 +23,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.kkinikong.be.cache.service.RedisTempleCacheService; +import com.kkinikong.be.cache.service.RedisTemplateCacheService; import com.kkinikong.be.cache.type.RedisKey; import com.kkinikong.be.community.domain.Comment; import com.kkinikong.be.community.domain.CommentLike; @@ -78,7 +78,7 @@ public class CommunityService { private final ApplicationEventPublisher eventPublisher; private final ImageService imageService; - private final RedisTempleCacheService redisTempleCacheService; + private final RedisTemplateCacheService redisTemplateCacheService; private final OpenSearchService openSearchService; final int maxRetries = 3; @@ -290,7 +290,7 @@ public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) public CommunityPostInfoResponse getCommunityPost(Long postId, Long userId) { CommunityPost communityPost = getCommunityPostOrThrow(postId); - redisTempleCacheService.increaseViewCounts(postId, RedisKey.COMMUNITY_POST_VIEWS_KEY); + redisTemplateCacheService.increaseViewCounts(postId, RedisKey.COMMUNITY_POST_VIEWS_KEY); List allComments = commentRepository.findAllByCommunityPostId(postId); List allImages = @@ -314,7 +314,7 @@ public Page searchCommunityPost( keyword = keyword.trim(); // 최근 검색어 추가 로직 if (userId != null) { - redisTempleCacheService.saveRecentSearch(userId, keyword); + redisTemplateCacheService.saveRecentSearch(userId, keyword); } // Elasticsearch에서 검색어로 커뮤니티 게시글 페이징해서 가져옴 @@ -345,7 +345,7 @@ public Page searchCommunityPost( } public List getRecentSearchKeywords(Long userId) { - List recentSearches = redisTempleCacheService.getRecentSearches(userId); + List recentSearches = redisTemplateCacheService.getRecentSearches(userId); if (recentSearches.isEmpty()) { return List.of(); @@ -355,7 +355,7 @@ public List getRecentSearchKeywords(Long userId) { public void deleteRecentSearchKeyword(Long userId, String keyword) { keyword = keyword.trim(); - redisTempleCacheService.deleteRecentSearches(userId, keyword); + redisTemplateCacheService.deleteRecentSearches(userId, keyword); } @Transactional diff --git a/src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java index 94a6be04..6bc25408 100644 --- a/src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/kkinikong/be/global/exception/handler/GlobalExceptionHandler.java @@ -88,7 +88,7 @@ public ResponseEntity handleS3Exception(final S3Exception e, HttpServlet } @ExceptionHandler(ReviewException.class) - public ResponseEntity handleS3Exception( + public ResponseEntity handleReviewException( final ReviewException e, HttpServletRequest request) { logInfo(e.getErrorCode(), e, request); return handleExceptionInternal(e.getErrorCode()); @@ -102,7 +102,7 @@ public ResponseEntity handleCommunityException( } @ExceptionHandler(FeedbackException.class) - public ResponseEntity handleCommunityException( + public ResponseEntity handleFeedbackException( final FeedbackException e, HttpServletRequest request) { logInfo(e.getErrorCode(), e, request); return handleExceptionInternal(e.getErrorCode()); diff --git a/src/main/java/com/kkinikong/be/store/service/StoreService.java b/src/main/java/com/kkinikong/be/store/service/StoreService.java index 6b4b7182..68d5e424 100644 --- a/src/main/java/com/kkinikong/be/store/service/StoreService.java +++ b/src/main/java/com/kkinikong/be/store/service/StoreService.java @@ -13,7 +13,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import com.kkinikong.be.cache.service.RedisTempleCacheService; +import com.kkinikong.be.cache.service.RedisTemplateCacheService; import com.kkinikong.be.cache.type.RedisKey; import com.kkinikong.be.global.response.PageResponse; import com.kkinikong.be.review.domain.type.Tag; @@ -43,7 +43,7 @@ public class StoreService { private final StoreRepository storeRepository; private final StoreKakaoApiClient storeKakaoApiClient; private final StoreGoogleApiClient storeGoogleApiClient; - private final RedisTempleCacheService redisTempleCacheService; + private final RedisTemplateCacheService redisTemplateCacheService; private final StoreScrapRepository storeScrapRepository; private final UserRepository userRepository; private final StoreTagCountRepository storeTagCountRepository; @@ -106,7 +106,7 @@ private double getOrDefault(Double value, double defaultValue) { public StoreInfoResponse getStoreInfo(Long storeId, Long userId) { Store store = getStoreOrThrow(storeId); - redisTempleCacheService.increaseViewCounts(storeId, RedisKey.STORE_VIEWS_KEY); + redisTemplateCacheService.increaseViewCounts(storeId, RedisKey.STORE_VIEWS_KEY); String representativeTag = storeTagCountRepository From 5370340147b3e4e506bd5cf42ad87de697fbdaa9 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Tue, 19 Aug 2025 00:30:23 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[SCRUM-364]=20Rename:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../convenience/dto/response/ConveniencePostListResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java b/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java index 3317e184..95b61bb8 100644 --- a/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java +++ b/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java @@ -4,7 +4,7 @@ import com.kkinikong.be.global.util.TimeUtil; public record ConveniencePostListResponse( - Long id, String name, boolean isAvailable, String CreatedAt) { + Long id, String name, boolean isAvailable, String relativeCreatedAt) { public static ConveniencePostListResponse from(ConveniencePost conveniencePost) { return new ConveniencePostListResponse( conveniencePost.getId(), From 49df9f85d03756eb0a6dba62ec2d0479883250ca Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Tue, 19 Aug 2025 00:40:58 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=EC=BB=A4?= =?UTF-8?q?=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=EB=B3=84=EB=8F=84=EC=9D=98=20=EC=8B=A4=ED=96=89=EA=B8=B0=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommunityLikeToggleExecutor.java | 65 +++++++++++++++++++ .../community/service/CommunityService.java | 56 +++------------- 2 files changed, 75 insertions(+), 46 deletions(-) create mode 100644 src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java b/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java new file mode 100644 index 00000000..81685afb --- /dev/null +++ b/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java @@ -0,0 +1,65 @@ +package com.kkinikong.be.community.service; + +import java.util.Optional; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +import com.kkinikong.be.community.domain.CommunityPost; +import com.kkinikong.be.community.domain.CommunityPostLike; +import com.kkinikong.be.community.dto.response.LikeToggleResponse; +import com.kkinikong.be.community.exception.CommunityException; +import com.kkinikong.be.community.exception.errorcode.CommunityErrorCode; +import com.kkinikong.be.community.repository.CommunityPostLikeRepository; +import com.kkinikong.be.community.repository.communityPost.CommunityPostRepository; +import com.kkinikong.be.notification.event.payload.CommunityLikeEvent; +import com.kkinikong.be.user.domain.User; +import com.kkinikong.be.user.exception.UserException; +import com.kkinikong.be.user.exception.errorcode.UserErrorCode; +import com.kkinikong.be.user.repository.UserRepository; + +@Service +@RequiredArgsConstructor +public class CommunityLikeToggleExecutor { + private final CommunityPostRepository communityPostRepository; + private final CommunityPostLikeRepository communityPostLikeRepository; + private final UserRepository userRepository; + private final ApplicationEventPublisher eventPublisher; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public LikeToggleResponse togglePostLikeOnce(Long postId, Long userId) { + + CommunityPost post = + communityPostRepository + .findById(postId) + .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND)); + User user = + userRepository + .findById(userId) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + + Optional existing = + communityPostLikeRepository.findByCommunityPostIdAndUserId(postId, userId); + + boolean isLiked; + if (existing.isPresent()) { + communityPostLikeRepository.delete(existing.get()); + post.decrementLikeCount(); + isLiked = false; + } else { + communityPostLikeRepository.save( + CommunityPostLike.builder().communityPost(post).user(user).build()); + post.incrementLikeCount(); + isLiked = true; + if (!post.getUser().getId().equals(userId)) { + eventPublisher.publishEvent(new CommunityLikeEvent(post.getUser(), user, post)); + } + } + communityPostRepository.saveAndFlush(post); // 버전 충돌 감지 + return LikeToggleResponse.from(isLiked, post.getLikeCount()); + } +} diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityService.java b/src/main/java/com/kkinikong/be/community/service/CommunityService.java index e8e6cf9e..94e9c863 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityService.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityService.java @@ -29,7 +29,6 @@ import com.kkinikong.be.community.domain.CommentLike; import com.kkinikong.be.community.domain.CommunityPost; import com.kkinikong.be.community.domain.CommunityPostImage; -import com.kkinikong.be.community.domain.CommunityPostLike; import com.kkinikong.be.community.domain.document.CommunityPostDocument; import com.kkinikong.be.community.domain.type.Category; import com.kkinikong.be.community.dto.request.CommunityCommentRequest; @@ -48,12 +47,10 @@ import com.kkinikong.be.community.exception.errorcode.CommunityErrorCode; import com.kkinikong.be.community.repository.CommentLikeRepository; import com.kkinikong.be.community.repository.CommunityPostImageRepository; -import com.kkinikong.be.community.repository.CommunityPostLikeRepository; import com.kkinikong.be.community.repository.comment.CommentRepository; import com.kkinikong.be.community.repository.communityPost.CommunityPostRepository; import com.kkinikong.be.notification.event.payload.CommentLikeEvent; import com.kkinikong.be.notification.event.payload.CommentReplyEvent; -import com.kkinikong.be.notification.event.payload.CommunityLikeEvent; import com.kkinikong.be.opensearch.service.OpenSearchService; import com.kkinikong.be.store.dto.response.StoreRecentSearchKeyword; import com.kkinikong.be.user.domain.User; @@ -73,15 +70,15 @@ public class CommunityService { private final UserRepository userRepository; private final CommunityPostImageRepository communityPostImageRepository; private final CommentRepository commentRepository; - private final CommunityPostLikeRepository communityPostLikeRepository; private final CommentLikeRepository commentLikeRepository; private final ApplicationEventPublisher eventPublisher; private final ImageService imageService; private final RedisTemplateCacheService redisTemplateCacheService; private final OpenSearchService openSearchService; + private final CommunityLikeToggleExecutor communityLikeToggleExecutor; - final int maxRetries = 3; + final int MAX_RETRIES = 3; @Transactional public CommunityPostResponse postCommunityPost(CommunityPostRequest request, Long userId) { @@ -192,49 +189,16 @@ public Page getCommunityPostList( return communityPosts.map(CommunityPostListResponse::from); } - @Transactional public LikeToggleResponse postCommunityPostLike(Long postId, Long userId) { - for (int attempt = 0; attempt < maxRetries; attempt++) { + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { try { - CommunityPost communityPost = - communityPostRepository - .findById(postId) - .orElseThrow( - () -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND)); - - User user = getUserOrThrow(userId); - - Optional postLike = - communityPostLikeRepository.findByCommunityPostIdAndUserId(postId, userId); - - boolean isLiked; - if (postLike.isPresent()) { - communityPostLikeRepository.delete(postLike.get()); - communityPost.decrementLikeCount(); - isLiked = false; - } else { - communityPostLikeRepository.save( - CommunityPostLike.builder().communityPost(communityPost).user(user).build()); - communityPost.incrementLikeCount(); - isLiked = true; - - User receiver = communityPost.getUser(); - // 알림 이벤트 발행 - if (!communityPost.getUser().getId().equals(userId)) { - eventPublisher.publishEvent(new CommunityLikeEvent(receiver, user, communityPost)); - } - } - - // 버전 충돌 조기 감지를 위해 flush - communityPostRepository.saveAndFlush(communityPost); - return LikeToggleResponse.from(isLiked, communityPost.getLikeCount()); + return communityLikeToggleExecutor.togglePostLikeOnce(postId, userId); } catch (ObjectOptimisticLockingFailureException e) { - if (attempt == maxRetries - 1) { - throw e; - } + if (attempt == MAX_RETRIES - 1) throw e; try { - Thread.sleep(30L * attempt); - } catch (InterruptedException ignored) { + Thread.sleep(30L * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); } } } @@ -243,7 +207,7 @@ public LikeToggleResponse postCommunityPostLike(Long postId, Long userId) { @Transactional public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) { - for (int attempt = 0; attempt < maxRetries; attempt++) { + for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { try { Comment comment = commentRepository @@ -275,7 +239,7 @@ public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) commentRepository.saveAndFlush(comment); return LikeToggleResponse.from(isLiked, comment.getLikeCount()); } catch (ObjectOptimisticLockingFailureException e) { - if (attempt == maxRetries - 1) { + if (attempt == MAX_RETRIES - 1) { throw e; } try { From cea7adfc8d6fd6bdd09022440d7b2640c52d66ee Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Tue, 19 Aug 2025 00:45:33 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=EC=BB=A4?= =?UTF-8?q?=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EB=8C=93=EA=B8=80=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20=EB=B3=84?= =?UTF-8?q?=EB=8F=84=EC=9D=98=20=EC=8B=A4=ED=96=89=EA=B8=B0=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CommunityLikeToggleExecutor.java | 48 +++++++++++++++++-- .../community/service/CommunityService.java | 42 ++-------------- 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java b/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java index 81685afb..fba8cebf 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java @@ -9,13 +9,18 @@ import lombok.RequiredArgsConstructor; +import com.kkinikong.be.community.domain.Comment; +import com.kkinikong.be.community.domain.CommentLike; import com.kkinikong.be.community.domain.CommunityPost; import com.kkinikong.be.community.domain.CommunityPostLike; import com.kkinikong.be.community.dto.response.LikeToggleResponse; import com.kkinikong.be.community.exception.CommunityException; import com.kkinikong.be.community.exception.errorcode.CommunityErrorCode; +import com.kkinikong.be.community.repository.CommentLikeRepository; import com.kkinikong.be.community.repository.CommunityPostLikeRepository; +import com.kkinikong.be.community.repository.comment.CommentRepository; import com.kkinikong.be.community.repository.communityPost.CommunityPostRepository; +import com.kkinikong.be.notification.event.payload.CommentLikeEvent; import com.kkinikong.be.notification.event.payload.CommunityLikeEvent; import com.kkinikong.be.user.domain.User; import com.kkinikong.be.user.exception.UserException; @@ -28,6 +33,9 @@ public class CommunityLikeToggleExecutor { private final CommunityPostRepository communityPostRepository; private final CommunityPostLikeRepository communityPostLikeRepository; private final UserRepository userRepository; + private final CommentRepository commentRepository; + private final CommentLikeRepository commentLikeRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -37,10 +45,7 @@ public LikeToggleResponse togglePostLikeOnce(Long postId, Long userId) { communityPostRepository .findById(postId) .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND)); - User user = - userRepository - .findById(userId) - .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + User user = getUserOrThrow(userId); Optional existing = communityPostLikeRepository.findByCommunityPostIdAndUserId(postId, userId); @@ -62,4 +67,39 @@ public LikeToggleResponse togglePostLikeOnce(Long postId, Long userId) { communityPostRepository.saveAndFlush(post); // 버전 충돌 감지 return LikeToggleResponse.from(isLiked, post.getLikeCount()); } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public LikeToggleResponse toggleCommentLikeOnce(Long commentId, Long userId) { + Comment comment = + commentRepository + .findById(commentId) + .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND)); + + User user = getUserOrThrow(userId); + + Optional commentLike = + commentLikeRepository.findByCommentIdAndUserId(commentId, userId); + + boolean isLiked; + if (commentLike.isPresent()) { + commentLikeRepository.delete(commentLike.get()); + comment.decrementLikeCount(); + isLiked = false; + } else { + commentLikeRepository.save(CommentLike.builder().comment(comment).user(user).build()); + comment.incrementLikeCount(); + isLiked = true; + if (!comment.getUser().getId().equals(userId)) { + eventPublisher.publishEvent(new CommentLikeEvent(comment.getUser(), user, comment)); + } + } + commentRepository.saveAndFlush(comment); + return LikeToggleResponse.from(isLiked, comment.getLikeCount()); + } + + private User getUserOrThrow(Long userId) { + return userRepository + .findById(userId) + .orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND)); + } } diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityService.java b/src/main/java/com/kkinikong/be/community/service/CommunityService.java index 94e9c863..00366433 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityService.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityService.java @@ -26,7 +26,6 @@ import com.kkinikong.be.cache.service.RedisTemplateCacheService; import com.kkinikong.be.cache.type.RedisKey; import com.kkinikong.be.community.domain.Comment; -import com.kkinikong.be.community.domain.CommentLike; import com.kkinikong.be.community.domain.CommunityPost; import com.kkinikong.be.community.domain.CommunityPostImage; import com.kkinikong.be.community.domain.document.CommunityPostDocument; @@ -49,7 +48,6 @@ import com.kkinikong.be.community.repository.CommunityPostImageRepository; import com.kkinikong.be.community.repository.comment.CommentRepository; import com.kkinikong.be.community.repository.communityPost.CommunityPostRepository; -import com.kkinikong.be.notification.event.payload.CommentLikeEvent; import com.kkinikong.be.notification.event.payload.CommentReplyEvent; import com.kkinikong.be.opensearch.service.OpenSearchService; import com.kkinikong.be.store.dto.response.StoreRecentSearchKeyword; @@ -205,46 +203,16 @@ public LikeToggleResponse postCommunityPostLike(Long postId, Long userId) { throw new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND); } - @Transactional public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) { for (int attempt = 0; attempt < MAX_RETRIES; attempt++) { try { - Comment comment = - commentRepository - .findById(commentId) - .orElseThrow(() -> new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND)); - User user = getUserOrThrow(userId); - - Optional commentLike = - commentLikeRepository.findByCommentIdAndUserId(commentId, userId); - - boolean isLiked; - if (commentLike.isPresent()) { - commentLikeRepository.delete(commentLike.get()); - comment.decrementLikeCount(); - isLiked = false; - } else { - commentLikeRepository.save(CommentLike.builder().comment(comment).user(user).build()); - comment.incrementLikeCount(); - isLiked = true; - - User receiver = comment.getUser(); - // 알림 이벤트 발행 - if (!comment.getUser().getId().equals(userId)) { - eventPublisher.publishEvent(new CommentLikeEvent(receiver, user, comment)); - } - } - - // 버전 충돌 조기 감지를 위해 flush - commentRepository.saveAndFlush(comment); - return LikeToggleResponse.from(isLiked, comment.getLikeCount()); + return communityLikeToggleExecutor.toggleCommentLikeOnce(commentId, userId); } catch (ObjectOptimisticLockingFailureException e) { - if (attempt == MAX_RETRIES - 1) { - throw e; - } + if (attempt == MAX_RETRIES - 1) throw e; try { - Thread.sleep(30L * attempt); - } catch (InterruptedException ignored) { + Thread.sleep(30L * (attempt + 1)); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); } } } From 1487bcbf6a1a2791712c6a4f8e52eba977649ce9 Mon Sep 17 00:00:00 2001 From: chaen-ing Date: Tue, 19 Aug 2025 14:03:25 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[SCRUM-364]=20Refactor:=20=EC=BB=A4?= =?UTF-8?q?=EB=AE=A4=EB=8B=88=ED=8B=B0=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=ED=9A=9F?= =?UTF-8?q?=EC=88=98=20=EC=B4=88=EA=B3=BC=20=EC=98=A4=EB=A5=98=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/errorcode/CommunityErrorCode.java | 3 ++- .../be/community/service/CommunityService.java | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java b/src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java index c14ddcf4..8eb94885 100644 --- a/src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java +++ b/src/main/java/com/kkinikong/be/community/exception/errorcode/CommunityErrorCode.java @@ -21,7 +21,8 @@ public enum CommunityErrorCode implements ErrorCode { NOT_TOP_COMMENT(HttpStatus.BAD_REQUEST, "최상위 댓글이 아닙니다."), COMMENT_NOT_OWNER(HttpStatus.FORBIDDEN, "댓글의 작성자가 아닙니다."), COMMENT_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "이미 삭제된 댓글입니다."), - ; + + RETRY_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "재시도 횟수를 초과했습니다. 잠시 후 다시 시도해주세요."); private final HttpStatus httpStatus; private final String message; } diff --git a/src/main/java/com/kkinikong/be/community/service/CommunityService.java b/src/main/java/com/kkinikong/be/community/service/CommunityService.java index 00366433..592e58c5 100644 --- a/src/main/java/com/kkinikong/be/community/service/CommunityService.java +++ b/src/main/java/com/kkinikong/be/community/service/CommunityService.java @@ -194,13 +194,17 @@ public LikeToggleResponse postCommunityPostLike(Long postId, Long userId) { } catch (ObjectOptimisticLockingFailureException e) { if (attempt == MAX_RETRIES - 1) throw e; try { + log.warn( + "Optimistic locking failure on comment like toggle, retrying... Attempt: {} of {}", + attempt + 1, + MAX_RETRIES); Thread.sleep(30L * (attempt + 1)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } - throw new CommunityException(CommunityErrorCode.COMMUNITY_POST_NOT_FOUND); + throw new CommunityException(CommunityErrorCode.RETRY_LIMIT_EXCEEDED); } public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) { @@ -210,13 +214,17 @@ public LikeToggleResponse postCommunityCommentLike(Long commentId, Long userId) } catch (ObjectOptimisticLockingFailureException e) { if (attempt == MAX_RETRIES - 1) throw e; try { + log.warn( + "Optimistic locking failure on comment like toggle, retrying... Attempt: {} of {}", + attempt + 1, + MAX_RETRIES); Thread.sleep(30L * (attempt + 1)); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } - throw new CommunityException(CommunityErrorCode.COMMENT_NOT_FOUND); + throw new CommunityException(CommunityErrorCode.RETRY_LIMIT_EXCEEDED); } public CommunityPostInfoResponse getCommunityPost(Long postId, Long userId) {