diff --git a/src/main/java/com/soyeon/nubim/domain/post/Post.java b/src/main/java/com/soyeon/nubim/domain/post/Post.java index 04b8520..a0328cb 100644 --- a/src/main/java/com/soyeon/nubim/domain/post/Post.java +++ b/src/main/java/com/soyeon/nubim/domain/post/Post.java @@ -61,6 +61,16 @@ public class Post extends BaseEntity { @OrderBy("createdAt DESC") private List comments = new ArrayList<>(); + /** + * 다른 엔티티 생성 시 매핑만을 위해 임시 Post 엔티티 생성 + * 실제 Post의 값은 가지지 않으니 사용 시 주의할 것 + */ + public Post(Long postId, Long userId) { + this.postId = postId; + this.user = new User(userId); + this.postTitle = "MAPPING_POST"; + } + public void linkAlbum(Album album) { this.album = album; album.linkPost(this); diff --git a/src/main/java/com/soyeon/nubim/domain/post/PostService.java b/src/main/java/com/soyeon/nubim/domain/post/PostService.java index 1df9496..e03a28b 100644 --- a/src/main/java/com/soyeon/nubim/domain/post/PostService.java +++ b/src/main/java/com/soyeon/nubim/domain/post/PostService.java @@ -36,24 +36,6 @@ public class PostService { private final CommentMapper commentMapper; private final UserMapper userMapper; - public PostDetailResponseDto findPostDetailById(Long id) { - Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException(id)); - - return postMapper.toPostDetailResponseDto(post); - } - - public PostSimpleResponseDto findPostSimpleById(Long id) { - Post post = postRepository.findById(id).orElseThrow(() -> new PostNotFoundException(id)); - - return postMapper.toPostSimpleResponseDto(post); - } - - public Page findAllPostsByUserOrderByCreatedAt(User user, Pageable pageable) { - Page postList = postRepository.findByUser(user, pageable); - return postList - .map(postMapper::toPostSimpleResponseDto); - } - public PostCreateResponseDto createPost(PostCreateRequestDto postCreateRequestDto, User authorUser) { Album linkedAlbum = albumService.findById(postCreateRequestDto.getAlbumId()); albumService.validateAlbumOwner(linkedAlbum.getAlbumId(), authorUser.getUserId()); @@ -65,39 +47,34 @@ public PostCreateResponseDto createPost(PostCreateRequestDto postCreateRequestDt return postMapper.toPostCreateResponseDto(post); } - public void deleteById(Long postId, Long userId) { - validatePostOwner(postId, userId); + public Post findPostByIdOrThrow(Long id) { + return postRepository.findById(id).orElseThrow(() -> new PostNotFoundException(id)); + } - Post post = findPostByIdOrThrow(postId); - post.unlinkAlbum(); + public PostSimpleResponseDto findPostSimpleById(Long id) { + Post post = findPostByIdOrThrow(id); - postRepository.deleteById(postId); + return postMapper.toPostSimpleResponseDto(post); } - public Post findPostByIdOrThrow(Long id) { + public PostDetailResponseDto findPostDetailById(Long id) { + Post post = findPostByIdOrThrow(id); - return postRepository - .findById(id) - .orElseThrow(() -> new PostNotFoundException(id)); + return postMapper.toPostDetailResponseDto(post); } - public void validatePostExist(Long postId) { - if (!postRepository.existsById(postId)) { - throw new PostNotFoundException(postId); - } + public Page searchPostsByTitleOrContent(Pageable pageable, String query) { + return postRepository.findByPostTitleContainingOrPostContentContaining(query, query, pageable) + .map(postMapper::toPostSimpleResponseDto); } - public void validatePostOwner(Long postId, Long userId) { - Post post = findPostByIdOrThrow(postId); - Long postOwnerId = post.getUser().getUserId(); - - if (!postOwnerId.equals(userId)) { - throw new UnauthorizedPostAccessException(postId); - } + public Page findAllPostsByUserOrderByCreatedAt(User user, Pageable pageable) { + Page postList = postRepository.findByUser(user, pageable); + return postList + .map(postMapper::toPostSimpleResponseDto); } - public Page findRecentPostsOfFollowees( - User user, Pageable pageable, int recentCriteriaDays) { + public Page findRecentPostsOfFollowees(User user, Pageable pageable, int recentCriteriaDays) { return postRepository.findRecentPostsByFollowees(user, LocalDateTime.now().minusDays(recentCriteriaDays), pageable) .map(post -> postMapper.toPostMainResponseDto(post, findRecentCommentByPostOrNull(post))); @@ -116,13 +93,6 @@ public Page findRandomPosts(Pageable pageable, Float random return customPage; } - private Float getOrGenerateRandomSeed(Float seed) { - if (seed == null) { - return random.nextFloat(); - } - return seed; - } - private CommentResponseDto findRecentCommentByPostOrNull(Post post) { Comment lastCommentByPost = post.getComments().stream().findFirst().orElse(null); if (lastCommentByPost == null) { @@ -134,8 +104,33 @@ private CommentResponseDto findRecentCommentByPostOrNull(Post post) { } } - public Page searchPostsByTitleOrContent(Pageable pageable, String query) { - return postRepository.findByPostTitleContainingOrPostContentContaining(query, query, pageable) - .map(postMapper::toPostSimpleResponseDto); + public void deleteById(Long postId, Long userId) { + Post post = findPostByIdOrThrow(postId); + validatePostOwner(post, userId); + + post.unlinkAlbum(); + + postRepository.deleteById(postId); + } + + public void validatePostExist(Long postId) { + if (!postRepository.existsById(postId)) { + throw new PostNotFoundException(postId); + } + } + + public void validatePostOwner(Post post, Long userId) { + Long postOwnerId = post.getUser().getUserId(); + + if (!postOwnerId.equals(userId)) { + throw new UnauthorizedPostAccessException(post.getPostId()); + } + } + + private Float getOrGenerateRandomSeed(Float seed) { + if (seed == null) { + return random.nextFloat(); + } + return seed; } } diff --git a/src/main/java/com/soyeon/nubim/domain/post/exceptions/PostNotFoundException.java b/src/main/java/com/soyeon/nubim/domain/post/exceptions/PostNotFoundException.java index 488cb75..92f42e2 100644 --- a/src/main/java/com/soyeon/nubim/domain/post/exceptions/PostNotFoundException.java +++ b/src/main/java/com/soyeon/nubim/domain/post/exceptions/PostNotFoundException.java @@ -4,8 +4,7 @@ import org.springframework.web.server.ResponseStatusException; public class PostNotFoundException extends ResponseStatusException { - public PostNotFoundException(Long albumId) { - super(HttpStatus.NOT_FOUND, "Album not found with id " + albumId); + public PostNotFoundException(Long postId) { + super(HttpStatus.NOT_FOUND, "Post not found with id " + postId); } - } diff --git a/src/main/java/com/soyeon/nubim/domain/postlike/PostLike.java b/src/main/java/com/soyeon/nubim/domain/postlike/PostLike.java new file mode 100644 index 0000000..2e5ea83 --- /dev/null +++ b/src/main/java/com/soyeon/nubim/domain/postlike/PostLike.java @@ -0,0 +1,39 @@ +package com.soyeon.nubim.domain.postlike; + +import com.soyeon.nubim.domain.post.Post; +import com.soyeon.nubim.domain.user.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PostLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long postLikeId; + + @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/soyeon/nubim/domain/postlike/PostLikeControllerV1.java b/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeControllerV1.java new file mode 100644 index 0000000..9ffd880 --- /dev/null +++ b/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeControllerV1.java @@ -0,0 +1,28 @@ +package com.soyeon.nubim.domain.postlike; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.soyeon.nubim.domain.postlike.dto.PostLikeCreateResponse; + +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/v1/postlikes") +@RequiredArgsConstructor +public class PostLikeControllerV1 { + + private final PostLikeService postLikeService; + + @Operation(description = "게시글 좋아요 및 좋아요 취소. 게시글에 좋아요를 누르고, 이미 좋아요가 되어있을 시 취소한다.") + @PostMapping("/{postId}") + public ResponseEntity togglePostLike(@PathVariable("postId") Long postId) { + PostLikeCreateResponse postLike = postLikeService.togglePostLike(postId); + + return ResponseEntity.ok().body(postLike); + } +} diff --git a/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeRepository.java b/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeRepository.java new file mode 100644 index 0000000..bfe8228 --- /dev/null +++ b/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeRepository.java @@ -0,0 +1,17 @@ +package com.soyeon.nubim.domain.postlike; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostLikeRepository extends JpaRepository { + + @Query("SELECT EXISTS (FROM PostLike pl WHERE pl.post.postId = :postId AND pl.user.userId = :userId)") + boolean existsPostLikeByPostAndUser(Long postId, Long userId); + + @Modifying + @Query("DELETE FROM PostLike pl WHERE pl.post.postId = :postId AND pl.user.userId = :userId") + int deletePostLikeByPostAndUser(Long postId, Long userId); +} diff --git a/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeService.java b/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeService.java new file mode 100644 index 0000000..ba7f2c6 --- /dev/null +++ b/src/main/java/com/soyeon/nubim/domain/postlike/PostLikeService.java @@ -0,0 +1,49 @@ +package com.soyeon.nubim.domain.postlike; + +import org.springframework.stereotype.Service; + +import com.soyeon.nubim.domain.post.Post; +import com.soyeon.nubim.domain.post.PostService; +import com.soyeon.nubim.domain.postlike.dto.PostLikeCreateResponse; +import com.soyeon.nubim.domain.postlike.exception.MultiplePostLikeDeleteException; +import com.soyeon.nubim.domain.user.User; +import com.soyeon.nubim.domain.user.UserService; + +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class PostLikeService { + + public static final int POST_LIKE_DELETE_SUCCESS = 1; + private final UserService userService; + private final PostService postService; + private final PostLikeRepository postLikeRepository; + + @Transactional + public PostLikeCreateResponse togglePostLike(Long postId) { + Long currentUserId = userService.getCurrentUserId(); + + postService.validatePostExist(postId); + userService.validateUserExists(currentUserId); + + // 좋아요 되어 있을 시 좋아요 삭제 + if (postLikeRepository.existsPostLikeByPostAndUser(postId, currentUserId)) { + int deleteResult = postLikeRepository.deletePostLikeByPostAndUser(postId, currentUserId); + + if (deleteResult != POST_LIKE_DELETE_SUCCESS) { + throw new MultiplePostLikeDeleteException(); + } + return new PostLikeCreateResponse("post like removed"); + } + + PostLike postLike = PostLike.builder() + .post(new Post(postId, currentUserId)) + .user(new User(currentUserId)) + .build(); + postLikeRepository.save(postLike); + + return new PostLikeCreateResponse("post like successfully"); + } +} diff --git a/src/main/java/com/soyeon/nubim/domain/postlike/dto/PostLikeCreateResponse.java b/src/main/java/com/soyeon/nubim/domain/postlike/dto/PostLikeCreateResponse.java new file mode 100644 index 0000000..36b6e1c --- /dev/null +++ b/src/main/java/com/soyeon/nubim/domain/postlike/dto/PostLikeCreateResponse.java @@ -0,0 +1,10 @@ +package com.soyeon.nubim.domain.postlike.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PostLikeCreateResponse { + private String message; +} diff --git a/src/main/java/com/soyeon/nubim/domain/postlike/exception/MultiplePostLikeDeleteException.java b/src/main/java/com/soyeon/nubim/domain/postlike/exception/MultiplePostLikeDeleteException.java new file mode 100644 index 0000000..95b77d8 --- /dev/null +++ b/src/main/java/com/soyeon/nubim/domain/postlike/exception/MultiplePostLikeDeleteException.java @@ -0,0 +1,11 @@ +package com.soyeon.nubim.domain.postlike.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +public class MultiplePostLikeDeleteException extends ResponseStatusException { + public MultiplePostLikeDeleteException() { + super(HttpStatus.INTERNAL_SERVER_ERROR, + "Multiple post like were unexpectedly deleted. Only one post like can be deleted"); + } +} diff --git a/src/main/java/com/soyeon/nubim/domain/user/User.java b/src/main/java/com/soyeon/nubim/domain/user/User.java index 5a79db1..041301a 100644 --- a/src/main/java/com/soyeon/nubim/domain/user/User.java +++ b/src/main/java/com/soyeon/nubim/domain/user/User.java @@ -93,6 +93,17 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "follower", fetch = FetchType.LAZY) private List followees = new ArrayList<>(); + /** + * 다른 엔티티 생성 시 매핑만을 위한 임시 User 엔티티 생성 + * 실제 User의 값은 가지지 않으니 사용 시 주의할 것 + */ + public User(Long userId) { + this.userId = userId; + this.username = "MAPPING_USER"; + this.nickname = "MAPPING_USER"; + this.email = "MAPPING_USER@email.com"; + } + public User updateNameFromOAuthProfile(String name) { this.username = name; return this;