Skip to content

fix: 카테고리별 아티클 조회시 조회조건 수정 #103

Merged
rootTiket merged 5 commits intodevfrom
fix/#102
Sep 20, 2025
Merged

fix: 카테고리별 아티클 조회시 조회조건 수정 #103
rootTiket merged 5 commits intodevfrom
fix/#102

Conversation

@rootTiket
Copy link
Contributor

@rootTiket rootTiket commented Sep 20, 2025

🚀 PR 요약

아티클 조회시 조회조건을 수정했습니다.

✨ PR 상세 내용

  1. 카테고리별 아티클 조회시 유저와 join되지 않아 전체 아티클이 조회되는 현상 수정
  2. 카테고리별 아티클 조회시 전체 아티클 개수의 condition 조건을 수정
  3. 읽음 조건을 null true false 세가지를 받을 수 있도록 수정

🚨 주의 사항

주의할 부분이 무엇인가요? - 지우고 작성

✅ 체크 리스트

  • 리뷰어 설정했나요?
  • Label 설정했나요?
  • 제목 양식 맞췄나요? (ex. feat: 기능 추가)
  • 변경 사항에 대한 테스트를 진행했나요?

Summary by CodeRabbit

  • New Features
    • Category article lists now support an optional Read Status filter — choose All, Read, or Unread when viewing a category.
    • Clearing the filter shows all articles in the category without requiring a read-status selection.
    • Category views now include a total-articles count per category alongside unread counts for clearer context.

@rootTiket rootTiket requested a review from ose0221 September 20, 2025 00:42
@rootTiket rootTiket self-assigned this Sep 20, 2025
@rootTiket rootTiket added BUG Something isn't working FIX labels Sep 20, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 20, 2025

Walkthrough

Makes the read-status filter optional by changing isRead from primitive boolean to nullable Boolean across controller, application ports/usecase, service, and repository; repository query now conditionally applies the isRead predicate and returns an added totalCategoryArticleCount value.

Changes

Cohort / File(s) Summary of edits
API Controller
api/src/main/java/com/pinback/api/article/controller/ArticleController.java
read-status request param changed to optional: @RequestParam(name = "read-status", required = false) Boolean isRead; docs updated.
Application Port (In)
application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java
getAllArticlesByCategory parameter isRead changed from boolean to Boolean.
Application Usecase
application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java
Method signature updated to accept Boolean isRead; forwards nullable value to service; response composition uses totalCategoryArticleCount.
Application DTO
application/src/main/java/com/pinback/application/article/dto/ArticlesWithUnreadCountDto.java
Added Long totalCategoryArticleCount field to the record (constructor arity increased).
Application Port (Out)
application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java
findAllByCategory signature changed to accept Boolean isRead.
Infrastructure Service
infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java
Signature uses Boolean isRead; converts infra result into DTO including totalCategoryArticleCount (new third arg).
Infrastructure Repository (API)
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java
findAllByCategory parameter changed to Boolean isRead.
Infrastructure Repository (Impl)
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java
Signature uses Boolean isRead; query builds base predicates (user, category) and conditionally adds isRead predicate when non-null; added fetch joins; produces Page via PageImpl and separate totalCategoryArticleCount; unread-count query filters by isRead = false.
Infrastructure Repository DTO
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticlesWithUnreadCount.java
Record extended with Long totalCategoryArticleCount (third component).
Tests
application/src/test/java/com/pinback/application/article/usecase/query/GetArticleUsecaseTest.java
Updated test DTO constructions to three-argument ArticlesWithUnreadCountDto(...) usages (third arg added where needed).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller as ArticleController
  participant Usecase as GetArticleUsecase
  participant Service as ArticleGetService
  participant Repo as ArticleRepositoryCustom

  Client->>Controller: GET /articles?categoryId&read-status?
  Controller->>Usecase: getAllArticlesByCategory(user, categoryId, isRead?, page)
  Usecase->>Service: findAllByCategory(user, category, isRead?, pageReq)
  Service->>Repo: findAllByCategory(userId, categoryId, isRead?, pageable)

  rect rgb(230,245,255)
    note over Repo: Build base predicates (user + category)
    alt isRead != null
      Repo-->>Repo: Add predicate article.isRead == isRead
    else isRead == null
      Repo-->>Repo: No isRead filter (all articles)
    end
    Repo-->>Repo: Compute unreadCount (isRead = false) and totalCategoryArticleCount
  end

  Repo-->>Service: Articles + unreadCount + totalCategoryArticleCount
  Service-->>Usecase: ArticlesWithUnreadCountDto
  Usecase-->>Controller: ArticlesPageResponse
  Controller-->>Client: 200 OK (paged result)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ose0221

Poem

I twitch my whiskers at a nullable flag,
A Boolean breeze that skips the snag.
Read, unread, or left unknown,
I hop through categories newly grown.
Crunching counts with carrot glee—🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "fix: 카테고리별 아티클 조회시 조회조건 수정" is concise and accurately summarizes the primary change — adjusting the query/filter logic for category-based article retrieval — and aligns with the code changes across controller, ports, repository, and DTOs (including making read-status nullable and fixing join/total-count conditions). It follows the repository's commit-title convention and is clear enough for a teammate scanning history.
Description Check ✅ Passed The PR description follows the repository template and clearly states the objectives and detailed changes (fixing user join to avoid global article exposure, adjusting total-count conditions, and allowing read-status to be null/true/false), matching the file-level summaries in the changeset. However, the "🚨 주의 사항" section still contains the template placeholder text and needs concrete caution/testing notes before merge.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#102

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java (1)

16-16: Rename parameter to categoryId to match semantics and impl.

Interface uses articleId but impl and usage are category-scoped. Rename for clarity.

Apply:

- ArticlesWithUnreadCount findAllByCategory(UUID userId, long articleId, Boolean isRead, Pageable pageable);
+ ArticlesWithUnreadCount findAllByCategory(UUID userId, long categoryId, Boolean isRead, Pageable pageable);
api/src/main/java/com/pinback/api/article/controller/ArticleController.java (1)

4-4: Remove unused import.

java.util.Optional is unused.

Apply:

-import java.util.Optional;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6030cd9 and 23a9063.

📒 Files selected for processing (7)
  • api/src/main/java/com/pinback/api/article/controller/ArticleController.java (2 hunks)
  • application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java (1 hunks)
  • application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java (1 hunks)
  • application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java (1 hunks)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustom.java (1 hunks)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java (2 hunks)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java (1 hunks)
🔇 Additional comments (9)
application/src/main/java/com/pinback/application/article/port/out/ArticleGetServicePort.java (1)

29-30: LGTM: Nullable read filter on category fetch.

Signature widening to Boolean isRead is consistent with the PR goal.

Please confirm if Javadoc should clarify: null ⇒ no filter, true/false ⇒ filtered.

application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java (1)

70-75: LGTM: Propagates nullable read filter end-to-end.

Pass-through to service is correct.

application/src/main/java/com/pinback/application/article/port/in/GetArticlePort.java (1)

19-20: LGTM: Public API supports optional read-status.

Matches controller and service ports.

api/src/main/java/com/pinback/api/article/controller/ArticleController.java (1)

97-103: LGTM: Optional read-status for category endpoint.

required = false with Boolean aligns with repository behavior.

infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java (1)

57-63: LGTM: Service port and repository wiring for nullable isRead.

Pass-through parameters are correct.

infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java (4)

61-69: LGTM: Apply isRead predicate only when provided.

Conditional predicate construction is correct.


72-74: LGTM: Add fetch joins for user and category.

Fixes prior over-broad result set and prevents N+1 on to-one relations.


80-85: Verify count semantics when isRead is provided.

countQuery ignores isRead, so totalElements = all in category, not filtered subset. If UI expects total for the filtered set, use the same conditions.

Apply if filtered count is desired:

- JPAQuery<Long> countQuery = queryFactory
-   .select(article.count())
-   .from(article)
-   .where(article.user.id.eq(userId).and(article.category.id.eq(categoryId)));
+ JPAQuery<Long> countQuery = queryFactory
+   .select(article.count())
+   .from(article)
+   .where(conditions);

If the product wants “category total regardless of isRead,” keep current code.


85-91: LGTM: Unread count scoped by user+category with isRead=false.

Matches the requirement to report unread independently of the current filter.

@rootTiket rootTiket merged commit 4485472 into dev Sep 20, 2025
4 of 5 checks passed
@rootTiket rootTiket deleted the fix/#102 branch September 20, 2025 01:08
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java (1)

168-183: Guard isRead == null in findTodayRemindWithCount — infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java:168–183

article.isRead.eq(isRead) will apply an "IS NULL" filter when isRead == null; mirror the null-guard used in other methods and add tests.

-		BooleanExpression conditions = baseConditions.and(article.isRead.eq(isRead));
+		BooleanExpression conditions = baseConditions;
+		if (isRead != null) {
+			conditions = conditions.and(article.isRead.eq(isRead));
+		}

Add unit tests covering isRead = null for the remind path to ensure parity with category/all-articles queries.

🧹 Nitpick comments (7)
application/src/main/java/com/pinback/application/article/dto/ArticlesWithUnreadCountDto.java (2)

7-11: Document the semantics and nullability of totalCategoryArticleCount.

Clarify that it represents the total number of the user's articles in the category, ignoring any isRead filter, and may be null when not category-scoped.

Suggested doc:

 package com.pinback.application.article.dto;

 import org.springframework.data.domain.Page;

 import com.pinback.domain.article.entity.Article;

+/**
+ * unReadCount: count of unread articles for the user (scope may vary by query).
+ * article: paged articles for the current query.
+ * totalCategoryArticleCount: total number of the user's articles in the category,
+ * ignoring any read filter; may be null when not category-scoped.
+ */
 public record ArticlesWithUnreadCountDto(
 	Long unReadCount,
 	Page<Article> article,
 	Long totalCategoryArticleCount
 ) {
 }

9-9: Nit: Use plural name for a page of items.

Prefer articles over article for clarity and consistency.

-	Page<Article> article,
+	Page<Article> articles,
application/src/test/java/com/pinback/application/article/usecase/query/GetArticleUsecaseTest.java (2)

140-154: Assert totalCategoryArticleCount is surfaced (if API exposes it).

You pass 5L but don’t verify it. If ArticlesPageResponse includes this field, add an assertion; otherwise confirm it’s intentionally not part of the API response.

Sample assertion (adjust to your response type):

assertThat(response.totalCategoryArticleCount()).isEqualTo(5L);

Also applies to: 142-143


144-149: Add tests for the new tri-state isRead (null/true/false) on category query.

Current test covers true only. Add cases for null (no filter) and false (unread) to prevent regressions.

Example (additional tests):

@DisplayName("카테고리별 조회시 isRead=null이면 읽음 필터를 적용하지 않는다")
@Test
void getAllArticlesByCategory_IsReadNull() {
    Long categoryId = 1L;
    PageQuery pageQuery = new PageQuery(0, 10);
    PageRequest pageRequest = PageRequest.of(0, 10);

    List<Article> articles = List.of(article, article, article, article, article);
    Page<Article> articlePage = new PageImpl<>(articles, pageRequest, 5);
    ArticlesWithUnreadCountDto dto = new ArticlesWithUnreadCountDto(2L, articlePage, 5L);

    when(getCategoryPort.getCategoryAndUser(categoryId, user)).thenReturn(category);
    when(articleGetServicePort.findAllByCategory(user, category, null, pageRequest)).thenReturn(dto);

    ArticlesPageResponse response = getArticleUsecase.getAllArticlesByCategory(user, categoryId, null, pageQuery);

    assertThat(response.articles()).hasSize(5);
    verify(articleGetServicePort).findAllByCategory(user, category, null, pageRequest);
}

@DisplayName("카테고리별 조회시 isRead=false이면 안읽은 아티클만 조회한다")
@Test
void getAllArticlesByCategory_IsReadFalse() {
    Long categoryId = 1L;
    PageQuery pageQuery = new PageQuery(0, 10);
    PageRequest pageRequest = PageRequest.of(0, 10);

    List<Article> articles = List.of(article, article, article);
    Page<Article> articlePage = new PageImpl<>(articles, pageRequest, 3);
    ArticlesWithUnreadCountDto dto = new ArticlesWithUnreadCountDto(3L, articlePage, 5L);

    when(getCategoryPort.getCategoryAndUser(categoryId, user)).thenReturn(category);
    when(articleGetServicePort.findAllByCategory(user, category, false, pageRequest)).thenReturn(dto);

    ArticlesPageResponse response = getArticleUsecase.getAllArticlesByCategory(user, categoryId, false, pageQuery);

    assertThat(response.articles()).hasSize(3);
    verify(articleGetServicePort).findAllByCategory(user, category, false, pageRequest);
}
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticlesWithUnreadCount.java (2)

7-11: Mirror the semantics doc here as well.

Document that totalCategoryArticleCount is category-scoped, ignores isRead, and may be null.

Suggested doc:

 package com.pinback.infrastructure.article.repository.dto;

 import org.springframework.data.domain.Page;

 import com.pinback.domain.article.entity.Article;

+/**
+ * unReadCount: unread count for the user in the current scope.
+ * article: page of articles for the current query.
+ * totalCategoryArticleCount: total (read+unread) for the user within the category; may be null.
+ */
 public record ArticlesWithUnreadCount(
 	Long unReadCount,
 	Page<Article> article,
 	Long totalCategoryArticleCount) {
 }

9-9: Nit: Prefer articles over article for Page<Article>.

Aligns with plural semantics and the application DTO.

-	Page<Article> article,
+	Page<Article> articles,
infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java (1)

95-104: Consider lazy count for consistency and performance.

Other methods use PageableExecutionUtils. Keep it here too to avoid unnecessary count queries on the last page.

-		return new ArticlesWithUnreadCount(unReadCount,
-			new PageImpl<>(articles, pageable, countQuery.fetchOne()),
-			totalCategoryArticleCount);
+		return new ArticlesWithUnreadCount(
+			unReadCount,
+			PageableExecutionUtils.getPage(articles, pageable, countQuery::fetchOne),
+			totalCategoryArticleCount
+		);

Also applies to: 102-104

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 23a9063 and 36f27f8.

📒 Files selected for processing (6)
  • application/src/main/java/com/pinback/application/article/dto/ArticlesWithUnreadCountDto.java (1 hunks)
  • application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java (2 hunks)
  • application/src/test/java/com/pinback/application/article/usecase/query/GetArticleUsecaseTest.java (3 hunks)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java (4 hunks)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/repository/dto/ArticlesWithUnreadCount.java (1 hunks)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • infrastructure/src/main/java/com/pinback/infrastructure/article/service/ArticleGetService.java
  • application/src/main/java/com/pinback/application/article/usecase/query/GetArticleUsecase.java
🔇 Additional comments (4)
application/src/test/java/com/pinback/application/article/usecase/query/GetArticleUsecaseTest.java (2)

117-123: Constructor update looks good; add a null-handling assertion.

Since totalCategoryArticleCount can be null outside category-scope, add an assertion that the mapping path doesn’t NPE and behaves as expected (e.g., absent/ignored in response).

Also applies to: 119-120


163-167: Constructor update for unread-articles path looks good.

Null third argument is appropriate outside category scope. Consider adding an assertion guarding null handling in the mapping, similar to the all-articles test.

Also applies to: 165-166

infrastructure/src/main/java/com/pinback/infrastructure/article/repository/ArticleRepositoryCustomImpl.java (2)

63-76: Good: category+user scoping and optional isRead filter fixed.

Adding user/category predicates and the nullable isRead condition addresses the over-fetching bug and the tri-state requirement.

Also applies to: 65-71, 74-75


87-99: Counts semantics LGTM.

Unread count ignores the isRead filter (by design) and totalCategoryArticleCount ignores it as well; matches the PR objectives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BUG Something isn't working FIX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant