Skip to content

Commit

Permalink
[FEAT] 게시글 찜하기 기능 구현, Mypage 찜한 게시글 목록 조회 (#143)
Browse files Browse the repository at this point in the history
* 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사용하여 구현
  • Loading branch information
injae-348 authored Sep 10, 2024
1 parent 22aa068 commit aec89d5
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Comment, Long> {
}
Original file line number Diff line number Diff line change
@@ -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<ApiResponse.CustomBody<Boolean>> 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<ApiResponse.CustomBody<PostCustomPage>> 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);
}

}
32 changes: 32 additions & 0 deletions src/main/java/econo/buddybridge/post/entity/PostLike.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,4 +35,5 @@ public HttpStatus getHttpStatus() {
public String getMessage() {
return message;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<PostLike, Long>, PostLikeRepositoryCustom {

}
Original file line number Diff line number Diff line change
@@ -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<PostLike> findByPostIdAndMemberId(Long postId, Long memberId);

PostCustomPage findPostsByLikes(Long memberId, Integer page, Integer size, String sort, PostType postType);
}
Original file line number Diff line number Diff line change
@@ -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<PostLike> 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<PostResDto> 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;
};
}

}
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -16,5 +13,4 @@ public interface PostRepository extends JpaRepository<Post, Long> {
@EntityGraph(attributePaths = {"author"})
Optional<Post> findById(Long postId);

Page<Post> findByPostType(Pageable pageable, PostType postType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
}
}
51 changes: 51 additions & 0 deletions src/main/java/econo/buddybridge/post/service/PostLikeService.java
Original file line number Diff line number Diff line change
@@ -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);
}



}

0 comments on commit aec89d5

Please sign in to comment.