-
Notifications
You must be signed in to change notification settings - Fork 0
Feat#9 좋아요기능 #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "feat#9--\uC88B\uC544\uC694\uAE30\uB2A5"
Feat#9 좋아요기능 #10
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package org.juniortown.backend.like.controller; | ||
|
|
||
| import org.juniortown.backend.like.dto.response.LikeResponse; | ||
| import org.juniortown.backend.like.service.LikeService; | ||
| import org.juniortown.backend.user.dto.CustomUserDetails; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.DeleteMapping; | ||
| 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 lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api") | ||
| @RequiredArgsConstructor | ||
| public class LikeController { | ||
| private final LikeService likeService; | ||
| @PostMapping("/posts/likes/{postId}") | ||
| public ResponseEntity<LikeResponse> like(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable Long postId) { | ||
| Long userId = customUserDetails.getUserId(); | ||
| LikeResponse response = likeService.likePost(userId, postId); | ||
| return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package org.juniortown.backend.like.dto.response; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @NoArgsConstructor | ||
| public class LikeResponse { | ||
| private Long userId; | ||
| private Long postId; | ||
| private Boolean isLiked; | ||
| @Builder | ||
| public LikeResponse(Long userId, Long postId, Boolean isLiked) { | ||
| this.userId = userId; | ||
| this.postId = postId; | ||
| this.isLiked = isLiked; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package org.juniortown.backend.like.entity; | ||
|
|
||
| import org.juniortown.backend.post.entity.Post; | ||
| import org.juniortown.backend.user.entity.User; | ||
|
|
||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.Table; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Table(name = "likes") | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @Getter | ||
| public class Like { | ||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
| private User user; | ||
|
|
||
| @ManyToOne | ||
| @JoinColumn(name = "post_id", nullable = false) | ||
| private Post post; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @Builder | ||
| public Like(User user, Post post) { | ||
| this.user = user; | ||
| this.post = post; | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package org.juniortown.backend.like.exception; | ||
|
|
||
| import org.juniortown.backend.exception.CustomException; | ||
|
|
||
| import lombok.Getter; | ||
|
|
||
|
|
||
| public class LikeFailureException extends CustomException { | ||
| public static final String MESSAGE = "게시글이 삭제되었거나, 좋아요 처리 실패"; | ||
| public LikeFailureException(Exception e) { | ||
| super(MESSAGE, e); | ||
| } | ||
| @Override | ||
| public int getStatusCode() { | ||
| return 500; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package org.juniortown.backend.like.repository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| import org.juniortown.backend.like.entity.Like; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface LikeRepository extends JpaRepository<Like, Long> { | ||
| Optional<Like> findByUserIdAndPostId(Long userId, Long postId); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package org.juniortown.backend.like.service; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.like.dto.response.LikeResponse; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.like.entity.Like; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.like.exception.LikeFailureException; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.like.repository.LikeRepository; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.entity.Post; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.repository.PostRepository; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.user.entity.User; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.user.repository.UserRepository; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.stereotype.Service; | ||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.transaction.annotation.Transactional; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import lombok.RequiredArgsConstructor; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @Service | ||||||||||||||||||||||||||||||||||||||||||||||
| @RequiredArgsConstructor | ||||||||||||||||||||||||||||||||||||||||||||||
| public class LikeService { | ||||||||||||||||||||||||||||||||||||||||||||||
| private final LikeRepository likeRepository; | ||||||||||||||||||||||||||||||||||||||||||||||
| private final UserRepository userRepository; | ||||||||||||||||||||||||||||||||||||||||||||||
| private final PostRepository postRepository; | ||||||||||||||||||||||||||||||||||||||||||||||
| @Transactional | ||||||||||||||||||||||||||||||||||||||||||||||
| public LikeResponse likePost(Long userId, Long postId) { | ||||||||||||||||||||||||||||||||||||||||||||||
| User user = userRepository.getReferenceById(userId); | ||||||||||||||||||||||||||||||||||||||||||||||
| Post post = postRepository.getReferenceById(postId); | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 엔티티 존재 여부 검증 필요
-User user = userRepository.getReferenceById(userId);
-Post post = postRepository.getReferenceById(postId);
+User user = userRepository.findById(userId)
+ .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+Post post = postRepository.findById(postId)
+ .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 만약 post나 user가 존재하지 않으면 likeRepository.save() 할 때 외래키 제약 조건에 의해서(너가 추천해줘서 그제야 넣긴했지만) 예외를 터뜨리기 때문에 굳이 매번 좋아요 할 때마다 user와 post를 DB에서 직접 조회할 필요는 없다고 생각함. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Doncham 맞습니다! 외래키 제약 조건이 있다면 실제로 존재하지 않는 엔티티에 대해 매번 좋아요 요청마다 사용자와 게시글 존재 여부를 확인하기 위해 추가 DB 조회를 하는 것보다, 데이터베이스 레벨에서 참조 무결성을 보장하는 방식이 성능상 더 유리합니다. 🐰 (nodding thoughtfully) ✏️ Learnings added
🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||
| Optional<Like> like = likeRepository.findByUserIdAndPostId(userId, postId); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if(like.isEmpty()){ | ||||||||||||||||||||||||||||||||||||||||||||||
| try{ | ||||||||||||||||||||||||||||||||||||||||||||||
| likeRepository.save(Like.builder() | ||||||||||||||||||||||||||||||||||||||||||||||
| .user(user) | ||||||||||||||||||||||||||||||||||||||||||||||
| // 저장하는 시점에 post가 삭제될 수 있으니까 try-catch로 잡음. | ||||||||||||||||||||||||||||||||||||||||||||||
| .post(post) | ||||||||||||||||||||||||||||||||||||||||||||||
| .build() | ||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||
| return LikeResponse.builder() | ||||||||||||||||||||||||||||||||||||||||||||||
| .userId(userId) | ||||||||||||||||||||||||||||||||||||||||||||||
| .postId(postId) | ||||||||||||||||||||||||||||||||||||||||||||||
| .isLiked(true) | ||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||
| }catch (Exception e) { | ||||||||||||||||||||||||||||||||||||||||||||||
| throw new LikeFailureException(e); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 예외 처리 개선 필요 현재 구현에서는 다음과 같은 문제가 있습니다:
if(like.isEmpty()){
try{
- likeRepository.save(Like.builder()
- .user(user)
- .post(post)
- .build()
- );
+ Like newLike = Like.builder()
+ .user(user)
+ .post(post)
+ .build();
+ likeRepository.save(newLike);
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(true)
.build();
- }catch (Exception e) {
+ } catch (DataIntegrityViolationException e) {
+ // 동시성으로 인한 중복 처리
+ throw new LikeFailureException("이미 좋아요가 처리되었습니다.");
+ } catch (Exception e) {
throw new LikeFailureException(e);
}필요한 import 추가: +import org.springframework.dao.DataIntegrityViolationException;🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| else{ | ||||||||||||||||||||||||||||||||||||||||||||||
| likeRepository.deleteById(like.get().getId()); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| return LikeResponse.builder() | ||||||||||||||||||||||||||||||||||||||||||||||
| .userId(userId) | ||||||||||||||||||||||||||||||||||||||||||||||
| .postId(postId) | ||||||||||||||||||||||||||||||||||||||||||||||
| .isLiked(false) | ||||||||||||||||||||||||||||||||||||||||||||||
| .build(); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 좋아요 취소 시 예외 처리 추가 좋아요 취소 시에도 예외 처리가 필요합니다. else{
- likeRepository.deleteById(like.get().getId());
+ try {
+ likeRepository.deleteById(like.get().getId());
+ } catch (Exception e) {
+ throw new LikeFailureException("좋아요 취소에 실패했습니다.", e);
+ }
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,8 +2,9 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.dto.request.PostCreateRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.dto.response.PageResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.dto.response.PostSearchResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.dto.response.PostWithLikeCount; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.dto.response.PostResponse; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.post.service.PostService; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.juniortown.backend.user.dto.CustomUserDetails; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.springframework.data.domain.Page; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -54,9 +55,12 @@ public ResponseEntity<PostResponse> update(@AuthenticationPrincipal CustomUserDe | |||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // 게시글 목록 조회, 페이지네이션 적용 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| @GetMapping("/posts/{page}") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public ResponseEntity<PageResponse<PostSearchResponse>> getPosts(@PathVariable int page) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Page<PostSearchResponse> posts = postService.getPosts(page); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| PageResponse<PostSearchResponse> response = new PageResponse<>(posts); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Long userId = customUserDetails.getUserId(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // null체크 | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return ResponseEntity.ok(response); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+58
to
65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인증 사용자 null 체크 구현 필요 주석에서 언급된 null 체크를 실제로 구현해야 합니다. public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
+ if (customUserDetails == null) {
+ throw new IllegalArgumentException("인증된 사용자 정보가 필요합니다.");
+ }
Long userId = customUserDetails.getUserId();
- // null체크
+ if (userId == null) {
+ throw new IllegalArgumentException("사용자 ID가 없습니다.");
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이거 따로 공부해야 하긴함 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Doncham 네, 맞습니다! Spring Security의 인증 처리는 별도로 공부하시는 것이 좋겠네요. 주요 학습 포인트들을 정리해드리면:
당장은 주석으로 남겨두시고, 나중에 Spring Security를 제대로 학습하신 후에 적용하시면 될 것 같습니다. 🐰 계속 배우려는 자세가 훌륭하네요! 차근차근 공부하시면서 진행하세요. ✏️ Learnings added
🧠 Learnings used |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,23 +7,35 @@ | |||||||||||||||||||||||||||||||||||||||||
| import lombok.Getter; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| @Getter | ||||||||||||||||||||||||||||||||||||||||||
| public class PostSearchResponse { | ||||||||||||||||||||||||||||||||||||||||||
| public class PostWithLikeCount { | ||||||||||||||||||||||||||||||||||||||||||
| private Long id; | ||||||||||||||||||||||||||||||||||||||||||
| private String title; | ||||||||||||||||||||||||||||||||||||||||||
| private String username; | ||||||||||||||||||||||||||||||||||||||||||
| private Long userId; | ||||||||||||||||||||||||||||||||||||||||||
| private Long likeCount; | ||||||||||||||||||||||||||||||||||||||||||
| private LocalDateTime createdAt; | ||||||||||||||||||||||||||||||||||||||||||
| private LocalDateTime updatedAt; | ||||||||||||||||||||||||||||||||||||||||||
| private LocalDateTime deletedAt; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| public PostSearchResponse(Post post) { | ||||||||||||||||||||||||||||||||||||||||||
| public PostWithLikeCount(Post post) { | ||||||||||||||||||||||||||||||||||||||||||
| this.id = post.getId(); | ||||||||||||||||||||||||||||||||||||||||||
| this.title = post.getTitle(); | ||||||||||||||||||||||||||||||||||||||||||
| // 이거 null 체크 어지럽네 | ||||||||||||||||||||||||||||||||||||||||||
| this.username = post.getUser().getName(); | ||||||||||||||||||||||||||||||||||||||||||
| this.userId = post.getUser().getId(); | ||||||||||||||||||||||||||||||||||||||||||
| this.createdAt = post.getCreatedAt(); | ||||||||||||||||||||||||||||||||||||||||||
| this.updatedAt = post.getUpdatedAt(); | ||||||||||||||||||||||||||||||||||||||||||
| this.deletedAt = post.getDeletedAt(); | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. likeCount 필드 초기화 누락 Post 객체를 받는 생성자에서 likeCount 필드가 초기화되지 않아 null 값이 될 수 있습니다. 이는 NullPointerException을 발생시킬 수 있습니다. 다음 수정을 제안합니다: public PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
+ this.likeCount = 0L; // 기본값 설정
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드는 사실상 사용을 하지 않음. JPA가 인터페이스 기반 프로젝션을 통해 구현체를 직접 주입해주기 때문에 삭제해도 무방할듯 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain@Doncham 맞습니다! 인터페이스 기반 프로젝션 방식이 훨씬 깔끔하네요. 확인해보겠습니다: 인터페이스 기반 프로젝션을 사용하면 JPA가 런타임에 구현체를 생성해주므로 별도의 DTO 클래스가 필요 없죠. 더 간결하고 효율적인 접근 방식입니다. 🏁 Script executed: Length of output: 22763 @Doncham ✏️ Learnings added
|
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // 생성자 기반 DTO 매핑 | ||||||||||||||||||||||||||||||||||||||||||
| public PostWithLikeCount(Long id, String title, String username, Long userId, Long likeCount, LocalDateTime createdAt, LocalDateTime updatedAt, LocalDateTime deletedAt) { | ||||||||||||||||||||||||||||||||||||||||||
| this.id = id; | ||||||||||||||||||||||||||||||||||||||||||
| this.title = title; | ||||||||||||||||||||||||||||||||||||||||||
| this.username = username; | ||||||||||||||||||||||||||||||||||||||||||
| this.userId = userId; | ||||||||||||||||||||||||||||||||||||||||||
| this.likeCount = likeCount; | ||||||||||||||||||||||||||||||||||||||||||
| this.createdAt = createdAt; | ||||||||||||||||||||||||||||||||||||||||||
| this.updatedAt = updatedAt; | ||||||||||||||||||||||||||||||||||||||||||
| this.deletedAt = deletedAt; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package org.juniortown.backend.post.dto.response; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public interface PostWithLikeCountProjection { | ||
| Long getId(); | ||
| String getTitle(); | ||
| String getUsername(); | ||
| Long getUserId(); | ||
| Long getLikeCount(); | ||
| Boolean getIsLiked(); | ||
| LocalDateTime getCreatedAt(); | ||
| LocalDateTime getUpdatedAt(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,31 @@ | ||
| package org.juniortown.backend.post.repository; | ||
|
|
||
|
|
||
|
|
||
| import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection; | ||
| import org.juniortown.backend.post.entity.Post; | ||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface PostRepository extends JpaRepository<Post, Long>, PostRepositoryCustom { | ||
| Page<Post> findAllByDeletedAtIsNull(Pageable pageable); | ||
|
|
||
| @Query( | ||
| "SELECT p.id AS id, p.title AS title, u.name AS username, u.id AS userId, COUNT(l.id) AS likeCount, " + | ||
| "p.createdAt AS createdAt, p.updatedAt AS updatedAt, " + | ||
| "CASE WHEN EXISTS (" + | ||
| " SELECT 1 FROM Like l2 WHERE l2.post.id = p.id AND l2.user.id = :userId" + | ||
| ") THEN true ELSE false END AS isLiked " + | ||
| "FROM Post p " + | ||
| "JOIN p.user u " + | ||
| "LEFT JOIN Like l ON l.post.id = p.id " + | ||
| "WHERE p.deletedAt IS NULL " + | ||
| "GROUP BY p.id, p.title, u.name, u.id, p.createdAt, p.updatedAt, p.deletedAt" | ||
| ) | ||
| Page<PostWithLikeCountProjection> findAllWithLikeCount(@Param("userId") Long userId, Pageable pageable); | ||
|
Comment on lines
+16
to
+28
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 성능 최적화 고려사항 복잡한 JPQL 쿼리이지만 기능적으로는 올바릅니다. 성능 최적화를 위한 몇 가지 제안사항이 있습니다:
더 나은 성능을 위해 다음과 같은 데이터베이스 인덱스를 추가하세요: CREATE INDEX idx_likes_user_post ON likes(user_id, post_id);
CREATE INDEX idx_posts_deleted_at ON posts(deleted_at);🤖 Prompt for AI Agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 성능 테스트하면서 진행할게요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Doncham 좋은 접근 방식이네요! 성능 테스트를 통해 실제 병목 지점을 파악하시는 것이 올바른 최적화 방법입니다. 테스트 시 다음 사항들을 모니터링하시면 도움이 될 것 같습니다:
필요하시면 언제든지 추가 조언을 요청해 주세요! 📊 🧠 Learnings used |
||
|
|
||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.