From 321c48dd681a99cb49ad192861570c7c212bbd66 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 15:10:07 +0900 Subject: [PATCH 01/13] =?UTF-8?q?[chore]=20api=20=EC=84=A4=EB=AA=85=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/adapter/in/web/UserCommandController.java | 10 +++++++++- .../thip/user/adapter/in/web/UserQueryController.java | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index e09821ab4..e7b011ef2 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -33,6 +33,9 @@ public class UserCommandController { private final UserFollowUsecase userFollowUsecase; private final JwtUtil jwtUtil; + /** + * 사용자 회원가입 + */ @PostMapping("/users/signup") public BaseResponse signup(@Valid @RequestBody final UserSignupRequest request, @Oauth2Id final String oauth2Id, @@ -43,6 +46,9 @@ public BaseResponse signup(@Valid @RequestBody final UserSig return BaseResponse.ok(UserSignupResponse.of(userId)); } + /** + * 닉네임 중복 확인 + */ @PostMapping("/users/nickname") public BaseResponse verifyNickname(@Valid @RequestBody final UserVerifyNicknameRequest request) { return BaseResponse.ok(UserVerifyNicknameResponse.of( @@ -50,7 +56,9 @@ public BaseResponse verifyNickname(@Valid @RequestBo ); } - // 팔루우 상태 변경 : true -> 팔로우, false -> 언팔로우 + /** + * 사용자 팔로우 상태 변경 : true -> 팔로우, false -> 언팔로우 + */ @PostMapping("/users/following/{followingUserId}") public BaseResponse followUser(@UserId final Long userId, @PathVariable final Long followingUserId, diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index 846abe69c..f6a922869 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -18,6 +18,9 @@ public class UserQueryController { private final UserViewAliasChoiceUseCase userViewAliasChoiceUseCase; private final UserGetFollowersUsecase userGetFollowersUsecase; + /** + * 사용자 별칭 선택 화면 조회 + */ @GetMapping("/users/alias") public BaseResponse showAliasChoiceView() { return BaseResponse.ok(UserViewAliasChoiceResponse.of( @@ -25,6 +28,9 @@ public BaseResponse showAliasChoiceView() { )); } + /** + * 사용자 팔로워 조회 + */ @GetMapping("/users/{userId}/followers") public BaseResponse showFollowers(@PathVariable final Long userId, @RequestParam(required = false) final String cursor) { From cd3407b86e88c2c8f5cb160ddccb0c12e41889c8 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 17:32:35 +0900 Subject: [PATCH 02/13] =?UTF-8?q?[refactor]=20CursorBasedList=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/util/CursorBasedList.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/util/CursorBasedList.java diff --git a/src/main/java/konkuk/thip/common/util/CursorBasedList.java b/src/main/java/konkuk/thip/common/util/CursorBasedList.java new file mode 100644 index 000000000..042234d15 --- /dev/null +++ b/src/main/java/konkuk/thip/common/util/CursorBasedList.java @@ -0,0 +1,17 @@ +package konkuk.thip.common.util; + +import java.util.List; + +public record CursorBasedList( + List contents, + String nextCursor, + boolean hasNext +) { + public static CursorBasedList of(List contents, String nextCursor) { + return new CursorBasedList<>( + contents, + nextCursor, + nextCursor != null + ); + } +} From 1a9f428521e1b1a2907a2eb0e4178fb55d8279af Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 17:40:02 +0900 Subject: [PATCH 03/13] =?UTF-8?q?[refactor]=20QueryDSL=EC=9A=A9=20Dto=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/out/dto/FollowerQueryDto.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java diff --git a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java new file mode 100644 index 000000000..d73584c53 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java @@ -0,0 +1,24 @@ +package konkuk.thip.user.application.port.out.dto; + +import com.querydsl.core.annotations.QueryProjection; +import io.jsonwebtoken.lang.Assert; + +import java.time.LocalDateTime; + +public record FollowerQueryDto(Long userId, + String nickname, + String profileImageUrl, + String aliasName, + Integer followerCount, + LocalDateTime createdAt) { + + @QueryProjection + public FollowerQueryDto { + Assert.notNull(userId, "userId must not be null"); + Assert.notNull(nickname, "nickname must not be null"); + Assert.notNull(profileImageUrl, "profileImageUrl must not be null"); + Assert.notNull(aliasName, "aliasName must not be null"); + Assert.notNull(followerCount, "followerCount must not be null"); + Assert.notNull(createdAt, "createdAt must not be null"); + } +} \ No newline at end of file From 12dfed7b1bfe5c4653062a3d53b81f76a8ccfeac Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 17:41:40 +0900 Subject: [PATCH 04/13] =?UTF-8?q?[refactor]=20CursorBasedList=20+=20QueryP?= =?UTF-8?q?rojection=20=EB=8F=84=EC=9E=85=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/UserQueryController.java | 8 ++++ .../web/response/UserFollowersResponse.java | 7 +-- .../FollowingQueryPersistenceAdapter.java | 47 ++++++------------- .../following/FollowingQueryRepository.java | 3 +- .../FollowingQueryRepositoryImpl.java | 20 ++++++-- .../port/in/UserGetFollowersUsecase.java | 2 +- .../port/out/FollowingQueryPort.java | 5 +- .../following/UserGetFollowersService.java | 23 ++++++++- .../in/web/UserGetFollowersApiTest.java | 6 +-- 9 files changed, 69 insertions(+), 52 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index f6a922869..7a4cdea95 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -36,4 +36,12 @@ public BaseResponse showFollowers(@PathVariable final Lon @RequestParam(required = false) final String cursor) { return BaseResponse.ok(userGetFollowersUsecase.getUserFollowers(userId, cursor)); } + + /** + * 내 팔로잉 리스트 조회 + */ +// @GetMapping("/users/my/following") +// public BaseResponse showMyFollowing(@RequestParam(required = false) final String cursor) { +// return BaseResponse.ok(userGetFollowersUsecase.getMyFollowing(cursor)); +// } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java index 9c923a4a9..56acf1d6c 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java @@ -2,15 +2,12 @@ import lombok.Builder; -import java.time.LocalDateTime; import java.util.List; @Builder public record UserFollowersResponse( - List followerList, - int size, - LocalDateTime nextCursor, - boolean isFirst, + List followers, + String nextCursor, boolean isLast ) { @Builder diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java index 278f7e3c2..61deb3510 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java @@ -1,9 +1,8 @@ package konkuk.thip.user.adapter.out.persistence; +import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.common.util.DateUtil; -import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; -import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; -import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.application.port.out.FollowingQueryPort; import lombok.RequiredArgsConstructor; @@ -19,39 +18,21 @@ public class FollowingQueryPersistenceAdapter implements FollowingQueryPort { private final FollowingJpaRepository followingJpaRepository; @Override - public UserFollowersResponse getFollowersByUserId(Long userId, String cursor, int size) { - LocalDateTime nextCursor = null; + public CursorBasedList getFollowersByUserId(Long userId, String cursor, int size) { + LocalDateTime cursorVal = null; if (cursor != null && !cursor.isBlank()) { - nextCursor = DateUtil.parseDateTime(cursor); + cursorVal = DateUtil.parseDateTime(cursor); } + List dtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( + userId, + cursorVal, + size + ); - List followerEntities = - followingJpaRepository.findFollowersByUserIdBeforeCreatedAt(userId, nextCursor, size); + boolean hasNext = dtos.size() > size; + List content = hasNext ? dtos.subList(0, size) : dtos; + String nextCursor = hasNext ? content.get(size - 1).createdAt().toString() : null; - List followers = followerEntities.stream() - .map(FollowingJpaEntity::getUserJpaEntity) // 팔로워 사용자 - .toList(); - - List followerList = followers.stream() - .map(follower -> UserFollowersResponse.Follower.builder() - .userId(follower.getUserId()) - .nickname(follower.getNickname()) - .profileImageUrl(follower.getAliasForUserJpaEntity().getImageUrl()) - .aliasName(follower.getAliasForUserJpaEntity().getValue()) - .followerCount(follower.getFollowerCount()) - .build()) - .toList(); - - boolean isLast = followerEntities.size() < size; - nextCursor = isLast ? null : - followerEntities.get(followerEntities.size() - 1).getCreatedAt(); - - return UserFollowersResponse.builder() - .followerList(followerList) - .size(followerList.size()) - .nextCursor(nextCursor) - .isFirst(cursor == null) // cursor가 null이면 첫 페이지 - .isLast(isLast) - .build(); + return CursorBasedList.of(content, nextCursor); } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java index cfe0c1253..028195249 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java @@ -1,6 +1,7 @@ package konkuk.thip.user.adapter.out.persistence.repository.following; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; +import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; import java.time.LocalDateTime; import java.util.List; @@ -9,5 +10,5 @@ public interface FollowingQueryRepository { Optional findByUserAndTargetUser(Long userId, Long targetUserId); - List findFollowersByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size); + List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java index ec2a2a118..a53a21716 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java @@ -7,6 +7,8 @@ import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.QFollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; +import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; +import konkuk.thip.user.application.port.out.dto.QFollowerQueryDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -34,7 +36,7 @@ public Optional findByUserAndTargetUser(Long userId, Long ta } @Override - public List findFollowersByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size) { + public List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size) { QFollowingJpaEntity following = QFollowingJpaEntity.followingJpaEntity; QUserJpaEntity user = QUserJpaEntity.userJpaEntity; QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; @@ -48,12 +50,20 @@ public List findFollowersByUserIdBeforeCreatedAt(Long userId } return jpaQueryFactory - .selectFrom(following) - .leftJoin(following.userJpaEntity, user).fetchJoin() // N+1 문제 방지를 위해 fetchJoin - .leftJoin(user.aliasForUserJpaEntity, alias).fetchJoin() + .select(new QFollowerQueryDto( + user.userId, + user.nickname, + alias.imageUrl, + alias.value, + user.followerCount, + following.createdAt + )) + .from(following) + .leftJoin(following.userJpaEntity, user) + .leftJoin(user.aliasForUserJpaEntity, alias) .where(condition) .orderBy(following.createdAt.desc()) - .limit(size) + .limit(size + 1) // hasNext 판단 위해 +1 .fetch(); } } diff --git a/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java b/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java index 673ad6793..d644c83f6 100644 --- a/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java +++ b/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java @@ -3,5 +3,5 @@ import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; public interface UserGetFollowersUsecase { - UserFollowersResponse getUserFollowers(Long userId, String nextCursor); + UserFollowersResponse getUserFollowers(Long userId, String cursor); } diff --git a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java index cfaa74bc3..dcc1ed167 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java @@ -1,8 +1,9 @@ package konkuk.thip.user.application.port.out; -import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; +import konkuk.thip.common.util.CursorBasedList; +import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; public interface FollowingQueryPort { - UserFollowersResponse getFollowersByUserId(Long userId, String cursor, int size); + CursorBasedList getFollowersByUserId(Long userId, String cursor, int size); } diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java index 6752c7fbe..832521a8b 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java @@ -1,6 +1,8 @@ package konkuk.thip.user.application.service.following; +import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; +import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; import konkuk.thip.user.application.port.in.UserGetFollowersUsecase; import konkuk.thip.user.application.port.out.FollowingQueryPort; import konkuk.thip.user.application.port.out.UserCommandPort; @@ -22,6 +24,25 @@ public class UserGetFollowersService implements UserGetFollowersUsecase { @Transactional(readOnly = true) public UserFollowersResponse getUserFollowers(Long userId, String cursor) { User user = userCommandPort.findById(userId); - return followingQueryPort.getFollowersByUserId(user.getId(), cursor, DEFAULT_PAGE_SIZE); + + CursorBasedList result = followingQueryPort.getFollowersByUserId( + user.getId(), cursor, DEFAULT_PAGE_SIZE + ); + + var followers = result.contents().stream() + .map(dto -> UserFollowersResponse.Follower.builder() + .userId(dto.userId()) + .nickname(dto.nickname()) + .profileImageUrl(dto.profileImageUrl()) + .aliasName(dto.aliasName()) + .followerCount(dto.followerCount()) + .build()) + .toList(); + + return UserFollowersResponse.builder() + .followers(followers) + .nextCursor(result.nextCursor()) + .isLast(!result.hasNext()) + .build(); } } diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java index 64deb6889..e3b59d27b 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java @@ -82,8 +82,7 @@ void getFollowersWithCursorPaging() throws Exception { ); firstPageResult.andExpect(status().isOk()) - .andExpect(jsonPath("$.data.followerList", hasSize(10))) - .andExpect(jsonPath("$.data.isFirst").value(true)) + .andExpect(jsonPath("$.data.followers", hasSize(10))) .andExpect(jsonPath("$.data.isLast").value(false)) .andExpect(jsonPath("$.data.nextCursor").exists()); @@ -101,8 +100,7 @@ void getFollowersWithCursorPaging() throws Exception { ); secondPageResult.andExpect(status().isOk()) - .andExpect(jsonPath("$.data.followerList", hasSize(2))) - .andExpect(jsonPath("$.data.isFirst").value(false)) + .andExpect(jsonPath("$.data.followers", hasSize(2))) .andExpect(jsonPath("$.data.isLast").value(true)) .andExpect(jsonPath("$.data.nextCursor").doesNotExist()); } From 2a6c8606fd71d8754f1c5e41bfa6ae9bd2cf3ec7 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 17:45:38 +0900 Subject: [PATCH 05/13] =?UTF-8?q?[refactor]=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20dto=EB=A5=BC=20=EC=9C=84=ED=95=B4=20?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/adapter/in/web/UserQueryController.java | 14 +++++++------- .../FollowingQueryPersistenceAdapter.java | 8 ++++---- .../following/FollowingQueryRepository.java | 4 ++-- .../following/FollowingQueryRepositoryImpl.java | 4 ++-- ...owersUsecase.java => UserGetFollowUsecase.java} | 4 +++- .../application/port/out/FollowingQueryPort.java | 5 +++-- .../{FollowerQueryDto.java => FollowQueryDto.java} | 14 +++++++------- ...owersService.java => UserGetFollowService.java} | 14 ++++++++++---- 8 files changed, 38 insertions(+), 29 deletions(-) rename src/main/java/konkuk/thip/user/application/port/in/{UserGetFollowersUsecase.java => UserGetFollowUsecase.java} (66%) rename src/main/java/konkuk/thip/user/application/port/out/dto/{FollowerQueryDto.java => FollowQueryDto.java} (64%) rename src/main/java/konkuk/thip/user/application/service/following/{UserGetFollowersService.java => UserGetFollowService.java} (78%) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index 7a4cdea95..e9c731f62 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -3,7 +3,7 @@ import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; import konkuk.thip.user.adapter.in.web.response.UserViewAliasChoiceResponse; -import konkuk.thip.user.application.port.in.UserGetFollowersUsecase; +import konkuk.thip.user.application.port.in.UserGetFollowUsecase; import konkuk.thip.user.application.port.in.UserViewAliasChoiceUseCase; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; @@ -16,7 +16,7 @@ public class UserQueryController { private final UserViewAliasChoiceUseCase userViewAliasChoiceUseCase; - private final UserGetFollowersUsecase userGetFollowersUsecase; + private final UserGetFollowUsecase userGetFollowUsecase; /** * 사용자 별칭 선택 화면 조회 @@ -34,14 +34,14 @@ public BaseResponse showAliasChoiceView() { @GetMapping("/users/{userId}/followers") public BaseResponse showFollowers(@PathVariable final Long userId, @RequestParam(required = false) final String cursor) { - return BaseResponse.ok(userGetFollowersUsecase.getUserFollowers(userId, cursor)); + return BaseResponse.ok(userGetFollowUsecase.getUserFollowers(userId, cursor)); } /** * 내 팔로잉 리스트 조회 */ -// @GetMapping("/users/my/following") -// public BaseResponse showMyFollowing(@RequestParam(required = false) final String cursor) { -// return BaseResponse.ok(userGetFollowersUsecase.getMyFollowing(cursor)); -// } + @GetMapping("/users/my/following") + public BaseResponse showMyFollowing(@RequestParam(required = false) final String cursor) { + return BaseResponse.ok(userGetFollowUsecase.getMyFollowing(cursor)); + } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java index 61deb3510..bf4d537b7 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java @@ -2,7 +2,7 @@ import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.common.util.DateUtil; -import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; +import konkuk.thip.user.application.port.out.dto.FollowQueryDto; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; import konkuk.thip.user.application.port.out.FollowingQueryPort; import lombok.RequiredArgsConstructor; @@ -18,19 +18,19 @@ public class FollowingQueryPersistenceAdapter implements FollowingQueryPort { private final FollowingJpaRepository followingJpaRepository; @Override - public CursorBasedList getFollowersByUserId(Long userId, String cursor, int size) { + public CursorBasedList getFollowersByUserId(Long userId, String cursor, int size) { LocalDateTime cursorVal = null; if (cursor != null && !cursor.isBlank()) { cursorVal = DateUtil.parseDateTime(cursor); } - List dtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( + List dtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( userId, cursorVal, size ); boolean hasNext = dtos.size() > size; - List content = hasNext ? dtos.subList(0, size) : dtos; + List content = hasNext ? dtos.subList(0, size) : dtos; String nextCursor = hasNext ? content.get(size - 1).createdAt().toString() : null; return CursorBasedList.of(content, nextCursor); diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java index 028195249..61f75b86f 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java @@ -1,7 +1,7 @@ package konkuk.thip.user.adapter.out.persistence.repository.following; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; -import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; +import konkuk.thip.user.application.port.out.dto.FollowQueryDto; import java.time.LocalDateTime; import java.util.List; @@ -10,5 +10,5 @@ public interface FollowingQueryRepository { Optional findByUserAndTargetUser(Long userId, Long targetUserId); - List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size); + List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java index a53a21716..2c3718a8e 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java @@ -7,7 +7,7 @@ import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.QFollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; -import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; +import konkuk.thip.user.application.port.out.dto.FollowQueryDto; import konkuk.thip.user.application.port.out.dto.QFollowerQueryDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -36,7 +36,7 @@ public Optional findByUserAndTargetUser(Long userId, Long ta } @Override - public List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size) { + public List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size) { QFollowingJpaEntity following = QFollowingJpaEntity.followingJpaEntity; QUserJpaEntity user = QUserJpaEntity.userJpaEntity; QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; diff --git a/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java b/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java similarity index 66% rename from src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java rename to src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java index d644c83f6..67fe53b8b 100644 --- a/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowersUsecase.java +++ b/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java @@ -2,6 +2,8 @@ import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; -public interface UserGetFollowersUsecase { +public interface UserGetFollowUsecase { UserFollowersResponse getUserFollowers(Long userId, String cursor); + + UserFollowersResponse getMyFollowing(String cursor); } diff --git a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java index dcc1ed167..b00426257 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java @@ -1,9 +1,10 @@ package konkuk.thip.user.application.port.out; import konkuk.thip.common.util.CursorBasedList; -import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; +import konkuk.thip.user.application.port.out.dto.FollowQueryDto; public interface FollowingQueryPort { - CursorBasedList getFollowersByUserId(Long userId, String cursor, int size); + CursorBasedList getFollowersByUserId(Long userId, String cursor, int size); + } diff --git a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java similarity index 64% rename from src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java rename to src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java index d73584c53..5bbaabb11 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowerQueryDto.java +++ b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java @@ -5,15 +5,15 @@ import java.time.LocalDateTime; -public record FollowerQueryDto(Long userId, - String nickname, - String profileImageUrl, - String aliasName, - Integer followerCount, - LocalDateTime createdAt) { +public record FollowQueryDto(Long userId, + String nickname, + String profileImageUrl, + String aliasName, + Integer followerCount, + LocalDateTime createdAt) { @QueryProjection - public FollowerQueryDto { + public FollowQueryDto { Assert.notNull(userId, "userId must not be null"); Assert.notNull(nickname, "nickname must not be null"); Assert.notNull(profileImageUrl, "profileImageUrl must not be null"); diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java similarity index 78% rename from src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java rename to src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java index 832521a8b..a29d24919 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowersService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java @@ -2,8 +2,8 @@ import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; -import konkuk.thip.user.application.port.out.dto.FollowerQueryDto; -import konkuk.thip.user.application.port.in.UserGetFollowersUsecase; +import konkuk.thip.user.application.port.out.dto.FollowQueryDto; +import konkuk.thip.user.application.port.in.UserGetFollowUsecase; import konkuk.thip.user.application.port.out.FollowingQueryPort; import konkuk.thip.user.application.port.out.UserCommandPort; import konkuk.thip.user.domain.User; @@ -13,7 +13,7 @@ @Service @RequiredArgsConstructor -public class UserGetFollowersService implements UserGetFollowersUsecase { +public class UserGetFollowService implements UserGetFollowUsecase { private final FollowingQueryPort followingQueryPort; private final UserCommandPort userCommandPort; @@ -25,7 +25,7 @@ public class UserGetFollowersService implements UserGetFollowersUsecase { public UserFollowersResponse getUserFollowers(Long userId, String cursor) { User user = userCommandPort.findById(userId); - CursorBasedList result = followingQueryPort.getFollowersByUserId( + CursorBasedList result = followingQueryPort.getFollowersByUserId( user.getId(), cursor, DEFAULT_PAGE_SIZE ); @@ -45,4 +45,10 @@ public UserFollowersResponse getUserFollowers(Long userId, String cursor) { .isLast(!result.hasNext()) .build(); } + + @Override + @Transactional(readOnly = true) + public UserFollowersResponse getMyFollowing(String cursor) { + return null; + } } From b6b5b22bc143047992052924ddb186cfc1d0fde5 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:44:14 +0900 Subject: [PATCH 06/13] =?UTF-8?q?[refactor]=20MapStruct=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index a537040b6..c007b4385 100644 --- a/build.gradle +++ b/build.gradle @@ -64,6 +64,10 @@ dependencies { //s3 버킷 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // MapStruct + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' } def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile From 38d5002a18eb205bbfe8f106b16d506450ad8413 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:44:49 +0900 Subject: [PATCH 07/13] =?UTF-8?q?[refactor]=20CursorBasedList=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=A8=EC=88=98=ED=98=95=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=EB=A5=BC=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EC=B5=9C=EC=A2=85=20contents=20=EB=B0=8F=20nextCursor=20?= =?UTF-8?q?=EC=B6=94=EC=B6=9C=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/util/CursorBasedList.java | 11 +++++------ .../java/konkuk/thip/common/util/CursorExtractor.java | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 src/main/java/konkuk/thip/common/util/CursorExtractor.java diff --git a/src/main/java/konkuk/thip/common/util/CursorBasedList.java b/src/main/java/konkuk/thip/common/util/CursorBasedList.java index 042234d15..e1228e534 100644 --- a/src/main/java/konkuk/thip/common/util/CursorBasedList.java +++ b/src/main/java/konkuk/thip/common/util/CursorBasedList.java @@ -7,11 +7,10 @@ public record CursorBasedList( String nextCursor, boolean hasNext ) { - public static CursorBasedList of(List contents, String nextCursor) { - return new CursorBasedList<>( - contents, - nextCursor, - nextCursor != null - ); + public static CursorBasedList of(List queryList, int size, CursorExtractor extractor) { + boolean hasNext = queryList.size() > size; + List contents = hasNext ? queryList.subList(0, size) : queryList; + String nextCursor = hasNext ? extractor.extractCursor(contents.get(size - 1)) : null; + return new CursorBasedList<>(contents, nextCursor, hasNext); } } diff --git a/src/main/java/konkuk/thip/common/util/CursorExtractor.java b/src/main/java/konkuk/thip/common/util/CursorExtractor.java new file mode 100644 index 000000000..dc2b009fb --- /dev/null +++ b/src/main/java/konkuk/thip/common/util/CursorExtractor.java @@ -0,0 +1,6 @@ +package konkuk.thip.common.util; + +@FunctionalInterface +public interface CursorExtractor { + String extractCursor(T lastElement); +} From cbcf0dc49119cb5c071548b2bf4f9d6a93202840 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:45:04 +0900 Subject: [PATCH 08/13] =?UTF-8?q?[feat]=20=EC=9D=91=EB=8B=B5=20dto=20?= =?UTF-8?q?=EB=A7=A4=ED=8D=BC=20=EC=B6=94=EA=B0=80=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/application/mapper/FollowDtoMapper.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java diff --git a/src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java b/src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java new file mode 100644 index 000000000..8c29f16b2 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/mapper/FollowDtoMapper.java @@ -0,0 +1,14 @@ +package konkuk.thip.user.application.mapper; + +import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; +import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; +import konkuk.thip.user.application.port.out.dto.FollowQueryDto; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface FollowDtoMapper { + + UserFollowersResponse.Follower toFollowerList(FollowQueryDto dto); + + UserFollowingResponse.Following toFollowingList(FollowQueryDto dto); +} \ No newline at end of file From 652f636aafe1eb6a018d4cb950544c39eaf2ce40 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:45:15 +0900 Subject: [PATCH 09/13] =?UTF-8?q?[chore]=20=ED=97=B7=EA=B0=88=EB=A6=AC?= =?UTF-8?q?=EB=8A=94=20=ED=95=84=EB=93=9C=20=EC=A3=BC=EC=84=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java index c457388a0..07767f6a7 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/FollowingJpaEntity.java @@ -21,9 +21,9 @@ public class FollowingJpaEntity extends BaseJpaEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") - private UserJpaEntity userJpaEntity; + private UserJpaEntity userJpaEntity; // 팔로잉 하는 유저 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "following_user_id") - private UserJpaEntity followingUserJpaEntity; + private UserJpaEntity followingUserJpaEntity; // 팔로우 당하는 유저 } From 83e97e26977d65840346df3f8281682cb5740af5 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:45:35 +0900 Subject: [PATCH 10/13] =?UTF-8?q?[feat]=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20dto=20=EC=A0=95=EB=A6=AC=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/response/UserFollowersResponse.java | 1 + .../web/response/UserFollowingResponse.java | 22 +++++++++++++++++++ .../port/out/dto/FollowQueryDto.java | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java index 56acf1d6c..32d5c907a 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowersResponse.java @@ -18,6 +18,7 @@ public record Follower( String aliasName, Integer followerCount ){ + } } diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java new file mode 100644 index 000000000..a099bb173 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/in/web/response/UserFollowingResponse.java @@ -0,0 +1,22 @@ +package konkuk.thip.user.adapter.in.web.response; + +import lombok.Builder; + +import java.util.List; + +@Builder +public record UserFollowingResponse( + List followings, + String nextCursor, + boolean isLast +) { + @Builder + public record Following( + Long userId, + String nickname, + String profileImageUrl, + String aliasName + ){ + } + +} diff --git a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java index 5bbaabb11..53f2d77ca 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java +++ b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java @@ -18,7 +18,7 @@ public record FollowQueryDto(Long userId, Assert.notNull(nickname, "nickname must not be null"); Assert.notNull(profileImageUrl, "profileImageUrl must not be null"); Assert.notNull(aliasName, "aliasName must not be null"); - Assert.notNull(followerCount, "followerCount must not be null"); +// Assert.notNull(followerCount, "followerCount must not be null"); // 내 팔로잉 목록 조회에서는 필요 x Assert.notNull(createdAt, "createdAt must not be null"); } } \ No newline at end of file From 491723626572c2afc04fd732e3900f12d6a72aaa Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:45:46 +0900 Subject: [PATCH 11/13] =?UTF-8?q?[feat]=20=EB=82=B4=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9E=89=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EA=B0=9C=EB=B0=9C=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/UserQueryController.java | 15 ++++++-- .../FollowingQueryPersistenceAdapter.java | 23 +++++++----- .../following/FollowingQueryRepository.java | 1 + .../FollowingQueryRepositoryImpl.java | 37 +++++++++++++++---- .../port/in/UserGetFollowUsecase.java | 5 ++- .../port/out/FollowingQueryPort.java | 2 +- .../following/UserGetFollowService.java | 36 ++++++++++++------ 7 files changed, 83 insertions(+), 36 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java index e9c731f62..8a2454f17 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java @@ -1,7 +1,11 @@ package konkuk.thip.user.adapter.in.web; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; +import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; import konkuk.thip.user.adapter.in.web.response.UserViewAliasChoiceResponse; import konkuk.thip.user.application.port.in.UserGetFollowUsecase; import konkuk.thip.user.application.port.in.UserViewAliasChoiceUseCase; @@ -33,15 +37,18 @@ public BaseResponse showAliasChoiceView() { */ @GetMapping("/users/{userId}/followers") public BaseResponse showFollowers(@PathVariable final Long userId, - @RequestParam(required = false) final String cursor) { - return BaseResponse.ok(userGetFollowUsecase.getUserFollowers(userId, cursor)); + @RequestParam(required = false) final String cursor, + @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { + return BaseResponse.ok(userGetFollowUsecase.getUserFollowers(userId, cursor, size)); } /** * 내 팔로잉 리스트 조회 */ @GetMapping("/users/my/following") - public BaseResponse showMyFollowing(@RequestParam(required = false) final String cursor) { - return BaseResponse.ok(userGetFollowUsecase.getMyFollowing(cursor)); + public BaseResponse showMyFollowing(@UserId final Long userId, + @RequestParam(required = false) final String cursor, + @RequestParam(defaultValue = "10") @Max(value = 10) @Min(value = 1) final int size) { + return BaseResponse.ok(userGetFollowUsecase.getMyFollowing(userId, cursor, size)); } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java index bf4d537b7..271528411 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingQueryPersistenceAdapter.java @@ -19,20 +19,25 @@ public class FollowingQueryPersistenceAdapter implements FollowingQueryPort { @Override public CursorBasedList getFollowersByUserId(Long userId, String cursor, int size) { - LocalDateTime cursorVal = null; - if (cursor != null && !cursor.isBlank()) { - cursorVal = DateUtil.parseDateTime(cursor); - } - List dtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( + LocalDateTime cursorVal = cursor != null && !cursor.isBlank() ? DateUtil.parseDateTime(cursor) : null; + List followerDtos = followingJpaRepository.findFollowerDtosByUserIdBeforeCreatedAt( userId, cursorVal, size ); - boolean hasNext = dtos.size() > size; - List content = hasNext ? dtos.subList(0, size) : dtos; - String nextCursor = hasNext ? content.get(size - 1).createdAt().toString() : null; + return CursorBasedList.of(followerDtos, size, followerDto -> followerDto.createdAt().toString()); + } + + @Override + public CursorBasedList getFollowingByUserId(Long userId, String cursor, int size) { + LocalDateTime cursorVal = cursor != null && !cursor.isBlank() ? DateUtil.parseDateTime(cursor) : null; + List followingDtos = followingJpaRepository.findFollowingDtosByUserIdBeforeCreatedAt( + userId, + cursorVal, + size + ); - return CursorBasedList.of(content, nextCursor); + return CursorBasedList.of(followingDtos, size, followingDto -> followingDto.createdAt().toString()); } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java index 61f75b86f..fac66996c 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepository.java @@ -11,4 +11,5 @@ public interface FollowingQueryRepository { Optional findByUserAndTargetUser(Long userId, Long targetUserId); List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size); + List findFollowingDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java index 2c3718a8e..68dc546c8 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingQueryRepositoryImpl.java @@ -8,7 +8,7 @@ import konkuk.thip.user.adapter.out.jpa.QFollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; import konkuk.thip.user.application.port.out.dto.FollowQueryDto; -import konkuk.thip.user.application.port.out.dto.QFollowerQueryDto; +import konkuk.thip.user.application.port.out.dto.QFollowQueryDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @@ -37,33 +37,54 @@ public Optional findByUserAndTargetUser(Long userId, Long ta @Override public List findFollowerDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size) { + return findFollowDtos( + userId, + cursor, + size, + true // isFollowerQuery + ); + } + + @Override + public List findFollowingDtosByUserIdBeforeCreatedAt(Long userId, LocalDateTime cursor, int size) { + return findFollowDtos( + userId, + cursor, + size, + false // isFollowingQuery + ); + } + + private List findFollowDtos(Long userId, LocalDateTime cursor, int size, boolean isFollowerQuery) { QFollowingJpaEntity following = QFollowingJpaEntity.followingJpaEntity; QUserJpaEntity user = QUserJpaEntity.userJpaEntity; QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; BooleanBuilder condition = new BooleanBuilder() - .and(following.followingUserJpaEntity.userId.eq(userId)) + .and((isFollowerQuery ? following.followingUserJpaEntity.userId.eq(userId) : following.userJpaEntity.userId.eq(userId))) .and(following.status.eq(StatusType.ACTIVE)); if (cursor != null) { condition.and(following.createdAt.lt(cursor)); } + QUserJpaEntity targetUser = isFollowerQuery ? following.userJpaEntity : following.followingUserJpaEntity; + return jpaQueryFactory - .select(new QFollowerQueryDto( - user.userId, - user.nickname, + .select(new QFollowQueryDto( + targetUser.userId, + targetUser.nickname, alias.imageUrl, alias.value, - user.followerCount, + targetUser.followerCount, following.createdAt )) .from(following) - .leftJoin(following.userJpaEntity, user) + .leftJoin(targetUser, user) .leftJoin(user.aliasForUserJpaEntity, alias) .where(condition) .orderBy(following.createdAt.desc()) - .limit(size + 1) // hasNext 판단 위해 +1 + .limit(size + 1) .fetch(); } } diff --git a/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java b/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java index 67fe53b8b..d35e8313e 100644 --- a/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java +++ b/src/main/java/konkuk/thip/user/application/port/in/UserGetFollowUsecase.java @@ -1,9 +1,10 @@ package konkuk.thip.user.application.port.in; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; +import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; public interface UserGetFollowUsecase { - UserFollowersResponse getUserFollowers(Long userId, String cursor); + UserFollowersResponse getUserFollowers(Long userId, String cursor, int size); - UserFollowersResponse getMyFollowing(String cursor); + UserFollowingResponse getMyFollowing(Long userId, String cursor, int size); } diff --git a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java index b00426257..4b3651b98 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/FollowingQueryPort.java @@ -5,6 +5,6 @@ public interface FollowingQueryPort { CursorBasedList getFollowersByUserId(Long userId, String cursor, int size); - + CursorBasedList getFollowingByUserId(Long userId, String cursor, int size); } diff --git a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java index a29d24919..24930a715 100644 --- a/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java +++ b/src/main/java/konkuk/thip/user/application/service/following/UserGetFollowService.java @@ -2,6 +2,8 @@ import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.user.adapter.in.web.response.UserFollowersResponse; +import konkuk.thip.user.adapter.in.web.response.UserFollowingResponse; +import konkuk.thip.user.application.mapper.FollowDtoMapper; import konkuk.thip.user.application.port.out.dto.FollowQueryDto; import konkuk.thip.user.application.port.in.UserGetFollowUsecase; import konkuk.thip.user.application.port.out.FollowingQueryPort; @@ -18,25 +20,21 @@ public class UserGetFollowService implements UserGetFollowUsecase { private final FollowingQueryPort followingQueryPort; private final UserCommandPort userCommandPort; - private static final int DEFAULT_PAGE_SIZE = 10; + private final FollowDtoMapper followDtoMapper; + + private static final int MAX_PAGE_SIZE = 10; @Override @Transactional(readOnly = true) - public UserFollowersResponse getUserFollowers(Long userId, String cursor) { + public UserFollowersResponse getUserFollowers(Long userId, String cursor, int size) { User user = userCommandPort.findById(userId); CursorBasedList result = followingQueryPort.getFollowersByUserId( - user.getId(), cursor, DEFAULT_PAGE_SIZE + user.getId(), cursor, Math.min(size, MAX_PAGE_SIZE) ); var followers = result.contents().stream() - .map(dto -> UserFollowersResponse.Follower.builder() - .userId(dto.userId()) - .nickname(dto.nickname()) - .profileImageUrl(dto.profileImageUrl()) - .aliasName(dto.aliasName()) - .followerCount(dto.followerCount()) - .build()) + .map(followDtoMapper::toFollowerList) .toList(); return UserFollowersResponse.builder() @@ -48,7 +46,21 @@ public UserFollowersResponse getUserFollowers(Long userId, String cursor) { @Override @Transactional(readOnly = true) - public UserFollowersResponse getMyFollowing(String cursor) { - return null; + public UserFollowingResponse getMyFollowing(Long userId, String cursor, int size) { + User user = userCommandPort.findById(userId); + + CursorBasedList result = followingQueryPort.getFollowingByUserId( + user.getId(), cursor, Math.min(size, MAX_PAGE_SIZE) + ); + + var following = result.contents().stream() + .map(followDtoMapper::toFollowingList) + .toList(); + + return UserFollowingResponse.builder() + .followings(following) + .nextCursor(result.nextCursor()) + .isLast(!result.hasNext()) + .build(); } } From 596c918e9e5b3002e0e54f6d9a56f12792f34411 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Mon, 21 Jul 2025 18:58:42 +0900 Subject: [PATCH 12/13] =?UTF-8?q?[test]=20=EB=82=B4=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9E=89=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/UserGetFollowersApiTest.java | 12 +- .../in/web/UserGetFollowingApiTest.java | 103 ++++++++++++++++++ 2 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java index e3b59d27b..8a59ec1f8 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowersApiTest.java @@ -4,10 +4,9 @@ import konkuk.thip.common.util.TestEntityFactory; import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; -import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -17,6 +16,7 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @@ -30,6 +30,7 @@ @ActiveProfiles("test") @AutoConfigureMockMvc(addFilters = false) @DisplayName("[통합] 팔로워 조회 API 통합 테스트") +@Transactional class UserGetFollowersApiTest { @Autowired @@ -66,13 +67,6 @@ void setUp() { } } - @AfterEach - void tearDown() { - followingJpaRepository.deleteAllInBatch(); - userJpaRepository.deleteAll(); - aliasJpaRepository.deleteAll(); - } - @Test @DisplayName("팔로워가 12명일 때 2페이지에 걸쳐 모두 조회되고 커서가 올바르게 작동한다.") void getFollowersWithCursorPaging() throws Exception { diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java new file mode 100644 index 000000000..b19215dec --- /dev/null +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserGetFollowingApiTest.java @@ -0,0 +1,103 @@ +package konkuk.thip.user.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 내 팔로잉 리스트 조회 API 통합 테스트") +@Transactional +class UserGetFollowingApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private AliasJpaRepository aliasJpaRepository; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private FollowingJpaRepository followingJpaRepository; + + private UserJpaEntity loginUser; // 팔로잉 리스트를 조회하는 로그인한 사용자 + private List followingUsers; // 팔로잉 12명 + + @BeforeEach + void setUp() { + AliasJpaEntity alias = aliasJpaRepository.save(TestEntityFactory.createLiteratureAlias()); + + // 로그인 사용자 + loginUser = userJpaRepository.save(TestEntityFactory.createUser(alias)); + + // 팔로잉 12명 생성 및 저장 + followingUsers = new ArrayList<>(); + for (int i = 0; i < 12; i++) { + UserJpaEntity followingUser = userJpaRepository.save(TestEntityFactory.createUser(alias)); + followingUsers.add(followingUser); + followingJpaRepository.save(TestEntityFactory.createFollowing(loginUser, followingUser)); + } + } + + @Test + @DisplayName("로그인한 사용자의 팔로잉 수가 12명일 때 2페이지에 걸쳐 모두 조회되고 커서가 올바르게 작동한다.") + void getFollowingsWithCursorPaging() throws Exception { + // 1. 첫 번째 요청 (cursor 없음) + ResultActions firstPageResult = mockMvc.perform( + get("/users/my/following") + .requestAttr("userId", loginUser.getUserId()) + ); + + firstPageResult.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.followings", hasSize(10))) + .andExpect(jsonPath("$.data.isLast").value(false)) + .andExpect(jsonPath("$.data.nextCursor").exists()); + + // 커서 추출 + String responseBody = firstPageResult.andReturn().getResponse().getContentAsString(); + String nextCursor = objectMapper.readTree(responseBody) + .path("data") + .path("nextCursor") + .asText(); + + // 2. 두 번째 요청 (cursor 사용) + ResultActions secondPageResult = mockMvc.perform( + get("/users/my/following") + .param("cursor", nextCursor) + .requestAttr("userId", loginUser.getUserId()) + ); + + secondPageResult.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.followings", hasSize(2))) + .andExpect(jsonPath("$.data.isLast").value(true)) + .andExpect(jsonPath("$.data.nextCursor").doesNotExist()); + } +} \ No newline at end of file From b61097a3fab55137667f57a709dd1bbfb3db14cd Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 22 Jul 2025 00:59:15 +0900 Subject: [PATCH 13/13] =?UTF-8?q?[fix]=20=EC=9E=98=EB=AA=BB=EB=90=9C=20imp?= =?UTF-8?q?ort=20=EC=88=98=EC=A0=95=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/user/application/port/out/dto/FollowQueryDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java index 53f2d77ca..0b88f7d54 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java +++ b/src/main/java/konkuk/thip/user/application/port/out/dto/FollowQueryDto.java @@ -1,7 +1,7 @@ package konkuk.thip.user.application.port.out.dto; import com.querydsl.core.annotations.QueryProjection; -import io.jsonwebtoken.lang.Assert; +import org.springframework.util.Assert; import java.time.LocalDateTime;