From aec89d5a4eaf956f8d277e6dbe42c9725d20f878 Mon Sep 17 00:00:00 2001 From: JeongInJae <93825184+injae-348@users.noreply.github.com> Date: Tue, 10 Sep 2024 23:12:07 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=B0=9C=ED=95=98=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=20Mypage=20=EC=B0=9C=ED=95=9C=20=EA=B2=8C=EC=8B=9C?= =?UTF-8?q?=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20(#143)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 좋아요 Entity, Repository 추가 * feat: 게시글 좋아요 엔드포인트 추가, Service 로직 작성, Custom Repository 추가 * fix: memberServicae -> memberService 오타 수정 * feat: @Repository 추가 * feat: Mypage 찜한 게시글 조회 * style: { 앞 공백 추가 * refactor: @Repository 제거 * refactor: 안쓰는 함수 제거 * feat: 게시글 조회시 잘못된 정렬 방식에 대한 예외 처리 * feat: PostLike에 Optional 적용 및 찜 관리시 조회 순서 변경 * refactor: 찜 관리 기능 map, orElseGet사용하여 구현 --- .../comment/repository/CommentRepository.java | 2 - .../post/controller/PostLikeController.java | 50 +++++++++++++ .../buddybridge/post/entity/PostLike.java | 32 +++++++++ .../post/exception/PostErrorCode.java | 2 + .../PostInvalidSortValueException.java | 12 ++++ .../post/repository/PostLikeRepository.java | 8 +++ .../repository/PostLikeRepositoryCustom.java | 14 ++++ .../PostLikeRepositoryCustomImpl.java | 70 +++++++++++++++++++ .../post/repository/PostRepository.java | 4 -- .../post/repository/PostRepositoryImpl.java | 3 +- .../post/service/PostLikeService.java | 51 ++++++++++++++ 11 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/main/java/econo/buddybridge/post/controller/PostLikeController.java create mode 100644 src/main/java/econo/buddybridge/post/entity/PostLike.java create mode 100644 src/main/java/econo/buddybridge/post/exception/PostInvalidSortValueException.java create mode 100644 src/main/java/econo/buddybridge/post/repository/PostLikeRepository.java create mode 100644 src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustom.java create mode 100644 src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustomImpl.java create mode 100644 src/main/java/econo/buddybridge/post/service/PostLikeService.java diff --git a/src/main/java/econo/buddybridge/comment/repository/CommentRepository.java b/src/main/java/econo/buddybridge/comment/repository/CommentRepository.java index 431e543..4535784 100644 --- a/src/main/java/econo/buddybridge/comment/repository/CommentRepository.java +++ b/src/main/java/econo/buddybridge/comment/repository/CommentRepository.java @@ -2,8 +2,6 @@ import econo.buddybridge.comment.entity.Comment; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -@Repository public interface CommentRepository extends JpaRepository { } diff --git a/src/main/java/econo/buddybridge/post/controller/PostLikeController.java b/src/main/java/econo/buddybridge/post/controller/PostLikeController.java new file mode 100644 index 0000000..2c8f235 --- /dev/null +++ b/src/main/java/econo/buddybridge/post/controller/PostLikeController.java @@ -0,0 +1,50 @@ +package econo.buddybridge.post.controller; + +import econo.buddybridge.post.dto.PostCustomPage; +import econo.buddybridge.post.entity.PostType; +import econo.buddybridge.post.service.PostLikeService; +import econo.buddybridge.utils.api.ApiResponse; +import econo.buddybridge.utils.api.ApiResponseGenerator; +import econo.buddybridge.utils.session.SessionUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/posts/likes") +@Tag(name = "게시글 좋아요 API", description = "게시글 좋아요 관련 API") +public class PostLikeController { + + private final PostLikeService postLikeService; + + @Operation(summary = "찜한 게시글 관리", description = "찜한 게시글 관리 찜을(생성, 삭제)합니다.") + @PostMapping("/{post-id}") + public ApiResponse> managePostLike( + @PathVariable(name = "post-id") Long postId, + HttpServletRequest request + ) { + Long memberId = SessionUtils.getMemberId(request); + Boolean isLike = postLikeService.managePostLike(memberId, postId); + return ApiResponseGenerator.success(isLike, HttpStatus.OK); + } + + + @Operation(summary = "찜한 게시글 목록 조회", description = "찜한 게시글 목록을 조회합니다.") + @GetMapping("/my-page") + public ApiResponse> getPostLikes( + @RequestParam("page") Integer page, + @RequestParam("size") Integer size, + @RequestParam(defaultValue = "desc", required = false) String sort, + @RequestParam(value = "post-type", required = false) PostType postType, + HttpServletRequest request + ) { + Long memberId = SessionUtils.getMemberId(request); + PostCustomPage posts = postLikeService.getPostsLikes(memberId, page, size, sort, postType); + return ApiResponseGenerator.success(posts, HttpStatus.OK); + } + +} diff --git a/src/main/java/econo/buddybridge/post/entity/PostLike.java b/src/main/java/econo/buddybridge/post/entity/PostLike.java new file mode 100644 index 0000000..8af4ba1 --- /dev/null +++ b/src/main/java/econo/buddybridge/post/entity/PostLike.java @@ -0,0 +1,32 @@ +package econo.buddybridge.post.entity; + +import econo.buddybridge.member.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "POST_LIKE") +public class PostLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + public PostLike(Post post, Member member) { + this.post = post; + this.member = member; + } + +} diff --git a/src/main/java/econo/buddybridge/post/exception/PostErrorCode.java b/src/main/java/econo/buddybridge/post/exception/PostErrorCode.java index 7c76839..97f3d44 100644 --- a/src/main/java/econo/buddybridge/post/exception/PostErrorCode.java +++ b/src/main/java/econo/buddybridge/post/exception/PostErrorCode.java @@ -8,6 +8,7 @@ public enum PostErrorCode implements ErrorCode { POST_DELETE_NOT_ALLOWED("P002", HttpStatus.FORBIDDEN, "본인의 게시글만 삭제할 수 있습니다."), POST_UPDATE_NOT_ALLOWED("P003", HttpStatus.FORBIDDEN, "본인의 게시글만 수정할 수 있습니다."), POST_UNAUTHORIZED_ACCESS("P004", HttpStatus.BAD_REQUEST, "회원님이 작성한 게시글이 아닙니다."), + POST_INVALID_SORT_VALUE("P005", HttpStatus.BAD_REQUEST, "유효하지 않은 정렬 값입니다.(desc, asc중 하나를 넣어주세요)"), ; private final String code; @@ -34,4 +35,5 @@ public HttpStatus getHttpStatus() { public String getMessage() { return message; } + } diff --git a/src/main/java/econo/buddybridge/post/exception/PostInvalidSortValueException.java b/src/main/java/econo/buddybridge/post/exception/PostInvalidSortValueException.java new file mode 100644 index 0000000..504d099 --- /dev/null +++ b/src/main/java/econo/buddybridge/post/exception/PostInvalidSortValueException.java @@ -0,0 +1,12 @@ +package econo.buddybridge.post.exception; + +import econo.buddybridge.common.exception.BusinessException; + +public class PostInvalidSortValueException extends BusinessException { + + public static BusinessException EXCEPTION = new PostInvalidSortValueException(); + + private PostInvalidSortValueException() { + super(PostErrorCode.POST_INVALID_SORT_VALUE); + } +} diff --git a/src/main/java/econo/buddybridge/post/repository/PostLikeRepository.java b/src/main/java/econo/buddybridge/post/repository/PostLikeRepository.java new file mode 100644 index 0000000..a1e26c5 --- /dev/null +++ b/src/main/java/econo/buddybridge/post/repository/PostLikeRepository.java @@ -0,0 +1,8 @@ +package econo.buddybridge.post.repository; + +import econo.buddybridge.post.entity.PostLike; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PostLikeRepository extends JpaRepository, PostLikeRepositoryCustom { + +} diff --git a/src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustom.java b/src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustom.java new file mode 100644 index 0000000..ba28092 --- /dev/null +++ b/src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustom.java @@ -0,0 +1,14 @@ +package econo.buddybridge.post.repository; + +import econo.buddybridge.post.dto.PostCustomPage; +import econo.buddybridge.post.entity.PostLike; +import econo.buddybridge.post.entity.PostType; + +import java.util.Optional; + +public interface PostLikeRepositoryCustom { + + Optional findByPostIdAndMemberId(Long postId, Long memberId); + + PostCustomPage findPostsByLikes(Long memberId, Integer page, Integer size, String sort, PostType postType); +} diff --git a/src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustomImpl.java b/src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustomImpl.java new file mode 100644 index 0000000..4a538ed --- /dev/null +++ b/src/main/java/econo/buddybridge/post/repository/PostLikeRepositoryCustomImpl.java @@ -0,0 +1,70 @@ +package econo.buddybridge.post.repository; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import econo.buddybridge.post.dto.PostCustomPage; +import econo.buddybridge.post.dto.PostResDto; +import econo.buddybridge.post.entity.PostLike; +import econo.buddybridge.post.entity.PostType; +import econo.buddybridge.post.exception.PostInvalidSortValueException; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; + +import static econo.buddybridge.post.entity.QPostLike.postLike; + +@RequiredArgsConstructor +public class PostLikeRepositoryCustomImpl implements PostLikeRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + @Override + public Optional findByPostIdAndMemberId(Long postId, Long memberId) { + + PostLike like = queryFactory.selectFrom(postLike) + .where(postLike.post.id.eq(postId).and(postLike.member.id.eq(memberId))) + .fetchOne(); + + return Optional.ofNullable(like); + } + + @Override + public PostCustomPage findPostsByLikes(Long memberId, Integer page, Integer size, String sort, PostType postType) { + + // Todo: 쿼리 최적화 고려 + List content = queryFactory + .select(postLike.post) + .from(postLike) + .where(postLike.member.id.eq(memberId), buildPostTypeExpression(postType)) + .offset((long) page * size) + .orderBy(buildOrderSpecifier(sort)) + .fetch() + .stream() + .map(PostResDto::new) + .toList(); + + Long totalElements = queryFactory + .select(postLike.count()) + .from(postLike) + .where(postLike.member.id.eq(memberId)) + .fetchOne(); + + return new PostCustomPage(content, totalElements, content.size() < size); + } + + + private BooleanExpression buildPostTypeExpression(PostType postType) { + return postType != null ? postLike.post.postType.eq(postType) : null; + } + + private OrderSpecifier buildOrderSpecifier(String sort) { + return switch (sort.toLowerCase()) { + case "desc" -> postLike.post.createdAt.desc(); + case "asc" -> postLike.post.createdAt.asc(); + default -> throw PostInvalidSortValueException.EXCEPTION; + }; + } + +} diff --git a/src/main/java/econo/buddybridge/post/repository/PostRepository.java b/src/main/java/econo/buddybridge/post/repository/PostRepository.java index 9c13c14..90de57a 100644 --- a/src/main/java/econo/buddybridge/post/repository/PostRepository.java +++ b/src/main/java/econo/buddybridge/post/repository/PostRepository.java @@ -1,10 +1,7 @@ package econo.buddybridge.post.repository; import econo.buddybridge.post.entity.Post; -import econo.buddybridge.post.entity.PostType; import lombok.NonNull; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; @@ -16,5 +13,4 @@ public interface PostRepository extends JpaRepository { @EntityGraph(attributePaths = {"author"}) Optional findById(Long postId); - Page findByPostType(Pageable pageable, PostType postType); } diff --git a/src/main/java/econo/buddybridge/post/repository/PostRepositoryImpl.java b/src/main/java/econo/buddybridge/post/repository/PostRepositoryImpl.java index efbfa51..8c4f227 100644 --- a/src/main/java/econo/buddybridge/post/repository/PostRepositoryImpl.java +++ b/src/main/java/econo/buddybridge/post/repository/PostRepositoryImpl.java @@ -10,6 +10,7 @@ import econo.buddybridge.post.entity.District; import econo.buddybridge.post.entity.PostStatus; import econo.buddybridge.post.entity.PostType; +import econo.buddybridge.post.exception.PostInvalidSortValueException; import lombok.RequiredArgsConstructor; import java.util.List; @@ -100,7 +101,7 @@ private OrderSpecifier buildOrderSpecifier(String sort) { return switch (sort.toLowerCase()) { case "desc" -> post.createdAt.desc(); case "asc" -> post.createdAt.asc(); - default -> throw new IllegalArgumentException("올바르지 않은 정렬 방식입니다."); + default -> throw PostInvalidSortValueException.EXCEPTION; }; } } diff --git a/src/main/java/econo/buddybridge/post/service/PostLikeService.java b/src/main/java/econo/buddybridge/post/service/PostLikeService.java new file mode 100644 index 0000000..41f3c19 --- /dev/null +++ b/src/main/java/econo/buddybridge/post/service/PostLikeService.java @@ -0,0 +1,51 @@ +package econo.buddybridge.post.service; + +import econo.buddybridge.member.entity.Member; +import econo.buddybridge.member.service.MemberService; +import econo.buddybridge.post.dto.PostCustomPage; +import econo.buddybridge.post.entity.Post; +import econo.buddybridge.post.entity.PostLike; +import econo.buddybridge.post.entity.PostType; +import econo.buddybridge.post.repository.PostLikeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PostLikeService { + + private final MemberService memberService; + private final PostService postService; + private final PostLikeRepository postLikeRepository; + + @Transactional + public Boolean managePostLike(Long memberId, Long postId) { + + Member member = memberService.findMemberByIdOrThrow(memberId); + Post post = postService.findPostByIdOrThrow(postId); + + return postLikeRepository.findByPostIdAndMemberId(postId, memberId) + .map(this::removeLike) + .orElseGet(() -> addLike(post, member)); + } + + private boolean removeLike(PostLike postLike) { + postLikeRepository.delete(postLike); + return false; + } + + private boolean addLike(Post post, Member member) { + PostLike newLike = new PostLike(post, member); + postLikeRepository.save(newLike); + return true; + } + + @Transactional(readOnly = true) + public PostCustomPage getPostsLikes(Long memberId, Integer page, Integer size, String sort, PostType postType) { + return postLikeRepository.findPostsByLikes(memberId, page - 1, size, sort, postType); + } + + + +}