Skip to content

Conversation

@leeedohyun
Copy link
Member

@leeedohyun leeedohyun commented Sep 7, 2025

📌 관련 이슈 (필수)

이 PR이 어떤 이슈를 해결하는지 작성해주세요. 예시: closes #123, resolves #456

📝 작업 내용 (필수)

이번 PR에서 작업한 내용을 간략히 설명해주세요.

태그 설정 부분에서 사용할 유저 태그 조회 API를 구현했습니다.

💬 리뷰 참고 사항 (선택)

리뷰어가 참고할 만한 사항이나 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.

Summary by CodeRabbit

  • New Features

    • Added API to retrieve your user tags with cursor-based pagination (hasNext, lastCursor, items).
  • Documentation

    • Added user tag retrieval guide with success and failure examples.
    • Added error-code documentation for user tag retrieval.
  • Tests

    • Added tests covering user tag retrieval responses and related error-code documentation.

@leeedohyun leeedohyun self-assigned this Sep 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Sep 7, 2025

Caution

Review failed

Failed to post review comments.

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bfdee69 and 7d46bff.

📒 Files selected for processing (7)
  • capturecat-core/src/docs/asciidoc/user.adoc (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java (3 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepository.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepositoryImpl.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (3 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java (3 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepository.java
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java
  • capturecat-core/src/docs/asciidoc/user.adoc
  • capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepositoryImpl.java
  • capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java
🧰 Additional context used
🧬 Code graph analysis (1)
capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (1)
capturecat-core/src/main/java/com/capturecat/core/support/util/CursorUtil.java (1)
  • CursorUtil (14-57)

Walkthrough

Adds a paginated GET endpoint to retrieve the authenticated user’s tags, including controller, service, repository (custom interface + QueryDSL impl), tests, and documentation/error-code docs. Introduces optional lastCursor + Pageable cursor pagination. No existing public API signatures are removed.

Changes

Cohort / File(s) Summary of changes
Docs: User Tag retrieval & error codes
capturecat-core/src/docs/asciidoc/user.adoc, capturecat-core/src/docs/asciidoc/error-codes.adoc
Adds "유저 태그 조회" documentation (success/failure sections, include for getUserTags error codes) and links the new error-code include.
API Controller: GET /v1/user-tags
capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
Adds getAll(...) GET endpoint returning ApiResponse<CursorResponse<TagResponse>>, accepts lastCursor and Pageable, delegates to userTagService.getAll(...).
Service: Retrieval logic
capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java
Adds getAll(LoginUser, Long lastCursor, Pageable) annotated @Transactional(readOnly = true), loads user, fetches Slice<UserTag>, maps to TagResponse, and builds CursorResponse via cursor utilities.
Repository + QueryDSL implementation
capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepository.java, .../UserTagCustomRepositoryImpl.java, .../UserTagRepository.java
Introduces UserTagCustomRepository with findAllByUser(User, Long lastCursor, Pageable), QueryDSL UserTagCustomRepositoryImpl implementing cursor/limit+1 logic and converting to Slice, and updates UserTagRepository to extend the custom interface.
Tests: Controller / Service / Error docs
capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java, .../UserErrorCodeControllerTest.java, .../service/user/UserTagServiceTest.java
Adds controller test for GET /v1/user-tags, error-code doc test for getUserTags, and service unit tests for getAll (success and user-not-found).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant Ctrl as UserTagController
  participant Svc as UserTagService
  participant URepo as UserRepository
  participant TRepo as UserTagRepository/CustomImpl
  note right of TRepo #DDEBF7: QueryDSL/JPA executes query (left join fetch tag,<br/>filter by user, order desc, limit pageSize+1)
  C->>Ctrl: GET /v1/user-tags (Auth, lastCursor, pageable)
  Ctrl->>Svc: getAll(loginUser, lastCursor, pageable)
  Svc->>URepo: findById(loginUser.id)
  alt user found
    URepo-->>Svc: User
    Svc->>TRepo: findAllByUser(User, lastCursor, pageable)
    TRepo-->>Svc: Slice<UserTag>
    Svc-->Svc: map -> TagResponse, build CursorResponse
    Svc-->>Ctrl: CursorResponse<TagResponse>
    Ctrl-->>C: 200 OK (ApiResponse success)
  else user not found
    URepo-->>Svc: empty
    Svc-->>Ctrl: throw USER_NOT_FOUND
    Ctrl-->>C: Error response (USER_NOT_FOUND)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jaelyangChoi

Poem

I nibble through tags, one hop at a time,
A cursor trail like carrot rhyme. 🥕
Slice and fetch, then map with care,
200s bloom for folks who dare.
— the rabbit dev, logging with flair.

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • Failed to retrieve linked issues from the platform client.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/#125-get-user-tags

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

🧹 Nitpick comments (9)
capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepository.java (1)

6-9: Specify deterministic sort and fetch strategy for cursor-friendly paging

To keep cursor pagination stable and avoid N+1s, make the contract explicit about ordering and fetching.

Apply this diff to add JavaDoc guidance:

 public interface UserTagCustomRepository {
 
-	Slice<UserTag> findAllByUser(User user, Pageable pageable);
+	/**
+	 * Returns a slice of the user's tags with a deterministic order suitable for cursor paging.
+	 * Implementations should:
+	 * - Order by tag.id (DESC) or another stable key documented here.
+	 * - Fetch-join the Tag association to prevent N+1 queries.
+	 */
+	Slice<UserTag> findAllByUser(User user, Pageable pageable);
 }
capturecat-core/src/docs/asciidoc/user.adoc (1)

52-63: Document pagination inputs and include query-parameters snippet

The endpoint returns a CursorResponse but docs don’t show any request params. If Pageable (size, sort) or a cursor is supported, surface that for clients and include the query-parameters snippet.

Apply this diff to augment the docs:

 [[유저-태그-조회]]
 === 유저 태그 조회
 사용자의 태그를 조회합니다.
 
 ==== 성공
-operation::getUserTags[snippets='curl-request,http-request,request-headers,http-response,response-fields']
+operation::getUserTags[snippets='curl-request,http-request,request-headers,query-parameters,http-response,response-fields']
 
 ==== 실패
 유저 태그 조회가 실패했다면 HTTP 상태 코드와 함께 <<에러-객체-형식, 에러 객체>>가 돌아옵니다.
 
 <<error-codes#유저-태그-조회, 유저 태그 조회 API에서 발생할 수 있는 에러>>를 살펴보세요.

Follow-up: Update the controller test to emit the query-parameters snippet (see my comment on UserTagControllerTest).

capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (1)

122-138: Strengthen assertions to cover pagination metadata

Also assert hasNext and lastCursor, and verify repository interaction for completeness.

Apply this diff:

 	// when
 	var response = userTagService.getAll(new LoginUser(user), PageRequest.of(0, 10));
 
 	// then
 	assertThat(response.items()).hasSize(1);
 	assertThat(response.items().get(0).name()).isEqualTo(tag.getName());
+	assertThat(response.hasNext()).isFalse();
+	assertThat(response.lastCursor()).isEqualTo(tag.getId());
+	verify(userTagRepository, times(1)).findAllByUser(eq(user), any());
capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (1)

55-66: Align cursor key with sort and cap page size

  • CursorResponse uses TagResponse::id (tag.id) as the cursor key. Ensure the repository sorts by tag.id consistently, or change the cursor key to match the actual sort key (e.g., userTag.id). Otherwise, pagination can skip/duplicate items when pages advance.
  • Consider capping page size and applying a default deterministic sort when none is provided.

Apply this minimal service-side guard; add imports for PageRequest and Sort:

 	@Transactional(readOnly = true)
 	public CursorResponse<TagResponse> getAll(LoginUser loginUser, Pageable pageable) {
 		User user = userRepository.findByUsername(loginUser.getUsername())
 			.orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));
-		Slice<UserTag> userTags = userTagRepository.findAllByUser(user, pageable);
+		int maxSize = 50;
+		boolean hasSort = pageable.getSort() != null && pageable.getSort().isSorted();
+		Pageable sanitized = PageRequest.of(
+			pageable.getPageNumber(),
+			Math.min(pageable.getPageSize(), maxSize),
+			hasSort ? pageable.getSort() : Sort.by(Sort.Direction.DESC, "tag.id")
+		);
+		Slice<UserTag> userTags = userTagRepository.findAllByUser(user, sanitized);
 
 		List<TagResponse> tags = userTags.stream()
 			.map(ut -> TagResponse.from(ut.getTag()))
 			.toList();
 
 		return CursorUtil.toCursorResponse(tags, userTags.hasNext(), TagResponse::id);
 	}

Add imports:

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

Verification (please run in the PR branch):

  • Confirm repository ordering matches the cursor key:
#!/bin/bash
# Inspect custom repo impl for ordering and pagination application
rg -nP 'class\s+UserTagCustomRepositoryImpl\b|orderBy\(|applyPagination\(|fetchJoin|join\(.*tag' -C3 --type=java
capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java (1)

69-91: Emit query-parameters snippet and mark lastCursor optional in docs

Expose pageable input (e.g., size) in docs and make lastCursor optional for empty pages.

Apply this diff:

 	@Test
 	void 유저_태그_조회() {
 		// given
 		BDDMockito.given(userTagService.getAll(any(), any())).willReturn(
 			new CursorResponse(false, 1L, List.of(new TagResponse(1L, "java"))));
 
 		// when & then
 		given()
 			.header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + ACCESS_TOKEN)
 			.contentType(ContentType.JSON)
+			.queryParam("size", 10)
 			.when().get("/v1/user-tags")
 			.then().status(HttpStatus.OK)
 			.apply(document("getUserTags", requestPreprocessor(), responsePreprocessor(),
-				requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("유효한 Access 토큰")),
+				requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("유효한 Access 토큰")),
+				queryParameters(
+					parameterWithName("size").optional().description("페이지 크기 (기본값/최대값은 서버 설정에 따름)")
+				),
 				responseFields(
 					fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과"),
 					fieldWithPath("data").type(JsonFieldType.OBJECT).description("커서 페이지 응답"),
 					fieldWithPath("data.hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부"),
-					fieldWithPath("data.lastCursor").type(JsonFieldType.NUMBER).description("마지막 커서 ID"),
+					fieldWithPath("data.lastCursor").type(JsonFieldType.NUMBER).optional().description("마지막 커서 ID"),
 					fieldWithPath("data.items").type(JsonFieldType.ARRAY).description("태그 목록"),
 					fieldWithPath("data.items[].id").type(JsonFieldType.NUMBER).description("태그 ID"),
 					fieldWithPath("data.items[].name").type(JsonFieldType.STRING).description("태그 이름"))));
 	}
capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java (1)

35-37: Cap page size and document defaults.

If you keep Pageable temporarily, set an explicit default and ensure a global max to avoid unbounded queries.

Minimal tweak if staying with Pageable:

-	public ApiResponse<CursorResponse<TagResponse>> getAll(@AuthenticationPrincipal LoginUser loginUser,
-		@PageableDefault Pageable pageable) {
+	public ApiResponse<CursorResponse<TagResponse>> getAll(@AuthenticationPrincipal LoginUser loginUser,
+		@PageableDefault(size = 20) Pageable pageable) {

Run to verify a max is enforced somewhere (properties or a customizer):

#!/bin/bash
rg -nP -C3 'spring\.data\.web\.pageable\.max-page-size|PageableHandlerMethodArgumentResolverCustomizer'
capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagRepositoryTest.java (1)

58-64: Strengthen assertions: verify hasNext and deterministic order.

Add checks for hasNext=false and descending order to align with cursor/keyset expectations.

Apply this diff:

 		// then
 		assertThat(result).isNotNull();
 		assertThat(result.getContent()).hasSize(2);
+		assertThat(result.hasNext()).isFalse();
+		var ids = result.getContent().stream().map(UserTag::getId).toList();
+		assertThat(ids).isSortedAccordingTo(java.util.Comparator.reverseOrder());
 		assertThat(result.getContent())
 			.extracting("tag.name")
-			.containsExactlyInAnyOrder("tag1", "tag2");
+			.containsExactlyInAnyOrder("tag1", "tag2");
capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepositoryImpl.java (2)

24-26: Join type: consider inner join if Tag is mandatory.

If UserTag.tag is non-nullable, replace leftJoin with join for a tighter plan.

-			.leftJoin(userTag.tag).fetchJoin()
+			.join(userTag.tag).fetchJoin()

23-33: Add supporting DB index for scan efficiency.

Add an index on (user_id, id desc) to match the filter and sort used by keyset.

Example DDL:

  • create index idx_user_tag_user_id_id_desc on user_tag(user_id, id desc);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 615d623 and b628d15.

📒 Files selected for processing (11)
  • capturecat-core/src/docs/asciidoc/error-codes.adoc (1 hunks)
  • capturecat-core/src/docs/asciidoc/user.adoc (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java (3 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepository.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepositoryImpl.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (3 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java (1 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java (3 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagRepositoryTest.java (1 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java (1)
capturecat-core/src/test/java/com/capturecat/core/api/tag/TagErrorCodeControllerTest.java (1)
  • TagErrorCodeControllerTest (13-20)
🔇 Additional comments (4)
capturecat-core/src/docs/asciidoc/error-codes.adoc (1)

97-99: Docs section wired correctly to generated snippets

Anchor and include path align with tests (errorCode/getUserTags). Looks good.

capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (1)

140-151: LGTM on failure path coverage

USER_NOT_FOUND path is exercised correctly.

capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java (1)

23-27: Docs generation for getUserTags error codes is correct

Includes USER_NOT_FOUND and targets the right snippet path.

capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java (1)

7-12: Extending the custom repository is appropriate

Interface composition looks correct; no issues.

Comment on lines +34 to +40
@GetMapping
public ApiResponse<CursorResponse<TagResponse>> getAll(@AuthenticationPrincipal LoginUser loginUser,
@PageableDefault Pageable pageable) {
CursorResponse<TagResponse> tagResponse = userTagService.getAll(loginUser, pageable);

return ApiResponse.success(tagResponse);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Cursor response but offset-based input — switch to true keyset pagination.

The endpoint returns CursorResponse but accepts Pageable (page/offset). With the current repository impl using offset/limit, large offsets will degrade and can cause inconsistent pages under concurrent writes. Expose cursor params (lastCursor, size) and have the service/repo use id-based keyset (id < lastCursor) instead of offset.

Apply this diff (requires coordinated service + repository interface changes):

-	@GetMapping
-	public ApiResponse<CursorResponse<TagResponse>> getAll(@AuthenticationPrincipal LoginUser loginUser,
-		@PageableDefault Pageable pageable) {
-		CursorResponse<TagResponse> tagResponse = userTagService.getAll(loginUser, pageable);
-		return ApiResponse.success(tagResponse);
-	}
+	@GetMapping
+	public ApiResponse<CursorResponse<TagResponse>> getAll(
+		@AuthenticationPrincipal LoginUser loginUser,
+		@RequestParam(name = "lastCursor", required = false) Long lastCursor,
+		@RequestParam(name = "size", defaultValue = "20") int size
+	) {
+		CursorResponse<TagResponse> tagResponse = userTagService.getAll(loginUser, lastCursor, size);
+		return ApiResponse.success(tagResponse);
+	}
📝 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
public ApiResponse<CursorResponse<TagResponse>> getAll(@AuthenticationPrincipal LoginUser loginUser,
@PageableDefault Pageable pageable) {
CursorResponse<TagResponse> tagResponse = userTagService.getAll(loginUser, pageable);
return ApiResponse.success(tagResponse);
}
@GetMapping
public ApiResponse<CursorResponse<TagResponse>> getAll(
@AuthenticationPrincipal LoginUser loginUser,
@RequestParam(name = "lastCursor", required = false) Long lastCursor,
@RequestParam(name = "size", defaultValue = "20") int size
) {
CursorResponse<TagResponse> tagResponse = userTagService.getAll(loginUser, lastCursor, size);
return ApiResponse.success(tagResponse);
}
🤖 Prompt for AI Agents
In
capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
around lines 34 to 40, the controller currently returns a CursorResponse but
accepts a Pageable (offset-based) which must be replaced with true keyset/cursor
pagination: change the method signature to accept cursor parameters (e.g., Long
lastCursor, int size) instead of Pageable, validate and default size,
convert/forward those params to the userTagService.getAll call, and update the
service interface to accept (lastCursor, size) so the repository can implement
id-based keyset queries (WHERE id < :lastCursor ORDER BY id DESC LIMIT :size or
similar) returning a CursorResponse<TagResponse>; ensure the controller maps
missing lastCursor to "start from newest" semantics and wraps the returned
CursorResponse in ApiResponse.success before returning.

Comment on lines +21 to +33
@Override
public Slice<UserTag> findAllByUser(User user, Pageable pageable) {
List<UserTag> userTags = queryFactory
.selectFrom(userTag)
.leftJoin(userTag.tag).fetchJoin()
.where(userTag.user.eq(user))
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.orderBy(userTag.id.desc())
.fetch();

return SliceUtil.toSlice(userTags, pageable);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Offset pagination in “cursor” flow — migrate to keyset (id < lastCursor).

Current query uses offset/limit; this breaks true cursor semantics and can suffer O(n) skips for large pages. Prefer keyset on id with size+1 fetch.

Apply this diff (and update the interface + service accordingly):

-	@Override
-	public Slice<UserTag> findAllByUser(User user, Pageable pageable) {
-		List<UserTag> userTags = queryFactory
-			.selectFrom(userTag)
-			.leftJoin(userTag.tag).fetchJoin()
-			.where(userTag.user.eq(user))
-			.offset(pageable.getOffset())
-			.limit(pageable.getPageSize() + 1)
-			.orderBy(userTag.id.desc())
-			.fetch();
-
-		return SliceUtil.toSlice(userTags, pageable);
-	}
+	@Override
+	public Slice<UserTag> findAllByUser(User user, Long lastCursor, int size) {
+		List<UserTag> userTags = queryFactory
+			.selectFrom(userTag)
+			.leftJoin(userTag.tag).fetchJoin()
+			.where(
+				userTag.user.eq(user),
+				lastCursor != null ? userTag.id.lt(lastCursor) : null
+			)
+			.orderBy(userTag.id.desc())
+			.limit(size + 1)
+			.fetch();
+
+		return SliceUtil.toSlice(userTags, org.springframework.data.domain.PageRequest.of(0, size));
+	}

Follow-ups:

  • Change UserTagCustomRepository signature to (User user, Long lastCursor, int size).
  • Adapt UserTagService and controller accordingly.
📝 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
@Override
public Slice<UserTag> findAllByUser(User user, Pageable pageable) {
List<UserTag> userTags = queryFactory
.selectFrom(userTag)
.leftJoin(userTag.tag).fetchJoin()
.where(userTag.user.eq(user))
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.orderBy(userTag.id.desc())
.fetch();
return SliceUtil.toSlice(userTags, pageable);
}
@Override
public Slice<UserTag> findAllByUser(User user, Long lastCursor, int size) {
List<UserTag> userTags = queryFactory
.selectFrom(userTag)
.leftJoin(userTag.tag).fetchJoin()
.where(
userTag.user.eq(user),
lastCursor != null ? userTag.id.lt(lastCursor) : null
)
.orderBy(userTag.id.desc())
.limit(size + 1)
.fetch();
return SliceUtil.toSlice(
userTags,
org.springframework.data.domain.PageRequest.of(0, size)
);
}
🤖 Prompt for AI Agents
In
capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagCustomRepositoryImpl.java
around lines 21 to 33, the current implementation uses offset/limit for
pagination which breaks cursor semantics; change the repository signature to
findAllByUser(User user, Long lastCursor, int size) (and update the
interface/service/controller to match), remove offset usage, add a keyset WHERE
clause userTag.id < lastCursor when lastCursor is non-null, set limit to size +
1, keep orderBy(userTag.id.desc()), fetch the results and return a Slice
constructed from the size+1 sentinel to determine hasNext. Ensure NPE-safe
handling of a null lastCursor (treat as head) and propagate the new parameters
through service and controller method signatures.

# Conflicts:
#	capturecat-core/src/docs/asciidoc/error-codes.adoc
#	capturecat-core/src/docs/asciidoc/user.adoc
#	capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
#	capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java
#	capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java
#	capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java
#	capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagRepositoryTest.java
#	capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java
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 (3)
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (2)

128-145: Happy-path retrieval test looks good; consider tightening expectations.

Good coverage of the mapping. Optionally, also assert pagination signals and repository interaction:

  • Verify repository call happens once with a Pageable.
  • Add an additional test for an empty Slice to ensure items() is empty and cursor fields behave as expected (if applicable).

Example additions within this test:

@@
   // then
   assertThat(response.items()).hasSize(1);
   assertThat(response.items().get(0).name()).isEqualTo(tag.getName());
+  verify(userTagRepository, times(1)).findAllByUser(eq(user), any());

147-158: Prefer asserting on errorType over message and verify no downstream calls.

Asserting on the exception message is brittle. CoreException exposes its ErrorType via a Lombok-generated getErrorType(), so assert on that instead of the message. Also verify that userTagRepository.findAllByUser is never invoked when the user is not found.

@@
- assertThatThrownBy(() -> userTagService.getAll(new LoginUser(user), PageRequest.of(0, 10)))
-     .isInstanceOf(CoreException.class)
-     .hasMessage(ErrorType.USER_NOT_FOUND.getCode().getMessage());
+ assertThatThrownBy(() -> userTagService.getAll(new LoginUser(user), PageRequest.of(0, 10)))
+     .isInstanceOf(CoreException.class)
+     .extracting(e -> ((CoreException) e).getErrorType())
+     .isEqualTo(ErrorType.USER_NOT_FOUND);
+
+ verify(userTagRepository, never()).findAllByUser(any(), any());
capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java (1)

9-9: LGTM: repository fragment extension is correct.

File: capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java
Lines: 9-9

public interface UserTagRepository extends JpaRepository<UserTag, Long>, UserTagCustomRepository {

Extending UserTagCustomRepository is the right hook for a QueryDSL-based slice query.

  • Verified that UserTagCustomRepository and UserTagCustomRepositoryImpl exist and that Slice<UserTag> findAllByUser(User, Pageable) is correctly declared and implemented.
  • Optional: fetch-join the tag association (or project to a DTO) in the QueryDSL query to prevent N+1 issues.
  • Optional: apply a stable, indexed sort (e.g., createdAt DESC or id DESC) for deterministic slicing/cursor behavior and align service/controller defaults accordingly.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b628d15 and bfdee69.

📒 Files selected for processing (8)
  • capturecat-core/src/docs/asciidoc/error-codes.adoc (1 hunks)
  • capturecat-core/src/docs/asciidoc/user.adoc (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java (3 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (3 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java (1 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java (3 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • capturecat-core/src/docs/asciidoc/error-codes.adoc
🚧 Files skipped from review as they are similar to previous changes (5)
  • capturecat-core/src/docs/asciidoc/user.adoc
  • capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java
  • capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java
🧰 Additional context used
🧬 Code graph analysis (1)
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (2)
capturecat-core/src/test/java/com/capturecat/core/DummyObject.java (1)
  • DummyObject (18-100)
capturecat-core/src/test/java/com/capturecat/core/domain/tag/TagFixture.java (1)
  • TagFixture (5-16)
⏰ 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). (2)
  • GitHub Check: build
  • GitHub Check: deploy-dev
🔇 Additional comments (1)
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (1)

15-15: LGTM: required imports only.

The added imports are minimal and directly support the new retrieval tests.

Also applies to: 23-24, 31-31

@leeedohyun leeedohyun force-pushed the feature/#125-get-user-tags branch from 7d46bff to bfdee69 Compare September 8, 2025 10:35
@leeedohyun leeedohyun merged commit 50709cf into develop Sep 8, 2025
9 checks passed
@leeedohyun leeedohyun deleted the feature/#125-get-user-tags branch September 8, 2025 10:38
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.

유저 태그 조회 API 유저 태그 수정 API

3 participants