728x90
320x100
🎯 오늘 개발 할 기능
- 팔로우(Follow) 도메인 구현
- 팔로우(Follow) 테스트
🛠️ 개발내용
📌 Follow.java 생성
package ddururi.bookbookclub.domain.follow.entity;
import ddururi.bookbookclub.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@AllArgsConstructor
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"follower_id", "following_id"}))
public class Follow {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "follower_id", nullable = false)
private User follower;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "following_id", nullable = false)
private User following;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
public static Follow create(User follower, User following) {
Follow follow = new Follow();
follow.follower = follower;
follow.following = following;
return follow;
}
}
📌 FollowRepository.java 생성
package ddururi.bookbookclub.domain.follow.repository;
import ddururi.bookbookclub.domain.follow.entity.Follow;
import ddururi.bookbookclub.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface FollowRepository extends JpaRepository<Follow, Long> {
boolean existsByFollowerAndFollowing(User follower, User following);
Optional<Follow> findByFollowerAndFollowing(User follower, User following);
List<Follow> findAllByFollower(User follower);
List<Follow> findAllByFollowing(User following);
long countByFollower(User follower);
long countByFollowing(User following);
}
📌 FollowService.java 생성
package ddururi.bookbookclub.domain.follow.service;
import ddururi.bookbookclub.domain.follow.dto.FollowActionResponse;
import ddururi.bookbookclub.domain.follow.dto.FollowResponse;
import ddururi.bookbookclub.domain.follow.entity.Follow;
import ddururi.bookbookclub.domain.follow.exception.AlreadyFollowingException;
import ddururi.bookbookclub.domain.follow.exception.FollowNotFoundException;
import ddururi.bookbookclub.domain.follow.repository.FollowRepository;
import ddururi.bookbookclub.domain.user.dto.UserSummaryResponse;
import ddururi.bookbookclub.domain.user.entity.User;
import ddururi.bookbookclub.domain.user.exception.UserNotFoundException;
import ddururi.bookbookclub.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
@Transactional
public class FollowService {
private final FollowRepository followRepository;
private final UserRepository userRepository;
/**
* 팔로우
*/
public FollowActionResponse follow(Long followerId, Long followingId) {
User follower = findUserById(followerId);
User following = findUserById(followingId);
if (followRepository.existsByFollowerAndFollowing(follower, following)) {
throw new AlreadyFollowingException();
}
Follow follow = Follow.create(follower, following);
followRepository.save(follow);
long followerCount = followRepository.countByFollowing(following);
return new FollowActionResponse(follow.getId(), "팔로우 완료", followerCount);
}
/**
* 언팔로우
*/
public FollowActionResponse unfollow(Long followerId, Long followingId) {
User follower = findUserById(followerId);
User following = findUserById(followingId);
Follow follow = followRepository.findByFollowerAndFollowing(follower, following)
.orElseThrow(FollowNotFoundException::new);
followRepository.delete(follow);
long followerCount = followRepository.countByFollowing(following);
return new FollowActionResponse(follow.getId(), "언팔로우 완료", followerCount);
}
/**
* 팔로워 목록
*/
public List<FollowResponse> getFollowers(Long userId) {
User user = findUserById(userId);
return followRepository.findAllByFollowing(user).stream()
.map(f -> new FollowResponse(f.getId(), UserSummaryResponse.from(f.getFollower())))
.toList();
}
/**
* 팔로잉 목록
*/
public List<FollowResponse> getFollowings(Long userId) {
User user = findUserById(userId);
return followRepository.findAllByFollower(user).stream()
.map(f -> new FollowResponse(f.getId(), UserSummaryResponse.from(f.getFollowing())))
.toList();
}
private User findUserById(Long id) {
return userRepository.findById(id).orElseThrow(UserNotFoundException::new);
}
}
📌 Follow 관련 예외클래스 생성
package ddururi.bookbookclub.domain.follow.exception;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;
@Getter
public class AccessDeniedException extends RuntimeException {
private final ErrorCode errorCode;
public AccessDeniedException(String message) {
super(message);
this.errorCode = ErrorCode.ACCESS_DENIED;
}
}
package ddururi.bookbookclub.domain.follow.exception;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;
@Getter
public class AlreadyFollowingException extends RuntimeException {
private final ErrorCode errorCode;
public AlreadyFollowingException() {
super(ErrorCode.ALREADY_FOLLOWING.getMessage());
this.errorCode = ErrorCode.ALREADY_FOLLOWING;
}
}
package ddururi.bookbookclub.domain.follow.exception;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;
@Getter
public class FollowNotFoundException extends RuntimeException {
private final ErrorCode errorCode;
public FollowNotFoundException() {
super(ErrorCode.FOLLOW_NOT_FOUND.getMessage());
this.errorCode = ErrorCode.FOLLOW_NOT_FOUND;
}
}
📌 FollowController.java 생성
package ddururi.bookbookclub.domain.follow.controller;
import ddururi.bookbookclub.domain.follow.dto.FollowActionResponse;
import ddururi.bookbookclub.domain.follow.dto.FollowResponse;
import ddururi.bookbookclub.domain.follow.exception.AccessDeniedException;
import ddururi.bookbookclub.domain.follow.service.FollowService;
import ddururi.bookbookclub.domain.user.dto.UserResponse;
import ddururi.bookbookclub.global.common.ApiResponse;
import ddururi.bookbookclub.global.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/users/{userId}/follow")
public class FollowController {
private final FollowService followService;
@PostMapping("/{targetId}")
public ResponseEntity<ApiResponse<FollowActionResponse>> follow(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long userId,
@PathVariable Long targetId) {
validateUserAccess(userDetails, userId);
FollowActionResponse response = followService.follow(userId, targetId);
return ResponseEntity.ok(ApiResponse.success(response));
}
@DeleteMapping("/{targetId}")
public ResponseEntity<ApiResponse<FollowActionResponse>> unfollow(
@AuthenticationPrincipal CustomUserDetails userDetails,
@PathVariable Long userId,
@PathVariable Long targetId) {
validateUserAccess(userDetails, userId);
FollowActionResponse response = followService.unfollow(userId, targetId);
return ResponseEntity.ok(ApiResponse.success(response));
}
@GetMapping("/followers")
public ResponseEntity<ApiResponse<List<FollowResponse>>> getFollowers(@PathVariable Long userId) {
List<FollowResponse> followers = followService.getFollowers(userId);
return ResponseEntity.ok(ApiResponse.success(followers));
}
@GetMapping("/followings")
public ResponseEntity<ApiResponse<List<FollowResponse>>> getFollowings(@PathVariable Long userId) {
List<FollowResponse> followings = followService.getFollowings(userId);
return ResponseEntity.ok(ApiResponse.success(followings));
}
private void validateUserAccess(CustomUserDetails userDetails, Long userId) {
if (!userDetails.getUser().getId().equals(userId)) {
throw new AccessDeniedException("본인 계정으로만 요청할 수 있습니다.");
}
}
}
📌 Follow 관련 DTO 생성
package ddururi.bookbookclub.domain.follow.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class FollowActionResponse {
private Long followId; // 팔로우 관계 ID
private String message; // 응답 메시지 ("팔로우 완료", "언팔로우 완료")
private Long followerCount; // 현재 대상의 팔로워 수 (선택)
}
package ddururi.bookbookclub.domain.follow.dto;
import ddururi.bookbookclub.domain.user.dto.UserSummaryResponse;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class FollowResponse {
private Long followId;
private UserSummaryResponse user;
}
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #035] 모놀리식 아키텍처를 MSA 아키텍처로 전환하기 (1) - bbc-user-service (User, EmailValidation, Follow 엔티티) (0) | 2025.05.08 |
---|---|
[개발일지 #033] 책(Book) 중복 등록 시 예외 처리 하기 (0) | 2025.05.02 |
[개발일지 #032] 각종 피드 조회 (특정 회원의 피드 목록 조회, 특정 회원이 좋아요 누른 피드 목록 조회) (0) | 2025.05.02 |
[개발일지 #031] 피드 검색 조회 (0) | 2025.05.02 |
[JPA/QueryDSL] 적용하기 (0) | 2025.05.02 |