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); + } +} 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/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/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/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/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/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/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/CommunityLikeToggleExecutor.java b/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java new file mode 100644 index 00000000..fba8cebf --- /dev/null +++ b/src/main/java/com/kkinikong/be/community/service/CommunityLikeToggleExecutor.java @@ -0,0 +1,105 @@ +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.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; +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 CommentRepository commentRepository; + private final CommentLikeRepository commentLikeRepository; + + 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 = getUserOrThrow(userId); + + 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()); + } + + @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 9e9914cd..592e58c5 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; @@ -22,13 +23,11 @@ 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; 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; @@ -47,12 +46,9 @@ 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; @@ -72,13 +68,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 RedisTempleCacheService redisTempleCacheService; + private final RedisTemplateCacheService redisTemplateCacheService; private final OpenSearchService openSearchService; + private final CommunityLikeToggleExecutor communityLikeToggleExecutor; + + final int MAX_RETRIES = 3; @Transactional public CommunityPostResponse postCommunityPost(CommunityPostRequest request, Long userId) { @@ -189,72 +187,50 @@ public Page getCommunityPostList( return communityPosts.map(CommunityPostListResponse::from); } - @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 < MAX_RETRIES; attempt++) { + try { + return communityLikeToggleExecutor.togglePostLikeOnce(postId, 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(); + } } } - return LikeToggleResponse.from(isLiked, communityPost.getLikeCount()); + throw new CommunityException(CommunityErrorCode.RETRY_LIMIT_EXCEEDED); } - @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 < MAX_RETRIES; attempt++) { + try { + return communityLikeToggleExecutor.toggleCommentLikeOnce(commentId, 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(); + } } } - return LikeToggleResponse.from(isLiked, comment.getLikeCount()); + throw new CommunityException(CommunityErrorCode.RETRY_LIMIT_EXCEEDED); } 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 = @@ -278,7 +254,7 @@ public Page searchCommunityPost( keyword = keyword.trim(); // 최근 검색어 추가 로직 if (userId != null) { - redisTempleCacheService.saveRecentSearch(userId, keyword); + redisTemplateCacheService.saveRecentSearch(userId, keyword); } // Elasticsearch에서 검색어로 커뮤니티 게시글 페이징해서 가져옴 @@ -309,7 +285,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(); @@ -319,7 +295,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/convenience/dto/response/ConveniencePostListResponse.java b/src/main/java/com/kkinikong/be/convenience/dto/response/ConveniencePostListResponse.java index 6f94c609..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 @@ -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 relativeCreatedAt) { 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/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/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/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 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;