From 9348b7f62e0f1557c4c2a7724ddbf3095493fd15 Mon Sep 17 00:00:00 2001 From: SeokGeunHo Date: Wed, 24 Sep 2025 12:07:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 2 - .../post/post/controller/PostController.java | 14 ++++ .../back/domain/post/post/entity/Post.java | 12 +++- .../domain/post/post/entity/PostLike.java | 65 +++++++++++++++++++ .../post/post/enums/PostLikeStatus.java | 14 ++++ .../post/repository/PostLikeRepository.java | 11 ++++ .../domain/post/post/service/PostService.java | 31 +++++++++ .../com/back/domain/user/entity/User.java | 29 +++++++-- 8 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/back/domain/post/post/entity/PostLike.java create mode 100644 src/main/java/com/back/domain/post/post/enums/PostLikeStatus.java create mode 100644 src/main/java/com/back/domain/post/post/repository/PostLikeRepository.java diff --git a/src/main/java/com/back/domain/post/comment/controller/CommentController.java b/src/main/java/com/back/domain/post/comment/controller/CommentController.java index 9407c19d..d0a60b50 100644 --- a/src/main/java/com/back/domain/post/comment/controller/CommentController.java +++ b/src/main/java/com/back/domain/post/comment/controller/CommentController.java @@ -4,8 +4,6 @@ import com.back.domain.post.comment.dto.request.CommentUpdateRequestDto; import com.back.domain.post.comment.dto.response.CommentResponseDto; import com.back.domain.post.comment.service.CommentService; -import com.back.domain.post.post.dto.request.PostUpdateRequestDto; -import com.back.domain.post.post.dto.response.PostResponseDto; import com.back.global.rsData.RsData; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; diff --git a/src/main/java/com/back/domain/post/post/controller/PostController.java b/src/main/java/com/back/domain/post/post/controller/PostController.java index af343c34..3e23c8f0 100644 --- a/src/main/java/com/back/domain/post/post/controller/PostController.java +++ b/src/main/java/com/back/domain/post/post/controller/PostController.java @@ -95,4 +95,18 @@ public RsData deletePost( postService.deletePost(postId); return RsData.successOf(null); // code=200, message="success" } + + /** + * 게시글 추천(좋아요) 토글 API + * @param postId 추천할 게시글 ID + * @return 추천 상태 변경 성공 메시지 + */ + @PostMapping("/{postId}/like") + @Operation(summary = "게시글 추천") + public RsData toggleLike( + @PathVariable Long postId + ) { + postService.toggleLike(postId); + return RsData.successOf(null); // code=200, message="success" + } } diff --git a/src/main/java/com/back/domain/post/post/entity/Post.java b/src/main/java/com/back/domain/post/post/entity/Post.java index 858865e3..9ce65b79 100644 --- a/src/main/java/com/back/domain/post/post/entity/Post.java +++ b/src/main/java/com/back/domain/post/post/entity/Post.java @@ -24,7 +24,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -83,6 +82,9 @@ public class Post { @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) private List postTags = new ArrayList<>(); + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List postLikes = new ArrayList<>(); + // 게시글 추천 수 (기본값: 0) @Builder.Default @Column(name = "like_count", nullable = false) @@ -124,4 +126,12 @@ public void addTag(Tag tag) { public void clearTags() { this.postTags.clear(); } + + public void increaseLikeCount() { + this.likeCount++; + } + + public void decreaseLikeCount() { + this.likeCount--; + } } diff --git a/src/main/java/com/back/domain/post/post/entity/PostLike.java b/src/main/java/com/back/domain/post/post/entity/PostLike.java new file mode 100644 index 00000000..86b1ec3d --- /dev/null +++ b/src/main/java/com/back/domain/post/post/entity/PostLike.java @@ -0,0 +1,65 @@ +package com.back.domain.post.post.entity; + +import com.back.domain.post.post.enums.PostLikeStatus; +import com.back.domain.user.entity.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +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 jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Entity +@Getter +// 같은 사용자(user_id)가 같은 게시글(post_id)을 중복 추천하지 못하도록 DB 레벨에서 보장. +@Table(name = "post_like", uniqueConstraints = { + @UniqueConstraint(columnNames = {"post_id", "user_id"}) +}) +@EntityListeners(AuditingEntityListener.class) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class PostLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + // 추천 생성 날짜 + @CreatedDate + private LocalDateTime createdAt; + + // 추천 상태 (기본값: 비추천) + @Builder.Default + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PostLikeStatus status = PostLikeStatus.NONE; + + public void updateStatus(PostLikeStatus status) { + this.status = status; + } +} diff --git a/src/main/java/com/back/domain/post/post/enums/PostLikeStatus.java b/src/main/java/com/back/domain/post/post/enums/PostLikeStatus.java new file mode 100644 index 00000000..83748088 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/enums/PostLikeStatus.java @@ -0,0 +1,14 @@ +package com.back.domain.post.post.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PostLikeStatus { + NONE("비추천", "해당 게시글에 추천을 아직 누르지 않은 상태"), + LIKE("추천", "해당 게시글에 추천을 누른 상태"); + + private final String title; + private final String description; +} diff --git a/src/main/java/com/back/domain/post/post/repository/PostLikeRepository.java b/src/main/java/com/back/domain/post/post/repository/PostLikeRepository.java new file mode 100644 index 00000000..f0a8bd41 --- /dev/null +++ b/src/main/java/com/back/domain/post/post/repository/PostLikeRepository.java @@ -0,0 +1,11 @@ +package com.back.domain.post.post.repository; + +import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.entity.PostLike; +import com.back.domain.user.entity.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostLikeRepository extends JpaRepository { + Optional findByPostAndUser(Post post, User user); +} diff --git a/src/main/java/com/back/domain/post/post/service/PostService.java b/src/main/java/com/back/domain/post/post/service/PostService.java index 22fe7a41..153b86c2 100644 --- a/src/main/java/com/back/domain/post/post/service/PostService.java +++ b/src/main/java/com/back/domain/post/post/service/PostService.java @@ -6,14 +6,18 @@ import com.back.domain.post.post.dto.request.PostUpdateRequestDto; import com.back.domain.post.post.dto.response.PostResponseDto; import com.back.domain.post.post.entity.Post; +import com.back.domain.post.post.entity.PostLike; import com.back.domain.post.post.entity.Tag; +import com.back.domain.post.post.enums.PostLikeStatus; import com.back.domain.post.post.enums.PostStatus; +import com.back.domain.post.post.repository.PostLikeRepository; import com.back.domain.post.post.repository.PostRepository; import com.back.domain.post.post.repository.TagRepository; import com.back.domain.user.entity.User; import com.back.global.rq.Rq; import java.util.List; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -26,6 +30,7 @@ public class PostService { private final PostRepository postRepository; private final CategoryRepository categoryRepository; private final TagRepository tagRepository; + private final PostLikeRepository postLikeRepository; private final Rq rq; // 게시글 작성 로직 @@ -129,6 +134,32 @@ public void deletePost(Long postId) { // postRepository.delete(post); } + @Transactional + public void toggleLike(Long postId) { + User user = rq.getActor(); // 현재 로그인한 사용자 + + Post post = postRepository.findById(postId) + .orElseThrow(() -> new NoSuchElementException("해당 게시글을 찾을 수 없습니다. ID: " + postId)); + + Optional existingLike = postLikeRepository.findByPostAndUser(post, user); + + if (existingLike.isPresent()) { + // 이미 추천했으면 취소 + existingLike.get().updateStatus(PostLikeStatus.NONE); + postLikeRepository.delete(existingLike.get()); + post.decreaseLikeCount(); + } else { + // 추천 추가 + PostLike postLike = PostLike.builder() + .post(post) + .user(user) + .status(PostLikeStatus.LIKE) + .build(); + postLikeRepository.save(postLike); + post.increaseLikeCount(); + } + } + // 태그 추가 메서드 private void addTag(List tagNames, Post post) { for (String tagName : tagNames) { diff --git a/src/main/java/com/back/domain/user/entity/User.java b/src/main/java/com/back/domain/user/entity/User.java index 4e8c5d43..08b6fe25 100644 --- a/src/main/java/com/back/domain/user/entity/User.java +++ b/src/main/java/com/back/domain/user/entity/User.java @@ -1,16 +1,27 @@ package com.back.domain.user.entity; -import jakarta.persistence.*; -import lombok.*; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; - +import com.back.domain.post.post.entity.PostLike; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; @Entity @Table(name = "users") // 예약어 충돌 방지를 위해 "users" 권장 @@ -51,6 +62,10 @@ public class User { @Column(nullable = false, length = 20) private String role = "USER"; + // 양방향 매핑을 위한 필드 + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List postLikes = new ArrayList<>(); + public boolean isAdmin() { return "ADMIN".equalsIgnoreCase(role); }