feat: user-interaction content read

This commit is contained in:
Rodrigo Verdiani 2026-03-19 11:20:11 -03:00
parent 1827e39471
commit 5da02723cb
11 changed files with 141 additions and 27 deletions

View File

@ -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;

View File

@ -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()));
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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<Void> 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.",

View File

@ -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);

View File

@ -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<Void> markContentAsRead(@PathVariable Long mangaContentId) {
userMangaContentReadService.setRead(mangaContentId);
return DefaultResponseDTO.ok().build();
}
}

View File

@ -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;
}

View File

@ -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<UserMangaContentRead, Long> {
boolean existsByUserAndMangaContent(User user, MangaContent mangaContent);
Optional<UserMangaContentRead> findByUserAndMangaContent(User user, MangaContent mangaContent);
}

View File

@ -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));
}
}

View File

@ -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)
);