diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7b016a8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.compile.nullAnalysis.mode": "automatic" +} \ No newline at end of file diff --git a/src/main/java/com/magamochi/userinteraction/controller/UserStatisticsController.java b/src/main/java/com/magamochi/userinteraction/controller/UserStatisticsController.java new file mode 100644 index 0000000..958b056 --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/controller/UserStatisticsController.java @@ -0,0 +1,29 @@ +package com.magamochi.userinteraction.controller; + +import com.magamochi.common.model.dto.DefaultResponseDTO; +import com.magamochi.userinteraction.model.dto.UserStatisticsDTO; +import com.magamochi.userinteraction.service.UserStatisticsService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/user/statistics") +@CrossOrigin(origins = "*") +@RequiredArgsConstructor +public class UserStatisticsController { + private final UserStatisticsService userStatisticsService; + + @Operation( + summary = "Get user statistics", + description = "Get statistics for the logged in user.", + tags = {"User Statistics"}, + operationId = "getUserStatistics") + @GetMapping + public DefaultResponseDTO getUserStatistics() { + return DefaultResponseDTO.ok(userStatisticsService.getUserStatistics()); + } +} diff --git a/src/main/java/com/magamochi/userinteraction/model/dto/UserRecentActivityDTO.java b/src/main/java/com/magamochi/userinteraction/model/dto/UserRecentActivityDTO.java new file mode 100644 index 0000000..e5af611 --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/model/dto/UserRecentActivityDTO.java @@ -0,0 +1,8 @@ +package com.magamochi.userinteraction.model.dto; + +import java.time.Instant; +import lombok.Builder; + +@Builder +public record UserRecentActivityDTO( + String mangaTitle, String contentTitle, Instant readAt, Long mangaId) {} diff --git a/src/main/java/com/magamochi/userinteraction/model/dto/UserStatisticsDTO.java b/src/main/java/com/magamochi/userinteraction/model/dto/UserStatisticsDTO.java new file mode 100644 index 0000000..f3203ad --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/model/dto/UserStatisticsDTO.java @@ -0,0 +1,13 @@ +package com.magamochi.userinteraction.model.dto; + +import java.util.List; +import lombok.Builder; + +@Builder +public record UserStatisticsDTO( + long favoriteMangaCount, + long mangaReadingCount, + long chaptersReadCount, + UserRecentActivityDTO lastReadContent, + boolean hasReadingActivity, + List recentActivities) {} diff --git a/src/main/java/com/magamochi/userinteraction/model/repository/UserFavoriteMangaRepository.java b/src/main/java/com/magamochi/userinteraction/model/repository/UserFavoriteMangaRepository.java index 2f5c2e5..888d819 100644 --- a/src/main/java/com/magamochi/userinteraction/model/repository/UserFavoriteMangaRepository.java +++ b/src/main/java/com/magamochi/userinteraction/model/repository/UserFavoriteMangaRepository.java @@ -13,4 +13,6 @@ public interface UserFavoriteMangaRepository extends JpaRepository findByUserAndManga(User user, Manga manga); Set findByUser(User user); + + long countByUser(User user); } diff --git a/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java b/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java index df9028a..48dbf0a 100644 --- a/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java +++ b/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java @@ -3,11 +3,25 @@ package com.magamochi.userinteraction.model.repository; import com.magamochi.content.model.entity.MangaContent; import com.magamochi.user.model.entity.User; import com.magamochi.userinteraction.model.entity.UserMangaContentRead; +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; public interface UserMangaContentReadRepository extends JpaRepository { boolean existsByUserAndMangaContent(User user, MangaContent mangaContent); Optional findByUserAndMangaContent(User user, MangaContent mangaContent); + + long countByUser(User user); + + @Query( + "SELECT COUNT(DISTINCT mcp.manga) FROM UserMangaContentRead umcr " + + "JOIN umcr.mangaContent mc " + + "JOIN mc.mangaContentProvider mcp " + + "WHERE umcr.user = :user") + long countDistinctMangaByUser(@Param("user") User user); + + List findTop10ByUserOrderByCreatedAtDesc(User user); } diff --git a/src/main/java/com/magamochi/userinteraction/service/UserStatisticsService.java b/src/main/java/com/magamochi/userinteraction/service/UserStatisticsService.java new file mode 100644 index 0000000..1289429 --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/service/UserStatisticsService.java @@ -0,0 +1,58 @@ +package com.magamochi.userinteraction.service; + +import com.magamochi.user.service.UserService; +import com.magamochi.userinteraction.model.dto.UserRecentActivityDTO; +import com.magamochi.userinteraction.model.dto.UserStatisticsDTO; +import com.magamochi.userinteraction.model.entity.UserMangaContentRead; +import com.magamochi.userinteraction.model.repository.UserFavoriteMangaRepository; +import com.magamochi.userinteraction.model.repository.UserMangaContentReadRepository; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UserStatisticsService { + private final UserService userService; + private final UserFavoriteMangaRepository userFavoriteMangaRepository; + private final UserMangaContentReadRepository userMangaContentReadRepository; + + public UserStatisticsDTO getUserStatistics() { + var user = userService.getLoggedUserThrowIfNotFound(); + + var favoriteMangaCount = userFavoriteMangaRepository.countByUser(user); + var mangaReadingCount = userMangaContentReadRepository.countDistinctMangaByUser(user); + var chaptersReadCount = userMangaContentReadRepository.countByUser(user); + + var recentReads = userMangaContentReadRepository.findTop10ByUserOrderByCreatedAtDesc(user); + + UserRecentActivityDTO lastReadContent = null; + if (!recentReads.isEmpty()) { + lastReadContent = mapToRecentActivityDTO(recentReads.getFirst()); + } + + var recentActivities = + recentReads.stream().map(this::mapToRecentActivityDTO).collect(Collectors.toList()); + + return UserStatisticsDTO.builder() + .favoriteMangaCount(favoriteMangaCount) + .mangaReadingCount(mangaReadingCount) + .chaptersReadCount(chaptersReadCount) + .lastReadContent(lastReadContent) + .hasReadingActivity(!recentReads.isEmpty()) + .recentActivities(recentActivities) + .build(); + } + + private UserRecentActivityDTO mapToRecentActivityDTO(UserMangaContentRead read) { + var mangaContent = read.getMangaContent(); + var manga = mangaContent.getMangaContentProvider().getManga(); + + return UserRecentActivityDTO.builder() + .mangaTitle(manga.getTitle()) + .contentTitle(mangaContent.getTitle()) + .readAt(read.getCreatedAt()) + .mangaId(manga.getId()) + .build(); + } +}