Skip to content

Commit c40b243

Browse files
committed
feat: 유저 태그 수정 API
1 parent 615d623 commit c40b243

File tree

10 files changed

+215
-2
lines changed

10 files changed

+215
-2
lines changed

capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package com.capturecat.core.api.user;
22

33
import org.springframework.security.core.annotation.AuthenticationPrincipal;
4+
import org.springframework.web.bind.annotation.PatchMapping;
45
import org.springframework.web.bind.annotation.PostMapping;
6+
import org.springframework.web.bind.annotation.RequestBody;
57
import org.springframework.web.bind.annotation.RequestMapping;
68
import org.springframework.web.bind.annotation.RequestParam;
79
import org.springframework.web.bind.annotation.RestController;
810

911
import lombok.RequiredArgsConstructor;
1012

13+
import com.capturecat.core.api.user.dto.TagRenameRequest;
1114
import com.capturecat.core.service.auth.LoginUser;
1215
import com.capturecat.core.service.image.TagResponse;
1316
import com.capturecat.core.service.user.UserTagService;
@@ -26,4 +29,12 @@ public ApiResponse<TagResponse> create(@AuthenticationPrincipal LoginUser loginU
2629

2730
return ApiResponse.success(tagResponse);
2831
}
32+
33+
@PatchMapping
34+
public ApiResponse<TagResponse> update(@AuthenticationPrincipal LoginUser loginUser,
35+
@RequestBody TagRenameRequest request) {
36+
TagResponse response = userTagService.update(loginUser, request.currentTagId(), request.newTagName());
37+
38+
return ApiResponse.success(response);
39+
}
2940
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.capturecat.core.api.user.dto;
2+
3+
public record TagRenameRequest(Long currentTagId, String newTagName) {
4+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package com.capturecat.core.domain.user;
22

3+
import java.util.Optional;
4+
35
import org.springframework.data.jpa.repository.JpaRepository;
46

57
import com.capturecat.core.domain.tag.Tag;
68

79
public interface UserTagRepository extends JpaRepository<UserTag, Long> {
810

11+
Optional<UserTag> findByUserAndTag(User user, Tag tag);
12+
913
boolean existsByUserAndTag(User user, Tag tag);
1014

1115
long countByUser(User user);

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import com.capturecat.core.domain.tag.Tag;
1111
import com.capturecat.core.domain.tag.TagRegister;
12+
import com.capturecat.core.domain.tag.TagRepository;
1213
import com.capturecat.core.domain.user.User;
1314
import com.capturecat.core.domain.user.UserRepository;
1415
import com.capturecat.core.domain.user.UserTag;
@@ -27,6 +28,7 @@ public class UserTagService {
2728

2829
private final UserRepository userRepository;
2930
private final UserTagRepository userTagRepository;
31+
private final TagRepository tagRepository;
3032
private final TagRegister tagRegister;
3133

3234
@Transactional
@@ -46,6 +48,24 @@ public TagResponse create(LoginUser loginUser, String tagName) {
4648
}
4749
}
4850

51+
@Transactional
52+
public TagResponse update(LoginUser loginUser, Long currentTagId, String newTagName) {
53+
User user = userRepository.findByUsername(loginUser.getUsername())
54+
.orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));
55+
Tag currentTag = tagRepository.findById(currentTagId)
56+
.orElseThrow(() -> new CoreException(ErrorType.TAG_NOT_FOUND));
57+
UserTag userTag = userTagRepository.findByUserAndTag(user, currentTag)
58+
.orElseThrow(() -> new CoreException(ErrorType.USER_TAG_NOT_FOUND));
59+
Tag newTag = tagRegister.registerTagsFor(newTagName);
60+
61+
validateDuplicateUserTag(user, newTag);
62+
63+
userTagRepository.delete(userTag);
64+
userTagRepository.save(UserTag.create(user, newTag));
65+
66+
return TagResponse.from(newTag);
67+
}
68+
4969
private void validate(User user, Tag tag) {
5070
validateDuplicateUserTag(user, tag);
5171
validateUserTagCountLimit(user);

capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public enum ErrorCode {
4040
INTERNAL_SERVER_ERROR("서버에서 오류가 발생했습니다."),
4141
INVALID_LOGOUT_AUTH_TOKEN("ACCESS 토큰 또는 REFRESH 토큰이 유효하지 않습니다."),
4242
ALREADY_EXISTS_USER_TAG("이미 등록된 유저 태그입니다."),
43-
EXCEED_MAX_USER_TAG_COUNT("태그는 한 계정당 최대 30개까지 추가할 수 있어요.");
43+
EXCEED_MAX_USER_TAG_COUNT("태그는 한 계정당 최대 30개까지 추가할 수 있어요."),
44+
NOT_FOUND_USER_TAG("유저 태그를 찾을 수 없습니다.");
4445

4546
private final String message;
4647

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public enum ErrorType {
4545
MISSING_PARAMETER(HttpStatus.BAD_REQUEST, ErrorCode.MISSING_PARAMETER, LogLevel.WARN),
4646
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR, LogLevel.ERROR),
4747
USER_TAG_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, ErrorCode.ALREADY_EXISTS_USER_TAG, LogLevel.WARN),
48-
TOO_MANY_USER_TAGS(HttpStatus.BAD_REQUEST, ErrorCode.EXCEED_MAX_USER_TAG_COUNT, LogLevel.WARN);
48+
TOO_MANY_USER_TAGS(HttpStatus.BAD_REQUEST, ErrorCode.EXCEED_MAX_USER_TAG_COUNT, LogLevel.WARN),
49+
USER_TAG_NOT_FOUND(HttpStatus.NOT_FOUND, ErrorCode.NOT_FOUND_USER_TAG, LogLevel.WARN);
4950

5051
private final HttpStatus status;
5152

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.capturecat.core.api.user;
22

3+
import static com.capturecat.core.support.error.ErrorType.TAG_NOT_FOUND;
34
import static com.capturecat.core.support.error.ErrorType.TOO_MANY_USER_TAGS;
45
import static com.capturecat.core.support.error.ErrorType.USER_NOT_FOUND;
56
import static com.capturecat.core.support.error.ErrorType.USER_TAG_ALREADY_EXISTS;
7+
import static com.capturecat.core.support.error.ErrorType.USER_TAG_NOT_FOUND;
68

79
import java.util.List;
810

@@ -19,4 +21,11 @@ class UserErrorCodeControllerTest extends ErrorCodeDocumentTest {
1921
TOO_MANY_USER_TAGS, USER_NOT_FOUND);
2022
generateErrorDocs("errorCode/createUserTag", errorCodeDescriptors);
2123
}
24+
25+
@Test
26+
void 유저_태그_수정_에러_코드_문서화() {
27+
List<ErrorCodeDescriptor> errorCodeDescriptors = generateErrorCodeDescriptors(USER_TAG_ALREADY_EXISTS,
28+
USER_NOT_FOUND, TAG_NOT_FOUND, USER_TAG_NOT_FOUND);
29+
generateErrorDocs("errorCode/updateUserTag", errorCodeDescriptors);
30+
}
2231
}

capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@
33
import static com.capturecat.test.api.RestDocsUtil.requestPreprocessor;
44
import static com.capturecat.test.api.RestDocsUtil.responsePreprocessor;
55
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyLong;
67
import static org.mockito.ArgumentMatchers.anyString;
78
import static org.mockito.Mockito.mock;
89
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
910
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
1011
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
1112
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
13+
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
1214
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
1315
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
1416
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
1517

18+
import java.util.Map;
19+
1620
import org.junit.jupiter.api.BeforeEach;
1721
import org.junit.jupiter.api.Test;
1822
import org.mockito.BDDMockito;
@@ -62,4 +66,29 @@ void setUp() {
6266
fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("태그 ID"),
6367
fieldWithPath("data.name").type(JsonFieldType.STRING).description("태그 이름"))));
6468
}
69+
70+
@Test
71+
void 유저_태그_수정() {
72+
// given
73+
BDDMockito.given(userTagService.update(any(), anyLong(), anyString())).willReturn(new TagResponse(1L, "java"));
74+
75+
// when & then
76+
given()
77+
.header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + ACCESS_TOKEN)
78+
.contentType(ContentType.JSON)
79+
.body(Map.of("currentTagId", 1L, "newTagName", "spring"))
80+
.when().patch("/v1/user-tags")
81+
.then().status(HttpStatus.OK)
82+
.apply(document("updateUserTag", requestPreprocessor(), responsePreprocessor(),
83+
requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("유효한 Access 토큰")),
84+
requestFields(
85+
fieldWithPath("currentTagId").type(JsonFieldType.NUMBER).description("수정할 태그 ID"),
86+
fieldWithPath("newTagName").type(JsonFieldType.STRING).description("새로운 태그 이름")
87+
),
88+
responseFields(
89+
fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과"),
90+
fieldWithPath("data").type(JsonFieldType.OBJECT).description("수정된 태그 정보"),
91+
fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("태그 ID"),
92+
fieldWithPath("data.name").type(JsonFieldType.STRING).description("태그 이름"))));
93+
}
6594
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.capturecat.core.domain.user;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import jakarta.persistence.EntityManager;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.springframework.beans.factory.annotation.Autowired;
9+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
10+
import org.springframework.context.annotation.Import;
11+
12+
import com.capturecat.core.DummyObject;
13+
import com.capturecat.core.config.JpaAuditingConfig;
14+
import com.capturecat.core.config.QueryDslConfig;
15+
import com.capturecat.core.domain.tag.Tag;
16+
import com.capturecat.core.domain.tag.TagFixture;
17+
import com.capturecat.core.domain.tag.TagRepository;
18+
19+
@DataJpaTest
20+
@Import({QueryDslConfig.class, JpaAuditingConfig.class})
21+
class UserTagRepositoryTest {
22+
23+
@Autowired
24+
EntityManager entityManager;
25+
26+
@Autowired
27+
UserRepository userRepository;
28+
29+
@Autowired
30+
TagRepository tagRepository;
31+
32+
@Autowired
33+
UserTagRepository userTagRepository;
34+
35+
@Test
36+
void findByUserAndTag() {
37+
// given
38+
User user = userRepository.save(DummyObject.newUser("test"));
39+
Tag tag = tagRepository.save(TagFixture.createTag("A"));
40+
UserTag userTag = userTagRepository.save(UserTag.create(user, tag));
41+
42+
entityManager.flush();
43+
entityManager.clear();
44+
45+
// when
46+
UserTag result = userTagRepository.findByUserAndTag(user, tag).orElseThrow();
47+
48+
// then
49+
assertThat(result.getId()).isEqualTo(userTag.getId());
50+
assertThat(result.getUser().getId()).isEqualTo(user.getId());
51+
assertThat(result.getTag().getId()).isEqualTo(tag.getId());
52+
}
53+
}

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.ArgumentMatchers.anyLong;
67
import static org.mockito.ArgumentMatchers.anyString;
78
import static org.mockito.ArgumentMatchers.eq;
89
import static org.mockito.BDDMockito.given;
10+
import static org.mockito.BDDMockito.willDoNothing;
911
import static org.mockito.Mockito.never;
1012
import static org.mockito.Mockito.times;
1113
import static org.mockito.Mockito.verify;
@@ -21,6 +23,7 @@
2123
import com.capturecat.core.DummyObject;
2224
import com.capturecat.core.domain.tag.TagFixture;
2325
import com.capturecat.core.domain.tag.TagRegister;
26+
import com.capturecat.core.domain.tag.TagRepository;
2427
import com.capturecat.core.domain.user.UserRepository;
2528
import com.capturecat.core.domain.user.UserTagFixture;
2629
import com.capturecat.core.domain.user.UserTagRepository;
@@ -37,6 +40,9 @@ class UserTagServiceTest {
3740
@Mock
3841
UserTagRepository userTagRepository;
3942

43+
@Mock
44+
TagRepository tagRepository;
45+
4046
@Mock
4147
TagRegister tagRegister;
4248

@@ -114,4 +120,79 @@ class UserTagServiceTest {
114120

115121
verify(userTagRepository, never()).save(any());
116122
}
123+
124+
@Test
125+
void 유저_태그를_수정한다() {
126+
// given
127+
var user = DummyObject.newUser("test");
128+
var currentTag = TagFixture.createTag(1L, "java");
129+
var newTag = TagFixture.createTag(2L, "spring");
130+
var currentUserTag = UserTagFixture.createUserTag(1L, user, currentTag);
131+
var newUserTag = UserTagFixture.createUserTag(2L, user, newTag);
132+
133+
given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user));
134+
given(tagRepository.findById(anyLong())).willReturn(Optional.of(currentTag));
135+
given(userTagRepository.findByUserAndTag(eq(user), eq(currentTag))).willReturn(Optional.of(currentUserTag));
136+
given(tagRegister.registerTagsFor(anyString())).willReturn(newTag);
137+
given(userTagRepository.existsByUserAndTag(eq(user), eq(newTag))).willReturn(false);
138+
willDoNothing().given(userTagRepository).delete(eq(currentUserTag));
139+
given(userTagRepository.save(any())).willReturn(newUserTag);
140+
141+
// when
142+
var response = userTagService.update(new LoginUser(user), 1L, "spring");
143+
144+
// then
145+
assertThat(response.id()).isEqualTo(newUserTag.getId());
146+
assertThat(response.name()).isEqualTo(newTag.getName());
147+
148+
verify(userTagRepository, times(1)).delete(eq(currentUserTag));
149+
verify(userTagRepository, times(1)).save(any());
150+
}
151+
152+
@Test
153+
void 유저_태그_수정_시_회원이_존재하지_않으면_실패한다() {
154+
// given
155+
given(userRepository.findByUsername(anyString())).willReturn(Optional.empty());
156+
157+
// when & then
158+
assertThatThrownBy(() -> userTagService.update(new LoginUser(DummyObject.newUser("test")), 1L, "spring"))
159+
.isInstanceOf(CoreException.class)
160+
.hasMessage(ErrorType.USER_NOT_FOUND.getCode().getMessage());
161+
162+
verify(userTagRepository, never()).delete(any());
163+
verify(userTagRepository, never()).save(any());
164+
}
165+
166+
@Test
167+
void 유저_태그_수정_시_기존_태그가_존재하지_않으면_실패한다() {
168+
// given
169+
var user = DummyObject.newUser("test");
170+
171+
given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user));
172+
given(tagRepository.findById(anyLong())).willReturn(Optional.empty());
173+
174+
// when & then
175+
assertThatThrownBy(() -> userTagService.update(new LoginUser(DummyObject.newUser("test")), 1L, "spring"))
176+
.isInstanceOf(CoreException.class)
177+
.hasMessage(ErrorType.TAG_NOT_FOUND.getCode().getMessage());
178+
}
179+
180+
@Test
181+
void 유저_태그_수정_시_기존_유저_태그가_존재하지_않으면_실패한다() {
182+
// given
183+
var user = DummyObject.newUser("test");
184+
var currentTag = TagFixture.createTag(1L, "java");
185+
186+
given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user));
187+
given(tagRepository.findById(anyLong())).willReturn(Optional.of(currentTag));
188+
given(userTagRepository.findByUserAndTag(eq(user), eq(currentTag))).willReturn(Optional.empty());
189+
190+
// when & then
191+
assertThatThrownBy(() -> userTagService.update(new LoginUser(DummyObject.newUser("test")), 1L, "spring"))
192+
.isInstanceOf(CoreException.class)
193+
.hasMessage(ErrorType.USER_TAG_NOT_FOUND.getCode().getMessage());
194+
195+
verify(userTagRepository, never()).delete(any());
196+
verify(userTagRepository, never()).save(any());
197+
}
117198
}

0 commit comments

Comments
 (0)