부트캠프/Dev

뉴스피드 프로젝트 3일차

nameless1004 2024. 9. 5. 09:32

기능 구현

오늘은 친구 기능과 북마크 기능을 구현하였다.

친구 기능

친구 기능은 구현할 때 테이블 설계에 두 가지 선택 사항이 있었다.

  • 친구 테이블 하나로 할지
  • 친구 테이블과 친구 요청 테이블을 분리할지

이번에는 친구 테이블 하나로 관리를 해줬다. 요청한 유저의 아이디와 요청받은 유저의 아이디, 그리고 enum으로 Waiting과 accepted상태를 관리해주었다.

유저 테이블과 친구 테이블은 1:N관계로 설계했다.

처음에는 url를 만들 때 애를 먹었던게 친구 삭제와 친구 요청 거절이 적고 보니까 똑같이 /api/friends/{id} 였다. 그래서 고민 끝에 요청, 요청 거절은 이름을 다르게 했다.

public class FriendshipController {

    private final FriendshipService friendshipService;
    
    /**
     * 다른 유저에게 친구요청
     *
     * @param userDetails 현재 로그인 유저
     * @param requestDto  receiver 아이디 필요
     * @return
     */
    @PostMapping("/api/friend-requests")
    public ResponseEntity<Void> addFriend(@AuthenticationPrincipal UserDetailsImpl userDetails,
        @RequestBody FriendRequestDto requestDto) {
        friendshipService.requestFriendship(userDetails.getUser(), requestDto);
        return ResponseEntity.ok()
            .build();
    }

    /**
     * 친구요청 수락
     *
     * @param userDetails 현재 로그인 유저
     * @param requesterId 요청한 유저 아이디
     * @return
     */
    @PutMapping("/api/friend-requests/{requesterId}/accept")
    public ResponseEntity<Void> acceptFriendship(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable("requesterId") Long requesterId) {
        friendshipService.acceptFriendShip(userDetails.getUser(), requesterId);
        return ResponseEntity.ok()
            .build();
    }

    /**
     * 친구요청 거절
     *
     * @param userDetails 현재 로그인 유저
     * @param requesterId 요청한 유저 아이디
     */
    @DeleteMapping("/api/friend-requests/{requesterId}/reject")
    public ResponseEntity<Void> rejectFriendRequest(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable("requesterId") Long requesterId) {
        friendshipService.rejectFriendshipRequest(userDetails.getUser(), requesterId);
        return ResponseEntity.ok()
            .build();
    }

    /**
     * 친구요청 목록
     *
     * @param userDetails 현재 로그인 유저
     */
    @GetMapping("/api/friend-requests")
    public ResponseEntity<List<FriendshipReceiveDto>> getRecieveList(
        @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseEntity.ok(friendshipService.getRecieveList(userDetails.getUser()));
    }

    /**
     * 친구목록 가져오기
     *
     * @param userDetails 현재 로그인 유저
     */
    @GetMapping("/api/friends")
    public ResponseEntity<List<FriendResponseDto>> getFriends(
        @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return ResponseEntity.ok(friendshipService.getFriendAll(userDetails.getUser()));
    }

    /**
     * 친구 삭제
     *
     * @param userDetails 현재 로그인 유저
     * @param id          삭제할 친구(유저) 아이디
     */
    @DeleteMapping("/api/friends/{id}")
    public ResponseEntity<Void> removeFriend(@AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable("id") Long id) {
        friendshipService.removeFriendship(userDetails.getUser(), id);
        return ResponseEntity.ok()
            .build();
    }

    /**
     * 모든 친구 요청 및 친구 삭제
     *
     * @param userDetails
     */
    @DeleteMapping("/api/friends")
    public ResponseEntity<Void> removeAllFriend(
        @AuthenticationPrincipal UserDetailsImpl userDetails) {
        friendshipService.removeAllFriendship(userDetails.getUser());
        return ResponseEntity.ok()
            .build();
    }
}
@Service
@RequiredArgsConstructor
@Transactional
public class FriendshipService {

    private final UserRepository userRepository;
    private final FriendShipRepository friendShipRepository;

    /**
     * 친구 요청
     *
     * @param user       현재 로그인 유저
     * @param requestDto 친구요청 받을 유저의 아이디
     */
    public void requestFriendship(User user, FriendRequestDto requestDto) {
        User receiver = getUserOrElseThrow(requestDto.getReceiverId());
        Friendship friendship = new Friendship(user, receiver);
        friendShipRepository.save(friendship);
    }

    /**
     * 친구요청 거절
     *
     * @param user        현재 로그인 유저
     * @param requesterId 친구요청한 유저의 아이디
     */
    public void rejectFriendshipRequest(User user, Long requesterId) {
        User requester = getUserOrElseThrow(requesterId);
        boolean isExists = friendShipRepository.existsByRequesterAndReceiverAndStatus(requester,
            user, FriendShipStatus.ACCEPTED);

        if (isExists) {
            throw new AlreadyAcceptedException();
        }

        friendShipRepository.deleteByRequesterAndReceiver(requester, user);
    }

    /**
     * 친구요청 수락
     *
     * @param user        현재 로그인 유저
     * @param requesterId 친구요청한 유저의 아이디
     */
    public void acceptFriendShip(User user, Long requesterId) {
        User requester = getUserOrElseThrow(requesterId);

        Friendship request = friendShipRepository.findByRequesterAndReceiver(requester, user)
            .orElseThrow(() -> new NoSuchResourceException("수락할 요청이 없습니다."));

        if (request.getStatus() == FriendShipStatus.ACCEPTED) {
            throw new AlreadyAcceptedException();
        }

        Friendship receive = new Friendship(user, requester);

        // 서로 요청 상태 accepted로 변경
        request.accept();
        receive.accept();

        // 서로 연결
        friendShipRepository.save(receive);
    }

    /**
     * 친구목록 가져오기, ACCEPTED 상태인 친구들만 가져옵니다.
     *
     * @param user 현재 로그인 유저
     */
    @Transactional(readOnly = true)
    public List<FriendResponseDto> getFriendAll(User user) {
        List<Friendship> friends = friendShipRepository.findAllByReceiverAndStatus(user,
            FriendShipStatus.ACCEPTED);

        return friends.stream()
            .map(x -> new FriendResponseDto(x.getRequester()))
            .toList();
    }

    /**
     * 친구삭제 or 요청 거절
     *
     * @param user         현재 로그인 유저
     * @param removeUserId 삭제(거절)할 친구(유저) 아이디
     */
    public void removeFriendship(User user, Long removeUserId) {
        User removeUser = getUserOrElseThrow(removeUserId);
        friendShipRepository.deleteByRequesterAndReceiver(removeUser, user);
        friendShipRepository.deleteByRequesterAndReceiver(user, removeUser);
    }

    /**
     * 친구요청 목록
     *
     * @param user
     */
    public List<FriendshipReceiveDto> getRecieveList(User user) {
        List<Friendship> findAll = friendShipRepository.findAllByReceiverAndStatus(user,
            FriendShipStatus.WAITING);

        return findAll.stream()
            .map(x -> FriendshipReceiveDto.builder()
                .friendshipId(x.getId())
                .requesterId(x.getRequester()
                    .getId())
                .receiverId(user.getId())
                .build())
            .toList();
    }

    /**
     * 친구 요청, 친구 요청받은 것, 친구 관계 삭제
     *
     * @param user 현재 로그인한 유저
     */
    public void removeAllFriendship(User user) {
        friendShipRepository.deleteAllByRequesterOrReceiver(user, user);
    }

    private User getUserOrElseThrow(Long id) {
        return userRepository.findById(id)
            .orElseThrow(InvalidIdException::new);
    }
}

북마크 기능

필수 구현이 끝나서 추가 기능으로 무엇을 할 지 생각해보다가 우리가 하는 컨셉에 북마크 기능이 어울릴 것 같아서 구현을 해보았다. 북마크는 간단하게 유저의 아이디와 보드의 아이디를 가지고 있다.

@RestController
@RequiredArgsConstructor
public class BookmarkController {

    private final BookmarkService bookmarkService;

    @PostMapping("/api/boards/{id}/bookmarks")
    private ResponseEntity<Void> bookmarking(@AuthenticationPrincipal UserDetailsImpl userDetails,
        @PathVariable("id") Long id) {
        bookmarkService.addBookmark(userDetails.getUser(), id);
        return ResponseEntity.status(HttpStatus.CREATED)
            .build();
    }

    @DeleteMapping("/api/boards/{id}/bookmarks")
    private ResponseEntity<Void> deleteBookmark(
        @AuthenticationPrincipal UserDetailsImpl userDetails, @PathVariable("id") Long id) {
        bookmarkService.removeBookmark(userDetails.getUser(), id);
        return ResponseEntity.ok()
            .build();
    }

    @DeleteMapping("/api/bookmarks")
    private ResponseEntity<Void> deleteAllBookmarks(
        @AuthenticationPrincipal UserDetailsImpl userDetails) {
        bookmarkService.removeAllBookmark(userDetails.getUser());
        return ResponseEntity.ok()
            .build();
    }

    @GetMapping("/api/bookmarks")
    private ResponseEntity<Page<BoardResponseDto>> getBookmarkBoards(
        @AuthenticationPrincipal UserDetailsImpl userDetails,
        @PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
        Page<BoardResponseDto> page = bookmarkService.getAllBookmarkedBoards(userDetails.getUser(),
            pageable);
        return ResponseEntity.ok(page);
    }
}
@Service
@RequiredArgsConstructor
@Transactional
public class BookmarkService {

    private final BookmarkRepository bookmarkRepository;
    private final BoardRepository boardRepository;

    /**
     * 유저의 북마크에 보드를 추가합니다.
     *
     * @param user    현재 로그인 유저
     * @param boardId 추가할 보드 아이디
     */
    public void addBookmark(User user, Long boardId) {
        Board board = getBoard(boardId);

        Bookmark bookmark = new Bookmark(user, board);
        bookmarkRepository.save(bookmark);
    }

    /**
     * 유저의 북마크에서 해당 보드를 제거합니다.
     *
     * @param user    현재 로그인 유저
     * @param boardId 제거할 보드 아이디
     */
    public void removeBookmark(User user, Long boardId) {
        bookmarkRepository.deleteByUserIdAndBoardId(user.getId(), boardId);
    }

    /**
     * 유저가 북마크에 저장한 글들을 페이징 해서 가져옵니다.
     *
     * @param user     현재 로그인 유저
     * @param pageable 페이징
     * @return
     */
    public Page<BoardResponseDto> getAllBookmarkedBoards(User user, Pageable pageable) {
        Page<Bookmark> bookmarks = bookmarkRepository.findAllByUser(user, pageable);
        return bookmarks.map(x -> new BoardResponseDto(x.getBoard(), x.getUser()));
    }

    /**
     * 유저가 저장한 모든 북마크를 삭제합니다.
     *
     * @param user 현재 로그인 유저
     */
    public void removeAllBookmark(User user) {
        if (!bookmarkRepository.existsByUser(user)) {
            throw new NoSuchResourceException();
        }
        bookmarkRepository.deleteAllByUser(user);
    }

    private Board getBoard(Long boardId) {
        Board board = boardRepository.findById(boardId)
            .orElseThrow(InvalidIdException::new);
        return board;
    }
}