diff --git a/src/main/java/com/example/codebase/controller/FollowController.java b/src/main/java/com/example/codebase/controller/FollowController.java index 6eefbb4f..54b5569d 100644 --- a/src/main/java/com/example/codebase/controller/FollowController.java +++ b/src/main/java/com/example/codebase/controller/FollowController.java @@ -1,17 +1,22 @@ package com.example.codebase.controller; +import com.example.codebase.domain.follow.dto.FollowMembersResponseDTO; import com.example.codebase.domain.follow.service.FollowService; import com.example.codebase.exception.LoginRequiredException; import com.example.codebase.util.SecurityUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.PositiveOrZero; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import java.util.Optional; + @RestController @Tag(name = "Follow", description = "팔로우 관련 API") @Validated @@ -45,4 +50,31 @@ public ResponseEntity unfollowMember(@PathVariable("username") String followUser return new ResponseEntity("언팔로잉 했습니다.", HttpStatus.NO_CONTENT); } + + @Operation(summary = "팔로잉", description = "해당 유저가 팔로잉 하는 사람 목록을 조회 합니다") + @GetMapping("/{username}/following") + public ResponseEntity getFollowingList(@PathVariable("username") String username, + @PositiveOrZero @RequestParam(defaultValue = "0") int page, + @PositiveOrZero @RequestParam(defaultValue = "10") int size) { + Optional loginUsername = SecurityUtil.getCurrentUsername(); + PageRequest pageRequest = PageRequest.of(page, size); + FollowMembersResponseDTO followingListResponse = followService.getFollowingList(loginUsername, username, pageRequest); + + return new ResponseEntity(followingListResponse, HttpStatus.OK); + } + + + @Operation(summary = "팔로워", description = "해당 유저를 팔로워 하는 사람 목록을 조회 합니다") + @GetMapping("{username}/follower") + public ResponseEntity getFollowerList(@PathVariable("username") String username, + @PositiveOrZero @RequestParam(defaultValue = "0") int page, + @PositiveOrZero @RequestParam(defaultValue = "10") int size) { + Optional loginUsername = SecurityUtil.getCurrentUsername(); + PageRequest pageRequest = PageRequest.of(page, size); + FollowMembersResponseDTO followingListResponse = followService.getFollowerList(loginUsername, username, pageRequest); + + return new ResponseEntity(followingListResponse, HttpStatus.OK); + } + + } diff --git a/src/main/java/com/example/codebase/domain/follow/dto/FollowMemberDetailResponseDTO.java b/src/main/java/com/example/codebase/domain/follow/dto/FollowMemberDetailResponseDTO.java new file mode 100644 index 00000000..de5b197e --- /dev/null +++ b/src/main/java/com/example/codebase/domain/follow/dto/FollowMemberDetailResponseDTO.java @@ -0,0 +1,33 @@ +package com.example.codebase.domain.follow.dto; + +import com.example.codebase.domain.member.entity.Member; +import com.example.codebase.domain.member.entity.RoleStatus; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@Builder +public class FollowMemberDetailResponseDTO { + + private String userId; + private String username; + private String profileImage; + private String introduction; + private RoleStatus roleStatus; + private String status; + + public static FollowMemberDetailResponseDTO of(Member member, String status){ + return FollowMemberDetailResponseDTO.builder() + .userId(member.getId().toString()) + .username(member.getUsername()) + .profileImage(member.getPicture()) + .introduction(member.getIntroduction()) + .roleStatus(member.getRoleStatus()) + .status(status) + .build(); + } +} diff --git a/src/main/java/com/example/codebase/domain/follow/dto/FollowMembersResponseDTO.java b/src/main/java/com/example/codebase/domain/follow/dto/FollowMembersResponseDTO.java new file mode 100644 index 00000000..d2f0149c --- /dev/null +++ b/src/main/java/com/example/codebase/domain/follow/dto/FollowMembersResponseDTO.java @@ -0,0 +1,25 @@ +package com.example.codebase.domain.follow.dto; + +import com.example.codebase.controller.dto.PageInfo; +import lombok.*; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class FollowMembersResponseDTO { + + private List followingList; + + private PageInfo pageInfo; + + public static FollowMembersResponseDTO of(List followMemberDetailResponseDTO, PageInfo pageInfo ) { + return FollowMembersResponseDTO.builder() + .followingList(followMemberDetailResponseDTO) + .pageInfo(pageInfo) + .build(); + } +} diff --git a/src/main/java/com/example/codebase/domain/follow/entity/Follow.java b/src/main/java/com/example/codebase/domain/follow/entity/Follow.java index ecef1dbc..f9f9ea34 100644 --- a/src/main/java/com/example/codebase/domain/follow/entity/Follow.java +++ b/src/main/java/com/example/codebase/domain/follow/entity/Follow.java @@ -18,28 +18,29 @@ public class Follow implements Persistable{ @Id @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "follow_id", columnDefinition = "BINARY(16)") - private Member follow; + @JoinColumn(name = "follower_id", columnDefinition = "BINARY(16)") + private Member follower; @Id @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "follower_id", columnDefinition = "BINARY(16)") - private Member follower; + @JoinColumn(name = "following_id", columnDefinition = "BINARY(16)") + private Member following; + @Column(name = "follow_time") @Builder.Default private LocalDateTime followTime = null; - public static Follow of(Member follow, Member follower) { + public static Follow of(Member follower, Member following) { return Follow.builder() - .follow(follow) .follower(follower) + .following(following) .build(); } @Override public FollowIds getId() { - return new FollowIds(this.follow.getId(), this.follower.getId()); + return new FollowIds(this.follower.getId(), this.following.getId()); } @Override diff --git a/src/main/java/com/example/codebase/domain/follow/entity/FollowIds.java b/src/main/java/com/example/codebase/domain/follow/entity/FollowIds.java index a5df9315..f2f75fc8 100644 --- a/src/main/java/com/example/codebase/domain/follow/entity/FollowIds.java +++ b/src/main/java/com/example/codebase/domain/follow/entity/FollowIds.java @@ -13,16 +13,17 @@ @AllArgsConstructor public class FollowIds implements Serializable { - private UUID follow; - private UUID follower; - public static FollowIds of(Member follow, Member follower) { - return new FollowIds(follow.getId(), follower.getId()); + private UUID following; + + + public static FollowIds of(Member follower, Member following) { + return new FollowIds(follower.getId(), following.getId()); } public void valid() { - if (follow == follower) { + if (follower == following) { throw new RuntimeException("자신을 팔로우 할 수 없습니다."); } } diff --git a/src/main/java/com/example/codebase/domain/follow/entity/FollowWithIsFollow.java b/src/main/java/com/example/codebase/domain/follow/entity/FollowWithIsFollow.java new file mode 100644 index 00000000..90e4cbb9 --- /dev/null +++ b/src/main/java/com/example/codebase/domain/follow/entity/FollowWithIsFollow.java @@ -0,0 +1,8 @@ +package com.example.codebase.domain.follow.entity; + +public interface FollowWithIsFollow { + + Follow getFollow(); + + String getStatus(); +} diff --git a/src/main/java/com/example/codebase/domain/follow/repository/FollowRepository.java b/src/main/java/com/example/codebase/domain/follow/repository/FollowRepository.java index 85bd6c6d..7b5b8911 100644 --- a/src/main/java/com/example/codebase/domain/follow/repository/FollowRepository.java +++ b/src/main/java/com/example/codebase/domain/follow/repository/FollowRepository.java @@ -2,7 +2,36 @@ import com.example.codebase.domain.follow.entity.Follow; import com.example.codebase.domain.follow.entity.FollowIds; +import com.example.codebase.domain.follow.entity.FollowWithIsFollow; +import com.example.codebase.domain.member.entity.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface FollowRepository extends JpaRepository { -} + + + @Query("SELECT f AS follow, " + + "CASE WHEN f.following = :loginMember THEN 'self' " + + "WHEN f2.follower = :loginMember THEN 'follow' ELSE 'none' END as status " + + "FROM Follow f LEFT JOIN Follow f2 ON f.following = f2.following " + + "AND f2.follower = :loginMember " + + "WHERE f.follower = :targetMember " + + "ORDER BY CASE WHEN f.following = :loginMember THEN 1 " + + "WHEN f2.follower = :loginMember THEN 2 ELSE 3 END, " + + "f.followTime ASC") + Page findFollowingByTargetMember(Member targetMember, Member loginMember, PageRequest pageRequest); + + + @Query("SELECT f AS follow, " + + "CASE WHEN f.follower = :loginMember THEN 'self' " + + "WHEN f2.follower = :loginMember THEN 'follow' ELSE 'none' END as status " + + "FROM Follow f LEFT JOIN Follow f2 ON f.follower = f2.following " + + "AND f2.follower = :loginMember " + + "WHERE f.following = :targetMember " + + "ORDER BY CASE WHEN f.follower = :loginMember THEN 1 " + + "WHEN f2.follower = :loginMember THEN 2 ELSE 3 END, " + + "f.followTime ASC") + Page findFollowerByTargetMember(Member targetMember, Member loginMember, PageRequest pageRequest); +} \ No newline at end of file diff --git a/src/main/java/com/example/codebase/domain/follow/service/FollowService.java b/src/main/java/com/example/codebase/domain/follow/service/FollowService.java index 82748be7..fb99c848 100644 --- a/src/main/java/com/example/codebase/domain/follow/service/FollowService.java +++ b/src/main/java/com/example/codebase/domain/follow/service/FollowService.java @@ -1,14 +1,22 @@ package com.example.codebase.domain.follow.service; +import com.example.codebase.controller.dto.PageInfo; +import com.example.codebase.domain.follow.dto.FollowMemberDetailResponseDTO; +import com.example.codebase.domain.follow.dto.FollowMembersResponseDTO; import com.example.codebase.domain.follow.entity.Follow; import com.example.codebase.domain.follow.entity.FollowIds; +import com.example.codebase.domain.follow.entity.FollowWithIsFollow; import com.example.codebase.domain.follow.repository.FollowRepository; import com.example.codebase.domain.member.entity.Member; +import com.example.codebase.domain.member.exception.NotFoundMemberException; import com.example.codebase.domain.member.repository.MemberRepository; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; import java.util.Optional; @Service @@ -55,4 +63,40 @@ public void unfollowMember(String username, String followUser) { throw new RuntimeException("팔로잉 중이 아닙니다."); } } + + @Transactional(readOnly = true) + public FollowMembersResponseDTO getFollowingList(Optional loginUsername, String targetUsername, PageRequest pageRequest) { + Member targetMember = memberRepository.findByUsername(targetUsername).orElseThrow(NotFoundMemberException::new); + Member loginMember = loginUsername.map(s -> memberRepository.findByUsername(s) + .orElseThrow(NotFoundMemberException::new)) + .orElse(null); + + Page followingList = followRepository.findFollowingByTargetMember(targetMember, loginMember, pageRequest); + PageInfo pageInfo = PageInfo.from(followingList); + + List followingMemberResponses = followingList.getContent().stream() + .map(followWithIsFollow -> + FollowMemberDetailResponseDTO.of(followWithIsFollow.getFollow().getFollowing(), followWithIsFollow.getStatus())) + .toList(); + + return FollowMembersResponseDTO.of(followingMemberResponses, pageInfo); + } + + @Transactional(readOnly = true) + public FollowMembersResponseDTO getFollowerList(Optional loginUsername, String targetUsername, PageRequest pageRequest) { + Member targetMember = memberRepository.findByUsername(targetUsername).orElseThrow(NotFoundMemberException::new); + Member loginMember = loginUsername.map(s -> memberRepository.findByUsername(s) + .orElseThrow(NotFoundMemberException::new)) + .orElse(null); + + Page followerList = followRepository.findFollowerByTargetMember(targetMember, loginMember, pageRequest); + PageInfo pageInfo = PageInfo.from(followerList); + + List followerMemberResponses = followerList.getContent().stream() + .map(followWithIsFollow -> + FollowMemberDetailResponseDTO.of(followWithIsFollow.getFollow().getFollower(), followWithIsFollow.getStatus())) + .toList(); + + return FollowMembersResponseDTO.of(followerMemberResponses, pageInfo); + } } diff --git a/src/main/resources/db/migration/V202402031753__modifiy_follow_id_name_and_position.sql b/src/main/resources/db/migration/V202402031753__modifiy_follow_id_name_and_position.sql new file mode 100644 index 00000000..de00cfd1 --- /dev/null +++ b/src/main/resources/db/migration/V202402031753__modifiy_follow_id_name_and_position.sql @@ -0,0 +1,3 @@ +ALTER TABLE `follow` + CHANGE COLUMN `follower_id` `following_id` BINARY(16) NOT NULL , + CHANGE COLUMN `follow_id` `follower_id` BINARY(16) NOT NULL ; \ No newline at end of file diff --git a/src/test/java/com/example/codebase/controller/FollowControllerTest.java b/src/test/java/com/example/codebase/controller/FollowControllerTest.java index d8717100..7ec8ff4c 100644 --- a/src/test/java/com/example/codebase/controller/FollowControllerTest.java +++ b/src/test/java/com/example/codebase/controller/FollowControllerTest.java @@ -119,7 +119,7 @@ public Member createOrLoadMember(String username, String... authorities) { } public Follow createOrLoadFollow(Member follower, Member following) { - return followRepository.save(Follow.of(follower,following)); + return followRepository.save(Follow.of(follower, following)); } @WithMockCustomUser(username = "testid", role = "USER") @@ -169,4 +169,140 @@ public Follow createOrLoadFollow(Member follower, Member following) { mockMvc.perform(delete(String.format("/api/follow/" + followUser.getUsername()))) .andExpect(status().isNoContent()); } + + @DisplayName("비 로그인 상태로 팔로잉 목록 조회시") + @Test + public void 비로그인_팔로잉_목록_조회() throws Exception { + Member member = createOrLoadMember(); + Member followUser = createOrLoadMember("followUser1", "ROLE_USER"); + Member followUser2 = createOrLoadMember("followUser2", "ROLE_USER"); + Member followUser3 = createOrLoadMember("followUser3", "ROLE_USER"); + Member followUser4 = createOrLoadMember("followUser4", "ROLE_USER"); + Member followUser5 = createOrLoadMember("followUser5", "ROLE_USER"); + + createOrLoadFollow(member, followUser4); + createOrLoadFollow(member, followUser); + createOrLoadFollow(member, followUser2); + createOrLoadFollow(member, followUser3); + createOrLoadFollow(member, followUser5); + + + mockMvc.perform(get(String.format("/api/follow/%s/following", member.getUsername()))) + .andDo(print()) + .andExpect(status().isOk()); + } + + @WithMockCustomUser(username = "testid", role = "USER") + @DisplayName("로그인 상태로 팔로잉 목록 조회시") + @Test + public void 로그인_상태로_팔로잉_목록_조회() throws Exception { + Member loginUser = createOrLoadMember(); + + Member member = createOrLoadMember("targetUser", "ROLE_USER"); + Member followUser1 = createOrLoadMember("followUser1", "ROLE_USER"); + Member followUser2 = createOrLoadMember("followUser2", "ROLE_USER"); + Member followUser3 = createOrLoadMember("followUser3", "ROLE_USER"); + Member followUser4 = createOrLoadMember("followUser4", "ROLE_USER"); + Member followUser5 = createOrLoadMember("followUser5", "ROLE_USER"); + Member followUser6 = createOrLoadMember("followUser6", "ROLE_USER"); + Member followUser7 = createOrLoadMember("followUser7", "ROLE_USER"); + Member followUser8 = createOrLoadMember("followUser8", "ROLE_USER"); + Member followUser9 = createOrLoadMember("followUser9", "ROLE_USER"); + Member followUser10 = createOrLoadMember("followUser10", "ROLE_USER"); + Member followUser11 = createOrLoadMember("followUser11", "ROLE_USER"); + Member followUser12 = createOrLoadMember("followUser12", "ROLE_USER"); + Member followUser13 = createOrLoadMember("followUser13", "ROLE_USER"); + + createOrLoadFollow(member, followUser4); + createOrLoadFollow(member, followUser1); + createOrLoadFollow(member, followUser2); + createOrLoadFollow(member, followUser3); + createOrLoadFollow(member, followUser5); + createOrLoadFollow(member, followUser6); + createOrLoadFollow(member, followUser7); + createOrLoadFollow(member, followUser8); + createOrLoadFollow(member, followUser9); + createOrLoadFollow(member, followUser10); + createOrLoadFollow(member, followUser11); + createOrLoadFollow(member, followUser12); + createOrLoadFollow(member, followUser13); + + createOrLoadFollow(member, loginUser); + + createOrLoadFollow(loginUser, followUser1); + createOrLoadFollow(loginUser, followUser4); + createOrLoadFollow(loginUser, followUser6); + + mockMvc.perform(get(String.format("/api/follow/%s/following", member.getUsername()))) + .andDo(print()) + .andExpect(status().isOk()); + } + + @DisplayName("비 로그인 상태로 팔로워 목록 조회시") + @Test + public void 비로그인_팔로워_목록_조회() throws Exception { + Member member = createOrLoadMember(); + + Member followUser = createOrLoadMember("followUser1", "ROLE_USER"); + Member followUser2 = createOrLoadMember("followUser2", "ROLE_USER"); + Member followUser3 = createOrLoadMember("followUser3", "ROLE_USER"); + Member followUser4 = createOrLoadMember("followUser4", "ROLE_USER"); + Member followUser5 = createOrLoadMember("followUser5", "ROLE_USER"); + + createOrLoadFollow(followUser4, member); + createOrLoadFollow(followUser, member); + createOrLoadFollow(followUser2, member); + createOrLoadFollow(followUser3, member); + createOrLoadFollow(followUser5, member); + + mockMvc.perform(get(String.format("/api/follow/%s/follower", member.getUsername()))) + .andDo(print()) + .andExpect(status().isOk()); + } + + @WithMockCustomUser(username = "testid", role = "USER") + @DisplayName("로그인 상태로 팔로워 목록 조회시") + @Test + public void 로그인_상태로_팔로워_목록_조회() throws Exception { + Member loginUser = createOrLoadMember(); + + Member member = createOrLoadMember("targetUser", "ROLE_USER"); + Member followUser1 = createOrLoadMember("followUser1", "ROLE_USER"); + Member followUser2 = createOrLoadMember("followUser2", "ROLE_USER"); + Member followUser3 = createOrLoadMember("followUser3", "ROLE_USER"); + Member followUser4 = createOrLoadMember("followUser4", "ROLE_USER"); + Member followUser5 = createOrLoadMember("followUser5", "ROLE_USER"); + Member followUser6 = createOrLoadMember("followUser6", "ROLE_USER"); + Member followUser7 = createOrLoadMember("followUser7", "ROLE_USER"); + Member followUser8 = createOrLoadMember("followUser8", "ROLE_USER"); + Member followUser9 = createOrLoadMember("followUser9", "ROLE_USER"); + Member followUser10 = createOrLoadMember("followUser10", "ROLE_USER"); + Member followUser11 = createOrLoadMember("followUser11", "ROLE_USER"); + + createOrLoadFollow(followUser4, member); + createOrLoadFollow(followUser1, member); + createOrLoadFollow(followUser2, member); + createOrLoadFollow(followUser3, member); + createOrLoadFollow(followUser5, member); + createOrLoadFollow(followUser6, member); + + createOrLoadFollow(followUser7, member); + createOrLoadFollow(followUser8, member); + + createOrLoadFollow(loginUser, followUser1); + createOrLoadFollow(loginUser, followUser4); + createOrLoadFollow(loginUser, followUser6); + createOrLoadFollow(loginUser, followUser2); + createOrLoadFollow(loginUser, followUser3); + + createOrLoadFollow(followUser9, member); + createOrLoadFollow(followUser10, member); + createOrLoadFollow(followUser11, member); + + createOrLoadFollow(loginUser, member); + + mockMvc.perform(get(String.format("/api/follow/%s/follower", member.getUsername()))) + .andDo(print()) + .andExpect(status().isOk()); + } } \ No newline at end of file