Post

TIL(20240606) [뉴스피드프로젝트:게시물 좋아요 기능구현]

TIL(20240606) [뉴스피드프로젝트:게시물 좋아요 기능구현]

📌 Spring

💡 뉴스피드 프로젝트 : 게시물 좋아요 기능구현

  • 좋아요 Entity
CREATE TABLE 좋아요 (
    ID BIGINT PRIMARY KEY,
    사용자_ID BIGINT 
    콘텐츠_ID BIGINT 
    콘텐츠_유형 VARCHAR(255) 
    생성일자 TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    수정일자 TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

// 위와 같이 테이블을 만든것은 아니고 조건 컬럼이 저러하다는 것!
  • 게시물 및 댓글 좋아요/ 좋아요 취소 기능
    1) 사용자가 게시물이나 댓글에 좋아요를 남기거나 취소할 수 있습니다.
    2) 본인이 작성한 게시물과 댓글에 좋아요를 남길 수 없습니다.
    3) 같은 게시물에는 사용자당 한 번만 좋아요가 가능합니다.

위의 조건을 가지고 현재 댓글 기능은 구현이 되지않아 게시물 먼저 좋아요 기능을 구현해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class LikesController {

    private final LikesService likesService;

    // 게시물의 좋아요
    @PostMapping("/post/{postId}/likes")
    public ResponseEntity<LikesResponseDto> likePost(@PathVariable Long postId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        Integer likesCount = likesService.likePost(postId,userDetails.getUser());
        LikesResponseDto responseDto = new LikesResponseDto(postId,likesCount,"게시물에 좋아요를 등록했습니다.");
        return ResponseEntity.status(HttpStatus.OK).body(responseDto);
    }

    // 게시물의 좋아요 취소
    @DeleteMapping("/post/{postId}/unlikes")
    public ResponseEntity<LikesResponseDto> unlikePost(@PathVariable Long postId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        Integer likesCount = likesService.unlikePost(postId,userDetails.getUser());
        LikesResponseDto responseDto = new LikesResponseDto(postId,likesCount,"게시물에 좋아요를 취소했습니다.");
        return ResponseEntity.status(HttpStatus.OK).body(responseDto);
    }

}

PostController에서 구현해도 되지만 댓글도 좋아요를 구현해야하기에 코드의 유지보수측면이나 가독성을 높이기 위해 LikesController를 만들었다. 좋아요를 요청하면 게시판Id와 좋아요 수를 확인하기 위해 반환형태를 위와 같이 구성했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Service
@RequiredArgsConstructor
public class LikesService {

    private final LikesRepository likesRepository;
    private final PostRepository postRepository;

    // 게시물의 좋아요 기능
    @Transactional
    public Integer likePost(Long postId, User user) {
        Post post = findPostId(postId);

        // 사용자가 게시물의 작성자인지 확인
        if (post.getUser().getId().equals(user.getId())) {
            throw new IllegalArgumentException("자신의 게시물에 좋아요를 할 수 없습니다.");
        }
        // 사용자가 이미 좋아요를 했는지 확인
        if (likesRepository.findByPostIdAndUserId(postId, user.getId()).isPresent()) {
            throw new IllegalArgumentException("중복 좋아요는 할 수 없습니다.");
        } else {
            post.setLikesCount(post.getLikesCount() + 1);
            likesRepository.save(new Likes(post, user, ContentType.POST));
        }
        return post.getLikesCount();
    }

    // 게시물의 좋아요 취소 기능
    @Transactional
    public Integer unlikePost(Long postId, User user) {
        Post post = findPostId(postId);

        // 사용자가 게시물에 좋아요를 했는지 확인
        Likes like = likesRepository.findByPostIdAndUserId(postId, user.getId()).orElseThrow(() ->
                new IllegalArgumentException("해당 게시물에 좋아요를 하지 않았습니다."));

        post.setLikesCount(post.getLikesCount() - 1);
        likesRepository.delete(like);
        return post.getLikesCount();
    }

    private Post findPostId(Long postId) {
        return postRepository.findById(postId).orElseThrow(() ->
                new IllegalArgumentException("해당 게시물은 존재하지 않습니다."));
    }

}

게시물의 좋아요를 구현하는데 꼬박 하루가 걸렸다.. 오후 1시부터 시작해 거의 밤 11시가 다되어서 테스트까지 마무리하고 코드 점검 후 push했다.

Service 기능을 구현하기 위해 다음과 같이 고민해보았다.

1
2
3
4
1) 자신의 게시물에는 좋아요를 할 수 없다
-> 게시물을 작성한 사용자 아이디와 현재 사용자 아이디가 같다면 좋아요를 할 수없다.
2) 같은 게시물에는 하나의 좋아요만 가능하다
-> likeRepository에 해당 게시물아이디에서 현재 사용자가  좋아요를 했는지 확인해야한다. 

그리고 Set컬렉션으로 한번 구현해보고 싶다는 생각을 했다. 중복 사용이 허용되지 않으니 자신의 게시물에 좋아요를 할 수 없다는 구현이 좀 더 쉬울 거라 생각했다.. 하지만 내 생각은 달랐다.. Set컬렉션이 아직까진 낯설었다. 여러가지 고민과 한참의 시도 끝에 count에 ++; –; 하자는 생각으로 구현을 해보았고, 사실 setter를 안쓰려고 노력을 해야하는데.. 다른 방법이 떠오르지 않았다. 그리고 좋아요 취소 같은 경우에는 해당 게시물에 좋아요를 하지 않았는데, 취소를 하는경우도 .. 있을까? 했지만 프론트 쪽에서 어떻게 하느냐에 따라 다르니 그냥 도전한다 생각하고 좋아요를 하지 않았는데, 취소를 하려고 한다면 위와 같이 예외처리를 할 수 있도록 했다.

이하 Entity, ResponseDto, 나중에 구현할 댓글을 위해 Enum으로 게시물/댓글을 구분한 것까지의 아래의 코드에서 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Entity
@Getter
@Setter
@Table(name = "likes")
@NoArgsConstructor
public class Likes extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "like_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "post_id")
    Post post;

//    @ManyToOne
//    @JoinColumn(name = "comment_id")
//    Comment comment;

    @Column(name = "content_type")
    @Enumerated(EnumType.STRING)
    private ContentType contentType;

    public Likes(Post post, User user, ContentType contentType) {
        this.post = post;
        this.user = user;
        this.contentType = contentType;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Setter
@Getter
@NoArgsConstructor
public class LikesResponseDto {
    private Long postId;
    private Integer likesCount;
    private String message;

    public LikesResponseDto(Long postId, Integer likesCount, String message) {
        this.postId = postId;
        this.likesCount = likesCount;
        this.message = message;
    }
}
1
2
3
4
5
6
7
8
@Getter
@RequiredArgsConstructor
public enum ContentType {
    POST("게시물"),
    COMMENT("댓글");

    private final String contentType;
}

롬북이 있으니.. 참 좋다.. 너무 편해요. 😁

아 그리고 공통 예외처리 반환을 어제 구현했던 profile에 적용했는데, 그 부분은 내일 다시 작성해보려고 한다. 이제 자야할 것 같다. 내일을 위해! 😊

This post is licensed under CC BY 4.0 by the author.