[24.06.07] 내일배움캠프 36일차 JAVA TIL - 좋아요 구현 , 테스트

2024. 6. 10. 09:23T.I.L

오늘 한 일

  • Web 강의 수강 - 1억 연봉 개발자 특강

 

 


좋아요 엔티티를 만들기 위해 기존 프로젝트에서 이슈를 생성했다.

 

이제는 이슈 생성과 pr을 올리는 방법을 어느정도 터득한거같다.. 아직 어렵긴 하지만!

 

좋아요 엔티티에 대한 요구사항을 보면 하나의 엔티티에서 댓글 좋아요 / 게시글 좋아요를 모두 구현하도록 설계되어있었다.

 

 

요구사항을 지키기 위해 하나의 엔티티에서 처리하도록 만들어줬다.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "likes_seq")
    private Long likesSeq;

    @Column(name = "user_id")
    private Long userId;

    @Column(name = "content_id", nullable = false)
    private Long contentId;

    @Column(name = "content_type", nullable = false)
    @Enumerated(value = EnumType.STRING)
    private LikeEnum contentType;

 

 

그리고, 해당 좋아요가 댓글의 좋아요인지 게시물의 좋아요인지 구분하기 위해 contentType을 enum으로 넘겨주었다.

public enum LikeEnum {
    COMMENT(ContentType.COMMENT),
    NEWSFEED(ContentType.NEWSFEED);

    private final String contentType;
    LikeEnum(String contentType) {this.contentType = contentType; }

    public static class ContentType{
        public static final String COMMENT = "COMMENT";
        public static final String NEWSFEED = "NEWSFEED";
    }
}

기존에 User에서 enum으로 상태를 표현한 경험이 있어 수월하게 구현했다. 문자열로 저장하기 위해 엔티티에서 STRING으로 저장해줬다.

 

컨트롤러의 경로를 보면 알 수 있겠지만, 게시물과 댓글의 경로는 엄연히 다르다.

하지만 동일한 기능을 구현하고 있기 때문에 따로 분리해주지 않고 하나의 컨트롤러에서 동작하도록 작성해줬다.

 

 

좋아요 추가 및 삭제 기능은 GetMapping으로 service로 넘어가면 해당 객체가 존재하는지 아닌지를 판별하고 각 상황에 맞는 동작을 하도록 작성해줬다.

@Tag(name = "Likes API", description = "Likes API 입니다")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/newsfeeds")
public class LikeController {

    private final LikeService likeService;

    // 게시물별 좋아요 수 조회
    @Operation(summary = "getNewsfeedsLikes", description = "뉴스피드별 좋아요 수 조회 기능입니다.")
    @GetMapping("/{newsfeedId}/like/count")
    public LikeCountResponseDto getNewsfeedsLikes(@PathVariable("newsfeedId") Long newsfeedId) {
        return new LikeCountResponseDto(likeService.getLikesCount(newsfeedId, LikeEnum.NEWSFEED));
    }

    // 댓글별 좋아요 수 조회
    @Operation(summary = "getCommentsLikes", description = "댓글별 좋아요 수 조회 기능입니다.")
    @GetMapping("/{newsfeedId}/comments/{commentsId}/like/count")
    public LikeCountResponseDto getCommentsLikes(@PathVariable("commentsId") Long commentsId) {
        return new LikeCountResponseDto(likeService.getLikesCount(commentsId, LikeEnum.COMMENT));
    }

    // 게시물별 좋아요 추가 및 삭제 (토글)
    @Operation(summary = "toggleNewsfeedLike", description = "게시물 좋아요 추가 및 삭제 기능입니다.")
    @PostMapping("/{newsfeedId}/like")
    public ResponseEntity<String> toggleNewsfeedLike(@PathVariable(name = "newsfeedId") long newsfeedId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return likeService.toggleLike(userDetails.getUser().getUserSeq(), newsfeedId, LikeEnum.NEWSFEED);
    }

    // 댓글별 좋아요 추가 및 삭제 (토글)
    @Operation(summary = "toggleCommentLike", description = "댓글 좋아요 추가 및 삭제 기능입니다.")
    @PostMapping("/{newsfeedId}/comments/{commentId}/like")
    public ResponseEntity<String> toggleCommentLike(@PathVariable(name = "newsfeedId") long newsfeedId, @PathVariable(name = "commentId") long commentId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return likeService.toggleLike(userDetails.getUser().getUserSeq(), commentId, LikeEnum.COMMENT);
    }

}

 

구현하면서도 delete와 save를 하나의 함수에서 사용해도 되는지 정확히 모르겠다.

의도는 토글처럼 하나의 함수를 이용하여 삭제 및 추가가 단 1번만 가능하도록 구현한것인데 이 점은 피드백을 받으며 추후 보완해보려고 한다.

@Service
@RequiredArgsConstructor
public class LikeService {


    private final LikeRepository likeRepository;
    private final NewsfeedRespository newsfeedRespository;
    private final CommentRepository commentRepository;

    // 게시물, 댓글별 좋아요 수 count
    public int getLikesCount(Long contentId, LikeEnum contentType) {
        return likeRepository.countByContentIdAndContentType(contentId, contentType);
    }

    // 게시물, 댓글별 좋아요 toggle 기능
    public ResponseEntity<String> toggleLike(Long userId, Long contentId, LikeEnum contentType) {
        validateLikeAction(userId, contentId, contentType);

        Optional<Like> likeOptional = findLike(userId, contentId, contentType);
        if (likeOptional.isPresent()) {
            likeRepository.delete(likeOptional.get());
            return ResponseEntity.ok("Like removed");
        } else {
            Like like = new Like(userId, contentId, contentType);
            likeRepository.save(like);
            return ResponseEntity.ok("Like added");
        }
    }

    // 좋아요 유효성 검사
    private void validateLikeAction(Long userId, Long contentId, LikeEnum contentType) {
        if (contentType == LikeEnum.NEWSFEED) {
            Newsfeed newsfeed = newsfeedRespository.findById(contentId).orElseThrow(() ->
                    new CustomException(ErrorCode.NEWSFEED_NOT_FOUND));
            if (newsfeed.getUser().getUserSeq().equals(userId)) {
                throw new CustomException(ErrorCode.NEWSFEED_SAME_USER);
            }
        } else if (contentType == LikeEnum.COMMENT) {
            Comment comment = commentRepository.findById(contentId).orElseThrow(() ->
                    new CustomException(ErrorCode.COMMENT_NOT_FOUND));
            if (comment.getUser().getUserSeq().equals(userId)) {
                throw new CustomException(ErrorCode.COMMENT_SAME_USER);
            }
        }
    }

    // 좋아요 객체 찾기
    private Optional<Like> findLike(Long userId, Long contentId, LikeEnum contentType) {
        return likeRepository.findByUserIdAndContentIdAndContentType(userId, contentId, contentType);
    }

}

 

스프링 부트의 security 부분이 어려워 개인 과제 진도가 더뎠었다. 그래서 스프링 부트에 대한 두려움이 있었는데 좋은 팀원분들을 만나 이해도도 크게 향상되고 팀원 분들의 코드와 내 코드를 비교해보며 크게 성장할 수 있었다.