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

✨ follow user #431

Open
wants to merge 30 commits into
base: be/dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
650cd90
Create PR for #430
github-actions[bot] Dec 5, 2024
f3b4963
Merge remote-tracking branch 'origin/be/dev' into be/feat/430
tackyu Dec 8, 2024
6d279bc
Merge remote-tracking branch 'origin/be/dev' into be/feat/430
tackyu Dec 16, 2024
445864c
:sparkles: add follow count field
tackyu Dec 16, 2024
56d5e0a
:sparkles: implement UserFollow
tackyu Dec 16, 2024
f9df833
:sparkles: follow User
tackyu Dec 16, 2024
d9751a5
:sparkles: unfollow User
tackyu Dec 16, 2024
d92c465
:sparkles: remove user with UserFollow
tackyu Dec 16, 2024
f2618a4
:white_check_mark: add data
tackyu Dec 16, 2024
1d45b50
:white_check_mark: test with UserFollow
tackyu Dec 16, 2024
517f013
:bug: add request field
tackyu Dec 28, 2024
6ccf1a5
Merge remote-tracking branch 'origin/be/dev' into be/feat/430
tackyu Dec 29, 2024
9e6adc5
:recycle: change name of field
tackyu Dec 30, 2024
c3b879f
:sparkles: add validation
tackyu Dec 30, 2024
a415d5d
:white_check_mark: test count of follow
tackyu Dec 30, 2024
7da55bf
:sparkles: add following condition
tackyu Dec 30, 2024
828d714
:sparkles: retrieve following recipes
tackyu Dec 30, 2024
bf8a555
:white_check_mark: UserFollow validation test
tackyu Dec 30, 2024
1e5bab0
:white_check_mark: UserFollow creation test
tackyu Dec 30, 2024
8411209
:recycle: add getUser
tackyu Jan 6, 2025
578ffb2
:sparkles: retrieve follow information of user
tackyu Jan 6, 2025
8d4edb0
:white_check_mark: test follow information retrieval
tackyu Jan 6, 2025
00195da
Merge remote-tracking branch 'origin/be/dev' into be/feat/430
tackyu Jan 6, 2025
05d9f95
:recycle: change return type
tackyu Jan 6, 2025
ebfb72c
:recycle: change code to single line
tackyu Feb 2, 2025
d798b4f
:recycle: remove unnecessary conversion to RecipeHomeResponse
tackyu Feb 2, 2025
472f323
:recycle: separate retrieve followingInfo API
tackyu Feb 2, 2025
1a12a2e
:recycle: unfollow user when blocking a user
tackyu Feb 2, 2025
dcab263
:sparkles: add validation
tackyu Feb 3, 2025
3ecc31d
:white_check_mark: add validation test
tackyu Feb 3, 2025
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 @@ -61,6 +61,14 @@ public List<RecipeHomeWithMineResponseV1> readLikeRecipesV1(@LoginUser UserInfo
return recipeService.readLikeRecipesV1(userInfo);
}

@GetMapping("/follows")
public List<RecipeHomeWithMineResponseV1> readFollowRecipes(
@LoginUser UserInfo userInfo,
@ModelAttribute @Valid PageRecipeRequest pageRecipeRequest
) {
return recipeService.readFollowRecipes(userInfo, pageRecipeRequest);
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public RecipeResponse createRecipe(@LoginUser UserInfo userInfo, @RequestBody @Valid RecipeRequest recipeRequest) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.time.LocalDateTime;
import net.pengcook.authentication.domain.UserInfo;
import net.pengcook.recipe.domain.Recipe;

public record RecipeHomeWithMineResponseV1(
long recipeId,
Expand Down Expand Up @@ -29,4 +30,20 @@ public RecipeHomeWithMineResponseV1(
userInfo.isSameUser(firstResponse.authorId())
);
}

public RecipeHomeWithMineResponseV1(
UserInfo userInfo,
Recipe recipe
) {
this(
recipe.getId(),
recipe.getTitle(),
new AuthorResponse(recipe.getAuthor()),
recipe.getThumbnail(),
recipe.getLikeCount(),
recipe.getCommentCount(),
recipe.getCreatedAt(),
userInfo.isSameUser(recipe.getAuthor().getId())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,7 @@ List<Long> findRecipeIdsByCategoryAndKeyword(
""")
List<Long> findRecipeIdsByUserId(long userId);

List<Recipe> findAllByAuthorIdIn(List<Long> authorIds, Pageable pageable);

int countByAuthorId(long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
import net.pengcook.recipe.repository.RecipeRepository;
import net.pengcook.recipe.repository.RecipeStepRepository;
import net.pengcook.user.domain.User;
import net.pengcook.user.domain.UserFollow;
import net.pengcook.user.repository.UserFollowRepository;
import net.pengcook.user.repository.UserRepository;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -46,6 +48,7 @@ public class RecipeService {
private final UserRepository userRepository;
private final RecipeLikeRepository likeRepository;
private final RecipeStepRepository recipeStepRepository;
private final UserFollowRepository userFollowRepository;

private final CategoryService categoryService;
private final IngredientService ingredientService;
Expand Down Expand Up @@ -144,6 +147,22 @@ public List<RecipeHomeWithMineResponseV1> readLikeRecipesV1(UserInfo userInfo) {
.toList();
}

@Transactional(readOnly = true)
public List<RecipeHomeWithMineResponseV1> readFollowRecipes(UserInfo userInfo,
PageRecipeRequest pageRecipeRequest) {
List<UserFollow> followings = userFollowRepository.findAllByFollowerId(userInfo.getId());
List<Long> followeeIds = followings.stream()
.map(userFollow -> userFollow.getFollowee().getId())
.toList();
List<Recipe> recipes = recipeRepository.findAllByAuthorIdIn(followeeIds, pageRecipeRequest.getPageable());

return recipes.stream()
.map(recipe -> new RecipeHomeWithMineResponseV1(userInfo, recipe))
.sorted(Comparator.comparing(RecipeHomeWithMineResponseV1::recipeId).reversed())
.toList();
}


@Transactional
public RecipeResponse createRecipe(UserInfo userInfo, RecipeRequest recipeRequest) {
User author = userRepository.findById(userInfo.getId()).orElseThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import net.pengcook.authentication.domain.UserInfo;
import net.pengcook.authentication.resolver.LoginUser;
import net.pengcook.user.dto.FollowInfoResponse;
import net.pengcook.user.dto.ProfileResponse;
import net.pengcook.user.dto.ReportReasonResponse;
import net.pengcook.user.dto.ReportRequest;
Expand All @@ -14,7 +15,10 @@
import net.pengcook.user.dto.UpdateProfileResponse;
import net.pengcook.user.dto.UserBlockRequest;
import net.pengcook.user.dto.UserBlockResponse;
import net.pengcook.user.dto.UserFollowRequest;
import net.pengcook.user.dto.UserFollowResponse;
import net.pengcook.user.dto.UsernameCheckResponse;
import net.pengcook.user.service.UserFollowService;
import net.pengcook.user.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand All @@ -32,15 +36,16 @@
public class UserController {

private final UserService userService;
private final UserFollowService userFollowService;

@GetMapping("/user/me")
public ProfileResponse getUserProfile(@LoginUser UserInfo userInfo) {
return userService.getUserById(userInfo.getId());
return userService.getProfile(userInfo.getId(), userInfo.getId());
}

@GetMapping("/user/{userId}")
public ProfileResponse getUserProfile(@PathVariable long userId) {
return userService.getUserById(userId);
public ProfileResponse getUserProfile(@LoginUser UserInfo userInfo, @PathVariable long userId) {
return userService.getProfile(userInfo.getId(), userId);
}
Comment on lines 41 to 49
Copy link
Contributor

Choose a reason for hiding this comment

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

이거 두개가 나눠져있는게 편한지 악어에게 물어봅시다
굳이 두개일 필요가 있을까 싶어서요~~~


@PatchMapping("/user/me")
Expand Down Expand Up @@ -84,4 +89,41 @@ public UserBlockResponse blockUser(
) {
return userService.blockUser(userInfo.getId(), userBlockRequest.blockeeId());
}

@PostMapping("/user/follow")
Copy link
Contributor

Choose a reason for hiding this comment

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

UserController에서는 왜 클래스단에 RequestMapping이 안묶여있죠???
그냥 궁금증...

Copy link
Contributor

Choose a reason for hiding this comment

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

이거 제가 차단목록 조회 하면서 묶을게욥

Choose a reason for hiding this comment

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

UserController에서는 왜 클래스단에 RequestMapping이 안묶여있죠???
그냥 궁금증...

그러게요 예전에 제가 작업한거긴한데 부탁드려요.

@ResponseStatus(HttpStatus.CREATED)
public UserFollowResponse followUser(
@LoginUser UserInfo userInfo,
@RequestBody @Valid UserFollowRequest userFollowRequest
) {
return userFollowService.followUser(userInfo.getId(), userFollowRequest.targetId());
}

@DeleteMapping("/user/follow")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void unfollowUser(
@LoginUser UserInfo userInfo,
@RequestBody @Valid UserFollowRequest userFollowRequest
) {
userFollowService.unfollowUser(userInfo.getId(), userFollowRequest.targetId());
}

@DeleteMapping("/user/follower")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void removeFollower(
@LoginUser UserInfo userInfo,
@RequestBody @Valid UserFollowRequest userFollowRequest
) {
userFollowService.unfollowUser(userFollowRequest.targetId(), userInfo.getId());
}

@GetMapping("/user/{userId}/follower")
public FollowInfoResponse getFollowerInfo(@PathVariable long userId) {
return userFollowService.getFollowerInfo(userId);
}

@GetMapping("/user/{userId}/following")
public FollowInfoResponse getFollowingInfo(@PathVariable long userId) {
return userFollowService.getFollowingInfo(userId);
}
}
35 changes: 33 additions & 2 deletions backend/src/main/java/net/pengcook/user/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnDefault;

@Entity
@Table(name = "users")
Expand Down Expand Up @@ -39,8 +40,22 @@ public class User {
@Column(nullable = false)
private String region;

public User(String email, String username, String nickname, String image, String region) {
this(0L, email, username, nickname, image, region);
@Column(nullable = false)
@ColumnDefault("0")
private long followerCount;

@Column(nullable = false)
@ColumnDefault("0")
private long followeeCount;

public User(
String email,
String username,
String nickname,
String image,
String region
) {
this(0L, email, username, nickname, image, region, 0, 0);
}

public boolean isSameUser(long userId) {
Expand All @@ -53,4 +68,20 @@ public void update(String username, String nickname, String image, String region
this.image = image;
this.region = region;
}

public void increaseFollowerCount() {
followerCount++;
}

public void decreaseFollowerCount() {
followerCount--;
}

public void increaseFolloweeCount() {
followeeCount++;
}

public void decreaseFolloweeCount() {
followeeCount--;
}
}
47 changes: 47 additions & 0 deletions backend/src/main/java/net/pengcook/user/domain/UserFollow.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.pengcook.user.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.pengcook.user.exception.BadArgumentException;

@Entity
@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"follower_id", "followee_id"})})
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = "id")
@Getter
public class UserFollow {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@ManyToOne
@JoinColumn(name = "follower_id")
private User follower;

@ManyToOne
@JoinColumn(name = "followee_id")
private User followee;

public UserFollow(User follower, User followee) {
validate(follower, followee);
this.follower = follower;
this.followee = followee;
}

private void validate(User follower, User followee) {
if (follower.equals(followee)) {
throw new BadArgumentException("자기 자신을 팔로우할 수 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.pengcook.user.dto;

import java.util.List;

public record FollowInfoResponse(
List<FollowUserInfoResponse> follows,
long followCount
) {
public FollowInfoResponse(List<FollowUserInfoResponse> follows) {
this(follows, follows.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.pengcook.user.dto;

import net.pengcook.user.domain.User;

public record FollowUserInfoResponse(String username, String image) {

public FollowUserInfoResponse(User user) {
this(user.getUsername(), user.getImage());
}
}
16 changes: 9 additions & 7 deletions backend/src/main/java/net/pengcook/user/dto/ProfileResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ public record ProfileResponse(
String image,
String region,
String introduction,
long follower,
long following,
long recipeCount
long followerCount,
long followingCount,
long recipeCount,
boolean isFollow
) {

public ProfileResponse(User user, long recipeCount) {
public ProfileResponse(User user, long recipeCount, boolean isFollow) {
this(
user.getId(),
user.getEmail(),
Expand All @@ -24,9 +25,10 @@ public ProfileResponse(User user, long recipeCount) {
user.getImage(),
user.getRegion(),
"hello world",
0,
0,
recipeCount
user.getFollowerCount(),
user.getFolloweeCount(),
recipeCount,
isFollow
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.pengcook.user.dto;

import jakarta.validation.constraints.NotNull;

public record UserFollowRequest(@NotNull long targetId) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.pengcook.user.dto;

import net.pengcook.user.domain.UserFollow;

public record UserFollowResponse(
long followerId,
long followeeId
) {
public UserFollowResponse(UserFollow userFollow) {
this(
userFollow.getFollower().getId(),
userFollow.getFollowee().getId()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.pengcook.user.exception;

import org.springframework.http.HttpStatus;

public class IllegalStateException extends UserException {

public IllegalStateException(String message) {
super(HttpStatus.FORBIDDEN, message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public interface UserBlockRepository extends JpaRepository<UserBlock, Long> {
void deleteByBlockeeId(long blockeeId);

void deleteByBlockerId(long blockerId);

boolean existsByBlockerIdAndBlockeeId(long blockerId, long blockeeId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.pengcook.user.repository;

import java.util.List;
import java.util.Optional;
import net.pengcook.user.domain.UserFollow;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserFollowRepository extends JpaRepository<UserFollow, Long> {

Optional<UserFollow> findByFollowerIdAndFolloweeId(Long followerId, Long followeeId);

List<UserFollow> findAllByFollowerId(long followerId);

List<UserFollow> findAllByFolloweeId(long followeeId);
Comment on lines +10 to +14

Choose a reason for hiding this comment

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

UserFollow 도 authorAble을 구현해야 할까요?
사용자가 차단 한 상황에 팔로우 목록에서 보여줄지 고민이 필요할것 같아요.

또는 차단 했을때 팔로우를 지울지도 고민이 필요해보여요. 이 부분은 정책 회의때 이야기 해봐야 할것 같네요.

Copy link
Contributor

Choose a reason for hiding this comment

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

팔로워 목록에서는 차단한 사용자를 제외할 수 있을 것 같은데
팔로잉 목록에서도 가능한지 얘기해보고 싶습니다😢

Copy link
Contributor

Choose a reason for hiding this comment

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

차단하면 당연히 팔로우 하는 사람 목록과 팔로우 받는 사람 목록에서 빠져야 된다고 생각합니다.
전 아예 지워버려야 하는 게 맞다고 봐요!
테이블에서도 지우고, 인원 카운트에서도 내리고요.

Copy link
Contributor

Choose a reason for hiding this comment

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

차단하면 실제 UserFollow의 값이 변해야 한다는 의견에 동의합니다!
팔로우 관계를 조회할 때, 모든 사용자가 같은 값을 받는 것이 적절할 것 같아요


boolean existsByFollowerIdAndFolloweeId(long followerId, long followeeId);
}
Loading