Skip to content

feat: 아티클 카테고리 조회 구현#27

Merged
rootTiket merged 8 commits intodevfrom
feat/#24
Jul 8, 2025
Merged

feat: 아티클 카테고리 조회 구현#27
rootTiket merged 8 commits intodevfrom
feat/#24

Conversation

@rootTiket
Copy link
Contributor

@rootTiket rootTiket commented Jul 8, 2025

🚀 PR 요약

#24 아티클을 카테고리를 입력받아 조회하는 기능을 구현합니다.

✨ PR 상세 내용

  1. 아티클을 카테고리와, 유저를 통해 조회하는 로직을 구현합니다.
  2. 아티클을 최신순으로 조회합니다.
  3. 아티클을 페이지네이션하여 조회합니다.

🚨 주의 사항

  1. 조회시 아티클 생성시간도 반환해주어야 해서 반영했습니다.
  2. 조회시 읽지 않은 아티클 수도 반환해야 해서 추가했습니다

✅ 체크 리스트

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

close #24

Summary by CodeRabbit

  • New Features

    • Added the ability to retrieve a paginated list of articles filtered by category.
    • Introduced a new endpoint to fetch articles by category with pagination options.
    • Included unread article counts alongside paginated article lists.
    • Enhanced article responses with creation timestamps.
    • Added methods to mark articles as read or unread.
  • Tests

    • Added tests to verify unread article counts and category-filtered retrieval with pagination.
    • Included assertions for article creation timestamps in responses.

@coderabbitai
Copy link

coderabbitai bot commented Jul 8, 2025

Walkthrough

A new feature was implemented to allow retrieval of articles filtered by category, including pagination and sorting by newest first. This involved adding a new endpoint, service, repository methods, and a corresponding test to ensure articles can be fetched by category with the correct count and order. Additionally, unread article counts were integrated into responses and DTOs were extended to include creation timestamps.

Changes

File(s) Change Summary
.../domain/article/application/ArticleManagementUsecase.java Modified getAllArticles to include unread count; added getAllArticlesByCategory method.
.../domain/article/domain/repository/ArticleRepositoryCustom.java
.../ArticleRepositoryCustomImpl.java
Changed return type of existing method to ArticlesWithUnreadCount; added and implemented category-filtered article retrieval method.
.../domain/article/domain/service/ArticleGetService.java Changed return type of findAll to ArticlesWithUnreadCount; added findAllByCategory method.
.../domain/article/presentation/ArticleController.java Added GET endpoint /articles/{categoryId} to fetch articles by category with pagination.
.../domain/article/application/ArticleManagementUsecaseTest.java Added tests for unread count and category-based article retrieval with pagination and sorting.
.../domain/article/domain/entity/Article.java Added toRead() and toUnRead() methods to update read status.
.../domain/article/domain/repository/dto/ArticlesWithUnreadCount.java Added new DTO class encapsulating unread count and paginated articles.
.../domain/article/presentation/dto/response/ArticleAllResponse.java Added totalUnreadArticle field and updated factory method.
.../domain/article/presentation/dto/response/ArticlesResponse.java Added createdAt field and updated factory method to include article creation timestamp.
.../domain/article/domain/fixture/TestFixture.java Added readArticle method creating an article marked as read.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Controller
    participant Usecase
    participant Service
    participant Repository

    Client->>Controller: GET /articles/{categoryId}?pageNumber=&pageSize=
    Controller->>Usecase: getAllArticlesByCategory(user, categoryId, pageNumber, pageSize)
    Usecase->>Service: findAllByCategory(userId, category, pageRequest)
    Service->>Repository: findAllByCategory(userId, categoryId, pageable)
    Repository-->>Service: ArticlesWithUnreadCount (page + unread count)
    Service-->>Usecase: ArticlesWithUnreadCount
    Usecase-->>Controller: ArticleAllResponse (total, unread, articles)
    Controller-->>Client: ResponseDto<ArticleAllResponse>
Loading

Assessment against linked issues

Objective Addressed Explanation
1. Retrieve articles by category ([#24])
2. Articles are sorted by newest first ([#24])
3. Return total article count for the category ([#24])

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Added toRead() and toUnRead() methods in Article entity (.../Article.java) Not mentioned in linked issue objectives; these methods relate to read status management beyond category filtering.
Added unread article count integration across service, repository, and DTOs Unread count was not specified in linked issue #24 which focuses on category-based retrieval only.
Added createdAt field to ArticlesResponse DTO and test enhancements Enhancements to response DTO and tests are beyond the scope of category filtering requirement.
Added new readArticle method in test fixture Test fixture extension is unrelated to category filtering objectives.

Possibly related PRs

  • Pinback-Team/pinback-server#22: Adds foundational article retrieval and pagination logic, which this PR extends by introducing category-based filtering.

Suggested reviewers

  • ose0221

Poem

In the warren of code, a new path appears,
Fetching articles by category, with no more fears.
Paginated and sorted, the newest on top,
Rabbits rejoice—no need to hop!
With tests in place, the logic is sound,
In burrows of data, the right posts are found.
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 2

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

13-13: Fix parameter naming for clarity.

The parameter articleId should be renamed to categoryId to accurately reflect its purpose and maintain consistency with how it's used in the service layer.

-	Page<Article> findAllByCategory(UUID userId, long articleId, Pageable pageable);
+	Page<Article> findAllByCategory(UUID userId, long categoryId, Pageable pageable);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c23ab8 and 3f05983.

📒 Files selected for processing (6)
  • src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java (3 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (2 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java (1 hunks)
  • src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (5)
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (2)

13-13: LGTM: Clean import addition.

The Category entity import is necessary for the new method signature.


36-38: LGTM: Clean service method implementation.

The method correctly delegates to the repository layer and properly extracts the category ID from the Category entity. The implementation follows the established pattern in this service class.

src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (1)

144-174: LGTM: Comprehensive test coverage for category-based article retrieval.

The test thoroughly validates the new functionality:

  • Proper filtering by category
  • Correct pagination behavior
  • Descending order sorting by article ID
  • Accurate total count calculation

The test setup and assertions are well-structured and follow the established patterns in the test class.

src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (1)

62-77: LGTM: Well-structured use case implementation.

The method correctly implements the category-based article retrieval with proper:

  • User authorization through categoryGetService.getCategoryAndUser
  • Delegation to service layer
  • Response mapping following established patterns
  • Consistent structure with the existing getAllArticles method
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java (1)

4-4: LGTM: Required imports for the new functionality

The static import for QCategory and the import for BooleanExpression are correctly added to support the new category-based filtering method.

Also applies to: 16-16

Comment on lines +48 to +56
@GetMapping("/{categoryId}")
public ResponseDto<ArticleAllResponse> 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);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix annotation mismatch for categoryId parameter.

The endpoint path uses {categoryId} as a path variable, but the parameter is annotated with @RequestParam. This will cause the endpoint to fail at runtime.

-	public ResponseDto<ArticleAllResponse> getAllByCategory(@CurrentUser User user, @RequestParam Long categoryId,
+	public ResponseDto<ArticleAllResponse> getAllByCategory(@CurrentUser User user, @PathVariable Long categoryId,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@GetMapping("/{categoryId}")
public ResponseDto<ArticleAllResponse> 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);
}
@GetMapping("/{categoryId}")
public ResponseDto<ArticleAllResponse> getAllByCategory(@CurrentUser User user, @PathVariable Long categoryId,
@RequestParam int pageNumber,
@RequestParam int pageSize) {
ArticleAllResponse response = articleManagementUsecase.getAllArticlesByCategory(user, categoryId, pageNumber,
pageSize);
return ResponseDto.ok(response);
}
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/article/presentation/ArticleController.java
around lines 48 to 56, the categoryId parameter is incorrectly annotated with
@RequestParam while the endpoint path uses {categoryId} as a path variable.
Change the annotation on the categoryId parameter from @RequestParam to
@PathVariable to match the path variable in the URL and fix the runtime failure.

Comment on lines 47 to 69
@Override
public Page<Article> findAllByCategory(UUID userId, long categoryId, Pageable pageable) {

BooleanExpression conditions = article.user.id.eq(userId)
.and(article.category.id.eq(categoryId));

List<Article> articles = queryFactory
.selectFrom(article)
.join(article.user, user).fetchJoin()
.join(article.category, category).fetchJoin()
.where(conditions)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(article.createdAt.desc())
.fetch();

JPAQuery<Long> countQuery = queryFactory
.select(article.count())
.from(article)
.where(article.user.id.eq(userId));

return PageableExecutionUtils.getPage(articles, pageable, countQuery::fetchOne);
}
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical bug: Count query doesn't filter by categoryId

The count query on lines 63-66 only filters by userId but omits the categoryId condition, while the main query filters by both. This inconsistency will cause incorrect pagination behavior as the total count will include all user articles, not just those in the specified category.

Apply this fix to include the categoryId filter in the count query:

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

This ensures both queries use the same filtering conditions for consistent pagination results.

🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java
between lines 47 and 69, the count query only filters by userId but misses
filtering by categoryId, causing incorrect pagination counts. Fix this by adding
the categoryId condition to the count query's where clause so it matches the
main query's filtering criteria, ensuring consistent and accurate pagination
results.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

그럴까봐 컨디션 뺐어

Copy link

Choose a reason for hiding this comment

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

@rootTiket 아, 조건을 변수로 뺐지만 count 쿼리에서는 아직 사용하지 않으셨네요!

맞습니다, conditions 변수를 만드셨지만 count 쿼리에서는 여전히 article.user.id.eq(userId)만 사용하고 있어서 카테고리 필터링이 빠져있습니다.

이렇게 수정하시면 됩니다:

JPAQuery<Long> countQuery = queryFactory
    .select(article.count())
    .from(article)
    .where(conditions);  // userId와 categoryId 모두 포함

이제 메인 쿼리와 count 쿼리가 동일한 조건을 사용해서 정확한 페이징 결과를 얻을 수 있습니다.


🧠 Learnings used
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.

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: 2

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

12-12: Fix misleading parameter name in method signature.

The parameter articleId in the findAllByCategory method is misleading. Based on the method name and context, this should be categoryId to clearly indicate it's used for filtering by category.

Apply this diff to improve clarity:

-	ArticlesWithUnreadCount findAllByCategory(UUID userId, long articleId, Pageable pageable);
+	ArticlesWithUnreadCount findAllByCategory(UUID userId, long categoryId, Pageable pageable);
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (1)

205-207: Fix duplicate display name

Both getByCategory and getByCategoryWithUnreadCount tests have the same display name, which could cause confusion in test reports.

-@DisplayName("카테고리 별로 게시글을 조회할 수 있다.")
+@DisplayName("카테고리 별로 게시글을 조회할 수 있고 읽지 않은 게시글 수를 확인할 수 있다.")
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 07587c3 and 03a940f.

📒 Files selected for processing (10)
  • src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java (2 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java (3 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/repository/dto/ArticlesWithUnreadCount.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (2 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticleAllResponse.java (1 hunks)
  • src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticlesResponse.java (1 hunks)
  • src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (2 hunks)
  • src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/pinback/pinback_server/domain/article/domain/repository/dto/ArticlesWithUnreadCount.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java (1)
Learnt from: rootTiket
PR: Pinback-Team/pinback-server#18
File: src/main/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecase.java:38-41
Timestamp: 2025-07-07T15:33:02.697Z
Learning: In the Pinback server project, article detail retrieval (getArticleDetail method) does not require user authorization checks. Articles can be accessed by ID without ownership verification as per the project's design decision.
🧬 Code Graph Analysis (1)
src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (1)
src/main/java/com/pinback/pinback_server/domain/article/exception/ArticleNotFoundException.java (1)
  • ArticleNotFoundException (6-10)
🔇 Additional comments (12)
src/main/java/com/pinback/pinback_server/domain/article/domain/entity/Article.java (1)

75-81: LGTM! Clean implementation of read status management methods.

The toRead() and toUnRead() methods provide a clear and convenient API for managing article read status. The implementation is straightforward and consistent with the existing code style.

src/test/java/com/pinback/pinback_server/domain/fixture/TestFixture.java (1)

37-41: LGTM! Useful test utility for creating read articles.

The readArticle() method provides a convenient way to create articles marked as read for testing scenarios. The implementation correctly leverages the new toRead() method and follows the existing pattern of other article creation methods in the fixture.

src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticlesResponse.java (1)

3-3: LGTM! Clean extension of DTO to include creation timestamp.

The addition of the createdAt field is properly implemented with the correct import, field definition, and factory method update. The changes are consistent and complete.

Also applies to: 11-11, 19-19

src/main/java/com/pinback/pinback_server/domain/article/presentation/dto/response/ArticleAllResponse.java (1)

7-7: LGTM! Proper extension of DTO to include unread article count.

The addition of the totalUnreadArticle field is correctly implemented with consistent updates to the factory method signature and constructor call. The changes are complete and maintain the DTO's integrity.

Also applies to: 10-10, 13-13

src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustom.java (1)

7-7: LGTM! Proper interface update for unread count integration.

The import addition and return type change from Page<Article> to ArticlesWithUnreadCount correctly support the new functionality for including unread article counts in the response.

Also applies to: 10-10

src/main/java/com/pinback/pinback_server/domain/article/domain/service/ArticleGetService.java (3)

11-11: LGTM: Import statements properly added

The new imports for ArticlesWithUnreadCount and Category are necessary for the updated method signatures and new functionality.

Also applies to: 13-13


32-34: LGTM: Method signature updated to return enhanced DTO

The change from Page<Article> to ArticlesWithUnreadCount aligns with the feature requirements to include unread article counts in the response.


36-38: LGTM: New method for category-based article retrieval

The new findAllByCategory method properly delegates to the repository layer and follows the same pattern as the existing findAll method.

src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java (1)

28-51: LGTM: Enhanced method to include unread count

The findAllCustom method has been properly updated to include an unread count query and return the new ArticlesWithUnreadCount DTO. All queries correctly filter by userId to ensure users only see their own articles.

src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java (3)

119-119: LGTM: Added assertion for createdAt field

The assertion verifies that the createdAt timestamp is properly populated in the response, which aligns with the DTO enhancements.


145-171: LGTM: Comprehensive test for unread article count

The test properly validates the unread article count functionality by creating a mix of read and unread articles and asserting the correct count.


173-203: LGTM: Test for category-based article retrieval

The test properly validates category-based filtering, pagination, and sorting functionality.

Comment on lines +54 to 78
public ArticlesWithUnreadCount findAllByCategory(UUID userId, long categoryId, Pageable pageable) {

List<Article> 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<Long> 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));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical Security Issue: Missing user authorization in category-based queries

The findAllByCategory method has a serious security flaw - all queries filter only by categoryId but omit the userId filter. This allows users to access articles from other users if they happen to be in the same category.

Apply this fix to add user authorization to all queries:

 	List<Article> articles = queryFactory
 		.selectFrom(article)
 		.join(article.category, category).fetchJoin()
-		.where(article.category.id.eq(categoryId))
+		.where(article.category.id.eq(categoryId).and(article.user.id.eq(userId)))
 		.offset(pageable.getOffset())
 		.limit(pageable.getPageSize())
 		.orderBy(article.createdAt.desc())
 		.fetch();

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

 	Long unReadCount = queryFactory
 		.select(article.count())
 		.from(article)
-		.where(article.category.id.eq(categoryId).and(article.isRead.isFalse()))
+		.where(article.category.id.eq(categoryId).and(article.user.id.eq(userId)).and(article.isRead.isFalse()))
 		.fetchOne();
🤖 Prompt for AI Agents
In
src/main/java/com/pinback/pinback_server/domain/article/domain/repository/ArticleRepositoryCustomImpl.java
lines 54 to 78, the method findAllByCategory filters articles only by categoryId
without considering userId, causing a security risk by exposing other users'
articles. To fix this, add a condition to all queries to also filter by userId,
ensuring only articles belonging to the specified user and category are
retrieved. Update the where clauses to include article.user.id.eq(userId)
combined with the existing categoryId condition.

Comment on lines +205 to +236
@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();

}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add security test for cross-user access prevention

The current tests don't verify that users cannot access articles from other users in the same category. This is critical given the security issue identified in the repository layer.

Add a test case like this to ensure proper user authorization:

@DisplayName("사용자는 다른 사용자의 아티클을 카테고리로 조회할 수 없다.")
@Test
void getCategoryArticlesWithDifferentUser() {
    //given
    User user1 = userRepository.save(user());
    User user2 = userRepository.save(userWithEmail("user2@test.com"));
    Category category = categoryRepository.save(category(user1));
    
    // User1 creates articles in the category
    for (int i = 0; i < 3; i++) {
        articleRepository.save(article(user1, "user1-article" + i, category));
    }
    
    // User2 should not see User1's articles
    ArticleAllResponse response = articleManagementUsecase.getAllArticlesByCategory(user2, category.getId(), 0, 10);
    
    //then
    assertThat(response.articles()).isEmpty();
    assertThat(response.totalArticle()).isEqualTo(0);
}
🤖 Prompt for AI Agents
In
src/test/java/com/pinback/pinback_server/domain/article/application/ArticleManagementUsecaseTest.java
around lines 205 to 236, add a new test method to verify that a user cannot
access articles belonging to another user within the same category. Create two
users, have one user create articles in a category, then attempt to retrieve
those articles using the second user. Assert that the returned article list is
empty and the total article count is zero to ensure proper authorization and
prevent cross-user data access.

Copy link
Contributor

@ose0221 ose0221 left a comment

Choose a reason for hiding this comment

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

수고하셨습니당 👍👍👍

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 아티클 카테고리 조회 구현

2 participants