diff --git a/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java b/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java index 3301093..87c3975 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java +++ b/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java @@ -18,7 +18,7 @@ @Getter @MappedSuperclass @EntityListeners(AuditingEntityListener.class) -public class BaseLike { +public abstract class BaseLike { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) diff --git a/src/main/java/com/jiwon/mylog/domain/post/dto/response/MainPostResponse.java b/src/main/java/com/jiwon/mylog/domain/post/dto/response/MainPostResponse.java index 50c62b8..fe2025b 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/dto/response/MainPostResponse.java +++ b/src/main/java/com/jiwon/mylog/domain/post/dto/response/MainPostResponse.java @@ -1,38 +1,14 @@ package com.jiwon.mylog.domain.post.dto.response; import com.fasterxml.jackson.annotation.JsonFormat; -import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.post.entity.PostStatus; -import com.jiwon.mylog.domain.user.dto.response.UserResponse; +import com.jiwon.mylog.domain.user.dto.response.UserSummaryResponse; import com.jiwon.mylog.global.common.enums.Visibility; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; import java.time.LocalDateTime; -@Getter -@Builder -@AllArgsConstructor -public class MainPostResponse { - private final Long postId; - private final String title; - private final String contentPreview; - private final PostStatus postStatus; - private final Visibility visibility; - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") - private final LocalDateTime createdAt; - private final UserResponse user; - - public static MainPostResponse fromPost(Post post) { - return MainPostResponse.builder() - .postId(post.getId()) - .title(post.getTitle()) - .contentPreview(post.getContentPreview()) - .postStatus(post.getPostStatus()) - .visibility(post.getVisibility()) - .createdAt(post.getCreatedAt()) - .user(UserResponse.fromUser(post.getUser())) - .build(); - } +public record MainPostResponse( + Long postId, String title, String contentPreview, PostStatus postStatus, Visibility visibility, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, + UserSummaryResponse user) { } diff --git a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java index f6cfbe2..0f4356a 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java @@ -29,14 +29,6 @@ public interface PostRepository extends JpaRepository, PostRepositor countQuery = "select count(p) from Post p where p.deletedAt is null and p.type = 'NOTICE'") Page findAllNotice(Pageable pageable); - @Query(value = "select p from Post p " + - "join fetch p.user u " + - "left join fetch u.profileImage " + - "where p.deletedAt is null and p.visibility = 'PUBLIC' and p.type = 'NORMAL' " + - "order by p.createdAt desc", - countQuery = "select count(p) from Post p where p.deletedAt is null and p.visibility = 'PUBLIC' and p.type = 'NORMAL'") - Page findAll(Pageable pageable); - @Query("select p from Post p join fetch p.user left join fetch p.category where p.id = :id") Optional findWithUserAndCategory(@Param("id") Long id); diff --git a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryCustom.java b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryCustom.java index 54f4de4..e657372 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryCustom.java +++ b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryCustom.java @@ -1,5 +1,6 @@ package com.jiwon.mylog.domain.post.repository; +import com.jiwon.mylog.domain.post.dto.response.MainPostResponse; import com.jiwon.mylog.domain.post.dto.response.PostDetailResponse; import com.jiwon.mylog.domain.post.dto.response.PostNavigationResponse; import com.jiwon.mylog.domain.post.dto.response.PostSummaryResponse; @@ -17,6 +18,8 @@ public interface PostRepositoryCustom { Page findFilteredPosts(Long userId, Long categoryId, List tagIds, String keyword, Pageable pageable); + Page findAllPosts(Pageable pageable); + Optional findPostDetail(Long postId); List findUserActivities(Long userId, LocalDate start, LocalDate end); 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 91d64a4..e69fdf8 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 @@ -6,10 +6,12 @@ import com.jiwon.mylog.domain.comment.entity.QComment; import com.jiwon.mylog.domain.image.entity.QProfileImage; import com.jiwon.mylog.domain.like.entity.QPostLike; +import com.jiwon.mylog.domain.post.dto.response.MainPostResponse; import com.jiwon.mylog.domain.post.dto.response.PostDetailResponse; import com.jiwon.mylog.domain.post.dto.response.PostNavigationResponse; import com.jiwon.mylog.domain.post.dto.response.PostSummaryResponse; import com.jiwon.mylog.domain.post.dto.response.RelatedPostResponse; +import com.jiwon.mylog.domain.post.entity.PostType; import com.jiwon.mylog.domain.post.entity.QPost; import com.jiwon.mylog.domain.tag.dto.response.TagResponse; import com.jiwon.mylog.domain.tag.entity.QPostTag; @@ -18,6 +20,7 @@ import com.jiwon.mylog.domain.user.dto.response.UserResponse; import com.jiwon.mylog.domain.user.dto.response.UserSummaryResponse; import com.jiwon.mylog.domain.user.entity.QUser; +import com.jiwon.mylog.global.common.enums.Visibility; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.Tuple; import com.querydsl.core.types.Projections; @@ -122,6 +125,42 @@ public Page findFilteredPosts( return new PageImpl<>(posts, pageable, total); } + @Override + public Page findAllPosts(Pageable pageable) { + BooleanBuilder conditions = new BooleanBuilder() + .and(postDeletedAtIsNull()) + .and(postVisibilityEq(Visibility.PUBLIC)) + .and(postTypeEq(PostType.NORMAL)); + + List posts = jpaQueryFactory + .select(Projections.constructor(MainPostResponse.class, + POST.id, + POST.title, + POST.contentPreview, + POST.postStatus, + POST.visibility, + POST.createdAt, + Projections.constructor(UserSummaryResponse.class, + USER.id, + USER.username, + PROFILE_IMAGE.fileKey.coalesce("") + ) + ) + ) + .from(POST) + .join(POST.user, USER) + .leftJoin(USER.profileImage, PROFILE_IMAGE) + .where(conditions) + .orderBy(POST.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + Long count = createCountQuery(conditions); + + return new PageImpl<>(posts, pageable, count); + } + private List createPostSummaryQuery(BooleanBuilder builder, Pageable pageable) { return jpaQueryFactory .select(Projections.constructor(PostSummaryResponse.class, @@ -478,6 +517,14 @@ private BooleanExpression postDeletedAtIsNull() { return POST.deletedAt.isNull(); } + private BooleanExpression postVisibilityEq(Visibility visibility) { + return POST.visibility.eq(visibility); + } + + private BooleanExpression postTypeEq(PostType type) { + return POST.type.eq(type); + } + private BooleanExpression titleContainsKeyword(String keyword) { return keyword != null ? POST.title.containsIgnoreCase(keyword) : null; } 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 8b98d7f..ce6d567 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 @@ -209,12 +209,10 @@ public PostDetailResponse getPost(Long postId) { condition = "#pageable != null") @Transactional(readOnly = true) public PageResponse getPosts(Pageable pageable) { - Page postPage = postRepository.findAll(pageable); - List posts = postPage.stream() - .map(MainPostResponse::fromPost) - .toList(); + Page postPage = postRepository.findAllPosts(pageable); + return PageResponse.from( - posts, + postPage.getContent(), postPage.getNumber(), postPage.getSize(), postPage.getTotalPages(), diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java index 9609730..7a017c0 100644 --- a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java +++ b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsController.java @@ -22,18 +22,11 @@ public class UserStatsController { private final UserStatsService userStatsService; @GetMapping("/users/rankers/weekly") - public ResponseEntity> getRanker( - @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, - @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate - ) { - LocalDate end = endDate != null ? endDate : LocalDate.now().minusDays(1); - LocalDate start = startDate != null ? startDate : end.minusDays(6); - - List response = userStatsService.getRanker(start, end); + public ResponseEntity> getRanker() { + List response = userStatsService.getRanker(); return ResponseEntity.ok(response); } - @GetMapping("/users/stats") public ResponseEntity getDailyStats( @LoginUser Long userId, diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java index 0f5e0e3..cf830ef 100644 --- a/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java +++ b/src/main/java/com/jiwon/mylog/domain/statistic/UserStatsService.java @@ -2,9 +2,14 @@ import com.jiwon.mylog.domain.statistic.dto.DailyReportResponse; import com.jiwon.mylog.domain.statistic.dto.UserRankResponse; +import com.jiwon.mylog.domain.statistic.entity.UserDailyStats; +import com.jiwon.mylog.domain.statistic.entity.UserWeeklyRanker; import com.jiwon.mylog.domain.statistic.repository.UserStatsRepository; +import com.jiwon.mylog.domain.statistic.repository.UserWeeklyRankerRepository; import com.jiwon.mylog.global.redis.key.RedisKey; import com.jiwon.mylog.global.redis.RedisUtil; +import java.util.Collections; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @@ -20,14 +25,26 @@ public class UserStatsService { private final RedisUtil redisUtil; private final UserStatsRepository userStatsRepository; + private final UserWeeklyRankerRepository userWeeklyRankerRepository; private final static String REDIS_SET_DEFAULT = "0"; private final static int REDIS_GET_DEFAULT = 0; - @Cacheable(value = "stat::ranker", key = "'start:' + #start + ':end:' + #end") + @Cacheable(value = "stat::ranker") @Transactional(readOnly = true) - public List getRanker(LocalDate start, LocalDate end) { - return userStatsRepository.findWeeklyTopUsers(start, end, 3); + public List getRanker() { + Optional latest = userWeeklyRankerRepository.findLatestWeekStartDate(); + + if (latest.isEmpty()) { + return Collections.emptyList(); + } + + List rankers = + userWeeklyRankerRepository.findAllByWeekStart(latest.get()); + + return rankers.stream() + .map(UserRankResponse::fromRanker) + .toList(); } @Transactional(readOnly = true) diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/dto/UserRankResponse.java b/src/main/java/com/jiwon/mylog/domain/statistic/dto/UserRankResponse.java index 4ae9de4..54fdd2e 100644 --- a/src/main/java/com/jiwon/mylog/domain/statistic/dto/UserRankResponse.java +++ b/src/main/java/com/jiwon/mylog/domain/statistic/dto/UserRankResponse.java @@ -1,7 +1,9 @@ package com.jiwon.mylog.domain.statistic.dto; -import com.jiwon.mylog.domain.user.dto.response.UserSummaryResponse; +import com.jiwon.mylog.domain.statistic.entity.UserWeeklyRanker; +import lombok.Builder; +@Builder public record UserRankResponse( Long userId, String username, @@ -10,5 +12,20 @@ public record UserRankResponse( int receivedComments, int createdPosts, int createdComments, - int total) { + long total) { + + public static UserRankResponse fromRanker(UserWeeklyRanker ranker) { + return UserRankResponse.builder() + .userId(ranker.getUser().getId()) + .username(ranker.getUser().getUsername()) + .imageKey(ranker.getUser().getProfileImage() != null ? + ranker.getUser().getProfileImage().getFileKey() : + "") + .receivedLikes(ranker.getReceivedLikes()) + .receivedComments(ranker.getReceivedComments()) + .createdComments(ranker.getCreatedComments()) + .createdPosts(ranker.getCreatedPosts()) + .total(ranker.getTotalScore()) + .build(); + } } diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/entity/BaseStats.java b/src/main/java/com/jiwon/mylog/domain/statistic/entity/BaseStats.java new file mode 100644 index 0000000..3d6d66f --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/entity/BaseStats.java @@ -0,0 +1,32 @@ +package com.jiwon.mylog.domain.statistic.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.ColumnDefault; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@SuperBuilder +@MappedSuperclass +public abstract class BaseStats { + + @Column(nullable = false) + @ColumnDefault("0") + private int receivedLikes = 0; + + @Column(nullable = false) + @ColumnDefault("0") + private int receivedComments = 0; + + @Column(nullable = false) + @ColumnDefault("0") + private int createdPosts = 0; + + @Column(nullable = false) + @ColumnDefault("0") + private int createdComments = 0; +} diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/UserDailyStats.java b/src/main/java/com/jiwon/mylog/domain/statistic/entity/UserDailyStats.java similarity index 78% rename from src/main/java/com/jiwon/mylog/domain/statistic/UserDailyStats.java rename to src/main/java/com/jiwon/mylog/domain/statistic/entity/UserDailyStats.java index a9ad814..88a3007 100644 --- a/src/main/java/com/jiwon/mylog/domain/statistic/UserDailyStats.java +++ b/src/main/java/com/jiwon/mylog/domain/statistic/entity/UserDailyStats.java @@ -1,4 +1,4 @@ -package com.jiwon.mylog.domain.statistic; +package com.jiwon.mylog.domain.statistic.entity; import com.jiwon.mylog.domain.user.entity.User; import jakarta.persistence.Entity; @@ -10,16 +10,15 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import lombok.AllArgsConstructor; -import lombok.Builder; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import java.time.LocalDate; +import lombok.experimental.SuperBuilder; -@NoArgsConstructor -@AllArgsConstructor -@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SuperBuilder @Getter @Entity @Table( @@ -28,7 +27,7 @@ columnNames = {"user_id", "date"} ) ) -public class UserDailyStats { +public class UserDailyStats extends BaseStats { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -39,14 +38,6 @@ public class UserDailyStats { 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) diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/entity/UserWeeklyRanker.java b/src/main/java/com/jiwon/mylog/domain/statistic/entity/UserWeeklyRanker.java new file mode 100644 index 0000000..8f62e02 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/entity/UserWeeklyRanker.java @@ -0,0 +1,50 @@ +package com.jiwon.mylog.domain.statistic.entity; + +import com.jiwon.mylog.domain.user.entity.User; +import jakarta.persistence.Column; +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 java.time.LocalDate; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.ColumnDefault; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SuperBuilder +@Getter +@Entity +@Table( + name = "user_weekly_ranker", + uniqueConstraints = @UniqueConstraint( + name = "user_weekly_ranker_uk", + columnNames = {"user_id", "week_start"} + ) +) +public class UserWeeklyRanker extends BaseStats { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(nullable = false) + private LocalDate weekStart; + + @Column(nullable = false) + @ColumnDefault("0") + private long totalScore; + + @Column(nullable = false) + private int rankOrder; +} \ No newline at end of file diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepository.java b/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepository.java index d0f7d08..3484c60 100644 --- a/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepository.java @@ -1,6 +1,6 @@ package com.jiwon.mylog.domain.statistic.repository; -import com.jiwon.mylog.domain.statistic.UserDailyStats; +import com.jiwon.mylog.domain.statistic.entity.UserDailyStats; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepositoryImpl.java b/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepositoryImpl.java index 77ffd3f..36ee5cc 100644 --- a/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepositoryImpl.java +++ b/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserStatsRepositoryImpl.java @@ -1,8 +1,8 @@ package com.jiwon.mylog.domain.statistic.repository; import com.jiwon.mylog.domain.image.entity.QProfileImage; -import com.jiwon.mylog.domain.statistic.QUserDailyStats; import com.jiwon.mylog.domain.statistic.dto.UserRankResponse; +import com.jiwon.mylog.domain.statistic.entity.QUserDailyStats; import com.jiwon.mylog.domain.user.entity.QUser; import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.NumberExpression; @@ -31,7 +31,7 @@ public List findWeeklyTopUsers(LocalDate startDate, LocalDate NumberExpression receivedCommentsSum = stat.receivedComments.sum(); NumberExpression createdPostsSum = stat.createdPosts.sum(); NumberExpression createdCommentsSum = stat.createdComments.sum(); - NumberExpression totalScore = receivedLikesSum + NumberExpression totalScore = receivedLikesSum.longValue() .add(receivedCommentsSum) .add(createdPostsSum) .add(createdCommentsSum); @@ -52,7 +52,7 @@ public List findWeeklyTopUsers(LocalDate startDate, LocalDate .leftJoin(user.profileImage, profileImage) .where(stat.date.between(startDate, endDate)) .groupBy(user.id) - .orderBy(totalScore.desc()) + .orderBy(totalScore.desc(), user.id.asc()) .limit(limit) .fetch(); } diff --git a/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserWeeklyRankerRepository.java b/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserWeeklyRankerRepository.java new file mode 100644 index 0000000..30d50ca --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/statistic/repository/UserWeeklyRankerRepository.java @@ -0,0 +1,26 @@ +package com.jiwon.mylog.domain.statistic.repository; + +import com.jiwon.mylog.domain.statistic.entity.UserWeeklyRanker; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserWeeklyRankerRepository extends JpaRepository { + + void deleteByWeekStart(LocalDate weekStart); + + @Query("select MAX(uwr.weekStart) from UserWeeklyRanker uwr") + Optional findLatestWeekStartDate(); + + @Query("select uwr from UserWeeklyRanker uwr " + + "join fetch uwr.user u " + + "left join fetch u.profileImage " + + "where uwr.weekStart = :weekStart " + + "order by uwr.rankOrder asc ") + List findAllByWeekStart(@Param("weekStart") LocalDate weekStart); +} diff --git a/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java b/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java new file mode 100644 index 0000000..f738a7d --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/common/config/AsyncConfig.java @@ -0,0 +1,30 @@ +package com.jiwon.mylog.global.common.config; + +import java.util.concurrent.Executor; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@EnableAsync +@Configuration +public class AsyncConfig implements AsyncConfigurer { + + @Bean("mailExecutor") + public Executor customTaskExecutor() { + ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor(); + ex.setCorePoolSize(5); + ex.setMaxPoolSize(10); + ex.setQueueCapacity(10); + ex.setThreadNamePrefix("Async-"); + ex.initialize(); + return ex; + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler(); + } +} diff --git a/src/main/java/com/jiwon/mylog/global/common/config/CacheConfig.java b/src/main/java/com/jiwon/mylog/global/common/config/CacheConfig.java index 8933677..62f48cd 100644 --- a/src/main/java/com/jiwon/mylog/global/common/config/CacheConfig.java +++ b/src/main/java/com/jiwon/mylog/global/common/config/CacheConfig.java @@ -25,6 +25,12 @@ public CacheManager cacheManager() { .expireAfterWrite(Duration.ofDays(1)) .build()); + cacheManager.registerCustomCache("stat::ranker", + Caffeine.newBuilder() + .maximumSize(5) + .expireAfterWrite(Duration.ofDays(1)) + .build()); + return cacheManager; } } diff --git a/src/main/java/com/jiwon/mylog/global/mail/controller/MailController.java b/src/main/java/com/jiwon/mylog/global/mail/controller/MailController.java index 22fdd6c..d2330f7 100644 --- a/src/main/java/com/jiwon/mylog/global/mail/controller/MailController.java +++ b/src/main/java/com/jiwon/mylog/global/mail/controller/MailController.java @@ -31,7 +31,7 @@ public class MailController { } ) public ResponseEntity sendCodeMail(@RequestBody MailRequest request) { - mailService.sendCodeMail(request.getEmail()); + mailService.sendCodeMailAsync(request.getEmail()); return new ResponseEntity<>("인증 코드가 발송되었습니다.", HttpStatus.OK); } 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 59cd718..2de41aa 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 @@ -11,12 +11,15 @@ import java.time.Duration; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class MailService { @@ -38,21 +41,23 @@ public void verifyEmailCode(String email, String code) { redisUtil.delete(email); } - @Transactional - public void sendCodeMail(String email) { + @Async("mailExecutor") + public void sendCodeMailAsync(String 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.set(email, code, Duration.ofMinutes(5)); try { + log.info("인증코드 전송 - email:{}", email); + MimeMessage message = createEmail(email ,subject, text ); javaMailSender.send(message); } catch (org.springframework.mail.MailException e) { + log.error("인증코드 전송 실패 - email:{}", email); throw new MailException(ErrorCode.FAIlED_MAIL_SEND); } } diff --git a/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java b/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java index 7d1f7cc..c22153f 100644 --- a/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java +++ b/src/main/java/com/jiwon/mylog/global/schedular/StatsScheduler.java @@ -1,11 +1,18 @@ package com.jiwon.mylog.global.schedular; +import com.jiwon.mylog.domain.statistic.dto.UserRankResponse; +import com.jiwon.mylog.domain.statistic.entity.UserWeeklyRanker; import com.jiwon.mylog.domain.statistic.repository.UserStatsRepository; +import com.jiwon.mylog.domain.statistic.repository.UserWeeklyRankerRepository; +import com.jiwon.mylog.domain.user.entity.User; import com.jiwon.mylog.global.redis.key.RedisKey; import com.jiwon.mylog.global.redis.RedisUtil; import com.jiwon.mylog.global.redis.key.UserStatsKey; +import jakarta.persistence.EntityManager; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; @@ -19,14 +26,68 @@ public class StatsScheduler { private final RedisUtil redisUtil; + private final EntityManager em; private final UserStatsRepository userStatsRepository; + private final UserWeeklyRankerRepository userWeeklyRankerRepository; + private final static String PATTERN = "user:stats:*:*:*"; + @Scheduled(cron = "0 20 0 * * *") + @CacheEvict(value = "stat::ranker", allEntries = true) + @Transactional + public void updateWeeklyRanking() { + + try { + LocalDate yesterday = LocalDate.now().minusDays(1); + LocalDate start = yesterday.minusDays(6); + + log.info("Ranker Scheduler Start - [{} ~ {}]", start, yesterday); + + List rankers = userStatsRepository.findWeeklyTopUsers(start, yesterday, 3); + + if (rankers == null || rankers.isEmpty()) { + userWeeklyRankerRepository.deleteByWeekStart(start); + return; + } + + saveRankers(rankers, start); + + log.info("Ranker Scheduler End - [{} ~ {}]", start, yesterday); + + } catch (Exception e) { + log.error("Weekly ranking update failed", e); + } + } + + private void saveRankers(List rankers, LocalDate start) { + userWeeklyRankerRepository.deleteByWeekStart(start); + + for(int idx = 0; idx < rankers.size(); idx++) { + UserRankResponse ranker = rankers.get(idx); + + User user = em.getReference(User.class, ranker.userId()); + + UserWeeklyRanker userWeeklyRanker = UserWeeklyRanker.builder() + .user(user) + .weekStart(start) + .totalScore(ranker.total()) + .rankOrder(idx + 1) + .createdComments(ranker.createdComments()) + .createdPosts(ranker.createdPosts()) + .receivedComments(ranker.receivedComments()) + .receivedLikes(ranker.receivedLikes()) + .build(); + + userWeeklyRankerRepository.save(userWeeklyRanker); + } + } + @Scheduled(cron = "0 10 0 * * *") @Transactional public void updateDailyStats() { LocalDate yesterday = LocalDate.now().minusDays(1); + log.info("DailyStats Scheduler Start - {}", yesterday); try { Set userStatsKeys = redisUtil.scanStatsKeys(PATTERN, yesterday); @@ -48,6 +109,7 @@ public void updateDailyStats() { } catch (Exception e) { log.error("유저 통계 스케줄러 오류 발생", e); } + log.info("DailyStats Scheduler End - {}", yesterday); } private void saveStats(UserStatsKey key, String receivedLikesKey, String receivedCommentsKey, String createdCommentsKey, String createdPostsKey) { diff --git a/src/main/java/com/jiwon/mylog/global/security/auth/controller/AuthController.java b/src/main/java/com/jiwon/mylog/global/security/auth/controller/AuthController.java index 2d8c450..4c1cc2a 100644 --- a/src/main/java/com/jiwon/mylog/global/security/auth/controller/AuthController.java +++ b/src/main/java/com/jiwon/mylog/global/security/auth/controller/AuthController.java @@ -13,6 +13,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CookieValue; @@ -21,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Slf4j @RequiredArgsConstructor @RequestMapping("/api/auth") @RestController diff --git a/src/main/java/com/jiwon/mylog/global/security/auth/service/AuthService.java b/src/main/java/com/jiwon/mylog/global/security/auth/service/AuthService.java index 8c6b5b5..d2ac419 100644 --- a/src/main/java/com/jiwon/mylog/global/security/auth/service/AuthService.java +++ b/src/main/java/com/jiwon/mylog/global/security/auth/service/AuthService.java @@ -25,6 +25,7 @@ import com.jiwon.mylog.global.utils.CookieUtil; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; @@ -36,6 +37,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @AllArgsConstructor @Service public class AuthService { @@ -129,7 +131,7 @@ public void sendPasswordResetMail(MailRequest mailRequest) { if (!userRepository.existsByEmail(email)) { throw new NotFoundException(ErrorCode.NOT_FOUND_USER); } - mailService.sendCodeMail(email); + mailService.sendCodeMailAsync(mailRequest.getEmail()); } @Transactional