Skip to content

Commit 4c52264

Browse files
authored
Merge pull request #29 from StudyLink-SW-Project/feature/#27
[FEAT] #27 게시글 좋아요 기능 구현
2 parents 4da6e24 + 6055e26 commit 4c52264

9 files changed

Lines changed: 217 additions & 19 deletions

File tree

src/main/java/com/example/be/domain/Post.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.*;
55

66
import java.time.LocalDateTime;
7+
import java.util.ArrayList;
78
import java.util.List;
89

910
@Entity
@@ -32,6 +33,9 @@ public class Post {
3233
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
3334
private List<Comment> comments;
3435

36+
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
37+
private List<PostLike> likes = new ArrayList<>();
38+
3539
@ManyToOne(fetch = FetchType.LAZY)
3640
@JoinColumn(name = "user_id")
3741
private User user;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.example.be.domain;
2+
3+
import jakarta.persistence.*;
4+
import lombok.*;
5+
6+
import java.time.LocalDateTime;
7+
8+
@Entity
9+
@Getter
10+
@Setter
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
@Table(uniqueConstraints = {
15+
@UniqueConstraint(columnNames = {"user_id", "post_id"})
16+
})
17+
public class PostLike {
18+
19+
@Id
20+
@GeneratedValue(strategy = GenerationType.IDENTITY)
21+
private Long id;
22+
23+
@ManyToOne(fetch = FetchType.LAZY)
24+
@JoinColumn(name = "user_id")
25+
private User user;
26+
27+
@ManyToOne(fetch = FetchType.LAZY)
28+
@JoinColumn(name = "post_id")
29+
private Post post;
30+
31+
private LocalDateTime createDate;
32+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.example.be.repository;
2+
3+
import com.example.be.domain.Post;
4+
import com.example.be.domain.PostLike;
5+
import com.example.be.domain.User;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
9+
10+
import java.util.Optional;
11+
12+
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
13+
14+
Optional<PostLike> findByUserAndPost(User user, Post post);
15+
16+
boolean existsByUserAndPost(User user, Post post);
17+
18+
long countByPost(Post post);
19+
20+
@Query("SELECT COUNT(pl) FROM PostLike pl WHERE pl.post.id = :postId")
21+
long countByPostId(@Param("postId") Long postId);
22+
}

src/main/java/com/example/be/repository/PostRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
public interface PostRepository extends JpaRepository<Post, Long> {
1414
// PostRepository에 추가
1515
@Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments ORDER BY p.createDate DESC")
16-
Page<Post> findAllWithCommentsOrderByCreateDateDesc(Pageable pageable);
16+
Page<Post> findAllByOrderByCreateDateDesc(Pageable pageable);
1717

1818
// ID로 게시글 조회 (댓글 포함)
1919
@EntityGraph(attributePaths = {"comments", "comments.user", "user"})
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package com.example.be.service;
2+
3+
import com.example.be.apiPayload.code.status.ErrorStatus;
4+
import com.example.be.apiPayload.exception.handler.UserHandler;
5+
import com.example.be.domain.Post;
6+
import com.example.be.domain.PostLike;
7+
import com.example.be.domain.User;
8+
import com.example.be.repository.PostLikeRepository;
9+
import com.example.be.repository.PostRepository;
10+
import com.example.be.repository.UserRepository;
11+
import com.example.be.web.dto.PostDTO;
12+
import jakarta.servlet.http.HttpServletRequest;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.transaction.annotation.Transactional;
16+
17+
import java.time.LocalDateTime;
18+
import java.util.Optional;
19+
import java.util.UUID;
20+
21+
@Service
22+
@RequiredArgsConstructor
23+
public class PostLikeServiceImpl {
24+
25+
private final JwtUtilServiceImpl jwtUtilService;
26+
private final UserRepository userRepository;
27+
private final PostRepository postRepository;
28+
private final PostLikeRepository postLikeRepository;
29+
30+
@Transactional
31+
public PostDTO.PostLikeResponseDTO togglePostLike(Long postId, HttpServletRequest request) {
32+
// 토큰에서 사용자 정보 가져오기
33+
String accessToken = jwtUtilService.extractTokenFromCookie(request, "accessToken");
34+
if (accessToken == null) {
35+
throw new UserHandler(ErrorStatus._NOT_FOUND_USER);
36+
}
37+
38+
String userId = jwtUtilService.getUserIdFromToken(accessToken);
39+
User user = userRepository.findByUserId(UUID.fromString(userId))
40+
.orElseThrow(() -> new UserHandler(ErrorStatus._NOT_FOUND_USER));
41+
42+
// 게시글 정보 가져오기
43+
Post post = postRepository.findById(postId)
44+
.orElseThrow(() -> new UserHandler(ErrorStatus._NOT_FOUND_POST));
45+
46+
// 이미 좋아요를 눌렀는지 확인
47+
Optional<PostLike> existingLike = postLikeRepository.findByUserAndPost(user, post);
48+
49+
boolean isLiked;
50+
if (existingLike.isPresent()) {
51+
// 좋아요가 이미 있으면 삭제 (좋아요 취소)
52+
postLikeRepository.delete(existingLike.get());
53+
isLiked = false;
54+
} else {
55+
// 좋아요가 없으면 추가
56+
PostLike postLike = PostLike.builder()
57+
.user(user)
58+
.post(post)
59+
.createDate(LocalDateTime.now())
60+
.build();
61+
postLikeRepository.save(postLike);
62+
isLiked = true;
63+
}
64+
65+
// 좋아요 수 조회
66+
long likeCount = postLikeRepository.countByPost(post);
67+
68+
return PostDTO.PostLikeResponseDTO.builder()
69+
.postId(postId)
70+
.liked(isLiked)
71+
.likeCount(likeCount)
72+
.build();
73+
}
74+
75+
// 특정 게시글에 대한 사용자의 좋아요 여부 확인
76+
public boolean isPostLikedByUser(Post post, User user) {
77+
return postLikeRepository.existsByUserAndPost(user, post);
78+
}
79+
80+
// 특정 게시글의 좋아요 수 조회
81+
public long getPostLikeCount(Post post) {
82+
return postLikeRepository.countByPost(post);
83+
}
84+
85+
// 특정 게시글 ID의 좋아요 수 조회
86+
public long getPostLikeCount(Long postId) {
87+
return postLikeRepository.countByPostId(postId);
88+
}
89+
}

src/main/java/com/example/be/service/PostServiceImpl.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ public class PostServiceImpl {
3131
private final JwtUtilServiceImpl jwtUtilService;
3232
private final UserRepository userRepository;
3333
private final PostRepository postRepository;
34+
private final PostLikeServiceImpl postLikeService;
3435

35-
public CommonDTO.IsSuccessDTO write(PostDTO.postRequestDTO request, HttpServletRequest req) {
3636

37+
//글 작성 메서드
38+
public CommonDTO.IsSuccessDTO write(PostDTO.postRequestDTO request, HttpServletRequest req) {
3739
String accessToken = jwtUtilService.extractTokenFromCookie(req, "accessToken");
3840

3941
// 토큰이 없는 경우 처리
@@ -63,7 +65,7 @@ public CommonDTO.IsSuccessDTO write(PostDTO.postRequestDTO request, HttpServletR
6365

6466
public PostDTO.PageResponseDTO getPosts(int page, int size) {
6567
Pageable pageable = PageRequest.of(page, size);
66-
Page<Post> postPage = postRepository.findAllWithCommentsOrderByCreateDateDesc(pageable);
68+
Page<Post> postPage = postRepository.findAllByOrderByCreateDateDesc(pageable);
6769

6870
List<PostDTO.postResponseDTO> postDtoList = postPage.getContent().stream()
6971
.map(post -> PostDTO.postResponseDTO.builder()
@@ -73,8 +75,8 @@ public PostDTO.PageResponseDTO getPosts(int page, int size) {
7375
.userName(post.getUser().getName())
7476
.createDate(post.getCreateDate().toLocalDate())
7577
.isDone(post.isDone())
76-
// 댓글 개수 추가
7778
.commentCount(post.getComments() != null ? post.getComments().size() : 0)
79+
.likeCount(postLikeService.getPostLikeCount(post))
7880
.build())
7981
.collect(Collectors.toList());
8082

@@ -89,10 +91,29 @@ public PostDTO.PageResponseDTO getPosts(int page, int size) {
8991
}
9092

9193
// 게시글 상세 조회 메서드
92-
public PostDTO.PostDetailResponseDTO getPostDetail(Long postId) {
94+
public PostDTO.PostDetailResponseDTO getPostDetail(Long postId, HttpServletRequest request) {
9395
Post post = postRepository.findById(postId)
9496
.orElseThrow(() -> new UserHandler(ErrorStatus._NOT_FOUND_POST));
9597

98+
// 현재 로그인한 사용자 정보 가져오기 (좋아요 여부 확인용)
99+
User currentUser = null;
100+
boolean isLiked = false;
101+
102+
try {
103+
String accessToken = jwtUtilService.extractTokenFromCookie(request, "accessToken");
104+
if (accessToken != null) {
105+
String userId = jwtUtilService.getUserIdFromToken(accessToken);
106+
currentUser = userRepository.findByUserId(UUID.fromString(userId)).orElse(null);
107+
108+
// 현재 사용자가 이 게시글에 좋아요를 눌렀는지 확인
109+
if (currentUser != null) {
110+
isLiked = postLikeService.isPostLikedByUser(post, currentUser);
111+
}
112+
}
113+
} catch (Exception e) {
114+
// 비로그인 사용자도 게시글을 볼 수 있도록 예외를 무시하고 진행
115+
}
116+
96117
// 댓글 목록 변환
97118
List<PostDTO.CommentResponseDTO> commentDtoList = post.getComments().stream()
98119
.map(comment -> PostDTO.CommentResponseDTO.builder()
@@ -104,6 +125,9 @@ public PostDTO.PostDetailResponseDTO getPostDetail(Long postId) {
104125
.build())
105126
.collect(Collectors.toList());
106127

128+
// 좋아요 수 조회
129+
long likeCount = postLikeService.getPostLikeCount(post);
130+
107131
// 게시글 상세 정보 변환
108132
return PostDTO.PostDetailResponseDTO.builder()
109133
.id(post.getId())
@@ -114,6 +138,9 @@ public PostDTO.PostDetailResponseDTO getPostDetail(Long postId) {
114138
.isDone(post.isDone())
115139
.commentCount(commentDtoList.size())
116140
.comments(commentDtoList)
141+
// 좋아요 정보 추가
142+
.likeCount(likeCount)
143+
.liked(isLiked)
117144
.build();
118145
}
119146
}

src/main/java/com/example/be/web/controller/PostController.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import com.example.be.domain.User;
77
import com.example.be.repository.UserRepository;
88
import com.example.be.service.JwtUtilServiceImpl;
9+
import com.example.be.service.PostLikeServiceImpl;
910
import com.example.be.service.PostServiceImpl;
11+
import com.example.be.service.UserServiceImpl;
1012
import com.example.be.web.dto.CommonDTO;
1113
import com.example.be.web.dto.PostDTO;
1214
import com.example.be.web.dto.UserDTO;
@@ -24,6 +26,7 @@
2426
public class PostController {
2527

2628
private final PostServiceImpl postService;
29+
private final PostLikeServiceImpl postLikeService;
2730

2831
@PostMapping("/write")
2932
@Operation(summary = "게시글 작성 API")
@@ -42,8 +45,18 @@ public ApiResponse<PostDTO.PageResponseDTO> getPosts(
4245
@GetMapping("/{postId}")
4346
@Operation(summary = "게시글 상세 조회 API", description = "게시글 ID로 게시글 상세 정보와 댓글 목록을 조회합니다.")
4447
public ApiResponse<PostDTO.PostDetailResponseDTO> getPostDetail(
45-
@Parameter(description = "게시글 ID") @PathVariable Long postId) {
46-
return ApiResponse.onSuccess(postService.getPostDetail(postId));
48+
@Parameter(description = "게시글 ID") @PathVariable Long postId,
49+
HttpServletRequest request) {
50+
return ApiResponse.onSuccess(postService.getPostDetail(postId, request));
51+
}
52+
53+
// 좋아요 토글 API 추가
54+
@PostMapping("/{postId}/like")
55+
@Operation(summary = "게시글 좋아요 토글 API", description = "게시글에 좋아요를 누르거나 취소합니다.")
56+
public ApiResponse<PostDTO.PostLikeResponseDTO> togglePostLike(
57+
@Parameter(description = "게시글 ID") @PathVariable Long postId,
58+
HttpServletRequest request) {
59+
return ApiResponse.onSuccess(postLikeService.togglePostLike(postId, request));
4760
}
4861

4962
}

src/main/java/com/example/be/web/dto/CommonDTO.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ public class CommonDTO {
1313
@AllArgsConstructor
1414
@Schema(title = "COMMON_RES_06 : API 실행 성공 여부 응답 DTO")
1515
public static class IsSuccessDTO{
16-
Boolean isSuccess;
16+
private Boolean isSuccess;
1717
}
1818
}

src/main/java/com/example/be/web/dto/PostDTO.java

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,24 @@ public class PostDTO {
1919
@AllArgsConstructor
2020

2121
public static class postRequestDTO{
22-
String title;
23-
String content;
22+
private String title;
23+
private String content;
2424
}
2525

2626
@Builder
2727
@Getter
2828
@NoArgsConstructor
2929
@AllArgsConstructor
3030
public static class postResponseDTO {
31-
Long id;
32-
String title;
33-
String content;
34-
String userName;
31+
private Long id;
32+
private String title;
33+
private String content;
34+
private String userName;
35+
private long likeCount;
3536
@JsonFormat(pattern = "yyyy-MM-dd")
36-
LocalDate createDate;
37-
int commentCount;
38-
boolean isDone;
37+
private LocalDate createDate;
38+
private int commentCount;
39+
private boolean isDone;
3940
}
4041

4142
@Builder
@@ -78,14 +79,24 @@ public static class PostDetailResponseDTO {
7879
private String title;
7980
private String content;
8081
private String userName;
81-
82+
private long likeCount;
83+
private boolean liked;
8284
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
8385
private LocalDateTime createDate;
84-
8586
private boolean isDone;
8687
private int commentCount;
8788
private List<CommentResponseDTO> comments;
8889
}
8990

91+
@Builder
92+
@Getter
93+
@NoArgsConstructor
94+
@AllArgsConstructor
95+
@Schema(description = "게시글 좋아요 응답 DTO")
96+
public static class PostLikeResponseDTO {
97+
private Long postId;
98+
private boolean liked;
99+
private long likeCount;
100+
}
90101

91102
}

0 commit comments

Comments
 (0)