diff --git a/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java b/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java index a48a5a3..302fff4 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java +++ b/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java @@ -17,6 +17,7 @@ import com.capturecat.core.api.user.dto.TagRenameRequest; import com.capturecat.core.service.auth.LoginUser; import com.capturecat.core.service.image.TagResponse; +import com.capturecat.core.service.tag.ImageTagService; import com.capturecat.core.service.user.UserTagService; import com.capturecat.core.support.response.ApiResponse; import com.capturecat.core.support.response.CursorResponse; @@ -27,6 +28,7 @@ public class UserTagController { private final UserTagService userTagService; + private final ImageTagService imageTagService; @PostMapping public ApiResponse create(@AuthenticationPrincipal LoginUser loginUser, @RequestParam String tagName) { @@ -44,9 +46,12 @@ public ApiResponse> getAll(@AuthenticationPrincipal } @PatchMapping - public ApiResponse update(@AuthenticationPrincipal LoginUser loginUser, - @RequestBody TagRenameRequest request) { + public ApiResponse update( + @AuthenticationPrincipal LoginUser loginUser, + @RequestBody TagRenameRequest request + ) { TagResponse response = userTagService.update(loginUser, request.currentTagId(), request.newTagName()); + imageTagService.update(loginUser, request.currentTagId(), response.id()); return ApiResponse.success(response); } @@ -54,6 +59,7 @@ public ApiResponse update(@AuthenticationPrincipal LoginUser loginU @DeleteMapping public ApiResponse delete(@AuthenticationPrincipal LoginUser loginUser, @RequestParam Long tagId) { userTagService.delete(loginUser, tagId); + imageTagService.delete(loginUser, tagId); return ApiResponse.success(); } diff --git a/capturecat-core/src/main/java/com/capturecat/core/domain/tag/ImageTagRepository.java b/capturecat-core/src/main/java/com/capturecat/core/domain/tag/ImageTagRepository.java index f1d8345..0e97049 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/domain/tag/ImageTagRepository.java +++ b/capturecat-core/src/main/java/com/capturecat/core/domain/tag/ImageTagRepository.java @@ -32,6 +32,10 @@ public interface ImageTagRepository extends JpaRepository { @Query("DELETE FROM ImageTag it WHERE it.tag = :tag AND it.image.user = :user") void deleteByTagAndUser(Tag tag, User user); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE ImageTag it SET it.tag.id = :newTagId WHERE it.tag.id = :oldTagId AND it.image.user.id = :userId") + void updateImageTagsForUser(Long userId, Long oldTagId, Long newTagId); + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM ImageTag it WHERE it.image.id IN (SELECT i.id FROM Image i WHERE i.user.id = :userId)") void deleteAllTagsByUserId(Long userId); diff --git a/capturecat-core/src/main/java/com/capturecat/core/service/tag/ImageTagService.java b/capturecat-core/src/main/java/com/capturecat/core/service/tag/ImageTagService.java new file mode 100644 index 0000000..e28b2f3 --- /dev/null +++ b/capturecat-core/src/main/java/com/capturecat/core/service/tag/ImageTagService.java @@ -0,0 +1,42 @@ +package com.capturecat.core.service.tag; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +import com.capturecat.core.domain.tag.ImageTagRepository; +import com.capturecat.core.domain.tag.Tag; +import com.capturecat.core.domain.tag.TagRepository; +import com.capturecat.core.domain.user.User; +import com.capturecat.core.domain.user.UserRepository; +import com.capturecat.core.service.auth.LoginUser; +import com.capturecat.core.support.error.CoreException; +import com.capturecat.core.support.error.ErrorType; + +@Service +@RequiredArgsConstructor +public class ImageTagService { + + private final ImageTagRepository imageTagRepository; + private final UserRepository userRepository; + private final TagRepository tagRepository; + + @Transactional + public void update(LoginUser loginUser, Long oldTagId, Long newTagId) { + User user = userRepository.findByUsername(loginUser.getUsername()) + .orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND)); + + imageTagRepository.updateImageTagsForUser(user.getId(), oldTagId, newTagId); + } + + @Transactional + public void delete(LoginUser loginUser, Long tagId) { + User user = userRepository.findByUsername(loginUser.getUsername()) + .orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND)); + Tag tag = tagRepository.findById(tagId) + .orElseThrow(() -> new CoreException(ErrorType.TAG_NOT_FOUND)); + + imageTagRepository.deleteByTagAndUser(tag, user); + } +} diff --git a/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java b/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java index b657f4c..87157b3 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java +++ b/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java @@ -30,7 +30,7 @@ @RequiredArgsConstructor public class UserTagService { - private static final int MAX_USER_TAG_COUNT = 30; + private static final int MAX_USER_TAG_COUNT = 40; private final UserRepository userRepository; private final UserTagRepository userTagRepository; diff --git a/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java index 7563e00..b0abfaf 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java @@ -29,6 +29,7 @@ import com.capturecat.core.config.jwt.JwtUtil; import com.capturecat.core.service.image.TagResponse; +import com.capturecat.core.service.tag.ImageTagService; import com.capturecat.core.service.user.UserTagService; import com.capturecat.core.support.response.CursorResponse; import com.capturecat.test.api.RestDocsTest; @@ -39,11 +40,13 @@ class UserTagControllerTest extends RestDocsTest { private UserTagController userTagController; private UserTagService userTagService; + private ImageTagService imageTagService; @BeforeEach void setUp() { userTagService = mock(UserTagService.class); - userTagController = new UserTagController(userTagService); + imageTagService = mock(ImageTagService.class); + userTagController = new UserTagController(userTagService, imageTagService); mockMvc = mockController(userTagController); } @@ -101,6 +104,7 @@ void setUp() { void 유저_태그_수정() { // given BDDMockito.given(userTagService.update(any(), anyLong(), anyString())).willReturn(new TagResponse(1L, "java")); + BDDMockito.willDoNothing().given(imageTagService).update(any(), anyLong(), anyLong()); // when & then given() @@ -126,6 +130,7 @@ void setUp() { void 유저_태그_삭제() { // given BDDMockito.given(userTagService.create(any(), anyString())).willReturn(new TagResponse(1L, "java")); + BDDMockito.willDoNothing().given(imageTagService).delete(any(), anyLong()); // when & then given() diff --git a/capturecat-core/src/test/java/com/capturecat/core/domain/tag/ImageTagRepositoryTest.java b/capturecat-core/src/test/java/com/capturecat/core/domain/tag/ImageTagRepositoryTest.java index 0dcb869..addff3d 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/domain/tag/ImageTagRepositoryTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/domain/tag/ImageTagRepositoryTest.java @@ -114,4 +114,63 @@ class ImageTagRepositoryTest { assertThat(user2Result).hasSize(1); assertThat(user2Result.get(0).getImage().getUser()).isEqualTo(user2); } + + @Test + void 지정한_사용자만_태그가_교체된다() { + // given + var user1 = userRepository.save(DummyObject.newUser("user1")); + var user2 = userRepository.save(DummyObject.newUser("user2")); + + var image1 = imageRepository.save(DummyObject.newMockUserImage(user1)); + var image2 = imageRepository.save(DummyObject.newMockUserImage(user2)); + + var oldTag = tagRepository.save(TagFixture.createTag("old")); + var newTag = tagRepository.save(TagFixture.createTag("new")); + + imageTagRepository.save(new ImageTag(image1, oldTag)); + imageTagRepository.save(new ImageTag(image2, oldTag)); + + entityManager.flush(); + entityManager.clear(); + + // when + imageTagRepository.updateImageTagsForUser(user1.getId(), oldTag.getId(), newTag.getId()); + + entityManager.flush(); + entityManager.clear(); + + // then + var it1 = imageTagRepository.findByImage(image1).get(0); + var it2 = imageTagRepository.findByImage(image2).get(0); + + assertThat(it1.getTag().getId()).isEqualTo(newTag.getId()); + assertThat(it2.getTag().getId()).isEqualTo(oldTag.getId()); + } + + @Test + void 태그가_없는_이미지에는_영향이_없다() { + // given + var user = userRepository.save(DummyObject.newUser("noTagUser")); + var imageWithNoTag = imageRepository.save(DummyObject.newMockUserImage(user)); + + var oldTag = tagRepository.save(TagFixture.createTag("old")); + var newTag = tagRepository.save(TagFixture.createTag("new")); + + var imageWithTag = imageRepository.save(DummyObject.newMockUserImage(user)); + imageTagRepository.save(new ImageTag(imageWithTag, oldTag)); + + entityManager.flush(); + entityManager.clear(); + + // when + imageTagRepository.updateImageTagsForUser(user.getId(), oldTag.getId(), newTag.getId()); + + entityManager.flush(); + entityManager.clear(); + + // then + assertThat(imageTagRepository.findByImage(imageWithNoTag)).isEmpty(); + var updated = imageTagRepository.findByImage(imageWithTag).get(0); + assertThat(updated.getTag().getId()).isEqualTo(newTag.getId()); + } } diff --git a/capturecat-core/src/test/java/com/capturecat/core/service/tag/ImageTagServiceTest.java b/capturecat-core/src/test/java/com/capturecat/core/service/tag/ImageTagServiceTest.java new file mode 100644 index 0000000..e670aa2 --- /dev/null +++ b/capturecat-core/src/test/java/com/capturecat/core/service/tag/ImageTagServiceTest.java @@ -0,0 +1,116 @@ +package com.capturecat.core.service.tag; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.capturecat.core.DummyObject; +import com.capturecat.core.domain.tag.ImageTagRepository; +import com.capturecat.core.domain.tag.Tag; +import com.capturecat.core.domain.tag.TagFixture; +import com.capturecat.core.domain.tag.TagRepository; +import com.capturecat.core.domain.user.User; +import com.capturecat.core.domain.user.UserRepository; +import com.capturecat.core.service.auth.LoginUser; +import com.capturecat.core.support.error.CoreException; + +@ExtendWith(MockitoExtension.class) +class ImageTagServiceTest { + + @Mock + ImageTagRepository imageTagRepository; + + @Mock + UserRepository userRepository; + + @Mock + TagRepository tagRepository; + + @InjectMocks + ImageTagService imageTagService; + + @Test + void 업데이트_사용자_존재하면_레포지토리_호출한다() { + // given + User user = DummyObject.newMockUser(123L); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); + + // when + imageTagService.update(new LoginUser(user), 10L, 20L); + + // then + verify(imageTagRepository, times(1)) + .updateImageTagsForUser(user.getId(), 10L, 20L); + } + + @Test + void 업데이트_사용자_없으면_CoreException_던진다() { + // given + User user = DummyObject.newUser("noUser"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> imageTagService.update(new LoginUser(user), 1L, 2L)) + .isInstanceOf(CoreException.class); + verifyNoInteractions(imageTagRepository); + } + + @Test + void 삭제_사용자_태그_존재하면_레포지토리_호출한다() { + // given + User user = DummyObject.newUser("test"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); + + Tag tag = TagFixture.createTag(10L, "testTag"); + given(tagRepository.findById(anyLong())).willReturn(Optional.of(tag)); + + // when + imageTagService.delete(new LoginUser(user), tag.getId()); + + // then + verify(imageTagRepository, times(1)).deleteByTagAndUser(tag, user); + } + + @Test + void 삭제_사용자_없으면_CoreException_던진다() { + // given + User user = DummyObject.newUser("noUser"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> imageTagService.delete(new LoginUser(user), 1L)) + .isInstanceOf(CoreException.class); + verifyNoInteractions(imageTagRepository, tagRepository); + } + + @Test + void 삭제_태그_없으면_CoreException_던진다() { + // given + User user = DummyObject.newUser("test"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); + given(tagRepository.findById(anyLong())).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> imageTagService.delete(new LoginUser(user), 2L)) + .isInstanceOf(CoreException.class); + verifyNoInteractions(imageTagRepository); + } +} + diff --git a/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java b/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java index 07dacdc..b443480 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java @@ -115,7 +115,7 @@ class UserTagServiceTest { given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); given(tagRegister.registerTagsFor(anyString())).willReturn(tag); given(userTagRepository.existsByUserAndTag(eq(user), eq(tag))).willReturn(false); - given(userTagRepository.countByUser(eq(user))).willReturn(30L); + given(userTagRepository.countByUser(eq(user))).willReturn(40L); // when & then assertThatThrownBy(() -> userTagService.create(new LoginUser(DummyObject.newUser("test")), "java"))