diff --git a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java index a979b406..83a5630e 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java @@ -2,13 +2,13 @@ import java.util.List; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.pinback.pinback_server.domain.article.application.command.ArticleCreateCommand; import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.repository.dto.ArticlesWithUnreadCount; import com.pinback.pinback_server.domain.article.domain.service.ArticleGetService; import com.pinback.pinback_server.domain.article.domain.service.ArticleSaveService; import com.pinback.pinback_server.domain.article.exception.ArticleAlreadyExistException; @@ -47,15 +47,36 @@ public ArticleDetailResponse getArticleDetail(long articleId) { } public ArticleAllResponse getAllArticles(User user, int pageNumber, int pageSize) { - Page
articles = articleGetService.findAll(user.getId(), PageRequest.of(pageNumber, pageSize)); + ArticlesWithUnreadCount projection = articleGetService.findAll(user.getId(), + PageRequest.of(pageNumber, pageSize)); - List articlesResponses = articles.stream() + List articlesResponses = projection.getArticle().stream() .map(ArticlesResponse::from) .toList(); return ArticleAllResponse.of( - articles.getTotalElements(), + projection.getArticle().getTotalElements(), + projection.getUnReadCount(), articlesResponses ); } + + public ArticleAllResponse getAllArticlesByCategory(User user, long categoryId, int pageNumber, int pageSize) { + + Category category = categoryGetService.getCategoryAndUser(categoryId, user); + + ArticlesWithUnreadCount projection = articleGetService.findAllByCategory(user.getId(), category, + PageRequest.of(pageNumber, pageSize)); + + List articlesResponses = projection.getArticle().stream() + .map(ArticlesResponse::from) + .toList(); + + return ArticleAllResponse.of( + projection.getArticle().getTotalElements(), + projection.getUnReadCount(), + articlesResponses + ); + } + } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java index d2c2de98..53e0bd4a 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java @@ -71,4 +71,12 @@ public static Article create(String url, String memo, User user, Category catego public boolean isRead() { return isRead; } + + public void toRead() { + this.isRead = true; + } + + public void toUnRead() { + this.isRead = false; + } } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java index 0d41a1b6..6e6ee5a6 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java @@ -2,11 +2,12 @@ import java.util.UUID; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.repository.dto.ArticlesWithUnreadCount; public interface ArticleRepositoryCustom { - Page
findAllCustom(UUID userId, Pageable pageable); + ArticlesWithUnreadCount findAllCustom(UUID userId, Pageable pageable); + + ArticlesWithUnreadCount findAllByCategory(UUID userId, long articleId, Pageable pageable); } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java index 7319efcd..fbf5ed01 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java @@ -1,17 +1,18 @@ package com.pinback.pinback_server.domain.article.domain.repository; import static com.pinback.pinback_server.domain.article.domain.entity.QArticle.*; +import static com.pinback.pinback_server.domain.category.domain.entity.QCategory.*; import static com.pinback.pinback_server.domain.user.domain.entity.QUser.*; import java.util.List; import java.util.UUID; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.stereotype.Repository; import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.pinback.pinback_server.domain.article.domain.repository.dto.ArticlesWithUnreadCount; import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -24,7 +25,7 @@ public class ArticleRepositoryCustomImpl implements ArticleRepositoryCustom { private final JPAQueryFactory queryFactory; @Override - public Page
findAllCustom(UUID userId, Pageable pageable) { + public ArticlesWithUnreadCount findAllCustom(UUID userId, Pageable pageable) { List
articles = queryFactory .selectFrom(article) .join(article.user, user).fetchJoin() @@ -39,6 +40,40 @@ public Page
findAllCustom(UUID userId, Pageable pageable) { .from(article) .where(article.user.id.eq(userId)); - return PageableExecutionUtils.getPage(articles, pageable, countQuery::fetchOne); + Long unReadCount = queryFactory + .select(article.count()) + .from(article) + .where(article.user.id.eq(userId).and(article.isRead.isFalse())) + .fetchOne(); + + return new ArticlesWithUnreadCount(unReadCount, + PageableExecutionUtils.getPage(articles, pageable, countQuery::fetchOne)); + } + + @Override + public ArticlesWithUnreadCount findAllByCategory(UUID userId, long categoryId, Pageable pageable) { + + List
articles = queryFactory + .selectFrom(article) + .join(article.category, category).fetchJoin() + .where(article.category.id.eq(categoryId)) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(article.createdAt.desc()) + .fetch(); + + JPAQuery countQuery = queryFactory + .select(article.count()) + .from(article) + .where(article.category.id.eq(categoryId)); + + Long unReadCount = queryFactory + .select(article.count()) + .from(article) + .where(article.category.id.eq(categoryId).and(article.isRead.isFalse())) + .fetchOne(); + + return new ArticlesWithUnreadCount(unReadCount, + PageableExecutionUtils.getPage(articles, pageable, countQuery::fetchOne)); } } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/dto/ArticlesWithUnreadCount.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/dto/ArticlesWithUnreadCount.java new file mode 100644 index 00000000..3dd66935 --- /dev/null +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/repository/dto/ArticlesWithUnreadCount.java @@ -0,0 +1,22 @@ +package com.pinback.pinback_server.domain.article.domain.repository.dto; + +import org.springframework.data.domain.Page; + +import com.pinback.pinback_server.domain.article.domain.entity.Article; +import com.querydsl.core.annotations.QueryProjection; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ArticlesWithUnreadCount { + private Long unReadCount; + private Page
article; + + @QueryProjection + public ArticlesWithUnreadCount(Long unReadCount, Page
article) { + this.unReadCount = unReadCount; + this.article = article; + } +} diff --git a/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java b/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java index 0822d224..a5e141c9 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java @@ -2,14 +2,15 @@ import java.util.UUID; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.pinback.pinback_server.domain.article.domain.entity.Article; import com.pinback.pinback_server.domain.article.domain.repository.ArticleRepository; +import com.pinback.pinback_server.domain.article.domain.repository.dto.ArticlesWithUnreadCount; import com.pinback.pinback_server.domain.article.exception.ArticleNotFoundException; +import com.pinback.pinback_server.domain.category.domain.entity.Category; import com.pinback.pinback_server.domain.user.domain.entity.User; import lombok.RequiredArgsConstructor; @@ -28,7 +29,11 @@ public Article findById(long articleId) { return articleRepository.findById(articleId).orElseThrow(ArticleNotFoundException::new); } - public Page
findAll(UUID userId, PageRequest pageRequest) { + public ArticlesWithUnreadCount findAll(UUID userId, PageRequest pageRequest) { return articleRepository.findAllCustom(userId, pageRequest); } + + public ArticlesWithUnreadCount findAllByCategory(UUID userId, Category category, PageRequest pageRequest) { + return articleRepository.findAllByCategory(userId, category.getId(), pageRequest); + } } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java b/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java index 42c422ff..dccebad6 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java @@ -45,4 +45,14 @@ public ResponseDto getAll(@CurrentUser User user, @RequestPa return ResponseDto.ok(response); } + @GetMapping("/{categoryId}") + public ResponseDto getAllByCategory(@CurrentUser User user, @RequestParam Long categoryId, + @RequestParam int pageNumber, + @RequestParam int pageSize) { + + ArticleAllResponse response = articleManagementUsecase.getAllArticlesByCategory(user, categoryId, pageNumber, + pageSize); + return ResponseDto.ok(response); + } + } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticleAllResponse.java b/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticleAllResponse.java index bbd395b0..59603a70 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticleAllResponse.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticleAllResponse.java @@ -4,11 +4,13 @@ public record ArticleAllResponse( long totalArticle, + long totalUnreadArticle, List articles ) { - public static ArticleAllResponse of(long totalArticle, List articles) { + public static ArticleAllResponse of(long totalArticle, long totalUnreadArticle, List articles) { return new ArticleAllResponse( totalArticle, + totalUnreadArticle, articles ); } diff --git a/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticlesResponse.java b/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticlesResponse.java index 0b3cf6e9..d54f459d 100644 --- a/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticlesResponse.java +++ b/src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticlesResponse.java @@ -1,11 +1,14 @@ package com.pinback.pinback_server.domain.article.presentation.dto.response; +import java.time.LocalDateTime; + import com.pinback.pinback_server.domain.article.domain.entity.Article; public record ArticlesResponse( long articleId, String url, String memo, + LocalDateTime createdAt, boolean isRead ) { public static ArticlesResponse from(Article article) { @@ -13,6 +16,7 @@ public static ArticlesResponse from(Article article) { article.getId(), article.getUrl(), article.getMemo(), + article.getCreatedAt(), article.isRead() ); } diff --git a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java index dc35d33e..1362bde4 100644 --- a/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java +++ b/src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java @@ -116,6 +116,7 @@ void getAllArticle() { //then assertThat(responses.articles()).hasSize(5); + assertThat(responses.articles().get(0).createdAt()).isNotNull(); assertThat(responses.totalArticle()).isEqualTo(12); } @@ -141,4 +142,97 @@ void getAllArticleOrderByCreatedAtDesc() { .isSortedAccordingTo(Comparator.reverseOrder()); } + @DisplayName("읽지 않은 게시글 수도 알려주어야 한다.") + @Test + void getAllWithUnReadArticles() { + //given + User user = userRepository.save(user()); + Category category = categoryRepository.save(category(user)); + + for (int i = 0; i < 5; i++) { + articleRepository.save(article(user, "article" + i, category)); + } + + for (int i = 0; i < 3; i++) { + articleRepository.save(readArticle(user, "article2" + i, category)); + } + + //when + ArticleAllResponse responses = articleManagementUsecase.getAllArticles(user, 0, 8); + + //then + assertThat(responses.articles()) + .hasSize(8) + .extracting(ArticlesResponse::articleId) + .isSortedAccordingTo(Comparator.reverseOrder()); + + assertThat(responses.totalUnreadArticle()) + .isEqualTo(5); + } + + @DisplayName("카테고리 별로 게시글을 조회할 수 있다.") + @Test + void getByCategory() { + //given + User user = userRepository.save(user()); + Category category = categoryRepository.save(category(user)); + Category category2 = categoryRepository.save(category(user)); + + for (int i = 0; i < 5; i++) { + articleRepository.save(article(user, "article" + i, category)); + } + + for (int i = 0; i < 5; i++) { + articleRepository.save(article(user, "article2" + i, category2)); + } + + //when + + ArticleAllResponse responses = articleManagementUsecase.getAllArticlesByCategory(user, category.getId(), 0, 5); + + //then + + assertThat(responses.articles()) + .hasSize(5) + .extracting(ArticlesResponse::articleId) + .isSortedAccordingTo(Comparator.reverseOrder()); + + assertThat(responses.totalArticle()) + .isEqualTo(5); + + } + + @DisplayName("카테고리 별로 게시글을 조회할 수 있다.") + @Test + void getByCategoryWithUnreadCount() { + //given + User user = userRepository.save(user()); + Category category = categoryRepository.save(category(user)); + Category category2 = categoryRepository.save(category(user)); + + for (int i = 0; i < 3; i++) { + articleRepository.save(article(user, "article" + i, category)); + } + + for (int i = 0; i < 5; i++) { + articleRepository.save(article(user, "article2" + i, category2)); + } + + for (int i = 0; i < 5; i++) { + articleRepository.save(readArticle(user, "article3" + i, category)); + } + + //when + + ArticleAllResponse responses = articleManagementUsecase.getAllArticlesByCategory(user, category.getId(), 0, 5); + + //then + + assertThat(responses.totalUnreadArticle()).isEqualTo(3); + assertThat(responses.totalArticle()) + .isEqualTo(8); + assertThat(responses.articles().get(0).createdAt()).isNotNull(); + + } + } diff --git a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java index e2787fe0..20b3afab 100644 --- a/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java +++ b/src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java @@ -33,4 +33,10 @@ public static Article articleWithCategory(User user, Category category) { public static Article article(User user, String url, Category category) { return Article.create(url, "testmemo", user, category, LocalDateTime.of(2025, 7, 7, 12, 0, 0)); } + + public static Article readArticle(User user, String url, Category category) { + Article article = Article.create(url, "testmemo", user, category, LocalDateTime.of(2025, 7, 7, 12, 0, 0)); + article.toRead(); + return article; + } }