diff --git a/src/main/java/com/example/be/domain/Post.java b/src/main/java/com/example/be/domain/Post.java index 9f4e92c..357674d 100644 --- a/src/main/java/com/example/be/domain/Post.java +++ b/src/main/java/com/example/be/domain/Post.java @@ -4,6 +4,7 @@ import lombok.*; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Entity @@ -32,6 +33,9 @@ public class Post { @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) private List comments; + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes = new ArrayList<>(); + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id") private User user; diff --git a/src/main/java/com/example/be/domain/PostLike.java b/src/main/java/com/example/be/domain/PostLike.java new file mode 100644 index 0000000..a6e968d --- /dev/null +++ b/src/main/java/com/example/be/domain/PostLike.java @@ -0,0 +1,32 @@ +package com.example.be.domain; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "post_id"}) +}) +public class PostLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + private LocalDateTime createDate; +} \ No newline at end of file diff --git a/src/main/java/com/example/be/repository/PostLikeRepository.java b/src/main/java/com/example/be/repository/PostLikeRepository.java new file mode 100644 index 0000000..5d80f61 --- /dev/null +++ b/src/main/java/com/example/be/repository/PostLikeRepository.java @@ -0,0 +1,22 @@ +package com.example.be.repository; + +import com.example.be.domain.Post; +import com.example.be.domain.PostLike; +import com.example.be.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface PostLikeRepository extends JpaRepository { + + Optional findByUserAndPost(User user, Post post); + + boolean existsByUserAndPost(User user, Post post); + + long countByPost(Post post); + + @Query("SELECT COUNT(pl) FROM PostLike pl WHERE pl.post.id = :postId") + long countByPostId(@Param("postId") Long postId); +} \ No newline at end of file diff --git a/src/main/java/com/example/be/repository/PostRepository.java b/src/main/java/com/example/be/repository/PostRepository.java index 161bd46..7afc8bb 100644 --- a/src/main/java/com/example/be/repository/PostRepository.java +++ b/src/main/java/com/example/be/repository/PostRepository.java @@ -13,7 +13,7 @@ public interface PostRepository extends JpaRepository { // PostRepository에 추가 @Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments ORDER BY p.createDate DESC") - Page findAllWithCommentsOrderByCreateDateDesc(Pageable pageable); + Page findAllByOrderByCreateDateDesc(Pageable pageable); // ID로 게시글 조회 (댓글 포함) @EntityGraph(attributePaths = {"comments", "comments.user", "user"}) diff --git a/src/main/java/com/example/be/service/PostLikeServiceImpl.java b/src/main/java/com/example/be/service/PostLikeServiceImpl.java new file mode 100644 index 0000000..8f7dcaa --- /dev/null +++ b/src/main/java/com/example/be/service/PostLikeServiceImpl.java @@ -0,0 +1,89 @@ +package com.example.be.service; + +import com.example.be.apiPayload.code.status.ErrorStatus; +import com.example.be.apiPayload.exception.handler.UserHandler; +import com.example.be.domain.Post; +import com.example.be.domain.PostLike; +import com.example.be.domain.User; +import com.example.be.repository.PostLikeRepository; +import com.example.be.repository.PostRepository; +import com.example.be.repository.UserRepository; +import com.example.be.web.dto.PostDTO; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class PostLikeServiceImpl { + + private final JwtUtilServiceImpl jwtUtilService; + private final UserRepository userRepository; + private final PostRepository postRepository; + private final PostLikeRepository postLikeRepository; + + @Transactional + public PostDTO.PostLikeResponseDTO togglePostLike(Long postId, HttpServletRequest request) { + // 토큰에서 사용자 정보 가져오기 + String accessToken = jwtUtilService.extractTokenFromCookie(request, "accessToken"); + if (accessToken == null) { + throw new UserHandler(ErrorStatus._NOT_FOUND_USER); + } + + String userId = jwtUtilService.getUserIdFromToken(accessToken); + User user = userRepository.findByUserId(UUID.fromString(userId)) + .orElseThrow(() -> new UserHandler(ErrorStatus._NOT_FOUND_USER)); + + // 게시글 정보 가져오기 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new UserHandler(ErrorStatus._NOT_FOUND_POST)); + + // 이미 좋아요를 눌렀는지 확인 + Optional existingLike = postLikeRepository.findByUserAndPost(user, post); + + boolean isLiked; + if (existingLike.isPresent()) { + // 좋아요가 이미 있으면 삭제 (좋아요 취소) + postLikeRepository.delete(existingLike.get()); + isLiked = false; + } else { + // 좋아요가 없으면 추가 + PostLike postLike = PostLike.builder() + .user(user) + .post(post) + .createDate(LocalDateTime.now()) + .build(); + postLikeRepository.save(postLike); + isLiked = true; + } + + // 좋아요 수 조회 + long likeCount = postLikeRepository.countByPost(post); + + return PostDTO.PostLikeResponseDTO.builder() + .postId(postId) + .liked(isLiked) + .likeCount(likeCount) + .build(); + } + + // 특정 게시글에 대한 사용자의 좋아요 여부 확인 + public boolean isPostLikedByUser(Post post, User user) { + return postLikeRepository.existsByUserAndPost(user, post); + } + + // 특정 게시글의 좋아요 수 조회 + public long getPostLikeCount(Post post) { + return postLikeRepository.countByPost(post); + } + + // 특정 게시글 ID의 좋아요 수 조회 + public long getPostLikeCount(Long postId) { + return postLikeRepository.countByPostId(postId); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/be/service/PostServiceImpl.java b/src/main/java/com/example/be/service/PostServiceImpl.java index 2f52132..bb8e015 100644 --- a/src/main/java/com/example/be/service/PostServiceImpl.java +++ b/src/main/java/com/example/be/service/PostServiceImpl.java @@ -31,9 +31,11 @@ public class PostServiceImpl { private final JwtUtilServiceImpl jwtUtilService; private final UserRepository userRepository; private final PostRepository postRepository; + private final PostLikeServiceImpl postLikeService; - public CommonDTO.IsSuccessDTO write(PostDTO.postRequestDTO request, HttpServletRequest req) { + //글 작성 메서드 + public CommonDTO.IsSuccessDTO write(PostDTO.postRequestDTO request, HttpServletRequest req) { String accessToken = jwtUtilService.extractTokenFromCookie(req, "accessToken"); // 토큰이 없는 경우 처리 @@ -63,7 +65,7 @@ public CommonDTO.IsSuccessDTO write(PostDTO.postRequestDTO request, HttpServletR public PostDTO.PageResponseDTO getPosts(int page, int size) { Pageable pageable = PageRequest.of(page, size); - Page postPage = postRepository.findAllWithCommentsOrderByCreateDateDesc(pageable); + Page postPage = postRepository.findAllByOrderByCreateDateDesc(pageable); List postDtoList = postPage.getContent().stream() .map(post -> PostDTO.postResponseDTO.builder() @@ -73,8 +75,8 @@ public PostDTO.PageResponseDTO getPosts(int page, int size) { .userName(post.getUser().getName()) .createDate(post.getCreateDate().toLocalDate()) .isDone(post.isDone()) - // 댓글 개수 추가 .commentCount(post.getComments() != null ? post.getComments().size() : 0) + .likeCount(postLikeService.getPostLikeCount(post)) .build()) .collect(Collectors.toList()); @@ -89,10 +91,29 @@ public PostDTO.PageResponseDTO getPosts(int page, int size) { } // 게시글 상세 조회 메서드 - public PostDTO.PostDetailResponseDTO getPostDetail(Long postId) { + public PostDTO.PostDetailResponseDTO getPostDetail(Long postId, HttpServletRequest request) { Post post = postRepository.findById(postId) .orElseThrow(() -> new UserHandler(ErrorStatus._NOT_FOUND_POST)); + // 현재 로그인한 사용자 정보 가져오기 (좋아요 여부 확인용) + User currentUser = null; + boolean isLiked = false; + + try { + String accessToken = jwtUtilService.extractTokenFromCookie(request, "accessToken"); + if (accessToken != null) { + String userId = jwtUtilService.getUserIdFromToken(accessToken); + currentUser = userRepository.findByUserId(UUID.fromString(userId)).orElse(null); + + // 현재 사용자가 이 게시글에 좋아요를 눌렀는지 확인 + if (currentUser != null) { + isLiked = postLikeService.isPostLikedByUser(post, currentUser); + } + } + } catch (Exception e) { + // 비로그인 사용자도 게시글을 볼 수 있도록 예외를 무시하고 진행 + } + // 댓글 목록 변환 List commentDtoList = post.getComments().stream() .map(comment -> PostDTO.CommentResponseDTO.builder() @@ -104,6 +125,9 @@ public PostDTO.PostDetailResponseDTO getPostDetail(Long postId) { .build()) .collect(Collectors.toList()); + // 좋아요 수 조회 + long likeCount = postLikeService.getPostLikeCount(post); + // 게시글 상세 정보 변환 return PostDTO.PostDetailResponseDTO.builder() .id(post.getId()) @@ -114,6 +138,9 @@ public PostDTO.PostDetailResponseDTO getPostDetail(Long postId) { .isDone(post.isDone()) .commentCount(commentDtoList.size()) .comments(commentDtoList) + // 좋아요 정보 추가 + .likeCount(likeCount) + .liked(isLiked) .build(); } } diff --git a/src/main/java/com/example/be/web/controller/PostController.java b/src/main/java/com/example/be/web/controller/PostController.java index 93a96c9..9e32227 100644 --- a/src/main/java/com/example/be/web/controller/PostController.java +++ b/src/main/java/com/example/be/web/controller/PostController.java @@ -6,7 +6,9 @@ import com.example.be.domain.User; import com.example.be.repository.UserRepository; import com.example.be.service.JwtUtilServiceImpl; +import com.example.be.service.PostLikeServiceImpl; import com.example.be.service.PostServiceImpl; +import com.example.be.service.UserServiceImpl; import com.example.be.web.dto.CommonDTO; import com.example.be.web.dto.PostDTO; import com.example.be.web.dto.UserDTO; @@ -24,6 +26,7 @@ public class PostController { private final PostServiceImpl postService; + private final PostLikeServiceImpl postLikeService; @PostMapping("/write") @Operation(summary = "게시글 작성 API") @@ -42,8 +45,18 @@ public ApiResponse getPosts( @GetMapping("/{postId}") @Operation(summary = "게시글 상세 조회 API", description = "게시글 ID로 게시글 상세 정보와 댓글 목록을 조회합니다.") public ApiResponse getPostDetail( - @Parameter(description = "게시글 ID") @PathVariable Long postId) { - return ApiResponse.onSuccess(postService.getPostDetail(postId)); + @Parameter(description = "게시글 ID") @PathVariable Long postId, + HttpServletRequest request) { + return ApiResponse.onSuccess(postService.getPostDetail(postId, request)); + } + + // 좋아요 토글 API 추가 + @PostMapping("/{postId}/like") + @Operation(summary = "게시글 좋아요 토글 API", description = "게시글에 좋아요를 누르거나 취소합니다.") + public ApiResponse togglePostLike( + @Parameter(description = "게시글 ID") @PathVariable Long postId, + HttpServletRequest request) { + return ApiResponse.onSuccess(postLikeService.togglePostLike(postId, request)); } } diff --git a/src/main/java/com/example/be/web/dto/CommonDTO.java b/src/main/java/com/example/be/web/dto/CommonDTO.java index 7fc1640..e844643 100644 --- a/src/main/java/com/example/be/web/dto/CommonDTO.java +++ b/src/main/java/com/example/be/web/dto/CommonDTO.java @@ -13,6 +13,6 @@ public class CommonDTO { @AllArgsConstructor @Schema(title = "COMMON_RES_06 : API 실행 성공 여부 응답 DTO") public static class IsSuccessDTO{ - Boolean isSuccess; + private Boolean isSuccess; } } diff --git a/src/main/java/com/example/be/web/dto/PostDTO.java b/src/main/java/com/example/be/web/dto/PostDTO.java index f346a87..22fef93 100644 --- a/src/main/java/com/example/be/web/dto/PostDTO.java +++ b/src/main/java/com/example/be/web/dto/PostDTO.java @@ -19,8 +19,8 @@ public class PostDTO { @AllArgsConstructor public static class postRequestDTO{ - String title; - String content; + private String title; + private String content; } @Builder @@ -28,14 +28,15 @@ public static class postRequestDTO{ @NoArgsConstructor @AllArgsConstructor public static class postResponseDTO { - Long id; - String title; - String content; - String userName; + private Long id; + private String title; + private String content; + private String userName; + private long likeCount; @JsonFormat(pattern = "yyyy-MM-dd") - LocalDate createDate; - int commentCount; - boolean isDone; + private LocalDate createDate; + private int commentCount; + private boolean isDone; } @Builder @@ -78,14 +79,24 @@ public static class PostDetailResponseDTO { private String title; private String content; private String userName; - + private long likeCount; + private boolean liked; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createDate; - private boolean isDone; private int commentCount; private List comments; } + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "게시글 좋아요 응답 DTO") + public static class PostLikeResponseDTO { + private Long postId; + private boolean liked; + private long likeCount; + } }