Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,4 +91,18 @@ public ApiResponse<SearchResponseDTO.GetCardListResponseDto> getCardsByHashtag(
SearchResponseDTO.GetCardListResponseDto result = searchService.getCardsByHashtag(request, hashtagId, limit, cursor);
return ApiResponse.onSuccess(result);
}

@Operation(summary = "해당 유저 팔로우 목록 조회", description = "팔로우 - 해시태그 검색 api")
@GetMapping("/followList")
public ApiResponse<SearchResponseDTO.GetAccountListResponseDtoWithFollow> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -82,6 +108,19 @@ public static class GetAccountListResponseDto {
private boolean hasNext;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class GetAccountListResponseDtoWithFollow {
private List<GetAccountResponseDtoWithFollow> accounts;
private Long nextCursor;
@NotNull
private int size;
@NotNull
private boolean hasNext;
}

// 탐색하기 검색창에서 검색 범위가 전체일 때 해시태그 검색하기 (해시태그 하나 ver)
@Builder
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Slice<Card> 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
""")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -208,4 +205,47 @@ private Map<Long, Long> getMapCardCountByHashtag(List<Long> 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<User> users = new SliceImpl<>(Collections.emptyList(), pageable, false);
switch(status){
case FOLLOWED -> { // 해당 유저를 팔로우한 사람 목록
users = userRepository.searchAccountNotInFollow(query, cursor, pageable, userId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

FOLLOWED 케이스의 로직에 오류가 있습니다. 주석에는 "해당 유저를 팔로우한 사람 목록"을 조회한다고 되어 있지만, 실제 호출되는 userRepository.searchAccountNotInFollow 메소드는 userId가 팔로우하지 않는 사용자 목록을 조회합니다. 이는 기능의 요구사항과 맞지 않는 심각한 버그입니다. userId를 팔로우하는 사용자(팔로워) 목록을 조회하는 새로운 레포지토리 메소드를 구현하고 호출해야 합니다. UserRepository에 제안한 변경사항을 참고하여 수정해주세요.

}
case FOLLOWING -> { // 해당 유저가 팔로우한 사람 목록
users = userRepository.searchAccountInFollow(query, cursor, pageable, userId);
}
}

// 검색 결과가 없으면 예외 발생
if (users.isEmpty()) {
throw new ExceptionHandler(ErrorStatus._NO_RESULTS_FOUND);
}

List<Long> targetUserIds = users.getContent().stream()
.map(User::getId)
.toList();
// 내가 팔로우한 유저 목록
Set<Long> alreadyFollowedIdSet = new HashSet<>(userFollowRepository.findFollowingUserIds(me.getId()));



List<SearchResponseDTO.GetAccountResponseDtoWithFollow> 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);
Comment on lines +239 to +249

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

페이징 처리를 위해 limit + 1개의 데이터를 조회한 후, hasNext가 true일 때 응답 결과 리스트에서 마지막 아이템을 제거하여 limit 개수만큼만 반환해야 합니다. 현재 코드에서는 이 부분이 누락되어 클라이언트가 의도보다 많은 데이터를 받을 수 있습니다. 다른 API들처럼 리스트를 잘라내는 로직을 추가해야 합니다.

        boolean hasNext = users.hasNext();
        Long nextCursor = hasNext ? users.getContent().get(users.getContent().size() - 1).getId() : null;

        List<User> userList = users.getContent();
        if (hasNext) {
            userList = userList.subList(0, limit);
        }

        List<SearchResponseDTO.GetAccountResponseDtoWithFollow> result = userList.stream()
                .map(user -> UserConverter.toAccountDtoWithFollow(
                        user,
                        alreadyFollowedIdSet.contains(user.getId()))
                ).toList();

        return new SearchResponseDTO.GetAccountListResponseDtoWithFollow(result, nextCursor, result.size(), hasNext);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import EatPic.spring.domain.user.mapping.UserFollow;
import org.springframework.data.domain.Page;

import java.util.List;

public class UserConverter {

// 이메일 회원가입
Expand Down Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package EatPic.spring.domain.user.entity;

public enum FollowStatus {
FOLLOWING, FOLLOWED
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ public interface UserFollowRepository extends JpaRepository<UserFollow,Long> {

Long countUserFollowByTargetUser(User targetUser);
Long countUserFollowByUser(User user);


}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,19 @@ Slice<User> searchAccountInAll(@Param("query") String query,
Slice<User> 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<User> searchAccountNotInFollow(@Param("query") String query,
@Param("cursor") Long cursor, Pageable pageable, @Param("loginUserId") Long userId);
Comment on lines +45 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

searchAccountNotInFollow 메소드는 이름 그대로 특정 유저가 '팔로우하지 않는' 유저를 검색합니다. 하지만 SearchServiceImplgetFollowList에서는 FollowStatus.FOLLOWED (팔로워 목록 조회) 케이스에서 이 메소드를 사용하고 있습니다. 이는 기능 요구사항과 맞지 않습니다. 팔로워 목록을 조회하기 위한 쿼리로 수정하고, 혼동을 피하기 위해 메소드 이름도 searchFollowersByNickname 등으로 변경하는 것을 강력히 권장합니다.

    @Query("""
    SELECT u
    FROM User u
    JOIN UserFollow uf ON u.id = uf.user.id
    WHERE uf.targetUser.id = :userId
    AND (:cursor IS NULL OR u.id > :cursor)
    AND u.nickname LIKE %:query%
    ORDER BY u.id ASC
    """)
    Slice<User> searchFollowersByNickname(@Param("query") String query,
            @Param("cursor") Long cursor, Pageable pageable, @Param("userId") Long userId);


}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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;
import EatPic.spring.domain.user.dto.response.LoginResponseDTO;
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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.*;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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.*;

Expand Down Expand Up @@ -282,4 +282,5 @@ public UserResponseDTO.ProfileDto updateIntroduce(HttpServletRequest request, Us
.introduce(user.getIntroduce())
.build();
}

}
Loading