diff --git a/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java b/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java index 1fd9442..b949ae5 100644 --- a/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java +++ b/src/main/java/com/jiwon/mylog/domain/comment/service/CommentService.java @@ -5,7 +5,8 @@ import com.jiwon.mylog.domain.comment.dto.response.CommentResponse; import com.jiwon.mylog.domain.comment.entity.Comment; import com.jiwon.mylog.domain.comment.repository.CommentRepository; -import com.jiwon.mylog.domain.event.dto.CommentCreatedEvent; +import com.jiwon.mylog.domain.event.dto.comment.CommentCreatedEvent; +import com.jiwon.mylog.domain.event.dto.comment.CommentDeletedEvent; import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.user.entity.User; import com.jiwon.mylog.global.common.error.ErrorCode; @@ -41,7 +42,7 @@ public CommentResponse createComment(Long userId, Long postId, CommentCreateRequ Comment parent = null; if (request.getParentId() != null) { - parent = getComment(request.getParentId()); + parent = getComment(request.getParentId()); } Comment comment = Comment.create(request, parent, user, post); @@ -49,16 +50,16 @@ public CommentResponse createComment(Long userId, Long postId, CommentCreateRequ Long receiverId = post.getUser().getId(); - if (!receiverId.equals(userId)) { - eventPublisher.publishEvent( - new CommentCreatedEvent( - postId, - post.getUser().getId(), - comment.getId(), - userId, - user.getUsername()) - ); - } + eventPublisher.publishEvent( + new CommentCreatedEvent( + postId, + receiverId, + savedComment.getId(), + userId, + user.getUsername(), + savedComment.getCreatedAt() + ) + ); return CommentResponse.fromComment(savedComment); } @@ -77,6 +78,19 @@ public CommentResponse updateComment(Long userId, Long postId, Long commentId, C public void deleteComment(Long userId, Long postId, Long commentId) { Comment comment = getComment(commentId); validateOwner(userId, comment); + + Long receiverId = comment.getPost().getUser().getId(); + + eventPublisher.publishEvent( + new CommentDeletedEvent( + postId, + receiverId, + comment.getId(), + userId, + comment.getCreatedAt() + ) + ); + comment.delete(); } diff --git a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java index e164555..f1539ea 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java +++ b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java @@ -1,9 +1,9 @@ package com.jiwon.mylog.domain.event; -import com.jiwon.mylog.domain.event.dto.CommentCreatedEvent; -import com.jiwon.mylog.domain.event.dto.FollowCreatedEvent; -import com.jiwon.mylog.domain.event.dto.FollowDeletedEvent; -import com.jiwon.mylog.domain.event.dto.LikeCreatedEvent; +import com.jiwon.mylog.domain.event.dto.comment.CommentCreatedEvent; +import com.jiwon.mylog.domain.event.dto.follow.FollowCreatedEvent; +import com.jiwon.mylog.domain.event.dto.follow.FollowDeletedEvent; +import com.jiwon.mylog.domain.event.dto.like.LikeCreatedEvent; import com.jiwon.mylog.domain.notification.entity.Notification; import com.jiwon.mylog.domain.notification.repository.NotificationRepository; import com.jiwon.mylog.domain.notification.service.NotificationService; @@ -30,66 +30,70 @@ public class NotificationEventListener { @Transactional @EventListener public void handleCommentCreated(CommentCreatedEvent event) { - Long receiverId = event.getPostWriterId(); - User receiver = getReceiver(receiverId); - - Notification notification = Notification.create( - receiver, - event.getCommentWriterName() + "왹이 외계 수집물에 발 사를 남기다!", - "/posts/" + event.getPostId(), - NotificationType.COMMENT - ); - notificationRepository.save(notification); + if (event.postWriterId().equals(event.commentWriterId())) { + return; + } - sendSSE(receiverId, notification); + try { + handleNotification( + event.postWriterId(), + event.commentWriterName() + "왹이 외계 수집물에 발 사를 남기다!", + "/posts/" + event.postId(), + NotificationType.COMMENT + ); + } catch (Exception e) { + log.warn("댓글 생성 이벤트 알림 처리 실패: {}", event, e); + } } @Transactional @EventListener public void handleLikeCreated(LikeCreatedEvent event) { - Long receiverId = event.getPostWriterId(); - User receiver = getReceiver(receiverId); - - Notification notification = Notification.create( - receiver, - event.getLikeWriterName() + "왹이 외계 수집물에 푸 딩을 달았다!", - "/posts/" + event.getPostId(), - NotificationType.LIKE - ); - notificationRepository.save(notification); - - sendSSE(receiverId, notification); + try { + handleNotification( + event.postWriterId(), + event.likeWriterName() + "왹이 외계 수집물에 푸 딩을 달았다!", + "/posts/" + event.postId(), + NotificationType.LIKE + ); + } catch (Exception e) { + log.warn("좋아요 생성 이벤트 알림 처리 실패: {}", event, e); + } } @Transactional @EventListener public void handleFollowCreated(FollowCreatedEvent event) { - Long receiverId = event.getReceiverId(); - User receiver = getReceiver(event.getReceiverId()); - - Notification notification = Notification.create( - receiver, - event.getFollowerName() + "왹이 잡 았다! 너 잡혔다!", - "/" + event.getFollowerId(), - NotificationType.FOLLOW - ); - notificationRepository.save(notification); - - sendSSE(receiverId, notification); + try { + handleNotification( + event.receiverId(), + event.followerName() + "왹이 잡 았다! 너 잡혔다!", + "/" + event.followerId(), + NotificationType.FOLLOW + ); + } catch (Exception e) { + log.warn("팔로우 이벤트 알림 처리 실패: {}", event, e); + } } @Transactional @EventListener public void handleUnFollowCreated(FollowDeletedEvent event) { - Long receiverId = event.getReceiverId(); - User receiver = getReceiver(receiverId); + try { + handleNotification( + event.receiverId(), + "오오자비로운" + event.followerName() + "왹께서널놓아주시니", + "/" + event.followerId(), + NotificationType.FOLLOW + ); + } catch (Exception e) { + log.warn("언팔로우 이벤트 알림 처리 실패: {}", event, e); + } + } - Notification notification = Notification.create( - receiver, - "오오자비로운" + event.getFollowerName() + "왹께서널놓아주시니", - "/" + event.getFollowerId(), - NotificationType.FOLLOW - ); + private void handleNotification(Long receiverId, String content, String url, NotificationType type) { + User receiver = getReceiver(receiverId); + Notification notification = Notification.create(receiver, content, url, type); notificationRepository.save(notification); sendSSE(receiverId, notification); diff --git a/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java index a8b2886..b584847 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java +++ b/src/main/java/com/jiwon/mylog/domain/event/PointEventListener.java @@ -1,15 +1,17 @@ package com.jiwon.mylog.domain.event; -import com.jiwon.mylog.domain.event.dto.CommentCreatedEvent; +import com.jiwon.mylog.domain.event.dto.comment.CommentCreatedEvent; import com.jiwon.mylog.domain.event.dto.DailyLoginEvent; -import com.jiwon.mylog.domain.event.dto.PostCreatedEvent; +import com.jiwon.mylog.domain.event.dto.post.PostCreatedEvent; import com.jiwon.mylog.domain.point.repository.PointHistoryRepository; import com.jiwon.mylog.domain.point.service.PointService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +@Slf4j @RequiredArgsConstructor @Component public class PointEventListener { @@ -27,33 +29,49 @@ public class PointEventListener { @Transactional @EventListener public void handleDailyLogin(DailyLoginEvent event) { - if(historyRepository.countDailyPointByDescription( - event.getUserId(), - DAILY_LOGIN_DESCRIPTION) >= DAILY_LOGIN_LIMIT) { - return; + try { + if (historyRepository.countDailyPointByDescription( + event.userId(), + DAILY_LOGIN_DESCRIPTION) >= DAILY_LOGIN_LIMIT) { + return; + } + pointService.earnPoint(event.userId(), 404, DAILY_LOGIN_DESCRIPTION); + } catch (Exception e) { + log.warn("로그인 포인트 이벤트 처리 실패: {}", event, e); } - pointService.earnPoint(event.getUserId(), 404, DAILY_LOGIN_DESCRIPTION); } @Transactional @EventListener public void handlePostCreated(PostCreatedEvent event) { - if (historyRepository.countDailyPointByDescription( - event.getUserId(), - POST_EARN_DESCRIPTION) >= POST_POINT_LIMIT) { - return; + try { + if (historyRepository.countDailyPointByDescription( + event.userId(), + POST_EARN_DESCRIPTION) >= POST_POINT_LIMIT) { + return; + } + pointService.earnPoint(event.userId(), 77, POST_EARN_DESCRIPTION); + } catch (Exception e) { + log.warn("게시글 작성 포인트 이벤트 처리 실패: {}", event, e); } - pointService.earnPoint(event.getUserId(), 77, POST_EARN_DESCRIPTION); } @Transactional @EventListener public void handleCommentCreated(CommentCreatedEvent event) { - if (historyRepository.countDailyPointByDescription( - event.getCommentWriterId(), - COMMENT_EARN_DESCRIPTION) >= COMMENT_POINT_LIMIT) { + if (!event.postWriterId().equals(event.commentWriterId())) { return; } - pointService.earnPoint(event.getCommentWriterId(), 44, COMMENT_EARN_DESCRIPTION); + + try { + if (historyRepository.countDailyPointByDescription( + event.commentWriterId(), + COMMENT_EARN_DESCRIPTION) >= COMMENT_POINT_LIMIT) { + return; + } + pointService.earnPoint(event.commentWriterId(), 44, COMMENT_EARN_DESCRIPTION); + } catch (Exception e) { + log.warn("댓글 작성 포인트 이벤트 처리 실패: {}", event, e); + } } } diff --git a/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java new file mode 100644 index 0000000..eaadeb3 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java @@ -0,0 +1,100 @@ +package com.jiwon.mylog.domain.event; + +import com.jiwon.mylog.domain.event.dto.comment.CommentCreatedEvent; +import com.jiwon.mylog.domain.event.dto.comment.CommentDeletedEvent; +import com.jiwon.mylog.domain.event.dto.like.LikeCreatedEvent; +import com.jiwon.mylog.domain.event.dto.like.LikeDeletedEvent; +import com.jiwon.mylog.domain.event.dto.post.PostCreatedEvent; +import com.jiwon.mylog.domain.event.dto.post.PostDeletedEvent; +import com.jiwon.mylog.domain.statistic.UserStatsService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Slf4j +@RequiredArgsConstructor +@Component +public class UserStatsEventListener { + + private final UserStatsService userStatsService; + + @EventListener + public void handleCommentCreated(CommentCreatedEvent event) { + if (!validateIsToday(event.createdAt())) return; + + try { + if (!event.postWriterId().equals(event.commentWriterId())) { + userStatsService.updateReceivedComments(event.postWriterId(), 1); + } + userStatsService.updateCreatedComments(event.commentWriterId(), 1); + } catch (Exception e) { + log.warn("댓글 생성 이벤트 통계 처리 실패: {}", event, e); + } + } + + @EventListener + public void handleCommentDeleted(CommentDeletedEvent event) { + if (!validateIsToday(event.createdAt())) return; + + try { + if (!event.postWriterId().equals(event.commentWriterId())) { + userStatsService.updateReceivedComments(event.postWriterId(), -1); + } + userStatsService.updateCreatedComments(event.commentWriterId(), -1); + } catch (Exception e) { + log.warn("댓글 삭제 이벤트 통계 처리 실패: {}", event, e); + } + } + + @EventListener + public void handleLikeCreated(LikeCreatedEvent event) { + if (!validateIsToday(event.createdAt())) return; + + try { + userStatsService.updateReceivedLikes(event.postWriterId(), 1); + } catch (Exception e) { + log.warn("좋아요 생성 이벤트 통계 처리 실패: {}", event, e); + } + } + + @EventListener + public void handleLikeDeleted(LikeDeletedEvent event) { + if (!validateIsToday(event.createdAt())) return; + + try { + userStatsService.updateReceivedLikes(event.postWriterId(), -1); + } catch (Exception e) { + log.warn("좋아요 삭제 이벤트 통계 처리 실패: {}", event, e); + } + } + + @EventListener + public void handlePostCreated(PostCreatedEvent event) { + if (!validateIsToday(event.createdAt())) return; + + try { + userStatsService.updateCreatedPosts(event.userId(), 1); + } catch (Exception e) { + log.warn("게시글 생성 이벤트 통계 처리 실패: {}", event, e); + } + } + + @EventListener + public void handlePostDeleted(PostDeletedEvent event) { + if (!validateIsToday(event.createdAt())) return; + + try { + userStatsService.updateCreatedPosts(event.userId(), -1); + } catch (Exception e) { + log.warn("게시글 삭제 이벤트 통계 처리 실패: {}", event, e); + } + } + + private boolean validateIsToday(LocalDateTime event) { + return event.toLocalDate().equals(LocalDate.now()); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java deleted file mode 100644 index fa87b3b..0000000 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/CommentCreatedEvent.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.jiwon.mylog.domain.event.dto; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class CommentCreatedEvent { - private final Long postId; - private final Long postWriterId; - private final Long commentId; - private final Long commentWriterId; - private final String commentWriterName; -} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/DailyLoginEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/DailyLoginEvent.java index 1b6b67b..d24ba0e 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/DailyLoginEvent.java +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/DailyLoginEvent.java @@ -1,10 +1,4 @@ package com.jiwon.mylog.domain.event.dto; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class DailyLoginEvent { - private final Long userId; +public record DailyLoginEvent(Long userId) { } diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/FollowCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/FollowCreatedEvent.java deleted file mode 100644 index 1fdab18..0000000 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/FollowCreatedEvent.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.jiwon.mylog.domain.event.dto; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class FollowCreatedEvent { - private final Long receiverId; - private final Long followerId; - private final String followerName; -} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/FollowDeletedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/FollowDeletedEvent.java deleted file mode 100644 index 3398860..0000000 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/FollowDeletedEvent.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.jiwon.mylog.domain.event.dto; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class FollowDeletedEvent { - private final Long receiverId; - private final Long followerId; - private final String followerName; -} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/LikeCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/LikeCreatedEvent.java deleted file mode 100644 index b827288..0000000 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/LikeCreatedEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.jiwon.mylog.domain.event.dto; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class LikeCreatedEvent { - private final Long postId; - private final Long postWriterId; - private final Long likeWriterId; - private final String likeWriterName; -} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/PostCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/PostCreatedEvent.java deleted file mode 100644 index 59c0c24..0000000 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/PostCreatedEvent.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.jiwon.mylog.domain.event.dto; - - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public class PostCreatedEvent { - private final Long userId; - private final Long postId; -} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java new file mode 100644 index 0000000..c3898dc --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentCreatedEvent.java @@ -0,0 +1,9 @@ +package com.jiwon.mylog.domain.event.dto.comment; + +import java.time.LocalDateTime; + +public record CommentCreatedEvent( + Long postId, Long postWriterId, + Long commentId, Long commentWriterId, String commentWriterName, + LocalDateTime createdAt) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentDeletedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentDeletedEvent.java new file mode 100644 index 0000000..3952f2b --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/comment/CommentDeletedEvent.java @@ -0,0 +1,9 @@ +package com.jiwon.mylog.domain.event.dto.comment; + +import java.time.LocalDateTime; + +public record CommentDeletedEvent( + Long postId, Long postWriterId, + Long commentId, Long commentWriterId, + LocalDateTime createdAt) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/follow/FollowCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/follow/FollowCreatedEvent.java new file mode 100644 index 0000000..2fa6fea --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/follow/FollowCreatedEvent.java @@ -0,0 +1,5 @@ +package com.jiwon.mylog.domain.event.dto.follow; + +public record FollowCreatedEvent( + Long receiverId, Long followerId, String followerName) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/follow/FollowDeletedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/follow/FollowDeletedEvent.java new file mode 100644 index 0000000..df7ddf0 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/follow/FollowDeletedEvent.java @@ -0,0 +1,5 @@ +package com.jiwon.mylog.domain.event.dto.follow; + +public record FollowDeletedEvent( + Long receiverId, Long followerId, String followerName) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java new file mode 100644 index 0000000..92a816a --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java @@ -0,0 +1,8 @@ +package com.jiwon.mylog.domain.event.dto.like; + +import java.time.LocalDateTime; + +public record LikeCreatedEvent( + Long postId, Long postWriterId, Long likeWriterId, String likeWriterName, + LocalDateTime createdAt) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java new file mode 100644 index 0000000..27c0f2c --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java @@ -0,0 +1,7 @@ +package com.jiwon.mylog.domain.event.dto.like; + +import java.time.LocalDateTime; + +public record LikeDeletedEvent( + Long postId, Long postWriterId, Long likeWriterId, LocalDateTime createdAt) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/post/PostCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/post/PostCreatedEvent.java new file mode 100644 index 0000000..e2d7c16 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/post/PostCreatedEvent.java @@ -0,0 +1,6 @@ +package com.jiwon.mylog.domain.event.dto.post; + +import java.time.LocalDateTime; + +public record PostCreatedEvent(Long userId, Long postId, LocalDateTime createdAt) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/post/PostDeletedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/post/PostDeletedEvent.java new file mode 100644 index 0000000..0ceb9c9 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/post/PostDeletedEvent.java @@ -0,0 +1,6 @@ +package com.jiwon.mylog.domain.event.dto.post; + +import java.time.LocalDateTime; + +public record PostDeletedEvent(Long userId, Long postId, LocalDateTime createdAt) { +} diff --git a/src/main/java/com/jiwon/mylog/domain/follow/service/FollowService.java b/src/main/java/com/jiwon/mylog/domain/follow/service/FollowService.java index cf3a285..efb38d1 100644 --- a/src/main/java/com/jiwon/mylog/domain/follow/service/FollowService.java +++ b/src/main/java/com/jiwon/mylog/domain/follow/service/FollowService.java @@ -1,7 +1,7 @@ package com.jiwon.mylog.domain.follow.service; -import com.jiwon.mylog.domain.event.dto.FollowCreatedEvent; -import com.jiwon.mylog.domain.event.dto.FollowDeletedEvent; +import com.jiwon.mylog.domain.event.dto.follow.FollowCreatedEvent; +import com.jiwon.mylog.domain.event.dto.follow.FollowDeletedEvent; import com.jiwon.mylog.domain.follow.dto.FollowCountResponse; import com.jiwon.mylog.domain.follow.dto.FollowListResponse; import com.jiwon.mylog.domain.follow.dto.FollowResponse; diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java b/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java index 9074aaf..2b0d5cf 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java @@ -1,18 +1,24 @@ package com.jiwon.mylog.domain.like; -import com.jiwon.mylog.domain.post.entity.Post; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.Objects; + @Repository public interface LikeRepository extends JpaRepository { boolean existsByUserIdAndPostId(Long userId, Long postId); + @Query(value = "SELECT p.user_id, l.created_at " + + "FROM likes l " + + "INNER JOIN posts p ON l.post_id = p.id " + + "WHERE l.user_id = :userId AND l.post_id = :postId", + nativeQuery = true) + Object[] findLikeDetails(@Param("userId") Long userId, @Param("postId") Long postId); + @Modifying @Query(value = "insert ignore into likes(user_id, post_id) values(:userId, :postId)", nativeQuery = true) void saveLike(@Param("userId") Long userId, @Param("postId") Long postId); diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeService.java b/src/main/java/com/jiwon/mylog/domain/like/LikeService.java index e583892..6bf20a8 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeService.java +++ b/src/main/java/com/jiwon/mylog/domain/like/LikeService.java @@ -1,6 +1,7 @@ package com.jiwon.mylog.domain.like; -import com.jiwon.mylog.domain.event.dto.LikeCreatedEvent; +import com.jiwon.mylog.domain.event.dto.like.LikeCreatedEvent; +import com.jiwon.mylog.domain.event.dto.like.LikeDeletedEvent; import com.jiwon.mylog.domain.post.dto.response.PostSummaryResponse; import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.post.repository.PostRepository; @@ -18,6 +19,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.Objects; + @RequiredArgsConstructor @Service @@ -38,7 +42,7 @@ public void createLike(Long userId, Long postId) { Long receiverId = post.getUser().getId(); Like like = Like.toLike(user, post); - likeRepository.save(like); + Like savedLike = likeRepository.save(like); if (!receiverId.equals(userId)) { eventPublisher.publishEvent( @@ -46,7 +50,9 @@ public void createLike(Long userId, Long postId) { postId, receiverId, userId, - user.getUsername()) + user.getUsername(), + savedLike.getCreatedAt() + ) ); } } @@ -56,7 +62,23 @@ public void createLike(Long userId, Long postId) { public void deleteLike(Long userId, Long postId) { validateUserExists(userId); validatePostExists(postId); + + Object[] likeDetails = likeRepository.findLikeDetails(userId, postId); + Long receiverId = ((Number) likeDetails[0]).longValue(); + LocalDateTime createdAt = (LocalDateTime) likeDetails[1]; + likeRepository.deleteLike(userId, postId); + + if (!receiverId.equals(userId)) { + eventPublisher.publishEvent( + new LikeDeletedEvent( + postId, + receiverId, + userId, + createdAt + ) + ); + } } @Cacheable(value = "like::count", key = "'postId:' + #postId", condition = "#postId != null") diff --git a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java index f9c25aa..50f7f4d 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java +++ b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java @@ -462,9 +462,12 @@ private BooleanExpression likeUserIdEq(Long userId) { } private BooleanExpression categoryIdEq(Long categoryId) { - return (categoryId == null || categoryId < 0L) ? - POST.category.id.isNull() : - POST.category.id.eq(categoryId); + if (categoryId == null || categoryId == -1L) { + return POST.category.id.isNull(); + } else if (categoryId == 0L) { + return null; + } + return POST.category.id.eq(categoryId); } private BooleanExpression userDeletedAtIsNull() { diff --git a/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java b/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java index aa94b32..fb637cc 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java +++ b/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java @@ -2,7 +2,8 @@ import com.jiwon.mylog.domain.category.entity.Category; import com.jiwon.mylog.domain.comment.entity.Comment; -import com.jiwon.mylog.domain.event.dto.PostCreatedEvent; +import com.jiwon.mylog.domain.event.dto.post.PostCreatedEvent; +import com.jiwon.mylog.domain.event.dto.post.PostDeletedEvent; import com.jiwon.mylog.domain.post.dto.request.PostRequest; import com.jiwon.mylog.domain.post.dto.response.MainPostResponse; import com.jiwon.mylog.domain.post.dto.response.NoticePostResponse; @@ -21,6 +22,7 @@ import com.jiwon.mylog.domain.category.repository.CategoryRepository; import com.jiwon.mylog.domain.user.repository.UserRepository; +import java.time.LocalDateTime; import java.util.List; import com.jiwon.mylog.domain.tag.service.TagService; @@ -67,7 +69,7 @@ public PostDetailResponse createPost(Long userId, PostRequest postRequest) { Post savedPost = postRepository.save(post); increaseRelatedPostInfo(category, tags); - eventPublisher.publishEvent(new PostCreatedEvent(userId, savedPost.getId())); + eventPublisher.publishEvent(new PostCreatedEvent(userId, savedPost.getId(), savedPost.getCreatedAt())); return PostDetailResponse.fromPost(savedPost); } @@ -109,6 +111,9 @@ public void deletePost(Long userId, Long postId) { validateOwner(post, userId); decreaseRelatedPostInfo(post); deleteRelatedPostInfo(post); + + eventPublisher.publishEvent(new PostDeletedEvent(userId, postId, post.getCreatedAt())); + post.delete(); } @@ -233,8 +238,7 @@ public PageResponse searchPosts( } private PageResponse findFilteredPosts(Long userId, Long categoryId, List tagIds, String keyword, Pageable pageable) { - Long realCategoryId = (categoryId == null || categoryId.equals(0L)) ? null : categoryId; - Page postPage = postRepository.findFilteredPosts(userId, realCategoryId, tagIds, keyword, pageable); + Page postPage = postRepository.findFilteredPosts(userId, categoryId, tagIds, keyword, pageable); return PageResponse.from( postPage.getContent(), postPage.getNumber(), diff --git a/src/main/java/com/jiwon/mylog/domain/post/service/PostViewService.java b/src/main/java/com/jiwon/mylog/domain/post/service/PostViewService.java index 437995f..b2fbfa1 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/service/PostViewService.java +++ b/src/main/java/com/jiwon/mylog/domain/post/service/PostViewService.java @@ -1,33 +1,36 @@ package com.jiwon.mylog.domain.post.service; +import com.jiwon.mylog.global.redis.key.RedisKey; import com.jiwon.mylog.global.redis.RedisUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service public class PostViewService { private final RedisUtil redisUtil; - private static final String VIEW_KEY_PREFIX = "post:view:"; - private static final String VIEW_COUNT_KEY_PREFIX = "post:view:count:"; public int incrementPostView(Long postId, int view, String userKey) { - String existKey = VIEW_KEY_PREFIX + postId; - String countKey = VIEW_COUNT_KEY_PREFIX + postId; + String existKey = RedisKey.VIEW_KEY.createKey(postId.toString()); + String countKey = RedisKey.VIEW_COUNT_KEY.createKey(postId.toString()); boolean exist = redisUtil.existPostViewUser(existKey, userKey); if (!exist) { redisUtil.addPostViewUser(existKey, userKey); - redisUtil.increasePostView(countKey, String.valueOf(view)); + redisUtil.incrementAndGet( + countKey, + String.valueOf(view), + RedisKey.VIEW_COUNT_KEY.getTtl(), + 1 + ); } return getPostView(postId, view); } public int getPostView(Long postId, int view) { - String key = VIEW_COUNT_KEY_PREFIX + postId; - return redisUtil.getPostView(key, view); + String key = RedisKey.VIEW_COUNT_KEY.createKey(postId.toString()); + return redisUtil.getInt(key, view); } } diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserDailyStats.java b/src/main/java/com/jiwon/mylog/domain/statistic/UserDailyStats.java new file mode 100644 index 0000000..a9ad814 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/UserDailyStats.java @@ -0,0 +1,59 @@ +package com.jiwon.mylog.domain.statistic; + +import com.jiwon.mylog.domain.user.entity.User; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Getter +@Entity +@Table( + uniqueConstraints = @UniqueConstraint( + name = "user_daily_stats_uk", + columnNames = {"user_id", "date"} + ) +) +public class UserDailyStats { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + private LocalDate date; + + private int receivedLikes = 0; + + private int receivedComments = 0; + + private int createdPosts = 0; + + private int createdComments = 0; + + public static UserDailyStats empty(LocalDate date) { + return UserDailyStats.builder() + .date(date) + .receivedComments(0) + .receivedLikes(0) + .createdComments(0) + .createdPosts(0) + .build(); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java new file mode 100644 index 0000000..7d3c449 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java @@ -0,0 +1,29 @@ +package com.jiwon.mylog.domain.statistic; + +import com.jiwon.mylog.domain.statistic.dto.DailyReportResponse; +import com.jiwon.mylog.global.security.auth.annotation.LoginUser; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; + +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class UserStatsController { + + private final UserStatsService userStatsService; + + @GetMapping("/users/stats") + public ResponseEntity getDailyStats( + @LoginUser Long userId, + @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam LocalDate date) { + DailyReportResponse response = userStatsService.getUserDailyStats(userId, date); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsRepository.java b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsRepository.java new file mode 100644 index 0000000..ba98925 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsRepository.java @@ -0,0 +1,27 @@ +package com.jiwon.mylog.domain.statistic; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDate; +import java.util.Optional; + +@Repository +public interface UserStatsRepository extends JpaRepository { + Optional findByUserIdAndDate(Long userId, LocalDate date); + + @Modifying + @Query(value = "insert into user_daily_stats(user_id, date, received_comments, received_likes, created_comments, created_posts)" + + "values (:userId, :date, :receivedComments, :receivedLikes, :createdComments, :createdPosts)", + nativeQuery = true) + void saveDailyStat(@Param("userId") Long userId, + @Param("date") LocalDate date, + @Param("receivedComments") int receivedComments, + @Param("receivedLikes") int receivedLikes, + @Param("createdComments") int createdComments, + @Param("createdPosts") int createdPosts + ); +} diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java new file mode 100644 index 0000000..0705aac --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java @@ -0,0 +1,81 @@ +package com.jiwon.mylog.domain.statistic; + +import com.jiwon.mylog.domain.statistic.dto.DailyReportResponse; +import com.jiwon.mylog.global.redis.key.RedisKey; +import com.jiwon.mylog.global.redis.RedisUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; + +@RequiredArgsConstructor +@Service +public class UserStatsService { + + private final RedisUtil redisUtil; + + private final UserStatsRepository userStatsRepository; + + private final static String REDIS_SET_DEFAULT = "0"; + private final static int REDIS_GET_DEFAULT = 0; + + @Transactional(readOnly = true) + public DailyReportResponse getUserDailyStats(Long userId, LocalDate date) { + if (date.isEqual(LocalDate.now())) { + return getUserTodayStats(userId, date); + } + + UserDailyStats userDailyStats = userStatsRepository.findByUserIdAndDate(userId, date) + .orElseGet(() -> UserDailyStats.empty(date)); + + return DailyReportResponse.builder() + .receivedComments(userDailyStats.getReceivedComments()) + .receivedLikes(userDailyStats.getReceivedLikes()) + .createdComments(userDailyStats.getCreatedComments()) + .createdPosts(userDailyStats.getCreatedPosts()) + .date(date) + .build(); + } + + private DailyReportResponse getUserTodayStats(Long userId, LocalDate date) { + String identifier = RedisKey.createStatsIdentifier(userId, date); + + String receivedCommentsKey = RedisKey.RECEIVED_COMMENTS.createKey(identifier); + String receivedLikesKey = RedisKey.RECEIVED_LIKES.createKey(identifier); + String createdPostsKey = RedisKey.CREATED_POSTS.createKey(identifier); + String createdCommentsKey = RedisKey.CREATED_COMMENTS.createKey(identifier); + + return DailyReportResponse.builder() + .receivedComments(redisUtil.getInt(receivedCommentsKey, REDIS_GET_DEFAULT)) + .receivedLikes(redisUtil.getInt(receivedLikesKey, REDIS_GET_DEFAULT)) + .createdPosts(redisUtil.getInt(createdPostsKey, REDIS_GET_DEFAULT)) + .createdComments(redisUtil.getInt(createdCommentsKey, REDIS_GET_DEFAULT)) + .date(date) + .build(); + } + + public void updateReceivedComments(Long userId, int increment) { + String identifier = RedisKey.createStatsIdentifier(userId, LocalDate.now()); + String key = RedisKey.RECEIVED_COMMENTS.createKey(identifier); + redisUtil.incrementAndGet(key, REDIS_SET_DEFAULT, RedisKey.RECEIVED_COMMENTS.getTtl(), increment); + } + + public void updateReceivedLikes(Long userId, int increment) { + String identifier = RedisKey.createStatsIdentifier(userId, LocalDate.now()); + String key = RedisKey.RECEIVED_LIKES.createKey(identifier); + redisUtil.incrementAndGet(key, REDIS_SET_DEFAULT, RedisKey.RECEIVED_LIKES.getTtl(), increment); + } + + public void updateCreatedPosts(Long userId, int increment) { + String identifier = RedisKey.createStatsIdentifier(userId, LocalDate.now()); + String key = RedisKey.CREATED_POSTS.createKey(identifier); + redisUtil.incrementAndGet(key, REDIS_SET_DEFAULT, RedisKey.CREATED_POSTS.getTtl(), increment); + } + + public void updateCreatedComments(Long userId, int increment) { + String identifier = RedisKey.createStatsIdentifier(userId, LocalDate.now()); + String key = RedisKey.CREATED_COMMENTS.createKey(identifier); + redisUtil.incrementAndGet(key, REDIS_SET_DEFAULT, RedisKey.CREATED_COMMENTS.getTtl(), increment); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/dto/DailyReportResponse.java b/src/main/java/com/jiwon/mylog/domain/statistic/dto/DailyReportResponse.java new file mode 100644 index 0000000..e28f19f --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/dto/DailyReportResponse.java @@ -0,0 +1,14 @@ +package com.jiwon.mylog.domain.statistic.dto; + +import lombok.Builder; + +import java.time.LocalDate; + +@Builder +public record DailyReportResponse( + LocalDate date, + int receivedLikes, + int receivedComments, + int createdPosts, + int createdComments) { +} diff --git a/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java b/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java index 5c24e5d..59cd718 100644 --- a/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java +++ b/src/main/java/com/jiwon/mylog/global/mail/service/MailService.java @@ -8,6 +8,8 @@ import java.io.UnsupportedEncodingException; import java.security.SecureRandom; +import java.time.Duration; + import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; @@ -26,27 +28,27 @@ public class MailService { @Transactional(readOnly = true) public void verifyEmailCode(String email, String code) { - String codeFindByEmail = redisUtil.getData(email); + String codeFindByEmail = redisUtil.get(email); if (codeFindByEmail == null) { throw new MailException(ErrorCode.NOT_FOUND_MAIL_CODE); } if (!codeFindByEmail.equals(code)) { throw new MailException(ErrorCode.INVALID_MAIL_CODE); } - redisUtil.deleteData(email); + redisUtil.delete(email); } @Transactional public void sendCodeMail(String email) { - if (redisUtil.existEmailData(email)) { - redisUtil.deleteData(email); + if (redisUtil.exist(email)) { + redisUtil.delete(email); } String subject = "[MyLog] 인증번호입니다."; String code = createCode(); String text = createCodeText(code); MimeMessage message = createEmail(email, subject, text); - redisUtil.setDataExpire(email, code, 60 * 5L); + redisUtil.set(email, code, Duration.ofMinutes(5)); try { javaMailSender.send(message); diff --git a/src/main/java/com/jiwon/mylog/global/redis/RedisUtil.java b/src/main/java/com/jiwon/mylog/global/redis/RedisUtil.java index 24cfb8e..6f0522b 100644 --- a/src/main/java/com/jiwon/mylog/global/redis/RedisUtil.java +++ b/src/main/java/com/jiwon/mylog/global/redis/RedisUtil.java @@ -1,12 +1,19 @@ package com.jiwon.mylog.global.redis; import java.time.Duration; +import java.time.LocalDate; +import java.time.format.DateTimeParseException; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import com.jiwon.mylog.global.redis.key.UserStatsKey; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; @@ -17,23 +24,79 @@ public class RedisUtil { private final StringRedisTemplate redisTemplate; - /** - * 이메일 관련 + /* + 기본 메서드 */ - public String getData(String key) { + public int incrementAndGet(String key, String value, Duration ttl, int increment) { + redisTemplate.opsForValue().setIfAbsent(key, value, ttl); + Long result = redisTemplate.opsForValue().increment(key, increment); + return result.intValue(); + } + + public void set(String key, String value, Duration ttl) { + redisTemplate.opsForValue().set(key, value, ttl); + } + + public String get(String key) { return redisTemplate.opsForValue().get(key); } - public boolean existEmailData(String key) { + public int getInt(String key, int defaultValue) { + String value = redisTemplate.opsForValue().get(key); + return value != null ? Integer.parseInt(value) : defaultValue; + } + + public void delete(String key) { + redisTemplate.delete(key); + } + + public boolean exist(String key) { return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } - public void setDataExpire(String key, String value, long duration) { - redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(duration)); + public Set scanStatsKeys(String pattern, LocalDate target) { + + Set keys = new HashSet<>(); + + ScanOptions options = ScanOptions + .scanOptions().match(pattern).count(50).build(); + + redisTemplate.execute((RedisCallback>) connection -> { + try (Cursor cursor = connection.scan(options)) { + while (cursor.hasNext()) { + try { + String key = new String(cursor.next()); + parseStatsKey(key, keys, target); + } catch (Exception e) { + log.warn("parse key failed", e); + } + } + } catch (Exception e) { + log.error("Redis scan error", e); + } + return keys; + }); + + return keys; } - public void deleteData(String key) { - redisTemplate.delete(key); + private void parseStatsKey(String key, Set keys, LocalDate target) { + String[] split = key.split(":"); + + if (split.length < 5) return; + + try { + Long userId = Long.valueOf(split[3]); + LocalDate date = LocalDate.parse(split[4]); + + if (date.isEqual(target)) { + keys.add(new UserStatsKey(userId, date)); + } + } catch (NumberFormatException e) { + log.warn("올바르지 않은 key 형식 (userId 형식 오류): {}", key); + } catch (DateTimeParseException e) { + log.warn("올바르지 않은 key 형식 (date 형식 오류): {}", key); + } } /** @@ -48,25 +111,12 @@ public void addPostViewUser(String key, String value) { redisTemplate.expire(key, Duration.ofHours(12)); } - public Long increasePostView(String key, String view) { - Boolean isNew = redisTemplate.opsForValue().setIfAbsent(key, view); - if (Boolean.TRUE.equals(isNew)) { - redisTemplate.expire(key, Duration.ofDays(7)); - } - return redisTemplate.opsForValue().increment(key, 1); - } - - public int getPostView(String key, int view) { - String value = redisTemplate.opsForValue().get(key); - return value != null ? Integer.parseInt(value) : view; - } - public Map getAllPostView(String keyPrefix) { Set keys = redisTemplate.keys(keyPrefix); return keys.stream() .collect(Collectors.toMap( key -> Long.parseLong(key.replace(keyPrefix, "")), - key -> getPostView(key, 0) + key -> getInt(key, 0) )); } diff --git a/src/main/java/com/jiwon/mylog/global/redis/key/RedisKey.java b/src/main/java/com/jiwon/mylog/global/redis/key/RedisKey.java new file mode 100644 index 0000000..12ba0cf --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/redis/key/RedisKey.java @@ -0,0 +1,36 @@ +package com.jiwon.mylog.global.redis.key; + +import lombok.RequiredArgsConstructor; + +import java.time.Duration; +import java.time.LocalDate; + +@RequiredArgsConstructor +public enum RedisKey { + RECEIVED_COMMENTS("user:stats:receivedComments:", Duration.ofDays(2)), + RECEIVED_LIKES("user:stats:receivedLikes:", Duration.ofDays(2)), + CREATED_POSTS("user:stats:createdPosts:", Duration.ofDays(2)), + CREATED_COMMENTS("user:stats:createdComments:", Duration.ofDays(2)), + + VIEW_KEY("post:view:", Duration.ofHours(12)), + VIEW_COUNT_KEY("post:view:count:", Duration.ofDays(7)); + + private final String prefix; + private final Duration ttl; + + public static String createStatsIdentifier(Long userId, LocalDate date) { + return userId + ":" + date; + } + + public String createKey(String identifier) { + return prefix + identifier; + } + + public String getPrefix() { + return prefix; + } + + public Duration getTtl() { + return ttl; + } +} diff --git a/src/main/java/com/jiwon/mylog/global/redis/key/UserStatsKey.java b/src/main/java/com/jiwon/mylog/global/redis/key/UserStatsKey.java new file mode 100644 index 0000000..db6595d --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/redis/key/UserStatsKey.java @@ -0,0 +1,5 @@ +package com.jiwon.mylog.global.redis.key; + +import java.time.LocalDate; + +public record UserStatsKey(Long userId, LocalDate date) { } \ No newline at end of file diff --git a/src/main/java/com/jiwon/mylog/global/schedular/PostViewScheduler.java b/src/main/java/com/jiwon/mylog/global/schedular/PostViewScheduler.java index 1454afb..cd4ddc4 100644 --- a/src/main/java/com/jiwon/mylog/global/schedular/PostViewScheduler.java +++ b/src/main/java/com/jiwon/mylog/global/schedular/PostViewScheduler.java @@ -1,6 +1,7 @@ package com.jiwon.mylog.global.schedular; import com.jiwon.mylog.domain.post.repository.PostRepository; +import com.jiwon.mylog.global.redis.key.RedisKey; import com.jiwon.mylog.global.redis.RedisUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -17,12 +18,11 @@ public class PostViewScheduler { private final PostRepository postRepository; private final RedisUtil redisUtil; - private static final String REDIS_KEY = "post:view:count:"; @Scheduled(fixedRate = 10 * 60 * 1000L) @Transactional public void syncPostViewToDB() { - Map postCounts = redisUtil.getAllPostView(REDIS_KEY); + Map postCounts = redisUtil.getAllPostView(RedisKey.VIEW_COUNT_KEY.getPrefix()); postCounts.forEach((postId, view) -> { try { postRepository.updatePostView(postId, view); diff --git a/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java b/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java new file mode 100644 index 0000000..7967f18 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java @@ -0,0 +1,75 @@ +package com.jiwon.mylog.global.schedular; + +import com.jiwon.mylog.domain.statistic.UserStatsRepository; +import com.jiwon.mylog.global.redis.key.RedisKey; +import com.jiwon.mylog.global.redis.RedisUtil; +import com.jiwon.mylog.global.redis.key.UserStatsKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Set; + +@Slf4j +@RequiredArgsConstructor +@Component +public class StatsScheduler { + + private final RedisUtil redisUtil; + private final UserStatsRepository userStatsRepository; + private final static String PATTERN = "user:stats:*:*:*"; + + @Scheduled(cron = "0 10 0 * * *") + @Transactional + public void updateDailyStats() { + LocalDate yesterday = LocalDate.now().minusDays(1); + + try { + Set userStatsKeys = redisUtil.scanStatsKeys(PATTERN, yesterday); + + for (UserStatsKey key : userStatsKeys) { + try { + String identifier = RedisKey.createStatsIdentifier(key.userId(), key.date()); + String receivedLikesKey = RedisKey.RECEIVED_LIKES.createKey(identifier); + String receivedCommentsKey = RedisKey.RECEIVED_COMMENTS.createKey(identifier); + String createdCommentsKey = RedisKey.CREATED_COMMENTS.createKey(identifier); + String createdPostsKey = RedisKey.CREATED_POSTS.createKey(identifier); + + saveStats(key, receivedLikesKey, receivedCommentsKey, createdCommentsKey, createdPostsKey); + deleteStatsFromRedis(receivedLikesKey, receivedCommentsKey, createdCommentsKey, createdPostsKey); + + } catch (Exception e) { + log.error("유저 통계 저장 실패: {}:{}", key.userId(), key.date(), e); + } + } + } catch (Exception e) { + log.error("유저 통계 스케줄러 오류 발생", e); + } + } + + private void saveStats(UserStatsKey key, String receivedLikesKey, String receivedCommentsKey, String createdCommentsKey, String createdPostsKey) { + int receivedLikes = redisUtil.getInt(receivedLikesKey, 0); + int receivedComments = redisUtil.getInt(receivedCommentsKey, 0); + int createdComments = redisUtil.getInt(createdCommentsKey, 0); + int createdPosts = redisUtil.getInt(createdPostsKey, 0); + + userStatsRepository.saveDailyStat( + key.userId(), + key.date(), + receivedComments, + receivedLikes, + createdComments, + createdPosts + ); + } + + private void deleteStatsFromRedis(String receivedLikesKey, String receivedCommentsKey, String createdCommentsKey, String createdPostsKey) { + redisUtil.delete(receivedLikesKey); + redisUtil.delete(receivedCommentsKey); + redisUtil.delete(createdCommentsKey); + redisUtil.delete(createdPostsKey); + } +}