Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ ResponseEntity<BaseResponse<PostDetailResponse>> createPost(
@RequestPart(value = "imageList", required = false)
List<MultipartFile> imageList);

@PostMapping("/{postId}/like")
@Operation(summary = "게시글 좋아요 토글", description = "게시글 좋아요를 등록/취소합니다.")
ResponseEntity<BaseResponse<Boolean>> togglePostLike(
@Parameter(description = "게시글 ID", example = "1") @PathVariable Long postId);

@GetMapping("/admin")
@Operation(summary = "[관리자] 게시글 전체 조회", description = "전체 게시글 리스트를 조회합니다.")
ResponseEntity<BaseResponse<List<PostDetailResponse>>> getAllPosts();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -35,6 +34,13 @@ public ResponseEntity<BaseResponse<PostDetailResponse>> createPost(
return ResponseEntity.ok(BaseResponse.success(response));
}

@Override
public ResponseEntity<BaseResponse<Boolean>> togglePostLike(@PathVariable Long postId) {

boolean liked = postService.togglePostLike(postId);
return ResponseEntity.ok(BaseResponse.success(liked));
}

@Override
public ResponseEntity<BaseResponse<List<PostDetailResponse>>> getAllPosts() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<String> imageUrlList;

Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/sku/refit/domain/post/entity/PostLike.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PostLike, Long> {

Optional<PostLike> 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<Object[]> countByPostIds(@Param("postIds") List<Long> postIds);

@Query(
"""
select pl.post.id
from PostLike pl
where pl.post.id in :postIds
and pl.user.id = :userId
""")
List<Long> findLikedPostIds(@Param("postIds") List<Long> postIds, @Param("userId") Long userId);
}
10 changes: 10 additions & 0 deletions src/main/java/com/sku/refit/domain/post/service/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ public interface PostService {
*/
PostDetailResponse createPost(PostRequest request, List<MultipartFile> images);

/**
* 게시글 좋아요를 토글합니다.
*
* <p>이미 좋아요가 되어 있으면 취소하고, 좋아요가 없으면 새로 생성합니다.
*
* @param postId 게시글 ID
* @return true: 좋아요 상태 / false: 좋아요 취소 상태
*/
boolean togglePostLike(Long postId);

/**
* 모든 게시글 목록을 조회합니다.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -72,7 +79,35 @@ public PostDetailResponse createPost(PostRequest request, List<MultipartFile> 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
Expand All @@ -82,9 +117,32 @@ public List<PostDetailResponse> getAllPosts() {
User user = userService.getCurrentUser();
List<Post> posts = postRepository.findAll();

if (posts.isEmpty()) {
return List.of();
}

List<Long> postIds = posts.stream().map(Post::getId).toList();

Map<Long, Long> likeCountMap = new HashMap<>();
List<Object[]> likeCounts = postLikeRepository.countByPostIds(postIds);
for (Object[] row : likeCounts) {
likeCountMap.put((Long) row[0], (Long) row[1]);
}

Set<Long> 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
Expand All @@ -111,8 +169,33 @@ public InfiniteResponse<PostDetailResponse> getPostsByCategory(
posts = posts.subList(0, size);
}

List<Long> postIds = posts.stream().map(Post::getId).toList();

Map<Long, Long> likeCountMap = new HashMap<>();
if (!postIds.isEmpty()) {
List<Object[]> likeCounts = postLikeRepository.countByPostIds(postIds);
for (Object[] row : likeCounts) {
Long postId = (Long) row[0];
Long count = (Long) row[1];
likeCountMap.put(postId, count);
}
}

Set<Long> likedPostIds =
postIds.isEmpty()
? Set.of()
: new HashSet<>(postLikeRepository.findLikedPostIds(postIds, user.getId()));

List<PostDetailResponse> 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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -190,9 +276,12 @@ public PostDetailResponse updatePost(Long id, PostRequest request, List<Multipar

post.update(request.getTitle(), request.getContent(), newImageUrls);

long likeCount = postLikeRepository.countByPostId(post.getId());
boolean isLiked = postLikeRepository.existsByPostIdAndUserId(post.getId(), user.getId());

log.info("[POST UPDATE COMPLETE] postId={}, userId={}", post.getId(), user.getId());

return postMapper.toDetailResponse(post, user);
return postMapper.toDetailResponse(post, likeCount, isLiked, user);
}

@Override
Expand Down