Skip to content

Commit 9348b7f

Browse files
committed
feat: 게시글 추천 기능 구현
1 parent ab96b9b commit 9348b7f

File tree

8 files changed

+168
-10
lines changed

8 files changed

+168
-10
lines changed

src/main/java/com/back/domain/post/comment/controller/CommentController.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import com.back.domain.post.comment.dto.request.CommentUpdateRequestDto;
55
import com.back.domain.post.comment.dto.response.CommentResponseDto;
66
import com.back.domain.post.comment.service.CommentService;
7-
import com.back.domain.post.post.dto.request.PostUpdateRequestDto;
8-
import com.back.domain.post.post.dto.response.PostResponseDto;
97
import com.back.global.rsData.RsData;
108
import io.swagger.v3.oas.annotations.Operation;
119
import io.swagger.v3.oas.annotations.tags.Tag;

src/main/java/com/back/domain/post/post/controller/PostController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,18 @@ public RsData<Void> deletePost(
9595
postService.deletePost(postId);
9696
return RsData.successOf(null); // code=200, message="success"
9797
}
98+
99+
/**
100+
* 게시글 추천(좋아요) 토글 API
101+
* @param postId 추천할 게시글 ID
102+
* @return 추천 상태 변경 성공 메시지
103+
*/
104+
@PostMapping("/{postId}/like")
105+
@Operation(summary = "게시글 추천")
106+
public RsData<Void> toggleLike(
107+
@PathVariable Long postId
108+
) {
109+
postService.toggleLike(postId);
110+
return RsData.successOf(null); // code=200, message="success"
111+
}
98112
}

src/main/java/com/back/domain/post/post/entity/Post.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import lombok.Builder;
2525
import lombok.Getter;
2626
import lombok.NoArgsConstructor;
27-
import lombok.Setter;
2827
import org.springframework.data.annotation.CreatedDate;
2928
import org.springframework.data.annotation.LastModifiedDate;
3029
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@@ -83,6 +82,9 @@ public class Post {
8382
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
8483
private List<PostTag> postTags = new ArrayList<>();
8584

85+
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
86+
private List<PostLike> postLikes = new ArrayList<>();
87+
8688
// 게시글 추천 수 (기본값: 0)
8789
@Builder.Default
8890
@Column(name = "like_count", nullable = false)
@@ -124,4 +126,12 @@ public void addTag(Tag tag) {
124126
public void clearTags() {
125127
this.postTags.clear();
126128
}
129+
130+
public void increaseLikeCount() {
131+
this.likeCount++;
132+
}
133+
134+
public void decreaseLikeCount() {
135+
this.likeCount--;
136+
}
127137
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.back.domain.post.post.entity;
2+
3+
import com.back.domain.post.post.enums.PostLikeStatus;
4+
import com.back.domain.user.entity.User;
5+
import jakarta.persistence.Column;
6+
import jakarta.persistence.Entity;
7+
import jakarta.persistence.EntityListeners;
8+
import jakarta.persistence.EnumType;
9+
import jakarta.persistence.Enumerated;
10+
import jakarta.persistence.FetchType;
11+
import jakarta.persistence.GeneratedValue;
12+
import jakarta.persistence.GenerationType;
13+
import jakarta.persistence.Id;
14+
import jakarta.persistence.JoinColumn;
15+
import jakarta.persistence.ManyToOne;
16+
import jakarta.persistence.Table;
17+
import jakarta.persistence.UniqueConstraint;
18+
import java.time.LocalDateTime;
19+
import lombok.AccessLevel;
20+
import lombok.AllArgsConstructor;
21+
import lombok.Builder;
22+
import lombok.Getter;
23+
import lombok.NoArgsConstructor;
24+
import org.springframework.data.annotation.CreatedDate;
25+
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
26+
27+
@Entity
28+
@Getter
29+
// 같은 사용자(user_id)가 같은 게시글(post_id)을 중복 추천하지 못하도록 DB 레벨에서 보장.
30+
@Table(name = "post_like", uniqueConstraints = {
31+
@UniqueConstraint(columnNames = {"post_id", "user_id"})
32+
})
33+
@EntityListeners(AuditingEntityListener.class)
34+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
35+
@AllArgsConstructor
36+
@Builder
37+
public class PostLike {
38+
39+
@Id
40+
@GeneratedValue(strategy = GenerationType.IDENTITY)
41+
@Column(name = "id")
42+
private Long id;
43+
44+
@ManyToOne(fetch = FetchType.LAZY)
45+
@JoinColumn(name = "post_id")
46+
private Post post;
47+
48+
@ManyToOne(fetch = FetchType.LAZY)
49+
@JoinColumn(name = "user_id")
50+
private User user;
51+
52+
// 추천 생성 날짜
53+
@CreatedDate
54+
private LocalDateTime createdAt;
55+
56+
// 추천 상태 (기본값: 비추천)
57+
@Builder.Default
58+
@Enumerated(EnumType.STRING)
59+
@Column(name = "status", nullable = false)
60+
private PostLikeStatus status = PostLikeStatus.NONE;
61+
62+
public void updateStatus(PostLikeStatus status) {
63+
this.status = status;
64+
}
65+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.back.domain.post.post.enums;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
6+
@Getter
7+
@RequiredArgsConstructor
8+
public enum PostLikeStatus {
9+
NONE("비추천", "해당 게시글에 추천을 아직 누르지 않은 상태"),
10+
LIKE("추천", "해당 게시글에 추천을 누른 상태");
11+
12+
private final String title;
13+
private final String description;
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.back.domain.post.post.repository;
2+
3+
import com.back.domain.post.post.entity.Post;
4+
import com.back.domain.post.post.entity.PostLike;
5+
import com.back.domain.user.entity.User;
6+
import java.util.Optional;
7+
import org.springframework.data.jpa.repository.JpaRepository;
8+
9+
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
10+
Optional<PostLike> findByPostAndUser(Post post, User user);
11+
}

src/main/java/com/back/domain/post/post/service/PostService.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
import com.back.domain.post.post.dto.request.PostUpdateRequestDto;
77
import com.back.domain.post.post.dto.response.PostResponseDto;
88
import com.back.domain.post.post.entity.Post;
9+
import com.back.domain.post.post.entity.PostLike;
910
import com.back.domain.post.post.entity.Tag;
11+
import com.back.domain.post.post.enums.PostLikeStatus;
1012
import com.back.domain.post.post.enums.PostStatus;
13+
import com.back.domain.post.post.repository.PostLikeRepository;
1114
import com.back.domain.post.post.repository.PostRepository;
1215
import com.back.domain.post.post.repository.TagRepository;
1316
import com.back.domain.user.entity.User;
1417
import com.back.global.rq.Rq;
1518
import java.util.List;
1619
import java.util.NoSuchElementException;
20+
import java.util.Optional;
1721
import java.util.stream.Collectors;
1822
import lombok.RequiredArgsConstructor;
1923
import org.springframework.stereotype.Service;
@@ -26,6 +30,7 @@ public class PostService {
2630
private final PostRepository postRepository;
2731
private final CategoryRepository categoryRepository;
2832
private final TagRepository tagRepository;
33+
private final PostLikeRepository postLikeRepository;
2934
private final Rq rq;
3035

3136
// 게시글 작성 로직
@@ -129,6 +134,32 @@ public void deletePost(Long postId) {
129134
// postRepository.delete(post);
130135
}
131136

137+
@Transactional
138+
public void toggleLike(Long postId) {
139+
User user = rq.getActor(); // 현재 로그인한 사용자
140+
141+
Post post = postRepository.findById(postId)
142+
.orElseThrow(() -> new NoSuchElementException("해당 게시글을 찾을 수 없습니다. ID: " + postId));
143+
144+
Optional<PostLike> existingLike = postLikeRepository.findByPostAndUser(post, user);
145+
146+
if (existingLike.isPresent()) {
147+
// 이미 추천했으면 취소
148+
existingLike.get().updateStatus(PostLikeStatus.NONE);
149+
postLikeRepository.delete(existingLike.get());
150+
post.decreaseLikeCount();
151+
} else {
152+
// 추천 추가
153+
PostLike postLike = PostLike.builder()
154+
.post(post)
155+
.user(user)
156+
.status(PostLikeStatus.LIKE)
157+
.build();
158+
postLikeRepository.save(postLike);
159+
post.increaseLikeCount();
160+
}
161+
}
162+
132163
// 태그 추가 메서드
133164
private void addTag(List<String> tagNames, Post post) {
134165
for (String tagName : tagNames) {

src/main/java/com/back/domain/user/entity/User.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
package com.back.domain.user.entity;
22

3-
import jakarta.persistence.*;
4-
import lombok.*;
5-
import org.springframework.security.core.GrantedAuthority;
6-
import org.springframework.security.core.authority.SimpleGrantedAuthority;
7-
import org.springframework.data.annotation.CreatedDate;
8-
import org.springframework.data.annotation.LastModifiedDate;
9-
3+
import com.back.domain.post.post.entity.PostLike;
4+
import jakarta.persistence.CascadeType;
5+
import jakarta.persistence.Column;
6+
import jakarta.persistence.Entity;
7+
import jakarta.persistence.GeneratedValue;
8+
import jakarta.persistence.GenerationType;
9+
import jakarta.persistence.Id;
10+
import jakarta.persistence.OneToMany;
11+
import jakarta.persistence.Table;
1012
import java.time.LocalDateTime;
1113
import java.util.ArrayList;
1214
import java.util.Collection;
1315
import java.util.List;
16+
import lombok.AllArgsConstructor;
17+
import lombok.Builder;
18+
import lombok.Getter;
19+
import lombok.NoArgsConstructor;
20+
import lombok.Setter;
21+
import org.springframework.data.annotation.CreatedDate;
22+
import org.springframework.data.annotation.LastModifiedDate;
23+
import org.springframework.security.core.GrantedAuthority;
24+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
1425

1526
@Entity
1627
@Table(name = "users") // 예약어 충돌 방지를 위해 "users" 권장
@@ -51,6 +62,10 @@ public class User {
5162
@Column(nullable = false, length = 20)
5263
private String role = "USER";
5364

65+
// 양방향 매핑을 위한 필드
66+
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
67+
private List<PostLike> postLikes = new ArrayList<>();
68+
5469
public boolean isAdmin() {
5570
return "ADMIN".equalsIgnoreCase(role);
5671
}

0 commit comments

Comments
 (0)