From 5da02723cb80f7aed9f8356df6831f31e90ab059 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Thu, 19 Mar 2026 11:20:11 -0300 Subject: [PATCH] feat: user-interaction content read --- .../content/controller/ContentController.java | 2 +- .../content/model/dto/MangaContentDTO.java | 4 +- .../model/dto/MangaContentImagesDTO.java | 2 +- .../content/service/ContentService.java | 10 ++- .../controller/MangaChapterController.java | 13 ---- .../service/MangaChapterService.java | 8 --- .../controller/UserInteractionController.java | 14 +++++ .../model/entity/UserMangaContentRead.java | 31 +++++++++ .../UserMangaContentReadRepository.java | 13 ++++ .../service/UserMangaContentReadService.java | 63 +++++++++++++++++++ .../db/migration/V0003__CONTENT_READ.sql | 8 +++ 11 files changed, 141 insertions(+), 27 deletions(-) rename src/main/java/com/magamochi/{ => content}/model/dto/MangaContentImagesDTO.java (95%) create mode 100644 src/main/java/com/magamochi/userinteraction/model/entity/UserMangaContentRead.java create mode 100644 src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java create mode 100644 src/main/java/com/magamochi/userinteraction/service/UserMangaContentReadService.java create mode 100644 src/main/resources/db/migration/V0003__CONTENT_READ.sql diff --git a/src/main/java/com/magamochi/content/controller/ContentController.java b/src/main/java/com/magamochi/content/controller/ContentController.java index c035ddf..7b2984a 100644 --- a/src/main/java/com/magamochi/content/controller/ContentController.java +++ b/src/main/java/com/magamochi/content/controller/ContentController.java @@ -2,8 +2,8 @@ package com.magamochi.content.controller; import com.magamochi.common.model.dto.DefaultResponseDTO; import com.magamochi.content.model.dto.MangaContentDTO; +import com.magamochi.content.model.dto.MangaContentImagesDTO; import com.magamochi.content.service.ContentService; -import com.magamochi.model.dto.MangaContentImagesDTO; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.constraints.NotNull; import java.util.List; diff --git a/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java b/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java index 86a52ca..d6ada57 100644 --- a/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java +++ b/src/main/java/com/magamochi/content/model/dto/MangaContentDTO.java @@ -10,12 +10,12 @@ public record MangaContentDTO( @NotNull Boolean downloaded, @NotNull Boolean isRead, LanguageDTO language) { - public static MangaContentDTO from(MangaContent mangaContent) { + public static MangaContentDTO from(MangaContent mangaContent, boolean isRead) { return new MangaContentDTO( mangaContent.getId(), mangaContent.getTitle(), mangaContent.getDownloaded(), - false, + isRead, LanguageDTO.from(mangaContent.getLanguage())); } } diff --git a/src/main/java/com/magamochi/model/dto/MangaContentImagesDTO.java b/src/main/java/com/magamochi/content/model/dto/MangaContentImagesDTO.java similarity index 95% rename from src/main/java/com/magamochi/model/dto/MangaContentImagesDTO.java rename to src/main/java/com/magamochi/content/model/dto/MangaContentImagesDTO.java index 72dd26a..8aa4bce 100644 --- a/src/main/java/com/magamochi/model/dto/MangaContentImagesDTO.java +++ b/src/main/java/com/magamochi/content/model/dto/MangaContentImagesDTO.java @@ -1,4 +1,4 @@ -package com.magamochi.model.dto; +package com.magamochi.content.model.dto; import com.magamochi.content.model.entity.MangaContent; import com.magamochi.content.model.entity.MangaContentImage; diff --git a/src/main/java/com/magamochi/content/service/ContentService.java b/src/main/java/com/magamochi/content/service/ContentService.java index 0cd4e79..d1f9db9 100644 --- a/src/main/java/com/magamochi/content/service/ContentService.java +++ b/src/main/java/com/magamochi/content/service/ContentService.java @@ -3,9 +3,10 @@ package com.magamochi.content.service; import com.magamochi.catalog.service.MangaContentProviderService; import com.magamochi.common.exception.NotFoundException; import com.magamochi.content.model.dto.MangaContentDTO; +import com.magamochi.content.model.dto.MangaContentImagesDTO; import com.magamochi.content.model.entity.MangaContent; import com.magamochi.content.model.repository.MangaContentRepository; -import com.magamochi.model.dto.MangaContentImagesDTO; +import com.magamochi.userinteraction.service.UserMangaContentReadService; import jakarta.validation.constraints.NotNull; import java.util.Comparator; import java.util.List; @@ -16,6 +17,7 @@ import org.springframework.stereotype.Service; @RequiredArgsConstructor public class ContentService { private final MangaContentProviderService mangaContentProviderService; + private final UserMangaContentReadService userMangaContentReadService; private final MangaContentRepository mangaContentRepository; @@ -24,7 +26,11 @@ public class ContentService { return mangaContentProvider.getMangaContents().stream() .sorted(Comparator.comparing(MangaContent::getId)) - .map(MangaContentDTO::from) + .map( + mangaContent -> { + var isRead = userMangaContentReadService.isRead(mangaContent.getId()); + return MangaContentDTO.from(mangaContent, isRead); + }) .toList(); } diff --git a/src/main/java/com/magamochi/controller/MangaChapterController.java b/src/main/java/com/magamochi/controller/MangaChapterController.java index 4d1cdf9..1e98783 100644 --- a/src/main/java/com/magamochi/controller/MangaChapterController.java +++ b/src/main/java/com/magamochi/controller/MangaChapterController.java @@ -1,6 +1,5 @@ package com.magamochi.controller; -import com.magamochi.common.model.dto.DefaultResponseDTO; import com.magamochi.model.enumeration.ArchiveFileType; import com.magamochi.service.MangaChapterService; import io.swagger.v3.oas.annotations.Operation; @@ -20,18 +19,6 @@ import org.springframework.web.bind.annotation.*; public class MangaChapterController { private final MangaChapterService mangaChapterService; - @Operation( - summary = "Mark a chapter as read", - description = "Mark a chapter as read by its ID.", - tags = {"Manga Chapter"}, - operationId = "markAsRead") - @PostMapping("/{chapterId}/mark-as-read") - public DefaultResponseDTO markAsRead(@PathVariable Long chapterId) { - mangaChapterService.markAsRead(chapterId); - - return DefaultResponseDTO.ok().build(); - } - @Operation( summary = "Download chapter archive", description = "Download a chapter as a compressed file by its ID.", diff --git a/src/main/java/com/magamochi/service/MangaChapterService.java b/src/main/java/com/magamochi/service/MangaChapterService.java index 01ac56e..8e89f2f 100644 --- a/src/main/java/com/magamochi/service/MangaChapterService.java +++ b/src/main/java/com/magamochi/service/MangaChapterService.java @@ -27,14 +27,6 @@ public class MangaChapterService { private final OldImageService oldImageService; - public void markAsRead(Long chapterId) { - // TODO: implement this - // var chapter = getMangaChapterThrowIfNotFound(chapterId); - // chapter.setRead(true); - // - // mangaChapterRepository.save(chapter); - } - public MangaChapterArchiveDTO downloadChapter(Long chapterId, ArchiveFileType archiveFileType) throws IOException { var chapter = getMangaChapterThrowIfNotFound(chapterId); diff --git a/src/main/java/com/magamochi/userinteraction/controller/UserInteractionController.java b/src/main/java/com/magamochi/userinteraction/controller/UserInteractionController.java index b905847..33f0d77 100644 --- a/src/main/java/com/magamochi/userinteraction/controller/UserInteractionController.java +++ b/src/main/java/com/magamochi/userinteraction/controller/UserInteractionController.java @@ -2,6 +2,7 @@ package com.magamochi.userinteraction.controller; import com.magamochi.common.model.dto.DefaultResponseDTO; import com.magamochi.userinteraction.service.UserFavoriteMangaService; +import com.magamochi.userinteraction.service.UserMangaContentReadService; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PathVariable; @@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class UserInteractionController { private final UserFavoriteMangaService userFavoriteMangaService; + private final UserMangaContentReadService userMangaContentReadService; @Operation( summary = "Favorite a manga", @@ -38,4 +40,16 @@ public class UserInteractionController { return DefaultResponseDTO.ok().build(); } + + @Operation( + summary = "Mark content as read", + description = "Mark content as read by its ID.", + tags = {"User Interaction"}, + operationId = "markContentAsRead") + @PostMapping("/content/{mangaContentId}/read") + public DefaultResponseDTO markContentAsRead(@PathVariable Long mangaContentId) { + userMangaContentReadService.setRead(mangaContentId); + + return DefaultResponseDTO.ok().build(); + } } diff --git a/src/main/java/com/magamochi/userinteraction/model/entity/UserMangaContentRead.java b/src/main/java/com/magamochi/userinteraction/model/entity/UserMangaContentRead.java new file mode 100644 index 0000000..f01f47b --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/model/entity/UserMangaContentRead.java @@ -0,0 +1,31 @@ +package com.magamochi.userinteraction.model.entity; + +import com.magamochi.content.model.entity.MangaContent; +import com.magamochi.user.model.entity.User; +import jakarta.persistence.*; +import java.time.Instant; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +@Entity +@Table(name = "user_manga_content_read") +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter +public class UserMangaContentRead { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "manga_content_id") + private MangaContent mangaContent; + + @CreationTimestamp private Instant createdAt; +} diff --git a/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java b/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java new file mode 100644 index 0000000..df9028a --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/model/repository/UserMangaContentReadRepository.java @@ -0,0 +1,13 @@ +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.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserMangaContentReadRepository extends JpaRepository { + boolean existsByUserAndMangaContent(User user, MangaContent mangaContent); + + Optional findByUserAndMangaContent(User user, MangaContent mangaContent); +} diff --git a/src/main/java/com/magamochi/userinteraction/service/UserMangaContentReadService.java b/src/main/java/com/magamochi/userinteraction/service/UserMangaContentReadService.java new file mode 100644 index 0000000..144ad15 --- /dev/null +++ b/src/main/java/com/magamochi/userinteraction/service/UserMangaContentReadService.java @@ -0,0 +1,63 @@ +package com.magamochi.userinteraction.service; + +import com.magamochi.common.exception.NotFoundException; +import com.magamochi.content.model.entity.MangaContent; +import com.magamochi.content.model.repository.MangaContentRepository; +import com.magamochi.user.service.UserService; +import com.magamochi.userinteraction.model.entity.UserMangaContentRead; +import com.magamochi.userinteraction.model.repository.UserMangaContentReadRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class UserMangaContentReadService { + private final UserService userService; + + private final UserMangaContentReadRepository userMangaContentReadRepository; + private final MangaContentRepository mangaContentRepository; + + public void setRead(Long id) { + try { + var user = userService.getLoggedUserThrowIfNotFound(); + var mangaContent = findMangaContent(id); + + if (userMangaContentReadRepository.existsByUserAndMangaContent(user, mangaContent)) { + return; + } + + userMangaContentReadRepository.save( + UserMangaContentRead.builder().user(user).mangaContent(mangaContent).build()); + } catch (NotFoundException _) { + } + } + + public void setUnread(Long id) { + var user = userService.getLoggedUserThrowIfNotFound(); + var mangaContent = findMangaContent(id); + + var mangaContentRead = + userMangaContentReadRepository.findByUserAndMangaContent(user, mangaContent); + + mangaContentRead.ifPresent(userMangaContentReadRepository::delete); + } + + public boolean isRead(Long id) { + try { + var user = userService.getLoggedUserThrowIfNotFound(); + var mangaContent = findMangaContent(id); + + return userMangaContentReadRepository.existsByUserAndMangaContent(user, mangaContent); + } catch (NotFoundException e) { + return false; + } + } + + private MangaContent findMangaContent(Long id) { + return mangaContentRepository + .findById(id) + .orElseThrow(() -> new NotFoundException("MangaContent not found for ID: " + id)); + } +} diff --git a/src/main/resources/db/migration/V0003__CONTENT_READ.sql b/src/main/resources/db/migration/V0003__CONTENT_READ.sql new file mode 100644 index 0000000..d66dca5 --- /dev/null +++ b/src/main/resources/db/migration/V0003__CONTENT_READ.sql @@ -0,0 +1,8 @@ +CREATE TABLE user_manga_content_read +( + id BIGSERIAL NOT NULL PRIMARY KEY, + manga_content_id BIGINT NOT NULL REFERENCES manga_contents (id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, manga_content_id) +); \ No newline at end of file