From 6fef1e39538c2622ef8575e6cb43d1c3c0c52801 Mon Sep 17 00:00:00 2001 From: uni-j-uni Date: Sun, 14 Dec 2025 00:07:22 +0900 Subject: [PATCH] =?UTF-8?q?:recycle:Refactor:=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=8C=93=EA=B8=80=20=EC=88=98?= =?UTF-8?q?,=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=88=98=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostController.java | 5 + .../post/controller/PostControllerImpl.java | 10 +- .../post/dto/response/PostDetailResponse.java | 9 ++ .../refit/domain/post/entity/PostLike.java | 34 +++++++ .../refit/domain/post/mapper/PostMapper.java | 6 +- .../post/repository/PostLikeRepository.java | 40 ++++++++ .../domain/post/service/PostService.java | 10 ++ .../domain/post/service/PostServiceImpl.java | 99 ++++++++++++++++++- 8 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/sku/refit/domain/post/entity/PostLike.java create mode 100644 src/main/java/com/sku/refit/domain/post/repository/PostLikeRepository.java diff --git a/src/main/java/com/sku/refit/domain/post/controller/PostController.java b/src/main/java/com/sku/refit/domain/post/controller/PostController.java index 0efa1fa..c076b20 100644 --- a/src/main/java/com/sku/refit/domain/post/controller/PostController.java +++ b/src/main/java/com/sku/refit/domain/post/controller/PostController.java @@ -48,6 +48,11 @@ ResponseEntity> createPost( @RequestPart(value = "imageList", required = false) List imageList); + @PostMapping("/{postId}/like") + @Operation(summary = "게시글 좋아요 토글", description = "게시글 좋아요를 등록/취소합니다.") + ResponseEntity> togglePostLike( + @Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId); + @GetMapping("/admin") @Operation(summary = "[관리자] 게시글 전체 조회", description = "전체 게시글 리스트를 조회합니다.") ResponseEntity>> getAllPosts(); diff --git a/src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java b/src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java index 325d411..3487769 100644 --- a/src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java +++ b/src/main/java/com/sku/refit/domain/post/controller/PostControllerImpl.java @@ -8,6 +8,7 @@ import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @@ -18,11 +19,9 @@ import com.sku.refit.global.response.BaseResponse; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; @RestController @RequiredArgsConstructor -@Slf4j public class PostControllerImpl implements PostController { private final PostService postService; @@ -35,6 +34,13 @@ public ResponseEntity> createPost( return ResponseEntity.ok(BaseResponse.success(response)); } + @Override + public ResponseEntity> togglePostLike(@PathVariable Long postId) { + + boolean liked = postService.togglePostLike(postId); + return ResponseEntity.ok(BaseResponse.success(liked)); + } + @Override public ResponseEntity>> getAllPosts() { diff --git a/src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java b/src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java index c9aae55..07b94f5 100644 --- a/src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java +++ b/src/main/java/com/sku/refit/domain/post/dto/response/PostDetailResponse.java @@ -32,6 +32,12 @@ public class PostDetailResponse { @Schema(description = "게시글 조회수", example = "100") private Long views; + @Schema(description = "게시글 좋아요수", example = "100") + private Long likes; + + @Schema(description = "게시글 댓글수", example = "100") + private Long comments; + @Schema(description = "게시글 작성 시간", example = "2025-12-03T14:37:17") private LocalDateTime createdAt; @@ -41,6 +47,9 @@ public class PostDetailResponse { @Schema(description = "작성자 본인 여부", example = "true") private Boolean isAuthor; + @Schema(description = "내가 좋아요를 눌렀는지 여부", example = "true") + private Boolean isLiked; + @Schema(description = "이미지 URL 리스트") private List imageUrlList; diff --git a/src/main/java/com/sku/refit/domain/post/entity/PostLike.java b/src/main/java/com/sku/refit/domain/post/entity/PostLike.java new file mode 100644 index 0000000..1198cbc --- /dev/null +++ b/src/main/java/com/sku/refit/domain/post/entity/PostLike.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.post.entity; + +import jakarta.persistence.*; + +import com.sku.refit.domain.user.entity.User; +import com.sku.refit.global.common.BaseTimeEntity; + +import lombok.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Table( + name = "post_like", + uniqueConstraints = {@UniqueConstraint(columnNames = {"post_id", "user_id"})}) +public class PostLike extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id", nullable = false) + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; +} diff --git a/src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java b/src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java index 1f595f5..3e084dd 100644 --- a/src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java +++ b/src/main/java/com/sku/refit/domain/post/mapper/PostMapper.java @@ -29,16 +29,20 @@ public Post toPost( .build(); } - public PostDetailResponse toDetailResponse(Post post, User user) { + public PostDetailResponse toDetailResponse( + Post post, Long likeCount, Boolean isLiked, User user) { return PostDetailResponse.builder() .postId(post.getId()) .title(post.getTitle()) .content(post.getContent()) .views(post.getViews()) + .likes(likeCount) + .comments((long) post.getCommentList().size()) .createdAt(post.getCreatedAt()) .nickname(post.getUser().getNickname()) .isAuthor(user != null && post.getUser().getUsername().equals(user.getUsername())) + .isLiked(isLiked) .category(post.getPostCategory()) .commentIdList(post.getCommentList().stream().map(Comment::getId).toList()) .build(); diff --git a/src/main/java/com/sku/refit/domain/post/repository/PostLikeRepository.java b/src/main/java/com/sku/refit/domain/post/repository/PostLikeRepository.java new file mode 100644 index 0000000..274b686 --- /dev/null +++ b/src/main/java/com/sku/refit/domain/post/repository/PostLikeRepository.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) SKU 다시입을Lab + */ +package com.sku.refit.domain.post.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.sku.refit.domain.post.entity.PostLike; + +public interface PostLikeRepository extends JpaRepository { + + Optional findByPostIdAndUserId(Long postId, Long userId); + + boolean existsByPostIdAndUserId(Long postId, Long userId); + + long countByPostId(Long postId); + + @Query( + """ + select pl.post.id, count(pl) + from PostLike pl + where pl.post.id in :postIds + group by pl.post.id +""") + List countByPostIds(@Param("postIds") List postIds); + + @Query( + """ + select pl.post.id + from PostLike pl + where pl.post.id in :postIds + and pl.user.id = :userId +""") + List findLikedPostIds(@Param("postIds") List postIds, @Param("userId") Long userId); +} diff --git a/src/main/java/com/sku/refit/domain/post/service/PostService.java b/src/main/java/com/sku/refit/domain/post/service/PostService.java index 16cd69d..c1443c9 100644 --- a/src/main/java/com/sku/refit/domain/post/service/PostService.java +++ b/src/main/java/com/sku/refit/domain/post/service/PostService.java @@ -36,6 +36,16 @@ public interface PostService { */ PostDetailResponse createPost(PostRequest request, List images); + /** + * 게시글 좋아요를 토글합니다. + * + *

이미 좋아요가 되어 있으면 취소하고, 좋아요가 없으면 새로 생성합니다. + * + * @param postId 게시글 ID + * @return true: 좋아요 상태 / false: 좋아요 취소 상태 + */ + boolean togglePostLike(Long postId); + /** * 모든 게시글 목록을 조회합니다. * diff --git a/src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java b/src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java index f4a89c0..3f034db 100644 --- a/src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/sku/refit/domain/post/service/PostServiceImpl.java @@ -4,7 +4,11 @@ package com.sku.refit.domain.post.service; import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -17,8 +21,10 @@ import com.sku.refit.domain.post.dto.response.PostDetailResponse; import com.sku.refit.domain.post.entity.Post; import com.sku.refit.domain.post.entity.PostCategory; +import com.sku.refit.domain.post.entity.PostLike; import com.sku.refit.domain.post.exception.PostErrorCode; import com.sku.refit.domain.post.mapper.PostMapper; +import com.sku.refit.domain.post.repository.PostLikeRepository; import com.sku.refit.domain.post.repository.PostRepository; import com.sku.refit.domain.user.entity.User; import com.sku.refit.domain.user.service.UserService; @@ -37,6 +43,7 @@ public class PostServiceImpl implements PostService { private final PostRepository postRepository; + private final PostLikeRepository postLikeRepository; private final S3Service s3Service; private final UserService userService; private final PostMapper postMapper; @@ -72,7 +79,35 @@ public PostDetailResponse createPost(PostRequest request, List im user.getId(), imageUrlList.size()); - return postMapper.toDetailResponse(post, user); + return postMapper.toDetailResponse(post, 0L, false, user); + } + + @Override + @Transactional + public boolean togglePostLike(Long postId) { + + User user = userService.getCurrentUser(); + Post post = + postRepository + .findById(postId) + .orElseThrow(() -> new CustomException(PostErrorCode.POST_NOT_FOUND)); + + return postLikeRepository + .findByPostIdAndUserId(postId, user.getId()) + .map( + like -> { + postLikeRepository.delete(like); + log.info("[POST LIKE CANCEL] postId={}, userId={}", postId, user.getId()); + return false; + }) + .orElseGet( + () -> { + PostLike postLike = PostLike.builder().post(post).user(user).build(); + + postLikeRepository.save(postLike); + log.info("[POST LIKE CREATE] postId={}, userId={}", postId, user.getId()); + return true; + }); } @Override @@ -82,9 +117,32 @@ public List getAllPosts() { User user = userService.getCurrentUser(); List posts = postRepository.findAll(); + if (posts.isEmpty()) { + return List.of(); + } + + List postIds = posts.stream().map(Post::getId).toList(); + + Map likeCountMap = new HashMap<>(); + List likeCounts = postLikeRepository.countByPostIds(postIds); + for (Object[] row : likeCounts) { + likeCountMap.put((Long) row[0], (Long) row[1]); + } + + Set likedPostIds = + new HashSet<>(postLikeRepository.findLikedPostIds(postIds, user.getId())); + log.info("[POST LIST] userId={}, postCount={}", user.getId(), posts.size()); - return posts.stream().map(post -> postMapper.toDetailResponse(post, user)).toList(); + return posts.stream() + .map( + post -> + postMapper.toDetailResponse( + post, + likeCountMap.getOrDefault(post.getId(), 0L), + likedPostIds.contains(post.getId()), + user)) + .toList(); } @Override @@ -111,8 +169,33 @@ public InfiniteResponse getPostsByCategory( posts = posts.subList(0, size); } + List postIds = posts.stream().map(Post::getId).toList(); + + Map likeCountMap = new HashMap<>(); + if (!postIds.isEmpty()) { + List likeCounts = postLikeRepository.countByPostIds(postIds); + for (Object[] row : likeCounts) { + Long postId = (Long) row[0]; + Long count = (Long) row[1]; + likeCountMap.put(postId, count); + } + } + + Set likedPostIds = + postIds.isEmpty() + ? Set.of() + : new HashSet<>(postLikeRepository.findLikedPostIds(postIds, user.getId())); + List postResponseList = - posts.stream().map(post -> postMapper.toDetailResponse(post, user)).toList(); + posts.stream() + .map( + post -> + postMapper.toDetailResponse( + post, + likeCountMap.getOrDefault(post.getId(), 0L), + likedPostIds.contains(post.getId()), + user)) + .toList(); Long newLastCursor = posts.isEmpty() ? null : posts.getLast().getId(); @@ -142,7 +225,10 @@ public PostDetailResponse getPostById(Long id) { user.getId(), post.getViews()); - return postMapper.toDetailResponse(post, user); + long likeCount = postLikeRepository.countByPostId(post.getId()); + boolean isLiked = postLikeRepository.existsByPostIdAndUserId(post.getId(), user.getId()); + + return postMapper.toDetailResponse(post, likeCount, isLiked, user); } @Override @@ -190,9 +276,12 @@ public PostDetailResponse updatePost(Long id, PostRequest request, List