diff --git a/src/main/java/com/leets/X/domain/follow/controller/FollowController.java b/src/main/java/com/leets/X/domain/follow/controller/FollowController.java new file mode 100644 index 0000000..0df2bea --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/controller/FollowController.java @@ -0,0 +1,49 @@ +package com.leets.X.domain.follow.controller; + +import com.leets.X.domain.follow.dto.response.FollowResponse; +import com.leets.X.domain.follow.service.FollowService; +import com.leets.X.global.common.response.ResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static com.leets.X.domain.follow.controller.ResponseMessage.*; + +@Tag(name = "FOLLOW") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/follows") +public class FollowController { + private final FollowService followService; + + @PostMapping("/{userId}") + @Operation(summary = "팔로우 하기") + public ResponseDto follow(@PathVariable Long userId, @AuthenticationPrincipal String email){ + followService.follow(userId, email); + return ResponseDto.response(FOLLOW_SUCCESS.getCode(), FOLLOW_SUCCESS.getMessage()); + } + + @GetMapping("follower/{userId}") + @Operation(summary = "해당 유저의 팔로워 조회(해당 유저를 팔로우 하는 사용자 목록") + public ResponseDto> getFollowers(@PathVariable Long userId){ + return ResponseDto.response(GET_FOLLOWER_SUCCESS.getCode(), GET_FOLLOWER_SUCCESS.getMessage(), followService.getFollowers(userId)); + } + + @GetMapping("following/{userId}") + @Operation(summary = "해당 유저의 팔로잉 조회(해당 유저가 팔로우 하는 사용자 목록") + public ResponseDto> getFollowings(@PathVariable Long userId){ + return ResponseDto.response(GET_FOLLOWING_SUCCESS.getCode(), GET_FOLLOWING_SUCCESS.getMessage(), followService.getFollowings(userId)); + } + + @DeleteMapping("/{userId}") + @Operation(summary = "언팔로우 하기") + public ResponseDto unfollow(@PathVariable Long userId, @AuthenticationPrincipal String email){ + followService.unfollow(userId, email); + return ResponseDto.response(UNFOLLOW_SUCCESS.getCode(), UNFOLLOW_SUCCESS.getMessage()); + } + +} diff --git a/src/main/java/com/leets/X/domain/follow/controller/ResponseMessage.java b/src/main/java/com/leets/X/domain/follow/controller/ResponseMessage.java new file mode 100644 index 0000000..68bd857 --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/controller/ResponseMessage.java @@ -0,0 +1,17 @@ +package com.leets.X.domain.follow.controller; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ResponseMessage { + + FOLLOW_SUCCESS(200, "팔로우에 성공했습니다."), + GET_FOLLOWER_SUCCESS(200, "팔로워 목록 조회에 성공했습니다."), + GET_FOLLOWING_SUCCESS(200, "팔로잉 목록 조회에 성공했습니다."), + UNFOLLOW_SUCCESS(200, "언팔로우에 성공했습니다."); + + private final int code; + private final String message; +} diff --git a/src/main/java/com/leets/X/domain/follow/domain/Follow.java b/src/main/java/com/leets/X/domain/follow/domain/Follow.java index 8f13cdc..df151f4 100644 --- a/src/main/java/com/leets/X/domain/follow/domain/Follow.java +++ b/src/main/java/com/leets/X/domain/follow/domain/Follow.java @@ -1,2 +1,35 @@ -package com.leets.X.domain.follow.domain;public class Follow { +package com.leets.X.domain.follow.domain; + +import com.leets.X.domain.user.domain.User; +import com.leets.X.global.common.domain.BaseTimeEntity; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Getter +public class Follow extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "follow_id") + private Long id; + + @ManyToOne + @JoinColumn(name = "follower_id") + private User follower; + + @ManyToOne + @JoinColumn(name = "followed_id") + private User followed; + + public static Follow of(User follower, User followed) { + return Follow.builder() + .follower(follower) + .followed(followed) + .build(); + } + } diff --git a/src/main/java/com/leets/X/domain/follow/dto/response/FollowResponse.java b/src/main/java/com/leets/X/domain/follow/dto/response/FollowResponse.java new file mode 100644 index 0000000..47658a2 --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/dto/response/FollowResponse.java @@ -0,0 +1,22 @@ +package com.leets.X.domain.follow.dto.response; + +import com.leets.X.domain.user.domain.User; +import lombok.Builder; + +@Builder +public record FollowResponse( + Long id, + String name, + String customId, + String introduce +// boolean followStatus +) { + public static FollowResponse from(User user) { + return FollowResponse.builder() + .id(user.getId()) + .name(user.getName()) + .customId(user.getCustomId()) + .introduce(user.getIntroduce()) + .build(); + } +} diff --git a/src/main/java/com/leets/X/domain/follow/exception/AlreadyFollowException.java b/src/main/java/com/leets/X/domain/follow/exception/AlreadyFollowException.java new file mode 100644 index 0000000..0b5f052 --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/exception/AlreadyFollowException.java @@ -0,0 +1,12 @@ +package com.leets.X.domain.follow.exception; + +import com.leets.X.global.common.exception.BaseException; + +import static com.leets.X.domain.follow.exception.ErrorMessage.*; + +public class AlreadyFollowException extends BaseException { + public AlreadyFollowException() { + super(ALREADY_FOLLOW.getCode(), ALREADY_FOLLOW.getMessage()); + } +} + diff --git a/src/main/java/com/leets/X/domain/follow/exception/ErrorMessage.java b/src/main/java/com/leets/X/domain/follow/exception/ErrorMessage.java new file mode 100644 index 0000000..9edae3d --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/exception/ErrorMessage.java @@ -0,0 +1,20 @@ +package com.leets.X.domain.follow.exception; + + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorMessage { + + ALREADY_FOLLOW(400,"이미 팔로우한 상태입니다."), + INVALID_FOLLOW(400, "자기 자신은 팔로우 할 수 없습니다."), + FOLLOW_NOT_FOUND(404, "팔로우 데이터를 찾을 수 없습니다."), + INVALID_UNFOLLOW(400, "팔로우 하지 않은 상대는 언팔로우 할 수 없습니다."); + + + private final int code; + private final String message; + +} diff --git a/src/main/java/com/leets/X/domain/follow/exception/FollowNotFoundException.java b/src/main/java/com/leets/X/domain/follow/exception/FollowNotFoundException.java new file mode 100644 index 0000000..54232fb --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/exception/FollowNotFoundException.java @@ -0,0 +1,12 @@ +package com.leets.X.domain.follow.exception; + +import com.leets.X.global.common.exception.BaseException; + +import static com.leets.X.domain.follow.exception.ErrorMessage.*; + +public class FollowNotFoundException extends BaseException { + public FollowNotFoundException() { + super(FOLLOW_NOT_FOUND.getCode(), FOLLOW_NOT_FOUND.getMessage()); + } +} + diff --git a/src/main/java/com/leets/X/domain/follow/exception/InvalidFollowException.java b/src/main/java/com/leets/X/domain/follow/exception/InvalidFollowException.java new file mode 100644 index 0000000..ce4d3b3 --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/exception/InvalidFollowException.java @@ -0,0 +1,11 @@ +package com.leets.X.domain.follow.exception; + +import com.leets.X.global.common.exception.BaseException; + +import static com.leets.X.domain.follow.exception.ErrorMessage.*; + +public class InvalidFollowException extends BaseException { + public InvalidFollowException() { + super(INVALID_FOLLOW.getCode(), INVALID_FOLLOW.getMessage()); + } +} diff --git a/src/main/java/com/leets/X/domain/follow/exception/InvalidUnfollowException.java b/src/main/java/com/leets/X/domain/follow/exception/InvalidUnfollowException.java new file mode 100644 index 0000000..87e8213 --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/exception/InvalidUnfollowException.java @@ -0,0 +1,11 @@ +package com.leets.X.domain.follow.exception; + +import com.leets.X.global.common.exception.BaseException; + +import static com.leets.X.domain.follow.exception.ErrorMessage.*; + +public class InvalidUnfollowException extends BaseException { + public InvalidUnfollowException() { + super(INVALID_UNFOLLOW.getCode(), INVALID_UNFOLLOW.getMessage()); + } +} diff --git a/src/main/java/com/leets/X/domain/follow/repository/FollowRepository.java b/src/main/java/com/leets/X/domain/follow/repository/FollowRepository.java new file mode 100644 index 0000000..8bea8e0 --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/repository/FollowRepository.java @@ -0,0 +1,21 @@ +package com.leets.X.domain.follow.repository; + +import com.leets.X.domain.follow.domain.Follow; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface FollowRepository extends JpaRepository { + + Optional findByFollowerIdAndFollowedId(Long followerId, Long followedId); + + // followerId가 포함된 객체 리스트 반환 + List findByFollowerId(Long followerId); + + // followedId가 포함된 객체 리스트 반환 + List findByFollowedId(Long followedId); + + boolean existsByFollowerIdAndFollowedId(Long followerId, Long followedId); + +} diff --git a/src/main/java/com/leets/X/domain/follow/service/FollowService.java b/src/main/java/com/leets/X/domain/follow/service/FollowService.java new file mode 100644 index 0000000..048bb4c --- /dev/null +++ b/src/main/java/com/leets/X/domain/follow/service/FollowService.java @@ -0,0 +1,96 @@ +package com.leets.X.domain.follow.service; + +import com.leets.X.domain.follow.domain.Follow; +import com.leets.X.domain.follow.dto.response.FollowResponse; +import com.leets.X.domain.follow.exception.AlreadyFollowException; +import com.leets.X.domain.follow.exception.FollowNotFoundException; +import com.leets.X.domain.follow.exception.InvalidFollowException; +import com.leets.X.domain.follow.exception.InvalidUnfollowException; +import com.leets.X.domain.follow.repository.FollowRepository; +import com.leets.X.domain.user.domain.User; +import com.leets.X.domain.user.service.UserService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +@Slf4j +@Service +@RequiredArgsConstructor +public class FollowService { + private final FollowRepository followRepository; + private final UserService userService; + + @Transactional + public void follow(Long userId, String email){ + User follower = userService.find(email); + User followed = userService.find(userId); + + validate(follower.getId(), followed.getId()); + + Follow follow = followRepository.save(Follow.of(follower, followed)); + + follower.addFollowing(follow); + followed.addFollower(follow); + } + + public List getFollowers(Long userId){ + User user = userService.find(userId); + + List followerList = user.getFollowerList(); + + return followerList.stream() + .map(follow -> { + return FollowResponse.from(follow.getFollower()); }) + .toList(); + } + + public List getFollowings(Long userId){ + User user = userService.find(userId); + + List followingList = user.getFollowingList(); + + return followingList.stream() + .map(follow -> { + return FollowResponse.from(follow.getFollowed()); }) + .toList(); + } + + @Transactional + public void unfollow(Long userId, String email){ + User follower = userService.find(email); + User followed = userService.find(userId); + + Follow follow = check(follower.getId(), followed.getId()); + + follower.removeFollowing(follow); + followed.removeFollower(follow); + + followRepository.delete(follow); + } + + public Follow find(Long followerId, Long followedId){ + return followRepository.findByFollowerIdAndFollowedId(followerId, followedId) + .orElseThrow(FollowNotFoundException::new); + } + + // 기존 팔로우 정보가 있는지, 나한테 요청을 하지 않는지 검증 + private void validate(Long followerId, Long followedId){ + if(followRepository.existsByFollowerIdAndFollowedId(followerId, followedId)){ + throw new AlreadyFollowException(); + } + if(followerId.equals(followedId)){ + throw new InvalidFollowException(); + } + } + + // 팔로우 되어 있는지 확인 + private Follow check(Long followerId, Long followedId){ + if(!followRepository.existsByFollowerIdAndFollowedId(followerId, followedId)){ + throw new InvalidUnfollowException(); + } + return find(followerId, followedId); + } + +} diff --git a/src/main/java/com/leets/X/domain/user/domain/User.java b/src/main/java/com/leets/X/domain/user/domain/User.java index 2378641..89a3001 100644 --- a/src/main/java/com/leets/X/domain/user/domain/User.java +++ b/src/main/java/com/leets/X/domain/user/domain/User.java @@ -1,5 +1,6 @@ package com.leets.X.domain.user.domain; +import com.leets.X.domain.follow.domain.Follow; import com.leets.X.domain.like.domain.Like; import com.leets.X.domain.post.domain.Post; import com.leets.X.domain.user.dto.request.UserInitializeRequest; @@ -55,6 +56,12 @@ public class User extends BaseTimeEntity { @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE, orphanRemoval = true) private List likes = new ArrayList<>(); + @OneToMany(mappedBy = "followed", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List followerList = new ArrayList<>(); + + @OneToMany(mappedBy = "follower", cascade = CascadeType.REMOVE, orphanRemoval = true) + private List followingList = new ArrayList<>(); + public void initProfile(UserInitializeRequest dto){ this.birth = dto.birth(); this.customId = dto.customId(); @@ -67,4 +74,22 @@ public void update(UserUpdateRequest dto){ this.webSite = dto.webSite(); } + public void addFollower(Follow follow) { + this.followerList.add(follow); + } + + public void addFollowing(Follow follow) { + this.followingList.add(follow); + } + + public void removeFollower(Follow follow) { + this.followerList.remove(follow); + } + + public void removeFollowing(Follow follow) { + this.followingList.remove(follow); + } + + + } diff --git a/src/main/java/com/leets/X/domain/user/service/UserService.java b/src/main/java/com/leets/X/domain/user/service/UserService.java index e967c5f..07d5d38 100644 --- a/src/main/java/com/leets/X/domain/user/service/UserService.java +++ b/src/main/java/com/leets/X/domain/user/service/UserService.java @@ -77,6 +77,11 @@ public UserProfileResponse getProfile(Long userId, String email){ // 아니라면 false return UserProfileResponse.from(user, false); } +// +// @Transactional +// public void delete(Long userId){ +// userRepository.deleteById(userId); +// } private UserSocialLoginResponse loginUser(String email) { User user = find(email); @@ -111,6 +116,11 @@ public User find(String email){ .orElseThrow(UserNotFoundException::new); } + public User find(Long userId){ + return userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + } + public boolean existUser(String email){ return userRepository.existsByEmail(email); } diff --git a/src/main/java/com/leets/X/global/config/SecurityConfig.java b/src/main/java/com/leets/X/global/config/SecurityConfig.java index 4e46a94..2e8a638 100644 --- a/src/main/java/com/leets/X/global/config/SecurityConfig.java +++ b/src/main/java/com/leets/X/global/config/SecurityConfig.java @@ -51,6 +51,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorize .requestMatchers("/v3/api-docs", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/swagger/**").permitAll() .requestMatchers("/api/v1/users/login").permitAll() + .requestMatchers("/api/v1/users/profile/{userId}").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)