Skip to content
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

Merged
merged 23 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e74376f
feat: 팔로우 엔티티 구현
hyxklee Nov 4, 2024
40d1fbf
feat: 팔로우 레포지토리 구현
hyxklee Nov 4, 2024
5791bed
feat: 팔로우, 팔로워 조회, 팔로잉 조회 구현
hyxklee Nov 4, 2024
3a51aa6
feat: 예외처리 추가
hyxklee Nov 4, 2024
2c49833
feat: 예외처리 추가
hyxklee Nov 4, 2024
a5af66c
feat: 팔로워/팔로잉 목록 조회 dto 구현
hyxklee Nov 4, 2024
78310df
feat: 반환메시지 상수 구현
hyxklee Nov 4, 2024
7c03a77
feat: userId로 사용자 찾는 메서드 추가
hyxklee Nov 4, 2024
2954eda
feat: 팔로우 되지 않은 상대를 언팔로우 할 때 예외 구현
hyxklee Nov 4, 2024
4e7d09e
feat: 팔로우 데이터가 없는 경우 예외 구현
hyxklee Nov 4, 2024
f63a1af
feat: 언팔로우 API 구현
hyxklee Nov 4, 2024
a8cbeec
feat: 팔로우 단건 조회
hyxklee Nov 4, 2024
adc9511
feat: 사용하지 않는 리스트 제거
hyxklee Nov 4, 2024
cea961f
feat: 스웨거 설명 추가
hyxklee Nov 4, 2024
beb7dd5
feat: 경로 허용
hyxklee Nov 4, 2024
5ffa9dd
fix: 경로 허용 수정
hyxklee Nov 4, 2024
2c0bac6
refactor: 코드 리팩토링, 주석 추가
hyxklee Nov 4, 2024
5e42ab6
refactor: 주석 추가
hyxklee Nov 5, 2024
8af69ea
refactor: 주석 제거
hyxklee Nov 5, 2024
c61ce74
refactor: 양방향 매핑 추가
hyxklee Nov 5, 2024
40a0974
refactor: 경로 수정
hyxklee Nov 5, 2024
89d5ed9
Merge branch 'refs/heads/main' into feat/#20/팔로우-도메인-개발
hyxklee Nov 6, 2024
caa2d82
feat: 의존성 추가
hyxklee Nov 6, 2024
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
@@ -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;
}
35 changes: 34 additions & 1 deletion src/main/java/com/leets/X/domain/follow/domain/Follow.java
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);

}
96 changes: 96 additions & 0 deletions src/main/java/com/leets/X/domain/follow/service/FollowService.java
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
Copy link
Member

Choose a reason for hiding this comment

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

FollowService내부에 log.info와 같은 코드는 보이지 않는데, @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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

follower와 followed에 각각 다른 매개변수를 넣는 것은 , 다른 사람임을 쉽게 구분하기 위함인가요!?
PR에 올려두신 설명으로 각 변수명의 역할은 이해했습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

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

return에서 stream객체의 map을 사용하여 코드를 효율적으로 사용하려는 것 같습니다.
그렇다면 메서드 체이닝을 적극적으로 사용하고 return, {}을 제거하는 방식은 어떨까요?

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);
}

}
27 changes: 27 additions & 0 deletions src/main/java/com/leets/X/domain/user/domain/User.java
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 테이블이 존재 하기 때문에 다른 이름으로 지정
Expand Down Expand Up @@ -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<>();

Copy link
Collaborator

Choose a reason for hiding this comment

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

다대다 관계를 1:N , N:1로 풀어서 구현하는 부분에 하나의 엔티티는 @manytoone으로 해야하는 것이 아닌가 궁금했는데! User입장에서, 하나의 유저는 다수의 팔로잉, 팔로워를 가질 수 있어서라고 생각하는 것이 맞을까요!?

Copy link
Member Author

Choose a reason for hiding this comment

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

유저 1 ㅣ N 팔로우 이기 때문에 "한 명의 유저는 여러 개의 팔로우를 할 수 있다" 로 이해하시면 될 것 같아용
따라서 유저 -> 팔로우는 OneToMany / 팔로우 -> 유저는 ManyToOne으로 이해하시면 될 것 같습니당

Copy link
Member Author

Choose a reason for hiding this comment

The 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();
Expand All @@ -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);
}



}
10 changes: 10 additions & 0 deletions src/main/java/com/leets/X/domain/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
Expand Down
Loading
Loading