diff --git a/src/main/java/im/toduck/domain/backoffice/domain/usecase/StatisticsUseCase.java b/src/main/java/im/toduck/domain/backoffice/domain/usecase/StatisticsUseCase.java index f9975d9d..9e91428a 100644 --- a/src/main/java/im/toduck/domain/backoffice/domain/usecase/StatisticsUseCase.java +++ b/src/main/java/im/toduck/domain/backoffice/domain/usecase/StatisticsUseCase.java @@ -1,7 +1,6 @@ package im.toduck.domain.backoffice.domain.usecase; import java.time.LocalDate; -import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; @@ -30,8 +29,6 @@ @UseCase @RequiredArgsConstructor public class StatisticsUseCase { - private static final int MAX_STATISTICS_DATE_RANGE_DAYS = 31; - private final UserService userService; private final DiaryService diaryService; private final RoutineService routineService; @@ -81,6 +78,11 @@ public MultiDateStatisticsResponse getMultiDateStatistics( ) { validateDateRange(startDate, endDate); + Map> statisticsByType = new EnumMap<>(StatisticsType.class); + for (StatisticsType type : types) { + statisticsByType.put(type, getDailyCountsByTypeAndDateRange(type, startDate, endDate)); + } + List statisticsDataList = new ArrayList<>(); LocalDate currentDate = startDate; @@ -88,8 +90,7 @@ public MultiDateStatisticsResponse getMultiDateStatistics( Map counts = new EnumMap<>(StatisticsType.class); for (StatisticsType type : types) { - long count = getStatisticsCountByTypeAndDate(type, currentDate); - counts.put(type, count); + counts.put(type, statisticsByType.get(type).getOrDefault(currentDate, 0L)); } statisticsDataList.add(StatisticsMapper.toDailyStatisticsResponse(currentDate, counts)); @@ -108,11 +109,6 @@ private void validateDateRange(final LocalDate startDate, final LocalDate endDat if (startDate.isAfter(endDate)) { throw CommonException.from(ExceptionCode.INVALID_STATISTICS_DATE_RANGE); } - - long daysBetween = ChronoUnit.DAYS.between(startDate, endDate); - if (daysBetween > MAX_STATISTICS_DATE_RANGE_DAYS) { - throw CommonException.from(ExceptionCode.INVALID_STATISTICS_DATE_RANGE); - } } private long getStatisticsCountByTypeAndDate(final StatisticsType type, final LocalDate date) { @@ -155,4 +151,19 @@ private long getStatisticsCountByTypeAndDateRange( }; } + private Map getDailyCountsByTypeAndDateRange( + final StatisticsType type, + final LocalDate startDate, + final LocalDate endDate + ) { + return switch (type) { + case NEW_USERS -> userService.getNewUsersCountByDateRangeGroupByDate(startDate, endDate); + case DELETED_USERS -> userService.getDeletedUsersCountByDateRangeGroupByDate(startDate, endDate); + case NEW_ROUTINES -> routineService.getRoutineCountByDateRangeGroupByDate(startDate, endDate); + case NEW_DIARIES -> diaryService.getDiaryCountByDateRangeGroupByDate(startDate, endDate); + case NEW_SOCIAL_POSTS -> socialBoardService.getSocialPostsCountByDateRangeGroupByDate(startDate, endDate); + case NEW_COMMENTS -> socialBoardService.getCommentsCountByDateRangeGroupByDate(startDate, endDate); + case NEW_SCHEDULES -> scheduleReadService.getSchedulesCountByDateRangeGroupByDate(startDate, endDate); + }; + } } diff --git a/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java b/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java index 4a1624fc..bd81d514 100644 --- a/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java +++ b/src/main/java/im/toduck/domain/diary/domain/service/DiaryService.java @@ -6,7 +6,9 @@ import java.time.YearMonth; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +25,7 @@ import im.toduck.domain.user.persistence.entity.User; import im.toduck.global.exception.CommonException; import im.toduck.global.exception.ExceptionCode; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -143,4 +146,20 @@ public Diary getDiaryByIdAndUserId(Long userId, Long diaryId) { return diaryRepository.getDiaryByUserIdAndId(userId, diaryId) .orElseThrow(() -> CommonException.from(ExceptionCode.NOT_FOUND_DIARY)); } + + @Transactional(readOnly = true) + public Map getDiaryCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = diaryRepository.countByCreatedAtBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream() + .collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } } diff --git a/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java b/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java index 3b343f89..bb58851e 100644 --- a/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java +++ b/src/main/java/im/toduck/domain/diary/persistence/repository/DiaryRepository.java @@ -11,11 +11,12 @@ import org.springframework.stereotype.Repository; import im.toduck.domain.diary.persistence.entity.Diary; +import im.toduck.domain.diary.persistence.repository.querydsl.DiaryRepositoryCustom; import im.toduck.domain.user.persistence.entity.User; import jakarta.validation.constraints.NotNull; @Repository -public interface DiaryRepository extends JpaRepository { +public interface DiaryRepository extends JpaRepository, DiaryRepositoryCustom { List findByUserIdAndDateBetweenOrderByDateDesc(Long userId, LocalDate startDate, LocalDate endDate); Diary findByUserIdAndDate(Long userId, @NotNull(message = "날짜는 비어있을 수 없습니다.") LocalDate date); diff --git a/src/main/java/im/toduck/domain/diary/persistence/repository/querydsl/DiaryRepositoryCustom.java b/src/main/java/im/toduck/domain/diary/persistence/repository/querydsl/DiaryRepositoryCustom.java new file mode 100644 index 00000000..33ccee8c --- /dev/null +++ b/src/main/java/im/toduck/domain/diary/persistence/repository/querydsl/DiaryRepositoryCustom.java @@ -0,0 +1,13 @@ +package im.toduck.domain.diary.persistence.repository.querydsl; + +import java.time.LocalDateTime; +import java.util.List; + +import im.toduck.global.persistence.projection.DailyCount; + +public interface DiaryRepositoryCustom { + List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); +} diff --git a/src/main/java/im/toduck/domain/diary/persistence/repository/querydsl/DiaryRepositoryCustomImpl.java b/src/main/java/im/toduck/domain/diary/persistence/repository/querydsl/DiaryRepositoryCustomImpl.java new file mode 100644 index 00000000..ad4a58a7 --- /dev/null +++ b/src/main/java/im/toduck/domain/diary/persistence/repository/querydsl/DiaryRepositoryCustomImpl.java @@ -0,0 +1,30 @@ +package im.toduck.domain.diary.persistence.repository.querydsl; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import im.toduck.domain.diary.persistence.entity.QDiary; +import im.toduck.global.persistence.helper.DailyCountQueryHelper; +import im.toduck.global.persistence.projection.DailyCount; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class DiaryRepositoryCustomImpl implements DiaryRepositoryCustom { + private final JPAQueryFactory queryFactory; + private final QDiary qDiary = QDiary.diary; + + @Override + public List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, qDiary, qDiary.createdAt, qDiary.count(), startDateTime, endDateTime + ); + } +} diff --git a/src/main/java/im/toduck/domain/routine/domain/service/RoutineService.java b/src/main/java/im/toduck/domain/routine/domain/service/RoutineService.java index db581272..a6601e87 100644 --- a/src/main/java/im/toduck/domain/routine/domain/service/RoutineService.java +++ b/src/main/java/im/toduck/domain/routine/domain/service/RoutineService.java @@ -25,6 +25,7 @@ import im.toduck.domain.user.persistence.entity.User; import im.toduck.global.exception.CommonException; import im.toduck.global.exception.ExceptionCode; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -176,4 +177,20 @@ public long getRoutineCountByDateRange(final LocalDate startDate, final LocalDat public long getActiveRoutineUsersCount() { return routineRepository.countDistinctUsers(); } + + @Transactional(readOnly = true) + public Map getRoutineCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = routineRepository.countByCreatedAtBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream() + .collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } } diff --git a/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustom.java b/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustom.java index 9c6ff72c..f532d1ac 100644 --- a/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustom.java +++ b/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustom.java @@ -1,11 +1,13 @@ package im.toduck.domain.routine.persistence.repository.querydsl; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import im.toduck.domain.routine.persistence.entity.Routine; import im.toduck.domain.routine.persistence.entity.RoutineRecord; import im.toduck.domain.user.persistence.entity.User; +import im.toduck.global.persistence.projection.DailyCount; public interface RoutineRepositoryCustom { List findUnrecordedRoutinesByDateMatchingDayOfWeek( @@ -25,4 +27,9 @@ List findRoutinesByDateBetween( void deleteAllUnsharedRoutinesByUser(User user); List findActiveRoutinesWithReminderForDates(LocalDate startDate, LocalDate endDate); + + List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); } diff --git a/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustomImpl.java b/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustomImpl.java index 32c67914..90ed1e49 100644 --- a/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustomImpl.java +++ b/src/main/java/im/toduck/domain/routine/persistence/repository/querydsl/RoutineRepositoryCustomImpl.java @@ -16,6 +16,8 @@ import im.toduck.domain.routine.persistence.entity.RoutineRecord; import im.toduck.domain.user.persistence.entity.User; import im.toduck.global.helper.DaysOfWeekBitmask; +import im.toduck.global.persistence.helper.DailyCountQueryHelper; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; @Repository @@ -141,4 +143,14 @@ private BooleanExpression routineMatchesDateRange(final LocalDate startDate, fin Byte.class, "function('bitand', {0}, CAST({1} as byte))", qRoutine.daysOfWeekBitmask, periodDaysBitmask ).gt((byte)0); } + + @Override + public List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, qRoutine, qRoutine.createdAt, qRoutine.count(), startDateTime, endDateTime + ); + } } diff --git a/src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReadService.java b/src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReadService.java index ca111825..ace73f3c 100644 --- a/src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReadService.java +++ b/src/main/java/im/toduck/domain/schedule/domain/service/ScheduleReadService.java @@ -5,7 +5,9 @@ import java.time.LocalTime; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,6 +22,7 @@ import im.toduck.domain.user.persistence.entity.User; import im.toduck.global.exception.CommonException; import im.toduck.global.exception.ExceptionCode; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; @Service @@ -74,4 +77,19 @@ public long getSchedulesCountByDateRange(final LocalDate startDate, final LocalD LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); return scheduleRepository.countByCreatedAtBetween(startDateTime, endDateTime); } + + @Transactional(readOnly = true) + public Map getSchedulesCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = scheduleRepository.countByCreatedAtBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream().collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } } diff --git a/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustom.java b/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustom.java index 220b6673..9a4b32f8 100644 --- a/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustom.java +++ b/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustom.java @@ -1,10 +1,17 @@ package im.toduck.domain.schedule.persistence.repository.querydsl; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import im.toduck.domain.schedule.persistence.entity.Schedule; +import im.toduck.global.persistence.projection.DailyCount; public interface ScheduleRepositoryCustom { List findSchedules(Long userId, LocalDate startDate, LocalDate endDate); + + List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); } diff --git a/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java b/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java index a8a94cc2..067964b6 100644 --- a/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java +++ b/src/main/java/im/toduck/domain/schedule/persistence/repository/querydsl/ScheduleRepositoryCustomImpl.java @@ -1,6 +1,7 @@ package im.toduck.domain.schedule.persistence.repository.querydsl; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import org.springframework.stereotype.Repository; @@ -10,6 +11,8 @@ import im.toduck.domain.schedule.persistence.entity.QSchedule; import im.toduck.domain.schedule.persistence.entity.Schedule; +import im.toduck.global.persistence.helper.DailyCountQueryHelper; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; @Repository @@ -55,4 +58,14 @@ private BooleanExpression isPeriodEvent(LocalDate startDate, LocalDate endDate) .and(schedule.scheduleDate.endDate.goe(startDate)) // 조회 시작일이 일정 종료일보다 같거나 작아야 함 .and(schedule.scheduleDate.startDate.loe(endDate)); // 일정 시작일이 조회 종료일보다 같거나 작아야 함 } + + @Override + public List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, schedule, schedule.createdAt, schedule.count(), startDateTime, endDateTime + ); + } } diff --git a/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java b/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java index 5d5697d7..527b9cd0 100644 --- a/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java +++ b/src/main/java/im/toduck/domain/social/domain/service/SocialBoardService.java @@ -4,7 +4,9 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -36,6 +38,7 @@ import im.toduck.domain.user.persistence.entity.User; import im.toduck.global.exception.CommonException; import im.toduck.global.exception.ExceptionCode; +import im.toduck.global.persistence.projection.DailyCount; import im.toduck.global.util.PaginationUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -285,5 +288,37 @@ public long getCommentsCountByDateRange(final LocalDate startDate, final LocalDa LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); return commentRepository.countByCreatedAtBetween(startDateTime, endDateTime); } + + @Transactional(readOnly = true) + public Map getSocialPostsCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = socialRepository.countByCreatedAtBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream() + .collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } + + @Transactional(readOnly = true) + public Map getCommentsCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = commentRepository.countByCreatedAtBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream() + .collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } } diff --git a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustom.java b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustom.java index 5e3ef0bd..2a35d31b 100644 --- a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustom.java +++ b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustom.java @@ -1,10 +1,12 @@ package im.toduck.domain.social.persistence.repository.querydsl; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Pageable; import im.toduck.domain.mypage.presentation.dto.response.MyCommentsResponse; +import im.toduck.global.persistence.projection.DailyCount; public interface CommentRepositoryCustom { List findMyCommentsWithProjection( @@ -12,4 +14,9 @@ List findMyCommentsWithProjection( Long cursor, Pageable pageable ); + + List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); } diff --git a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustomImpl.java b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustomImpl.java index 90d4cc8e..f8cf7be1 100644 --- a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustomImpl.java +++ b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/CommentRepositoryCustomImpl.java @@ -1,5 +1,6 @@ package im.toduck.domain.social.persistence.repository.querydsl; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Pageable; @@ -19,6 +20,8 @@ import im.toduck.domain.social.presentation.dto.response.CommentLikeDto; import im.toduck.domain.social.presentation.dto.response.OwnerDto; import im.toduck.domain.user.persistence.entity.QUser; +import im.toduck.global.persistence.helper.DailyCountQueryHelper; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; @Repository @@ -86,4 +89,14 @@ private Expression commentLikeProjection(Long userId) { qComment.likeCount ); } + + @Override + public List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, qComment, qComment.createdAt, qComment.count(), startDateTime, endDateTime + ); + } } diff --git a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustom.java b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustom.java index d2a3821f..e2d7949e 100644 --- a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustom.java +++ b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustom.java @@ -1,10 +1,12 @@ package im.toduck.domain.social.persistence.repository.querydsl; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Pageable; import im.toduck.domain.social.persistence.entity.Social; +import im.toduck.global.persistence.projection.DailyCount; public interface SocialRepositoryCustom { List findSocialsExcludingBlocked( @@ -27,4 +29,9 @@ List findUserSocials( Long cursor, Pageable pageable ); + + List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); } diff --git a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java index 8a723101..47260f4a 100644 --- a/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java +++ b/src/main/java/im/toduck/domain/social/persistence/repository/querydsl/SocialRepositoryCustomImpl.java @@ -1,5 +1,6 @@ package im.toduck.domain.social.persistence.repository.querydsl; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Pageable; @@ -14,6 +15,8 @@ import im.toduck.domain.social.persistence.entity.QSocialCategoryLink; import im.toduck.domain.social.persistence.entity.Social; import im.toduck.domain.user.persistence.entity.QBlock; +import im.toduck.global.persistence.helper.DailyCountQueryHelper; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; @Repository @@ -133,4 +136,14 @@ private void applyCategoryFilter(JPAQuery query, List categoryIds) .having(qSocialCategoryLink.socialCategory.id.countDistinct().eq((long)categoryIds.size())); } } + + @Override + public List countByCreatedAtBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, qSocial, qSocial.createdAt, qSocial.count(), startDateTime, endDateTime + ); + } } diff --git a/src/main/java/im/toduck/domain/user/domain/service/UserService.java b/src/main/java/im/toduck/domain/user/domain/service/UserService.java index 9431ed5a..0fb64933 100644 --- a/src/main/java/im/toduck/domain/user/domain/service/UserService.java +++ b/src/main/java/im/toduck/domain/user/domain/service/UserService.java @@ -4,7 +4,9 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -20,6 +22,7 @@ import im.toduck.domain.user.persistence.repository.UserRepository; import im.toduck.global.exception.CommonException; import im.toduck.global.exception.ExceptionCode; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -170,4 +173,36 @@ public long getTotalDeletedUsersCount() { public long getCountByProvider(final String provider) { return userRepository.countByProvider(provider); } + + @Transactional(readOnly = true) + public Map getNewUsersCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = userRepository.countNewUsersByDateBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream() + .collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } + + @Transactional(readOnly = true) + public Map getDeletedUsersCountByDateRangeGroupByDate( + final LocalDate startDate, + final LocalDate endDate + ) { + LocalDateTime startDateTime = startDate.atStartOfDay(); + LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX); + + List dailyCounts = userRepository.countDeletedUsersByDateBetweenGroupByDate( + startDateTime, endDateTime + ); + + return dailyCounts.stream() + .collect(Collectors.toMap(DailyCount::date, DailyCount::count)); + } } diff --git a/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustom.java b/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustom.java index 8801898c..531c51dd 100644 --- a/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustom.java +++ b/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustom.java @@ -8,6 +8,7 @@ import im.toduck.domain.user.persistence.entity.User; import im.toduck.domain.user.persistence.entity.UserRole; +import im.toduck.global.persistence.projection.DailyCount; public interface UserRepositoryCustom { List findAllActiveUserIds(); @@ -40,4 +41,14 @@ Page findUsersWithFilters( ); long countByProvider(final String provider); + + List countNewUsersByDateBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); + + List countDeletedUsersByDateBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ); } diff --git a/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustomImpl.java b/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustomImpl.java index d6343b36..0ad183af 100644 --- a/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustomImpl.java +++ b/src/main/java/im/toduck/domain/user/persistence/repository/querydsl/UserRepositoryCustomImpl.java @@ -1,5 +1,6 @@ package im.toduck.domain.user.persistence.repository.querydsl; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; @@ -19,6 +20,8 @@ import im.toduck.domain.user.persistence.entity.QUser; import im.toduck.domain.user.persistence.entity.User; import im.toduck.domain.user.persistence.entity.UserRole; +import im.toduck.global.persistence.helper.DailyCountQueryHelper; +import im.toduck.global.persistence.projection.DailyCount; import lombok.RequiredArgsConstructor; @Repository @@ -265,4 +268,24 @@ public long countByProvider(final String provider) { return count != null ? count : 0L; } + + @Override + public List countNewUsersByDateBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, qUser, qUser.createdAt, qUser.count(), startDateTime, endDateTime + ); + } + + @Override + public List countDeletedUsersByDateBetweenGroupByDate( + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + return DailyCountQueryHelper.countGroupByDate( + queryFactory, qUser, qUser.deletedAt, qUser.count(), startDateTime, endDateTime + ); + } } diff --git a/src/main/java/im/toduck/global/persistence/helper/DailyCountQueryHelper.java b/src/main/java/im/toduck/global/persistence/helper/DailyCountQueryHelper.java new file mode 100644 index 00000000..0a509cd1 --- /dev/null +++ b/src/main/java/im/toduck/global/persistence/helper/DailyCountQueryHelper.java @@ -0,0 +1,62 @@ +package im.toduck.global.persistence.helper; + +import java.time.LocalDateTime; +import java.util.List; + +import com.querydsl.core.Tuple; +import com.querydsl.core.types.EntityPath; +import com.querydsl.core.types.dsl.DateTemplate; +import com.querydsl.core.types.dsl.DateTimePath; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import im.toduck.global.persistence.projection.DailyCount; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * 일별 집계 통계 쿼리를 위한 공통 헬퍼 클래스. + * QueryDSL GROUP BY DATE() 쿼리의 중복 코드를 제거합니다. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class DailyCountQueryHelper { + + /** + * 지정된 기간 동안의 일별 카운트를 조회합니다. + * + * @param queryFactory JPAQueryFactory 인스턴스 + * @param entity 조회 대상 엔티티 (예: QUser.user) + * @param dateTimePath 집계 기준이 되는 datetime 필드 (예: qUser.createdAt) + * @param countExpr 카운트 표현식 (예: qUser.count()) + * @param startDateTime 조회 시작 일시 + * @param endDateTime 조회 종료 일시 + * @return 일별 카운트 목록 + */ + public static List countGroupByDate( + final JPAQueryFactory queryFactory, + final EntityPath entity, + final DateTimePath dateTimePath, + final NumberExpression countExpr, + final LocalDateTime startDateTime, + final LocalDateTime endDateTime + ) { + DateTemplate dateExpr = Expressions.dateTemplate( + java.sql.Date.class, "DATE({0})", dateTimePath); + + List tuples = queryFactory + .select(dateExpr, countExpr) + .from(entity) + .where(dateTimePath.between(startDateTime, endDateTime)) + .groupBy(dateExpr) + .orderBy(dateExpr.asc()) + .fetch(); + + return tuples.stream() + .map(tuple -> new DailyCount( + tuple.get(dateExpr).toLocalDate(), + tuple.get(countExpr) + )) + .toList(); + } +} diff --git a/src/main/java/im/toduck/global/persistence/projection/DailyCount.java b/src/main/java/im/toduck/global/persistence/projection/DailyCount.java new file mode 100644 index 00000000..cef37abc --- /dev/null +++ b/src/main/java/im/toduck/global/persistence/projection/DailyCount.java @@ -0,0 +1,13 @@ +package im.toduck.global.persistence.projection; + +import java.time.LocalDate; + +/** + * 일별 집계 통계를 위한 Projection record. + * GROUP BY DATE() 쿼리 결과를 매핑하는 데 사용됩니다. + */ +public record DailyCount( + LocalDate date, + Long count +) { +}