diff --git a/src/main/java/EatPic/spring/domain/card/controller/SearchController.java b/src/main/java/EatPic/spring/domain/card/controller/SearchController.java index 61d722a..e967a64 100644 --- a/src/main/java/EatPic/spring/domain/card/controller/SearchController.java +++ b/src/main/java/EatPic/spring/domain/card/controller/SearchController.java @@ -3,6 +3,7 @@ import EatPic.spring.domain.card.dto.response.SearchResponseDTO; import EatPic.spring.domain.card.repository.CardRepository; import EatPic.spring.domain.card.service.SearchServiceImpl; +import EatPic.spring.domain.user.entity.FollowStatus; import EatPic.spring.global.common.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -90,4 +91,18 @@ public ApiResponse getCardsByHashtag( SearchResponseDTO.GetCardListResponseDto result = searchService.getCardsByHashtag(request, hashtagId, limit, cursor); return ApiResponse.onSuccess(result); } + + @Operation(summary = "해당 유저 팔로우 목록 조회", description = "팔로우 - 해시태그 검색 api") + @GetMapping("/followList") + public ApiResponse searchFollowList( + HttpServletRequest request, + @RequestParam(value = "follow status")FollowStatus status, + @RequestParam(value = "userId")Long userId, + @RequestParam(value = "query") String query, + @RequestParam(value = "limit", required = false, defaultValue = "10") int limit, + @RequestParam(value = "cursor", required = false) Long cursor + ) { + SearchResponseDTO.GetAccountListResponseDtoWithFollow result = searchService.getFollowList(request,userId,status,query,limit,cursor); + return ApiResponse.onSuccess(result); + } } diff --git a/src/main/java/EatPic/spring/domain/card/dto/response/SearchResponseDTO.java b/src/main/java/EatPic/spring/domain/card/dto/response/SearchResponseDTO.java index aa72390..525e44f 100644 --- a/src/main/java/EatPic/spring/domain/card/dto/response/SearchResponseDTO.java +++ b/src/main/java/EatPic/spring/domain/card/dto/response/SearchResponseDTO.java @@ -68,6 +68,32 @@ public static class GetAccountResponseDto { private String profileImageUrl; // 프사 } + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class GetAccountResponseDtoWithFollow{ + @JsonProperty("user_id") + @NotNull + private Long userId; + + @JsonProperty("name_id") + @NotNull + private String nameId; // 유저 아이디 + + @JsonProperty("nickname") + @NotNull + private String nickname; // 유저 닉네임 + + @JsonProperty("profile_image_url") + @NotNull + private String profileImageUrl; // 프사 + + @JsonProperty("isFollowed") + @NotNull + private boolean isFollowed; + } + // 탐색하기 검색창에서 검색 범위가 전체일 때 계정 검색하기 (계정 여러 개 리스트로..) @Builder @Getter @@ -82,6 +108,19 @@ public static class GetAccountListResponseDto { private boolean hasNext; } + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class GetAccountListResponseDtoWithFollow { + private List accounts; + private Long nextCursor; + @NotNull + private int size; + @NotNull + private boolean hasNext; + } + // 탐색하기 검색창에서 검색 범위가 전체일 때 해시태그 검색하기 (해시태그 하나 ver) @Builder @Getter diff --git a/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java b/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java index cb2ef6a..eb4db2c 100644 --- a/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java +++ b/src/main/java/EatPic/spring/domain/card/repository/CardRepository.java @@ -69,6 +69,7 @@ Slice findFeedExcludeBlocked( @Query(""" SELECT c FROM Card c JOIN Reaction r ON r.card = c + WHERE c.isDeleted = false GROUP BY c HAVING COUNT(r) >= 1 """) diff --git a/src/main/java/EatPic/spring/domain/card/service/SearchService.java b/src/main/java/EatPic/spring/domain/card/service/SearchService.java index 482e15f..9d30207 100644 --- a/src/main/java/EatPic/spring/domain/card/service/SearchService.java +++ b/src/main/java/EatPic/spring/domain/card/service/SearchService.java @@ -1,6 +1,7 @@ package EatPic.spring.domain.card.service; import EatPic.spring.domain.card.dto.response.SearchResponseDTO; +import EatPic.spring.domain.user.entity.FollowStatus; import jakarta.servlet.http.HttpServletRequest; public interface SearchService { @@ -10,4 +11,5 @@ public interface SearchService { SearchResponseDTO.GetHashtagListResponseDto getHashtagInFollow(HttpServletRequest request, String query, int limit, Long cursor); SearchResponseDTO.GetHashtagListResponseDto getHashtagInAll(HttpServletRequest request, String query, int limit, Long cursor); SearchResponseDTO.GetCardListResponseDto getCardsByHashtag(HttpServletRequest request, Long hashtagId, int limit, Long cursor); + SearchResponseDTO.GetAccountListResponseDtoWithFollow getFollowList(HttpServletRequest request, Long userId, FollowStatus status, String query, int limit, Long cursor); } diff --git a/src/main/java/EatPic/spring/domain/card/service/SearchServiceImpl.java b/src/main/java/EatPic/spring/domain/card/service/SearchServiceImpl.java index b56be23..75a3fec 100644 --- a/src/main/java/EatPic/spring/domain/card/service/SearchServiceImpl.java +++ b/src/main/java/EatPic/spring/domain/card/service/SearchServiceImpl.java @@ -10,6 +10,7 @@ import EatPic.spring.domain.reaction.repository.ReactionRepository; import EatPic.spring.domain.reaction.service.ReactionService; import EatPic.spring.domain.user.converter.UserConverter; +import EatPic.spring.domain.user.entity.FollowStatus; import EatPic.spring.domain.user.entity.User; import EatPic.spring.domain.user.repository.UserFollowRepository; import EatPic.spring.domain.user.repository.UserRepository; @@ -18,14 +19,10 @@ import EatPic.spring.global.common.exception.handler.ExceptionHandler; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; // 자동으로 생성자 주입 -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; +import org.springframework.data.domain.*; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Service @@ -208,4 +205,47 @@ private Map getMapCardCountByHashtag(List hashtagIds){ row -> (Long) row[1] // count )); } + + @Override + public SearchResponseDTO.GetAccountListResponseDtoWithFollow getFollowList(HttpServletRequest request, Long userId, FollowStatus status, String query, int limit, Long cursor) { + + User me = userService.getLoginUser(request); + + // 페이징 처리 하기 + Pageable pageable = PageRequest.of(0, limit + 1, Sort.by("id").ascending()); + Slice users = new SliceImpl<>(Collections.emptyList(), pageable, false); + switch(status){ + case FOLLOWED -> { // 해당 유저를 팔로우한 사람 목록 + users = userRepository.searchAccountNotInFollow(query, cursor, pageable, userId); + } + case FOLLOWING -> { // 해당 유저가 팔로우한 사람 목록 + users = userRepository.searchAccountInFollow(query, cursor, pageable, userId); + } + } + + // 검색 결과가 없으면 예외 발생 + if (users.isEmpty()) { + throw new ExceptionHandler(ErrorStatus._NO_RESULTS_FOUND); + } + + List targetUserIds = users.getContent().stream() + .map(User::getId) + .toList(); + // 내가 팔로우한 유저 목록 + Set alreadyFollowedIdSet = new HashSet<>(userFollowRepository.findFollowingUserIds(me.getId())); + + + + List result = users.getContent().stream() + .map(user -> UserConverter.toAccountDtoWithFollow( + user, + alreadyFollowedIdSet.contains(user.getId())) + ).toList(); + + + boolean hasNext = users.hasNext(); + Long nextCursor = hasNext ? users.getContent().get(users.getContent().size() - 1).getId() : null; + + return new SearchResponseDTO.GetAccountListResponseDtoWithFollow(result, nextCursor, result.size(), hasNext); + } } \ No newline at end of file diff --git a/src/main/java/EatPic/spring/domain/user/converter/UserConverter.java b/src/main/java/EatPic/spring/domain/user/converter/UserConverter.java index 21d4917..1e9de97 100644 --- a/src/main/java/EatPic/spring/domain/user/converter/UserConverter.java +++ b/src/main/java/EatPic/spring/domain/user/converter/UserConverter.java @@ -10,6 +10,8 @@ import EatPic.spring.domain.user.mapping.UserFollow; import org.springframework.data.domain.Page; +import java.util.List; + public class UserConverter { // 이메일 회원가입 @@ -113,6 +115,17 @@ public static SearchResponseDTO.GetAccountResponseDto toAccountDto(User user) { .build(); } + public static SearchResponseDTO.GetAccountResponseDtoWithFollow toAccountDtoWithFollow(User user, boolean isFollowed) { + return SearchResponseDTO.GetAccountResponseDtoWithFollow.builder() + .userId(user.getId()) + .nameId(user.getNameId()) + .nickname(user.getNickname()) + .profileImageUrl(user.getProfileImageUrl()) + .isFollowed(isFollowed) + .build(); + } + + public static UserResponseDTO.UserActionResponseDto toUserActionResponseDto(UserBlock userBlock) { return UserResponseDTO.UserActionResponseDto.builder() .userId(userBlock.getUser().getId()) diff --git a/src/main/java/EatPic/spring/domain/user/entity/FollowStatus.java b/src/main/java/EatPic/spring/domain/user/entity/FollowStatus.java new file mode 100644 index 0000000..5b30f66 --- /dev/null +++ b/src/main/java/EatPic/spring/domain/user/entity/FollowStatus.java @@ -0,0 +1,5 @@ +package EatPic.spring.domain.user.entity; + +public enum FollowStatus { + FOLLOWING, FOLLOWED +} diff --git a/src/main/java/EatPic/spring/domain/user/repository/UserFollowRepository.java b/src/main/java/EatPic/spring/domain/user/repository/UserFollowRepository.java index 635264b..08257aa 100644 --- a/src/main/java/EatPic/spring/domain/user/repository/UserFollowRepository.java +++ b/src/main/java/EatPic/spring/domain/user/repository/UserFollowRepository.java @@ -26,4 +26,6 @@ public interface UserFollowRepository extends JpaRepository { Long countUserFollowByTargetUser(User targetUser); Long countUserFollowByUser(User user); + + } diff --git a/src/main/java/EatPic/spring/domain/user/repository/UserRepository.java b/src/main/java/EatPic/spring/domain/user/repository/UserRepository.java index 107c104..3a2b195 100644 --- a/src/main/java/EatPic/spring/domain/user/repository/UserRepository.java +++ b/src/main/java/EatPic/spring/domain/user/repository/UserRepository.java @@ -42,4 +42,19 @@ Slice searchAccountInAll(@Param("query") String query, Slice searchAccountInFollow(@Param("query") String query, @Param("cursor") Long cursor, Pageable pageable, @Param("loginUserId") Long userId); + @Query(""" + SELECT u + FROM User u + WHERE u.id NOT IN ( + SELECT uf.targetUser.id + FROM UserFollow uf + WHERE uf.user.id = :loginUserId + ) + AND (:cursor IS NULL OR u.id > :cursor) + AND u.nickname LIKE %:query% + ORDER BY u.id ASC + """) + Slice searchAccountNotInFollow(@Param("query") String query, + @Param("cursor") Long cursor, Pageable pageable, @Param("loginUserId") Long userId); + } \ No newline at end of file diff --git a/src/main/java/EatPic/spring/domain/user/service/UserService.java b/src/main/java/EatPic/spring/domain/user/service/UserService.java index 363ca3a..e2d7eaf 100644 --- a/src/main/java/EatPic/spring/domain/user/service/UserService.java +++ b/src/main/java/EatPic/spring/domain/user/service/UserService.java @@ -1,5 +1,6 @@ package EatPic.spring.domain.user.service; +import EatPic.spring.domain.card.dto.response.SearchResponseDTO; import EatPic.spring.domain.user.dto.*; import EatPic.spring.domain.user.dto.request.LoginRequestDTO; import EatPic.spring.domain.user.dto.request.UserRequest; @@ -7,6 +8,7 @@ import EatPic.spring.domain.user.dto.response.UserResponseDTO; import EatPic.spring.domain.user.dto.request.SignupRequestDTO; import EatPic.spring.domain.user.dto.response.SignupResponseDTO; +import EatPic.spring.domain.user.entity.FollowStatus; import EatPic.spring.domain.user.entity.User; import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.multipart.MultipartFile; diff --git a/src/main/java/EatPic/spring/domain/user/service/UserServiceImpl.java b/src/main/java/EatPic/spring/domain/user/service/UserServiceImpl.java index 87ffb73..cbc42d4 100644 --- a/src/main/java/EatPic/spring/domain/user/service/UserServiceImpl.java +++ b/src/main/java/EatPic/spring/domain/user/service/UserServiceImpl.java @@ -1,5 +1,6 @@ package EatPic.spring.domain.user.service; +import EatPic.spring.domain.card.dto.response.SearchResponseDTO; import EatPic.spring.domain.card.repository.CardRepository; import EatPic.spring.domain.user.converter.UserConverter; import EatPic.spring.domain.user.dto.*; @@ -9,6 +10,7 @@ import EatPic.spring.domain.user.dto.response.LoginResponseDTO; import EatPic.spring.domain.user.dto.response.SignupResponseDTO; import EatPic.spring.domain.user.dto.response.UserResponseDTO; +import EatPic.spring.domain.user.entity.FollowStatus; import EatPic.spring.domain.user.entity.User; import EatPic.spring.domain.user.entity.UserStatus; import EatPic.spring.domain.user.mapping.UserBlock; @@ -22,8 +24,7 @@ import EatPic.spring.global.config.jwt.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.*; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; @@ -32,8 +33,7 @@ import org.springframework.web.multipart.MultipartFile; import EatPic.spring.global.aws.s3.*; -import java.util.Collections; -import java.util.UUID; +import java.util.*; import static EatPic.spring.global.common.code.status.ErrorStatus.*; @@ -282,4 +282,5 @@ public UserResponseDTO.ProfileDto updateIntroduce(HttpServletRequest request, Us .introduce(user.getIntroduce()) .build(); } + } \ No newline at end of file