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

    • You can now delete a user tag via the API using a DELETE request with the tag ID. Successful deletions return a standard success response.
  • Documentation

    • Added user-facing guides for deleting a user tag, including request/response examples.
    • Expanded error code references for user tag deletion (e.g., user not found, tag not found, user tag not found).

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

coderabbitai bot commented Sep 7, 2025

Walkthrough

Adds a DELETE /v1/user-tags endpoint to remove a user tag by id, implements corresponding service logic with validations and deletion, and updates API/docs to include success and error documentation. Introduces controller, service, and test coverage for success and error scenarios; no changes to other features.

Changes

Cohort / File(s) Change Summary
Docs: User Tag Delete
capturecat-core/src/docs/asciidoc/error-codes.adoc, capturecat-core/src/docs/asciidoc/user.adoc
Added “유저 태그 삭제” sections documenting the deleteUserTag operation and its error codes; inserted anchors and snippets; no runtime changes.
API Controller
capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
Added DELETE endpoint method delete(LoginUser, Long tagId) mapped to /v1/user-tags; calls service.delete and returns ApiResponse.success.
Service Logic
capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java
Added transactional delete(LoginUser, Long tagId): validates user, loads tag, finds UserTag, deletes; throws USER_NOT_FOUND, TAG_NOT_FOUND, USER_TAG_NOT_FOUND as applicable.
Tests: Controller
capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java
Added 유저_태그_삭제 test for DELETE /v1/user-tags with tagId, documents request/response snippets; stubs service and asserts 200.
Tests: Error Codes
capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java
Added 유저_태그_삭제_에러_코드_문서화 to generate error docs for USER_NOT_FOUND, TAG_NOT_FOUND, USER_TAG_NOT_FOUND under errorCode/deleteUserTag.
Tests: Service
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java
Added success and three failure tests for delete: user missing, tag missing, userTag missing; mocks repositories and verifies interactions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller as UserTagController
  participant Service as UserTagService
  participant UserRepo as UserRepository
  participant TagRepo as TagRepository
  participant UTRepo as UserTagRepository

  Client->>Controller: DELETE /v1/user-tags?tagId={id}\nAuthorization
  Controller->>Service: delete(loginUser, tagId)

  Service->>UserRepo: findByUsername(loginUser.username)
  alt User not found
    Service-->>Controller: throw USER_NOT_FOUND
    Controller-->>Client: 4xx with error
  else User found
    Service->>TagRepo: findById(tagId)
    alt Tag not found
      Service-->>Controller: throw TAG_NOT_FOUND
      Controller-->>Client: 4xx with error
    else Tag found
      Service->>UTRepo: findByUserAndTag(user, tag)
      alt UserTag not found
        Service-->>Controller: throw USER_TAG_NOT_FOUND
        Controller-->>Client: 4xx with error
      else UserTag found
        Service->>UTRepo: delete(userTag)
        Service-->>Controller: void
        Controller-->>Client: 200 ApiResponse.success()
      end
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • jaelyangChoi

Poem

Hop, hop—snip the tag, goodbye!
Clean trails under autumn sky.
Docs now whisper what to do,
Tests thump-proof the path anew.
A tidy burrow, neat and sweet—
One less carrot at my feet. 🥕🐇

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/#126-delete-user-tag

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 (7)
capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java (1)

66-83: Stub the correct service method in delete test.

You're stubbing create() in a delete test; use delete() to reflect the behavior under test.

Apply this diff:

@@
 	@Test
 	void 유저_태그_삭제() {
 		// given
-		BDDMockito.given(userTagService.create(any(), anyString())).willReturn(new TagResponse(1L, "java"));
+		BDDMockito.willDoNothing().given(userTagService).delete(any(), anyLong());
 
 		// when & then
 		given()
 			.header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + ACCESS_TOKEN)
 			.contentType(ContentType.JSON)
 			.queryParam("tagId", 1L)
 			.when().delete("/v1/user-tags")
 			.then().status(HttpStatus.OK)
 			.apply(document("deleteUserTag", requestPreprocessor(), responsePreprocessor(),
 				requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("유효한 Access 토큰")),
 				queryParameters(parameterWithName("tagId").description("태그 ID")),
 				responseFields(
 					fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과"))));
 	}

If you want to assert interaction as well, consider:
BDDMockito.then(userTagService).should().delete(any(), anyLong());

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

11-12: Consider direct delete to avoid extra SELECT and add a uniqueness guarantee.

  • Optional: add deleteByUserAndTag(...) and rely on affected rows to detect not-found, reducing one query.
  • Ensure a unique constraint/index on (user_id, tag_id) to prevent duplicates and guarantee deterministic deletes.

Proposed repository addition:

 public interface UserTagRepository extends JpaRepository<UserTag, Long> {
 
 	Optional<UserTag> findByUserAndTag(User user, Tag tag);
+	
+	int deleteByUserAndTag(User user, Tag tag);
 
 	boolean existsByUserAndTag(User user, Tag tag);
 
 	long countByUser(User user);
 }
capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java (1)

45-50: Return ApiResponse to signal no body on delete.

Minor clarity improvement; semantics unchanged.

Apply this diff:

-	@DeleteMapping
-	public ApiResponse<?> delete(@AuthenticationPrincipal LoginUser loginUser, @RequestParam Long tagId) {
+	@DeleteMapping
+	public ApiResponse<Void> delete(@AuthenticationPrincipal LoginUser loginUser, @RequestParam Long tagId) {
 		userTagService.delete(loginUser, tagId);
 
 		return ApiResponse.success();
 	}
capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (1)

51-61: Reduce queries and make delete atomic with a single deleteBy... repository method

Current flow performs 3 selects then delete. Consider a single-statement delete to cut round-trips and avoid loading the entity.

  • Assumes a unique constraint on (user_id, tag_id). Please confirm it exists.

Apply within this method:

-		UserTag userTag = userTagRepository.findByUserAndTag(user, tag)
-			.orElseThrow(() -> new CoreException(ErrorType.USER_TAG_NOT_FOUND));
-
-		userTagRepository.delete(userTag);
+		long deleted = userTagRepository.deleteByUserAndTag(user, tag);
+		if (deleted == 0) {
+			throw new CoreException(ErrorType.USER_TAG_NOT_FOUND);
+		}

Add to repository (outside this hunk) if not present:

long deleteByUserAndTag(User user, Tag tag);
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (3)

141-152: Optionally assert no downstream calls after USER_NOT_FOUND

Helps ensure early exit behavior.

Apply:

 	verify(userTagRepository, never()).delete(any());
+	verify(tagRepository, never()).findById(anyLong());
+	verify(userTagRepository, never()).findByUserAndTag(any(), any());

154-169: Optionally verify no lookup of UserTag when TAG_NOT_FOUND

Tighter interaction contract for negative path.

Apply:

 	verify(userTagRepository, never()).delete(any());
+	verify(userTagRepository, never()).findByUserAndTag(any(), any());

170-185: Strengthen assertion: check the specific error message for USER_TAG_NOT_FOUND

Aligns with other negative-path tests and guards error-type regressions.

Apply:

-		assertThatThrownBy(() -> userTagService.delete(new LoginUser(DummyObject.newUser("test")), 1L))
-			.isInstanceOf(CoreException.class);
+		assertThatThrownBy(() -> userTagService.delete(new LoginUser(DummyObject.newUser("test")), 1L))
+			.isInstanceOf(CoreException.class)
+			.hasMessage(ErrorType.USER_TAG_NOT_FOUND.getCode().getMessage());
📜 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 6501d51.

📒 Files selected for processing (10)
  • 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 (2 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/main/java/com/capturecat/core/support/error/ErrorCode.java (1 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java (1 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java (2 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java (1 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (4 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 (11)
capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java (1)

43-44: LGTM: New user-tag error codes registered correctly.

Delimiter fix and additions look consistent with existing enums.

capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java (1)

48-49: LGTM: Mapped user-tag errors to appropriate HTTP statuses.

BAD_REQUEST for count overflow and NOT_FOUND for missing user-tag are appropriate.

capturecat-core/src/docs/asciidoc/error-codes.adoc (1)

97-99: Docs entry for deleteUserTag added in the right section.

Anchor, heading, and snippet path align with tests.

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

33-37: LGTM: Error-code docs for delete path are covered.

The descriptors list matches expected failures (USER_NOT_FOUND, TAG_NOT_FOUND, USER_TAG_NOT_FOUND).

capturecat-core/src/docs/asciidoc/user.adoc (1)

52-60: LGTM: Added delete section mirrors create section and links error codes correctly.

Consistent snippet usage and cross-links.

capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (2)

12-12: LGTM: TagRepository import is appropriate for delete flow

The dependency aligns with the new deletion path.


32-32: Constructor-injected TagRepository looks good

Final field + @requiredargsconstructor wires it correctly.

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

6-6: LGTM: anyLong import added for new stubs

Matches usage with tagRepository.findById(anyLong()).


25-25: LGTM: TagRepository import for delete tests

Consistent with service dependency.


42-44: LGTM: Added @mock TagRepository

Mock wiring is correct.


123-139: LGTM: Successful delete path is well asserted

Verifies delete was invoked with the expected entity.

@leeedohyun leeedohyun force-pushed the feature/#126-delete-user-tag branch from 6501d51 to 1986468 Compare September 8, 2025 12:19
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 (5)
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (5)

241-250: Tighten interaction verification in success case

Add explicit verifications that the correct tag id is looked up and the user-tag association is queried once.

Apply:

 		// then
 		verify(userTagRepository, times(1)).delete(eq(userTag));
+		verify(tagRepository, times(1)).findById(eq(1L));
+		verify(userTagRepository, times(1)).findByUserAndTag(eq(user), eq(tag));

253-263: Assert short-circuiting when user is missing

Also verify no downstream repository calls happen.

Apply:

 		// when & then
 		assertThatThrownBy(() -> userTagService.delete(new LoginUser(DummyObject.newUser("test")), 1L))
 			.isInstanceOf(CoreException.class)
 			.hasMessage(ErrorType.USER_NOT_FOUND.getCode().getMessage());

 		verify(userTagRepository, never()).delete(any());
+		verify(tagRepository, never()).findById(anyLong());
+		verify(userTagRepository, never()).findByUserAndTag(any(), any());

265-279: Verify tag lookup and absence of further queries when tag is missing

Confirm we queried the right id and didn’t proceed to userTag lookup.

Apply:

 		// when & then
 		assertThatThrownBy(() -> userTagService.delete(new LoginUser(DummyObject.newUser("test")), 1L))
 			.isInstanceOf(CoreException.class)
 			.hasMessage(ErrorType.TAG_NOT_FOUND.getCode().getMessage());

 		verify(userTagRepository, never()).delete(any());
+		verify(tagRepository, times(1)).findById(eq(1L));
+		verify(userTagRepository, never()).findByUserAndTag(any(), any());

281-296: Assert the specific error code for missing UserTag

Strengthen the assertion to ensure USER_TAG_NOT_FOUND is emitted; optionally verify the lookup occurred exactly once.

Apply:

 		// when & then
 		assertThatThrownBy(() -> userTagService.delete(new LoginUser(DummyObject.newUser("test")), 1L))
-			.isInstanceOf(CoreException.class);
+			.isInstanceOf(CoreException.class)
+			.hasMessage(ErrorType.USER_TAG_NOT_FOUND.getCode().getMessage());
 
 		verify(userTagRepository, never()).delete(any());
+		verify(userTagRepository, times(1)).findByUserAndTag(eq(user), eq(tag));

258-258: Minor: reuse the local ‘user’ for LoginUser construction

Use new LoginUser(user) for consistency/readability instead of instantiating a second dummy user inline.

Also applies to: 274-274, 292-292

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6501d51 and 1986468.

📒 Files selected for processing (7)
  • 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 (2 hunks)
  • capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java (1 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 (1 hunks)
  • capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java
  • capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java
  • capturecat-core/src/docs/asciidoc/error-codes.adoc
  • capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java
  • capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java
  • capturecat-core/src/docs/asciidoc/user.adoc
🧰 Additional context used
🧬 Code graph analysis (1)
capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java (3)
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)
capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagFixture.java (1)
  • UserTagFixture (7-14)
⏰ 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)

233-297: Good coverage for delete path

Happy-path and three failure cases are covered and read cleanly.

@leeedohyun leeedohyun merged commit 7d004e4 into develop Sep 9, 2025
7 checks passed
@leeedohyun leeedohyun deleted the feature/#126-delete-user-tag branch September 9, 2025 11:28
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