Skip to content

독서 랭킹 및 팔로우/언팔로우 API 구현#106

Merged
millkk04 merged 4 commits into
devfrom
feat/readingRanking/1
Jan 31, 2026
Merged

독서 랭킹 및 팔로우/언팔로우 API 구현#106
millkk04 merged 4 commits into
devfrom
feat/readingRanking/1

Conversation

@icarus0616
Copy link
Copy Markdown
Collaborator

@icarus0616 icarus0616 commented Jan 31, 2026

#105
#104

Summary by CodeRabbit

Release Notes

  • New Features
    • View your friends' monthly reading rankings with top 3 highlights
    • Browse extended friend rankings with infinite scrolling pagination
    • Follow and unfollow other users
    • See mutual friends with infinite scrolling pagination

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 31, 2026

📝 Walkthrough

Walkthrough

This PR introduces user follow relationship management and friend reading ranking features through new REST endpoints, service layer logic, repository queries with CTEs, and supporting DTOs, enabling users to follow/unfollow others, retrieve friend reading rankings with pagination, and discover mutual friends.

Changes

Cohort / File(s) Summary
Controllers
domain/users/controller/MeFriendsController.java, domain/users/controller/UserFollowController.java
Two new REST controllers exposing six endpoints: reading ranking (top3 and infinite scroll), follow/unfollow actions, and mutual friends retrieval with cursor-based pagination; all endpoints use CustomUserDetails for authentication and wrap responses in ApiResponse.
Response DTOs
domain/users/dto/FriendReadingRankingTop3Response.java, domain/users/dto/FriendReadingRankingInfiniteResponse.java, domain/users/dto/FollowActionResponse.java, domain/users/dto/MutualFriendsResponse.java, domain/users/dto/FriendReadingRankingResponse.java
Five new record-based DTOs defining API response structures for ranking results, follow actions, and mutual friends with pagination metadata.
Query Result & Item DTOs
domain/users/dto/FriendReadingRankingItem.java, domain/users/dto/FriendReadingRankingRow.java, domain/users/dto/MutualFriendItem.java, domain/users/dto/MutualFriendRow.java
Four DTOs modeling ranking items, mutual friend items, and row projection interfaces for database query result mapping.
Repositories
domain/users/repository/FriendsReadingRankingQueryRepository.java, domain/users/repository/UserFollowsRepository.java, domain/users/repository/UsersRepository.java
Three repository interfaces: FriendsReadingRankingQueryRepository with native SQL query using CTEs for ranked friend analytics; UserFollowsRepository for follow relationship CRUD; UsersRepository extended with mutual friends cursor pagination query.
Services
domain/users/service/FriendsReadingRankingService.java, domain/users/service/UserFollowService.java
Two service classes providing business logic for reading rankings (top3 and infinite scroll with date range filtering) and follow management (follow, unfollow, mutual friends retrieval with validation and idempotency).
Status Codes
global/auth/exception/AuthSuccessCode.java, global/common/apiPayload/code/status/ErrorStatus.java
New enum constants: three success codes (READ_SUCCESS, FOLLOW_SUCCESS, UNFOLLOW_SUCCESS) and one error code (SELF_FOLLOW_NOT_ALLOWED) for response handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jaehyeon4406
  • ktg3891
  • millkk04

Poem

🐰 A curious rabbit hops through the code with glee,
Following friends and rankings, social bonds run free,
Mutual hops and pagination dance in perfect grace,
Top-3 peaks and infinite scrolls light up the place! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title '독서 랭킹 및 팔로우/언팔로우 API 구현' clearly summarizes the two main changes: reading ranking API implementation and follow/unfollow API implementation, which are the primary features added across all the new files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/readingRanking/1

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
Copy Markdown

@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: 1

🤖 Fix all issues with AI agents
In
`@booklog/src/main/java/com/example/booklog/domain/users/service/FriendsReadingRankingService.java`:
- Around line 90-95: In FriendsReadingRankingService.range(String month)
validate the input by catching java.time.format.DateTimeParseException around
YearMonth.parse(month) and convert it into a controlled 400-level error: throw
new GeneralException(ErrorStatus.INVALID_MONTH_FORMAT) (after adding
INVALID_MONTH_FORMAT to ErrorStatus if missing) so invalid formats (e.g.,
"2026-1" or "invalid") return a Bad Request; add the necessary imports for
DateTimeParseException, ErrorStatus, and GeneralException and keep the rest of
the method logic (start/end calculation) unchanged.
🧹 Nitpick comments (7)
booklog/src/main/java/com/example/booklog/global/auth/exception/AuthSuccessCode.java (1)

15-20: Consider separating non-auth success codes into a dedicated enum.

READ_SUCCESS, FOLLOW_SUCCESS, and UNFOLLOW_SUCCESS are not authentication-related operations, yet they're placed in AuthSuccessCode. This mixing of concerns may cause confusion as the codebase grows.

Consider creating a separate enum (e.g., GeneralSuccessCode or UserSuccessCode) for non-auth operations to maintain clearer domain boundaries.

booklog/src/main/java/com/example/booklog/domain/users/repository/UserFollowsRepository.java (1)

15-22: Consider adding clearAutomatically = true to @Modifying annotation.

When using @Modifying with JPQL bulk delete operations, the persistence context may retain stale managed entities. Adding clearAutomatically = true ensures the persistence context is cleared after the query executes, preventing potential staleness issues in subsequent operations within the same transaction.

♻️ Suggested improvement
-    `@Modifying`
+    `@Modifying`(clearAutomatically = true)
     `@Query`("""
         delete from UserFollows uf
         where uf.followerId = :followerId
           and uf.followeeId = :followeeId
     """)
     int deleteByFollowerIdAndFolloweeId(`@Param`("followerId") Long followerId,
                                         `@Param`("followeeId") Long followeeId);
booklog/src/main/java/com/example/booklog/domain/users/controller/MeFriendsController.java (2)

54-64: Consider adding input validation for the month parameter.

The month parameter is accepted as a raw String without validation. If an invalid format is provided (e.g., "invalid", "2026-1", "01-2026"), it will likely cause a parsing exception in the service layer, resulting in an unclear 500 error instead of a proper 400 Bad Request.

♻️ Suggested approach using `@Pattern`
+import jakarta.validation.constraints.Pattern;
+
 `@GetMapping`("/reading-ranking/top3")
 public ApiResponse<FriendReadingRankingTop3Response> getReadingRankingTop3(
         `@AuthenticationPrincipal` CustomUserDetails me,
         `@Parameter`(description = "조회 월 (YYYY-MM)", example = "2026-01", required = true)
-        `@RequestParam` String month
+        `@RequestParam` `@Pattern`(regexp = "^\\d{4}-(0[1-9]|1[0-2])$", message = "month must be in YYYY-MM format") String month
 ) {

Note: Ensure @Validated is added to the controller class or method-level validation is enabled.


103-122: Same validation recommendation applies to the infinite scroll endpoint.

The month parameter should also be validated here for consistency. Additionally, consider adding validation constraints for size (e.g., @Min(1) @max(100)) to prevent abuse or unexpected behavior with extreme values.

♻️ Suggested parameter validation
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Pattern;
+
 `@GetMapping`("/reading-ranking")
 public ApiResponse<FriendReadingRankingInfiniteResponse> getReadingRankingInfinite(
         `@AuthenticationPrincipal` CustomUserDetails me,
         `@Parameter`(description = "조회 월 (YYYY-MM)", example = "2026-01", required = true)
-        `@RequestParam` String month,
+        `@RequestParam` `@Pattern`(regexp = "^\\d{4}-(0[1-9]|1[0-2])$", message = "month must be in YYYY-MM format") String month,

         `@Parameter`(
                 description = "커서(마지막으로 받은 rank). 첫 호출은 생략 또는 0, 이후 호출부터 nextCursor를 넣어 요청",
                 example = "23"
         )
-        `@RequestParam`(required = false) Integer cursor,
+        `@RequestParam`(required = false) `@Min`(0) Integer cursor,

         `@Parameter`(description = "한 번에 가져올 개수 (기본 20)", example = "20")
-        `@RequestParam`(required = false) Integer size
+        `@RequestParam`(required = false) `@Min`(1) `@Max`(100) Integer size
 ) {
booklog/src/main/java/com/example/booklog/domain/users/repository/FriendsReadingRankingQueryRepository.java (1)

11-12: Consider using a more appropriate repository base.

This repository extends JpaRepository<UserFollows, UserFollows.UserFollowId> but only defines a custom query method without using any standard CRUD operations on UserFollows. A cleaner approach would be to use a standalone @Repository class with @PersistenceContext EntityManager or simply extend Repository<UserFollows, UserFollows.UserFollowId> (marker interface) if no CRUD methods are needed.

This is a minor structural concern and doesn't affect functionality.

booklog/src/main/java/com/example/booklog/domain/users/controller/UserFollowController.java (1)

15-18: Consider moving /api/v1/me/friends/mutual endpoint to MeFriendsController.

The mutualFriends endpoint at /api/v1/me/friends/mutual follows the same URL pattern as MeFriendsController which handles /api/v1/me/friends/* routes. Colocating related endpoints improves discoverability and maintains consistent organization.

Alternatively, add a class-level @RequestMapping to group related endpoints if they should remain here.

Also applies to: 152-167

booklog/src/main/java/com/example/booklog/domain/users/service/FriendsReadingRankingService.java (1)

13-17: Consider adding @Transactional(readOnly = true) at class level.

This service only performs read operations. Adding @Transactional(readOnly = true) provides:

  • Optimized database connection handling (some databases use read-only mode)
  • Consistent transaction boundaries for repository calls
  • Clear intent signaling for maintenance
♻️ Suggested change
 `@Service`
 `@RequiredArgsConstructor`
+@Transactional(readOnly = true)
 public class FriendsReadingRankingService {

Import needed:

import org.springframework.transaction.annotation.Transactional;

Comment on lines +90 to +95
private DateRange range(String month) {
YearMonth ym = YearMonth.parse(month); // "YYYY-MM"
LocalDate start = ym.atDay(1);
LocalDate end = ym.plusMonths(1).atDay(1);
return new DateRange(start, end);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invalid month format will result in 500 instead of 400 error.

YearMonth.parse(month) throws DateTimeParseException for invalid formats (e.g., "2026-1" or "invalid"). This uncaught exception will propagate as a 500 Internal Server Error rather than a 400 Bad Request with a meaningful message.

🛡️ Suggested fix with explicit validation
 private DateRange range(String month) {
-    YearMonth ym = YearMonth.parse(month); // "YYYY-MM"
+    YearMonth ym;
+    try {
+        ym = YearMonth.parse(month); // "YYYY-MM"
+    } catch (DateTimeParseException e) {
+        throw new GeneralException(ErrorStatus.INVALID_MONTH_FORMAT);
+    }
     LocalDate start = ym.atDay(1);
     LocalDate end = ym.plusMonths(1).atDay(1);
     return new DateRange(start, end);
 }

Requires adding a new error code like INVALID_MONTH_FORMAT to ErrorStatus and importing:

import java.time.format.DateTimeParseException;
import com.example.booklog.global.common.apiPayload.code.status.ErrorStatus;
import com.example.booklog.global.common.apiPayload.exception.GeneralException;
🤖 Prompt for AI Agents
In
`@booklog/src/main/java/com/example/booklog/domain/users/service/FriendsReadingRankingService.java`
around lines 90 - 95, In FriendsReadingRankingService.range(String month)
validate the input by catching java.time.format.DateTimeParseException around
YearMonth.parse(month) and convert it into a controlled 400-level error: throw
new GeneralException(ErrorStatus.INVALID_MONTH_FORMAT) (after adding
INVALID_MONTH_FORMAT to ErrorStatus if missing) so invalid formats (e.g.,
"2026-1" or "invalid") return a Bad Request; add the necessary imports for
DateTimeParseException, ErrorStatus, and GeneralException and keep the rest of
the method logic (start/end calculation) unchanged.

@millkk04 millkk04 merged commit 88b70ca into dev Jan 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants