-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[feat] #21 팔로우 도메인 개발 #21
The head ref may contain hidden characters: "feat/#20/\uD314\uB85C\uC6B0-\uB3C4\uBA54\uC778-\uAC1C\uBC1C"
Changes from 21 commits
e74376f
40d1fbf
5791bed
3a51aa6
2c49833
a5af66c
78310df
7c03a77
2954eda
4e7d09e
f63a1af
a8cbeec
adc9511
cea961f
beb7dd5
5ffa9dd
2c0bac6
5e42ab6
8af69ea
c61ce74
40a0974
89d5ed9
caa2d82
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String> 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<List<FollowResponse>> 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<List<FollowResponse>> getFollowings(@PathVariable Long userId){ | ||
return ResponseDto.response(GET_FOLLOWING_SUCCESS.getCode(), GET_FOLLOWING_SUCCESS.getMessage(), followService.getFollowings(userId)); | ||
} | ||
|
||
@DeleteMapping("/{userId}") | ||
@Operation(summary = "언팔로우 하기") | ||
public ResponseDto<String> unfollow(@PathVariable Long userId, @AuthenticationPrincipal String email){ | ||
followService.unfollow(userId, email); | ||
return ResponseDto.response(UNFOLLOW_SUCCESS.getCode(), UNFOLLOW_SUCCESS.getMessage()); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Follow, Long> { | ||
|
||
Optional<Follow> findByFollowerIdAndFollowedId(Long followerId, Long followedId); | ||
|
||
// followerId가 포함된 객체 리스트 반환 | ||
List<Follow> findByFollowerId(Long followerId); | ||
|
||
// followedId가 포함된 객체 리스트 반환 | ||
List<Follow> findByFollowedId(Long followedId); | ||
|
||
boolean existsByFollowerIdAndFollowedId(Long followerId, Long followedId); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. follower와 followed에 각각 다른 매개변수를 넣는 것은 , 다른 사람임을 쉽게 구분하기 위함인가요!? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "팔로우"의 경우 현재 서비스를 이용하는 "본인"이 다른 사람을 팔로우하기 때문에 현재 JWT 토큰을 이용해 인증된 사용자의 email을 가져와서 follower(나 자신)로 조회를 하고, 팔로우 하는 대상은 userId로 입력을 받아서 followed로 조회를 했습니당 |
||
|
||
validate(follower.getId(), followed.getId()); | ||
|
||
Follow follow = followRepository.save(Follow.of(follower, followed)); | ||
|
||
follower.addFollowing(follow); | ||
followed.addFollower(follow); | ||
} | ||
|
||
public List<FollowResponse> getFollowers(Long userId){ | ||
User user = userService.find(userId); | ||
|
||
List<Follow> followerList = user.getFollowerList(); | ||
|
||
return followerList.stream() | ||
.map(follow -> { | ||
return FollowResponse.from(follow.getFollower()); }) | ||
.toList(); | ||
} | ||
|
||
public List<FollowResponse> getFollowings(Long userId){ | ||
User user = userService.find(userId); | ||
|
||
List<Follow> followingList = user.getFollowingList(); | ||
|
||
return followingList.stream() | ||
.map(follow -> { | ||
return FollowResponse.from(follow.getFollowed()); }) | ||
.toList(); | ||
Comment on lines
+38
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return에서 stream객체의 map을 사용하여 코드를 효율적으로 사용하려는 것 같습니다. return userService.find(userId).getFollowerList().stream()
.map(follow -> FollowResponse.from(follow.getFollower()))
.toList(); return userService.find(userId).getFollowingList().stream()
.map(follow -> 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); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
package com.leets.X.domain.user.domain; | ||
|
||
import com.leets.X.domain.follow.domain.Follow; | ||
import com.leets.X.domain.user.dto.request.UserInitializeRequest; | ||
import com.leets.X.domain.user.dto.request.UserUpdateRequest; | ||
import com.leets.X.global.common.domain.BaseTimeEntity; | ||
import jakarta.persistence.*; | ||
import lombok.*; | ||
|
||
import java.time.LocalDate; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
|
||
@Entity | ||
// mysql에서 user 테이블이 존재 하기 때문에 다른 이름으로 지정 | ||
|
@@ -46,6 +49,12 @@ public class User extends BaseTimeEntity { | |
|
||
// private Image image; | ||
|
||
@OneToMany(mappedBy = "followed", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
private List<Follow> followerList = new ArrayList<>(); | ||
|
||
@OneToMany(mappedBy = "follower", cascade = CascadeType.REMOVE, orphanRemoval = true) | ||
private List<Follow> followingList = new ArrayList<>(); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다대다 관계를 1:N , N:1로 풀어서 구현하는 부분에 하나의 엔티티는 @manytoone으로 해야하는 것이 아닌가 궁금했는데! User입장에서, 하나의 유저는 다수의 팔로잉, 팔로워를 가질 수 있어서라고 생각하는 것이 맞을까요!? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유저의 입장에서는 팔로워 / 팔로잉이 나눠져 있기 때문에 의미 적으로 나눠서 저장했다고 생각하시면 될 것 같아요! |
||
public void initProfile(UserInitializeRequest dto){ | ||
this.birth = dto.birth(); | ||
this.customId = dto.customId(); | ||
|
@@ -58,4 +67,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); | ||
} | ||
|
||
|
||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FollowService내부에 log.info와 같은 코드는 보이지 않는데, @slf4j를 사용하신 이유가 궁금합니다