Skip to content

Commit bb59a83

Browse files
committed
feat: 유저 태그 조회 API
1 parent 615d623 commit bb59a83

File tree

9 files changed

+208
-1
lines changed

9 files changed

+208
-1
lines changed

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

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

3+
import org.springframework.data.domain.Pageable;
4+
import org.springframework.data.web.PageableDefault;
35
import org.springframework.security.core.annotation.AuthenticationPrincipal;
6+
import org.springframework.web.bind.annotation.GetMapping;
47
import org.springframework.web.bind.annotation.PostMapping;
58
import org.springframework.web.bind.annotation.RequestMapping;
69
import org.springframework.web.bind.annotation.RequestParam;
@@ -12,6 +15,7 @@
1215
import com.capturecat.core.service.image.TagResponse;
1316
import com.capturecat.core.service.user.UserTagService;
1417
import com.capturecat.core.support.response.ApiResponse;
18+
import com.capturecat.core.support.response.CursorResponse;
1519

1620
@RestController
1721
@RequiredArgsConstructor
@@ -26,4 +30,12 @@ public ApiResponse<TagResponse> create(@AuthenticationPrincipal LoginUser loginU
2630

2731
return ApiResponse.success(tagResponse);
2832
}
33+
34+
@GetMapping
35+
public ApiResponse<CursorResponse<TagResponse>> getAll(@AuthenticationPrincipal LoginUser loginUser,
36+
@PageableDefault Pageable pageable) {
37+
CursorResponse<TagResponse> tagResponse = userTagService.getAll(loginUser, pageable);
38+
39+
return ApiResponse.success(tagResponse);
40+
}
2941
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.capturecat.core.domain.user;
2+
3+
import org.springframework.data.domain.Pageable;
4+
import org.springframework.data.domain.Slice;
5+
6+
public interface UserTagCustomRepository {
7+
8+
Slice<UserTag> findAllByUser(User user, Pageable pageable);
9+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.capturecat.core.domain.user;
2+
3+
import static com.capturecat.core.domain.user.QUserTag.*;
4+
5+
import java.util.List;
6+
7+
import org.springframework.data.domain.Pageable;
8+
import org.springframework.data.domain.Slice;
9+
10+
import com.querydsl.jpa.impl.JPAQueryFactory;
11+
12+
import lombok.RequiredArgsConstructor;
13+
14+
import com.capturecat.core.support.util.SliceUtil;
15+
16+
@RequiredArgsConstructor
17+
public class UserTagCustomRepositoryImpl implements UserTagCustomRepository {
18+
19+
private final JPAQueryFactory queryFactory;
20+
21+
@Override
22+
public Slice<UserTag> findAllByUser(User user, Pageable pageable) {
23+
List<UserTag> userTags = queryFactory
24+
.selectFrom(userTag)
25+
.leftJoin(userTag.tag).fetchJoin()
26+
.where(userTag.user.eq(user))
27+
.offset(pageable.getOffset())
28+
.limit(pageable.getPageSize() + 1)
29+
.orderBy(userTag.id.desc())
30+
.fetch();
31+
32+
return SliceUtil.toSlice(userTags, pageable);
33+
}
34+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import com.capturecat.core.domain.tag.Tag;
66

7-
public interface UserTagRepository extends JpaRepository<UserTag, Long> {
7+
public interface UserTagRepository extends JpaRepository<UserTag, Long>, UserTagCustomRepository {
88

99
boolean existsByUserAndTag(User user, Tag tag);
1010

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

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

3+
import java.util.List;
4+
35
import org.springframework.dao.DataIntegrityViolationException;
6+
import org.springframework.data.domain.Pageable;
7+
import org.springframework.data.domain.Slice;
48
import org.springframework.stereotype.Service;
59
import org.springframework.transaction.annotation.Transactional;
610

@@ -17,6 +21,8 @@
1721
import com.capturecat.core.service.image.TagResponse;
1822
import com.capturecat.core.support.error.CoreException;
1923
import com.capturecat.core.support.error.ErrorType;
24+
import com.capturecat.core.support.response.CursorResponse;
25+
import com.capturecat.core.support.util.CursorUtil;
2026

2127
@Slf4j
2228
@Service
@@ -46,6 +52,19 @@ public TagResponse create(LoginUser loginUser, String tagName) {
4652
}
4753
}
4854

55+
@Transactional(readOnly = true)
56+
public CursorResponse<TagResponse> getAll(LoginUser loginUser, Pageable pageable) {
57+
User user = userRepository.findByUsername(loginUser.getUsername())
58+
.orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND));
59+
Slice<UserTag> userTags = userTagRepository.findAllByUser(user, pageable);
60+
61+
List<TagResponse> tags = userTags.stream()
62+
.map(ut -> TagResponse.from(ut.getTag()))
63+
.toList();
64+
65+
return CursorUtil.toCursorResponse(tags, userTags.hasNext(), TagResponse::id);
66+
}
67+
4968
private void validate(User user, Tag tag) {
5069
validateDuplicateUserTag(user, tag);
5170
validateUserTagCountLimit(user);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ class UserErrorCodeControllerTest extends ErrorCodeDocumentTest {
1919
TOO_MANY_USER_TAGS, USER_NOT_FOUND);
2020
generateErrorDocs("errorCode/createUserTag", errorCodeDescriptors);
2121
}
22+
23+
@Test
24+
void 유저_태그_조회_에러_코드_문서화() {
25+
List<ErrorCodeDescriptor> errorCodeDescriptors = generateErrorCodeDescriptors(USER_NOT_FOUND);
26+
generateErrorDocs("errorCode/getUserTags", errorCodeDescriptors);
27+
}
2228
}

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
1414
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
1515

16+
import java.util.List;
17+
1618
import org.junit.jupiter.api.BeforeEach;
1719
import org.junit.jupiter.api.Test;
1820
import org.mockito.BDDMockito;
@@ -25,6 +27,7 @@
2527
import com.capturecat.core.config.jwt.JwtUtil;
2628
import com.capturecat.core.service.image.TagResponse;
2729
import com.capturecat.core.service.user.UserTagService;
30+
import com.capturecat.core.support.response.CursorResponse;
2831
import com.capturecat.test.api.RestDocsTest;
2932

3033
class UserTagControllerTest extends RestDocsTest {
@@ -62,4 +65,28 @@ void setUp() {
6265
fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("태그 ID"),
6366
fieldWithPath("data.name").type(JsonFieldType.STRING).description("태그 이름"))));
6467
}
68+
69+
@Test
70+
void 유저_태그_조회() {
71+
// given
72+
BDDMockito.given(userTagService.getAll(any(), any())).willReturn(
73+
new CursorResponse(false, 1L, List.of(new TagResponse(1L, "java"))));
74+
75+
// when & then
76+
given()
77+
.header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + ACCESS_TOKEN)
78+
.contentType(ContentType.JSON)
79+
.when().get("/v1/user-tags")
80+
.then().status(HttpStatus.OK)
81+
.apply(document("getUserTags", requestPreprocessor(), responsePreprocessor(),
82+
requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("유효한 Access 토큰")),
83+
responseFields(
84+
fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과"),
85+
fieldWithPath("data").type(JsonFieldType.OBJECT).description("커서 페이지 응답"),
86+
fieldWithPath("data.hasNext").type(JsonFieldType.BOOLEAN).description("다음 페이지 존재 여부"),
87+
fieldWithPath("data.lastCursor").type(JsonFieldType.NUMBER).description("마지막 커서 ID"),
88+
fieldWithPath("data.items").type(JsonFieldType.ARRAY).description("태그 목록"),
89+
fieldWithPath("data.items[].id").type(JsonFieldType.NUMBER).description("태그 ID"),
90+
fieldWithPath("data.items[].name").type(JsonFieldType.STRING).description("태그 이름"))));
91+
}
6592
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.capturecat.core.domain.user;
2+
3+
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
4+
5+
import java.util.List;
6+
7+
import jakarta.persistence.EntityManager;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
12+
import org.springframework.context.annotation.Import;
13+
import org.springframework.data.domain.PageRequest;
14+
import org.springframework.data.domain.Slice;
15+
16+
import com.capturecat.core.DummyObject;
17+
import com.capturecat.core.config.JpaAuditingConfig;
18+
import com.capturecat.core.config.QueryDslConfig;
19+
import com.capturecat.core.domain.tag.TagFixture;
20+
import com.capturecat.core.domain.tag.TagRepository;
21+
22+
@DataJpaTest
23+
@Import({QueryDslConfig.class, JpaAuditingConfig.class})
24+
class UserTagRepositoryTest {
25+
26+
@Autowired
27+
EntityManager entityManager;
28+
29+
@Autowired
30+
UserRepository userRepository;
31+
32+
@Autowired
33+
TagRepository tagRepository;
34+
35+
@Autowired
36+
UserTagRepository userTagRepository;
37+
38+
@Test
39+
void findAllByUser_ShouldReturnUserTags() {
40+
// given
41+
var user = DummyObject.newUser("test");
42+
userRepository.save(user);
43+
44+
var tag1 = TagFixture.createTag("tag1");
45+
var tag2 = TagFixture.createTag("tag2");
46+
tagRepository.saveAll(List.of(tag1, tag2));
47+
48+
UserTag userTag1 = UserTag.create(user, tag1);
49+
UserTag userTag2 = UserTag.create(user, tag2);
50+
userTagRepository.saveAll(List.of(userTag1, userTag2));
51+
52+
entityManager.flush();
53+
entityManager.clear();
54+
55+
// when
56+
Slice<UserTag> result = userTagRepository.findAllByUser(user, PageRequest.of(0, 10));
57+
58+
// then
59+
assertThat(result).isNotNull();
60+
assertThat(result.getContent()).hasSize(2);
61+
assertThat(result.getContent())
62+
.extracting("tag.name")
63+
.containsExactlyInAnyOrder("tag1", "tag2");
64+
}
65+
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,22 @@
1010
import static org.mockito.Mockito.times;
1111
import static org.mockito.Mockito.verify;
1212

13+
import java.util.List;
1314
import java.util.Optional;
1415

1516
import org.junit.jupiter.api.Test;
1617
import org.junit.jupiter.api.extension.ExtendWith;
1718
import org.mockito.InjectMocks;
1819
import org.mockito.Mock;
1920
import org.mockito.junit.jupiter.MockitoExtension;
21+
import org.springframework.data.domain.PageRequest;
22+
import org.springframework.data.domain.SliceImpl;
2023

2124
import com.capturecat.core.DummyObject;
2225
import com.capturecat.core.domain.tag.TagFixture;
2326
import com.capturecat.core.domain.tag.TagRegister;
2427
import com.capturecat.core.domain.user.UserRepository;
28+
import com.capturecat.core.domain.user.UserTag;
2529
import com.capturecat.core.domain.user.UserTagFixture;
2630
import com.capturecat.core.domain.user.UserTagRepository;
2731
import com.capturecat.core.service.auth.LoginUser;
@@ -114,4 +118,35 @@ class UserTagServiceTest {
114118

115119
verify(userTagRepository, never()).save(any());
116120
}
121+
122+
@Test
123+
void 유저_태그를_조회한다() {
124+
// given
125+
var user = DummyObject.newUser("test");
126+
var tag = TagFixture.createTag(1L, "java");
127+
128+
given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user));
129+
given(userTagRepository.findAllByUser(eq(user), any()))
130+
.willReturn(new SliceImpl<>(List.of(UserTag.create(user, tag))));
131+
132+
// when
133+
var response = userTagService.getAll(new LoginUser(user), PageRequest.of(0, 10));
134+
135+
// then
136+
assertThat(response.items()).hasSize(1);
137+
assertThat(response.items().get(0).name()).isEqualTo(tag.getName());
138+
}
139+
140+
@Test
141+
void 유저_태그를_조회_시_사용자가_존재하지_않으면_실패한다() {
142+
// given
143+
var user = DummyObject.newUser("test");
144+
145+
given(userRepository.findByUsername(anyString())).willReturn(Optional.empty());
146+
147+
// when & then
148+
assertThatThrownBy(() -> userTagService.getAll(new LoginUser(user), PageRequest.of(0, 10)))
149+
.isInstanceOf(CoreException.class)
150+
.hasMessage(ErrorType.USER_NOT_FOUND.getCode().getMessage());
151+
}
117152
}

0 commit comments

Comments
 (0)