독서 랭킹 및 팔로우/언팔로우 API 구현#106
Conversation
📝 WalkthroughWalkthroughThis 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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, andUNFOLLOW_SUCCESSare not authentication-related operations, yet they're placed inAuthSuccessCode. This mixing of concerns may cause confusion as the codebase grows.Consider creating a separate enum (e.g.,
GeneralSuccessCodeorUserSuccessCode) 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 addingclearAutomatically = trueto@Modifyingannotation.When using
@Modifyingwith JPQL bulk delete operations, the persistence context may retain stale managed entities. AddingclearAutomatically = trueensures 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 themonthparameter.The
monthparameter 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
@Validatedis added to the controller class or method-level validation is enabled.
103-122: Same validation recommendation applies to the infinite scroll endpoint.The
monthparameter should also be validated here for consistency. Additionally, consider adding validation constraints forsize(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 onUserFollows. A cleaner approach would be to use a standalone@Repositoryclass with@PersistenceContext EntityManageror simply extendRepository<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/mutualendpoint toMeFriendsController.The
mutualFriendsendpoint at/api/v1/me/friends/mutualfollows the same URL pattern asMeFriendsControllerwhich handles/api/v1/me/friends/*routes. Colocating related endpoints improves discoverability and maintains consistent organization.Alternatively, add a class-level
@RequestMappingto 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;
| 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); | ||
| } |
There was a problem hiding this comment.
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.
#105
#104
Summary by CodeRabbit
Release Notes
✏️ Tip: You can customize this high-level summary in your review settings.