diff --git a/src/main/java/cotato/bookitlist/book/controller/BookController.java b/src/main/java/cotato/bookitlist/book/controller/BookController.java index 686af2e..c26c7f1 100644 --- a/src/main/java/cotato/bookitlist/book/controller/BookController.java +++ b/src/main/java/cotato/bookitlist/book/controller/BookController.java @@ -2,10 +2,7 @@ import cotato.bookitlist.book.annotation.IsValidIsbn; import cotato.bookitlist.book.dto.request.BookIsbn13Request; -import cotato.bookitlist.book.dto.response.BookApiListResponse; -import cotato.bookitlist.book.dto.response.BookApiResponse; -import cotato.bookitlist.book.dto.response.BookListResponse; -import cotato.bookitlist.book.dto.response.BookResponse; +import cotato.bookitlist.book.dto.response.*; import cotato.bookitlist.book.service.BookService; import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; @@ -80,6 +77,11 @@ public ResponseEntity registerBook( return ResponseEntity.created(location).build(); } + @GetMapping("/recommend") + public ResponseEntity recommendBook() { + return ResponseEntity.ok(bookService.recommendBook()); + } + } diff --git a/src/main/java/cotato/bookitlist/book/dto/response/BookRecommendListResponse.java b/src/main/java/cotato/bookitlist/book/dto/response/BookRecommendListResponse.java new file mode 100644 index 0000000..53eb3bf --- /dev/null +++ b/src/main/java/cotato/bookitlist/book/dto/response/BookRecommendListResponse.java @@ -0,0 +1,8 @@ +package cotato.bookitlist.book.dto.response; + +import java.util.List; + +public record BookRecommendListResponse( + List bookList +) { +} diff --git a/src/main/java/cotato/bookitlist/book/dto/response/BookRecommendResponse.java b/src/main/java/cotato/bookitlist/book/dto/response/BookRecommendResponse.java new file mode 100644 index 0000000..e5471e9 --- /dev/null +++ b/src/main/java/cotato/bookitlist/book/dto/response/BookRecommendResponse.java @@ -0,0 +1,21 @@ +package cotato.bookitlist.book.dto.response; + +import cotato.bookitlist.book.dto.BookDto; + +public record BookRecommendResponse( + String title, + String author, + String description, + String isbn13, + String cover +) { + public static BookRecommendResponse from(BookDto dto) { + return new BookRecommendResponse( + dto.title(), + dto.author(), + dto.description(), + dto.isbn13(), + dto.cover() + ); + } +} diff --git a/src/main/java/cotato/bookitlist/book/repository/BookRepository.java b/src/main/java/cotato/bookitlist/book/repository/BookRepository.java index 090f8cb..dc8562f 100644 --- a/src/main/java/cotato/bookitlist/book/repository/BookRepository.java +++ b/src/main/java/cotato/bookitlist/book/repository/BookRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface BookRepository extends JpaRepository { @@ -14,9 +15,12 @@ public interface BookRepository extends JpaRepository { Optional findByIsbn13(String isbn13); // TODO: 단순 like query로 한영에 따라 결과가 다르게 나온다. 이를 해결해야함!! - @Query("select b from Book b where lower(b.title) like lower(concat('%', :keyword, '%')) or lower(b.author) like lower(concat('%', :keyword, '%')) or lower(b.description) like lower(concat('%', :keyword, '%'))") + @Query("select b from Book b where LOWER(b.title) like LOWER(CONCAT('%', :keyword, '%')) or LOWER(b.author) like LOWER(CONCAT('%', :keyword, '%')) or LOWER(b.description) like LOWER(CONCAT('%', :keyword, '%'))") Page findAllByKeyword(@Param("keyword") String keyword, Pageable pageable); @Query("select b from BookLike l join l.book b join l.member m where m.id = :memberId") Page findLikeBookByMemberId(Long memberId, Pageable pageable); + + @Query(value = "select * from Book b order by RAND() LIMIT :count", nativeQuery = true) + List findBooksByRandom(int count); } diff --git a/src/main/java/cotato/bookitlist/book/service/BookService.java b/src/main/java/cotato/bookitlist/book/service/BookService.java index 2b36600..12a2646 100644 --- a/src/main/java/cotato/bookitlist/book/service/BookService.java +++ b/src/main/java/cotato/bookitlist/book/service/BookService.java @@ -5,21 +5,29 @@ import cotato.bookitlist.book.dto.BookDto; import cotato.bookitlist.book.dto.response.BookApiListResponse; import cotato.bookitlist.book.dto.response.BookListResponse; +import cotato.bookitlist.book.dto.response.BookRecommendListResponse; +import cotato.bookitlist.book.dto.response.BookRecommendResponse; import cotato.bookitlist.book.redis.BookApiCache; import cotato.bookitlist.book.repository.BookRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @Transactional(readOnly = true) @RequiredArgsConstructor public class BookService { + @Value("${recommend.count.book}") + private int recommendCount; + private final BookApiComponent bookApiComponent; private final BookApiCacheService bookApiCacheService; private final BookRepository bookRepository; @@ -66,4 +74,13 @@ public Long registerBook(String isbn13) { public BookListResponse getLikeBooks(Long memberId, Pageable pageable) { return BookListResponse.from(bookRepository.findLikeBookByMemberId(memberId, pageable)); } + + public BookRecommendListResponse recommendBook() { + List bookRecommendList = bookRepository.findBooksByRandom(recommendCount).stream() + .map(BookDto::from) + .map(BookRecommendResponse::from) + .toList(); + + return new BookRecommendListResponse(bookRecommendList); + } } diff --git a/src/main/java/cotato/bookitlist/common/domain/RecommendType.java b/src/main/java/cotato/bookitlist/common/domain/RecommendType.java new file mode 100644 index 0000000..17ac297 --- /dev/null +++ b/src/main/java/cotato/bookitlist/common/domain/RecommendType.java @@ -0,0 +1,5 @@ +package cotato.bookitlist.common.domain; + +public enum RecommendType { + LIKE, NEW +} diff --git a/src/main/java/cotato/bookitlist/member/controller/MemberController.java b/src/main/java/cotato/bookitlist/member/controller/MemberController.java index 380b4a3..7cae936 100644 --- a/src/main/java/cotato/bookitlist/member/controller/MemberController.java +++ b/src/main/java/cotato/bookitlist/member/controller/MemberController.java @@ -2,6 +2,7 @@ import cotato.bookitlist.config.security.jwt.AuthDetails; import cotato.bookitlist.member.dto.request.NameChangeRequest; +import cotato.bookitlist.member.dto.response.MemberRecommendListResponse; import cotato.bookitlist.member.dto.response.MemberResponse; import cotato.bookitlist.member.dto.response.ProfileResponse; import cotato.bookitlist.member.service.MemberService; @@ -57,4 +58,9 @@ public ResponseEntity changeName( return ResponseEntity.ok().build(); } + @GetMapping("/recommend/new") + public ResponseEntity getNewMembers() { + return ResponseEntity.ok(memberService.getNewMembers()); + } + } diff --git a/src/main/java/cotato/bookitlist/member/domain/Member.java b/src/main/java/cotato/bookitlist/member/domain/Member.java index faafc3f..bbef49d 100644 --- a/src/main/java/cotato/bookitlist/member/domain/Member.java +++ b/src/main/java/cotato/bookitlist/member/domain/Member.java @@ -36,7 +36,7 @@ public class Member extends BaseEntity { private String profileLink; @Enumerated(EnumType.STRING) - private ProfileStatus profileStatus = ProfileStatus.PUBLIC; + private ProfileStatus status = ProfileStatus.PUBLIC; private boolean deleted = false; @@ -66,16 +66,16 @@ public String updateProfileLink(String url) { } public void validatePublicProfile(Long memberId) { - if (profileStatus.equals(ProfileStatus.PRIVATE) && !id.equals(memberId)) { + if (status.equals(ProfileStatus.PRIVATE) && !id.equals(memberId)) { throw new AccessDeniedException("권한이 존재하지 않는 멤버입니다."); } } public void changeProfileStatus() { - if (profileStatus.equals(ProfileStatus.PRIVATE)) { - profileStatus = ProfileStatus.PUBLIC; + if (status.equals(ProfileStatus.PRIVATE)) { + status = ProfileStatus.PUBLIC; } else { - profileStatus = ProfileStatus.PRIVATE; + status = ProfileStatus.PRIVATE; } } diff --git a/src/main/java/cotato/bookitlist/member/dto/MemberDto.java b/src/main/java/cotato/bookitlist/member/dto/MemberDto.java index 7e93d56..8918d8f 100644 --- a/src/main/java/cotato/bookitlist/member/dto/MemberDto.java +++ b/src/main/java/cotato/bookitlist/member/dto/MemberDto.java @@ -18,7 +18,7 @@ public static MemberDto from(Member entity, Long memberId) { entity.getEmail(), entity.getName(), entity.getProfileLink(), - entity.getProfileStatus(), + entity.getStatus(), entity.getId().equals(memberId) ); } diff --git a/src/main/java/cotato/bookitlist/member/dto/response/MemberRecommendListResponse.java b/src/main/java/cotato/bookitlist/member/dto/response/MemberRecommendListResponse.java new file mode 100644 index 0000000..9e46228 --- /dev/null +++ b/src/main/java/cotato/bookitlist/member/dto/response/MemberRecommendListResponse.java @@ -0,0 +1,16 @@ +package cotato.bookitlist.member.dto.response; + +import cotato.bookitlist.member.domain.Member; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record MemberRecommendListResponse ( + List memberList +) { + public static MemberRecommendListResponse of(Page page) { + return new MemberRecommendListResponse( + page.stream().map(MemberRecommendResponse::of).toList() + ); + } +} diff --git a/src/main/java/cotato/bookitlist/member/dto/response/MemberRecommendResponse.java b/src/main/java/cotato/bookitlist/member/dto/response/MemberRecommendResponse.java new file mode 100644 index 0000000..32f7f5b --- /dev/null +++ b/src/main/java/cotato/bookitlist/member/dto/response/MemberRecommendResponse.java @@ -0,0 +1,12 @@ +package cotato.bookitlist.member.dto.response; + +import cotato.bookitlist.member.domain.Member; + +public record MemberRecommendResponse( + Long memberId, + String profileLink +) { + public static MemberRecommendResponse of(Member member) { + return new MemberRecommendResponse(member.getId(), member.getProfileLink()); + } +} diff --git a/src/main/java/cotato/bookitlist/member/repository/MemberRepository.java b/src/main/java/cotato/bookitlist/member/repository/MemberRepository.java index ee1f881..4e81528 100644 --- a/src/main/java/cotato/bookitlist/member/repository/MemberRepository.java +++ b/src/main/java/cotato/bookitlist/member/repository/MemberRepository.java @@ -2,9 +2,16 @@ import cotato.bookitlist.config.security.oauth.AuthProvider; import cotato.bookitlist.member.domain.Member; +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; public interface MemberRepository extends JpaRepository { Member findByOauth2IdAndAuthProvider(String oauth2Id, AuthProvider authProvider); + + @Query("select m from Member m where m.status = 'PUBLIC'") + Page findPublicMember(Pageable pageable); + } diff --git a/src/main/java/cotato/bookitlist/member/service/MemberService.java b/src/main/java/cotato/bookitlist/member/service/MemberService.java index 564bd20..f074996 100644 --- a/src/main/java/cotato/bookitlist/member/service/MemberService.java +++ b/src/main/java/cotato/bookitlist/member/service/MemberService.java @@ -3,9 +3,14 @@ import cotato.bookitlist.file.service.FileService; import cotato.bookitlist.member.domain.Member; import cotato.bookitlist.member.dto.MemberDto; +import cotato.bookitlist.member.dto.response.MemberRecommendListResponse; import cotato.bookitlist.member.repository.MemberRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -15,6 +20,9 @@ @RequiredArgsConstructor public class MemberService { + @Value("${recommend.count.member}") + private int recommendCount; + private static final String PROFILE_FILE_NAME = "profile"; private final MemberRepository memberRepository; @@ -48,4 +56,10 @@ public void changeName(String name, Long memberId) { member.changeName(name); } + + public MemberRecommendListResponse getNewMembers() { + Pageable pageable = PageRequest.of(0, recommendCount, Sort.by("createdAt").descending()); + + return MemberRecommendListResponse.of(memberRepository.findPublicMember(pageable)); + } } diff --git a/src/main/java/cotato/bookitlist/post/controller/PostController.java b/src/main/java/cotato/bookitlist/post/controller/PostController.java index af3b84b..f01c30a 100644 --- a/src/main/java/cotato/bookitlist/post/controller/PostController.java +++ b/src/main/java/cotato/bookitlist/post/controller/PostController.java @@ -1,6 +1,7 @@ package cotato.bookitlist.post.controller; import cotato.bookitlist.book.annotation.IsValidIsbn; +import cotato.bookitlist.common.domain.RecommendType; import cotato.bookitlist.config.security.jwt.AuthDetails; import cotato.bookitlist.post.dto.request.PostRegisterRequest; import cotato.bookitlist.post.dto.request.PostUpdateRequest; @@ -139,6 +140,18 @@ public ResponseEntity getMyPosts( return ResponseEntity.ok(postService.getMyPosts(details.getId(), pageable)); } + @GetMapping("/recommend") + public ResponseEntity getRecommendPosts( + @RequestParam RecommendType type, + @RequestParam int start, + @AuthenticationPrincipal AuthDetails details + ) { + if (details == null) { + return ResponseEntity.ok(postService.getRecommendPosts(type, start, DEFAULT_USER_ID)); + } + return ResponseEntity.ok(postService.getRecommendPosts(type, start, details.getId())); + } + private void handlePostViewCount(HttpServletRequest request, HttpServletResponse response, Long postId) { Cookie[] cookies = request.getCookies(); Cookie postViewCookie = findCookie(cookies); diff --git a/src/main/java/cotato/bookitlist/post/repository/PostRepository.java b/src/main/java/cotato/bookitlist/post/repository/PostRepository.java index 3ba69dd..dfd74b1 100644 --- a/src/main/java/cotato/bookitlist/post/repository/PostRepository.java +++ b/src/main/java/cotato/bookitlist/post/repository/PostRepository.java @@ -11,10 +11,10 @@ public interface PostRepository extends PostRepositoryCustom, JpaRepository { - @Query("select p from Post p where p.status = 'PUBLIC' and p.member.profileStatus = 'PUBLIC'") + @Query("select p from Post p where p.status = 'PUBLIC' and p.member.status = 'PUBLIC'") Page findPublicPostAll(Pageable pageable); - @Query("select count(p) from Post p where p.status = 'PUBLIC' and p.book.isbn13 = :isbn13 and p.member.profileStatus = 'PUBLIC'") + @Query("select count(p) from Post p where p.status = 'PUBLIC' and p.book.isbn13 = :isbn13 and p.member.status = 'PUBLIC'") int countPublicPostByBook_Isbn13(String isbn13); Optional findByIdAndMemberId(Long postId, Long memberId); diff --git a/src/main/java/cotato/bookitlist/post/repository/querydsl/PostRepositoryCustomImpl.java b/src/main/java/cotato/bookitlist/post/repository/querydsl/PostRepositoryCustomImpl.java index 634b91d..b580ef9 100644 --- a/src/main/java/cotato/bookitlist/post/repository/querydsl/PostRepositoryCustomImpl.java +++ b/src/main/java/cotato/bookitlist/post/repository/querydsl/PostRepositoryCustomImpl.java @@ -52,7 +52,7 @@ public Page findPublicPostWithLikedByIsbn13(String isbn13, Long memberI ) .from(post) .join(post.member, member) - .where(isbnEq(isbn13), memberIdEq(memberId), post.status.eq(PostStatus.PUBLIC), post.member.profileStatus.eq(ProfileStatus.PUBLIC)) + .where(isbnEq(isbn13), memberIdEq(memberId), post.status.eq(PostStatus.PUBLIC), post.member.status.eq(ProfileStatus.PUBLIC)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); @@ -159,6 +159,6 @@ private BooleanExpression buildPostAccessCondition(NumberPath postMemberId .when(postMemberId.eq(memberId)) .then(true) .otherwise(post.status.eq(PostStatus.PUBLIC) - .and(post.member.profileStatus.eq(ProfileStatus.PUBLIC))); + .and(post.member.status.eq(ProfileStatus.PUBLIC))); } } diff --git a/src/main/java/cotato/bookitlist/post/service/PostService.java b/src/main/java/cotato/bookitlist/post/service/PostService.java index 2cf55f6..a54b253 100644 --- a/src/main/java/cotato/bookitlist/post/service/PostService.java +++ b/src/main/java/cotato/bookitlist/post/service/PostService.java @@ -3,6 +3,7 @@ import cotato.bookitlist.book.domain.Book; import cotato.bookitlist.book.repository.BookRepository; import cotato.bookitlist.book.service.BookService; +import cotato.bookitlist.common.domain.RecommendType; import cotato.bookitlist.member.domain.Member; import cotato.bookitlist.member.repository.MemberRepository; import cotato.bookitlist.post.domain.entity.Post; @@ -14,7 +15,11 @@ import cotato.bookitlist.post.repository.PostRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +28,9 @@ @RequiredArgsConstructor public class PostService { + @Value("${recommend.count.post}") + private int recommendCount; + private final BookService bookService; private final PostRepository postRepository; private final MemberRepository memberRepository; @@ -92,4 +100,26 @@ public PostListResponse searchLikePost(Long memberId, Pageable pageable) { public PostListResponse getMyPosts(Long memberId, Pageable pageable) { return PostListResponse.from(postRepository.findByMemberId(memberId, pageable), memberId); } + + @Transactional(readOnly = true) + public PostListResponse getRecommendPosts(RecommendType recommendType, int start, Long memberId) { + return switch (recommendType) { + case LIKE -> getMostLikePosts(start, memberId); + case NEW -> getNewPosts(start, memberId); + }; + } + + public PostListResponse getMostLikePosts(int start, Long memberId) { + Pageable pageable = PageRequest.of(start, recommendCount, Sort.by("likeCount").descending()); + Page postPage = postRepository.findPublicPostAll(pageable); + + return PostListResponse.from(postPage, memberId); + } + + public PostListResponse getNewPosts(int start, Long memberId) { + Pageable pageable = PageRequest.of(start, recommendCount, Sort.by("createdAt").descending()); + Page postPage = postRepository.findPublicPostAll(pageable); + + return PostListResponse.from(postPage, memberId); + } } diff --git a/src/main/java/cotato/bookitlist/review/controller/ReviewController.java b/src/main/java/cotato/bookitlist/review/controller/ReviewController.java index cdc8db7..75ea4d2 100644 --- a/src/main/java/cotato/bookitlist/review/controller/ReviewController.java +++ b/src/main/java/cotato/bookitlist/review/controller/ReviewController.java @@ -1,6 +1,7 @@ package cotato.bookitlist.review.controller; import cotato.bookitlist.book.annotation.IsValidIsbn; +import cotato.bookitlist.common.domain.RecommendType; import cotato.bookitlist.config.security.jwt.AuthDetails; import cotato.bookitlist.review.dto.request.ReviewRegisterRequest; import cotato.bookitlist.review.dto.request.ReviewUpdateRequest; @@ -119,6 +120,18 @@ public ResponseEntity deleteReview( return ResponseEntity.noContent().build(); } + @GetMapping("/recommend") + public ResponseEntity getRecommendReviews( + @RequestParam RecommendType type, + @RequestParam int start, + @AuthenticationPrincipal AuthDetails details + ) { + if (details == null) { + return ResponseEntity.ok(reviewService.getRecommendReviews(type, start, DEFAULT_USER_ID)); + } + return ResponseEntity.ok(reviewService.getRecommendReviews(type, start, DEFAULT_USER_ID)); + } + private void handleReviewViewCount(HttpServletRequest request, HttpServletResponse response, Long reviewId) { Cookie[] cookies = request.getCookies(); Cookie reviewViewCookie = findCookie(cookies); diff --git a/src/main/java/cotato/bookitlist/review/repository/querydsl/ReviewRepositoryCustomImpl.java b/src/main/java/cotato/bookitlist/review/repository/querydsl/ReviewRepositoryCustomImpl.java index 433615a..f5bcc24 100644 --- a/src/main/java/cotato/bookitlist/review/repository/querydsl/ReviewRepositoryCustomImpl.java +++ b/src/main/java/cotato/bookitlist/review/repository/querydsl/ReviewRepositoryCustomImpl.java @@ -50,7 +50,7 @@ public Page findPublicReviewWithLikedByIsbn13(String isbn13, Long mem ) .from(review) .join(review.member, member) - .where(review.book.isbn13.eq(isbn13), review.status.eq(ReviewStatus.PUBLIC), review.member.profileStatus.eq(ProfileStatus.PUBLIC)) + .where(review.book.isbn13.eq(isbn13), review.status.eq(ReviewStatus.PUBLIC), review.member.status.eq(ProfileStatus.PUBLIC)) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); @@ -88,7 +88,7 @@ public Optional findPublicReviewDetailByReviewId(Long reviewId, ) .from(review) .join(review.member, member) - .where(review.id.eq(reviewId), review.status.eq(ReviewStatus.PUBLIC), review.member.profileStatus.eq(ProfileStatus.PUBLIC)) + .where(review.id.eq(reviewId), review.status.eq(ReviewStatus.PUBLIC), review.member.status.eq(ProfileStatus.PUBLIC)) .fetchOne()); } diff --git a/src/main/java/cotato/bookitlist/review/service/ReviewService.java b/src/main/java/cotato/bookitlist/review/service/ReviewService.java index a841ebb..5ac6238 100644 --- a/src/main/java/cotato/bookitlist/review/service/ReviewService.java +++ b/src/main/java/cotato/bookitlist/review/service/ReviewService.java @@ -3,6 +3,7 @@ import cotato.bookitlist.book.domain.Book; import cotato.bookitlist.book.repository.BookRepository; import cotato.bookitlist.book.service.BookService; +import cotato.bookitlist.common.domain.RecommendType; import cotato.bookitlist.member.domain.Member; import cotato.bookitlist.member.repository.MemberRepository; import cotato.bookitlist.review.domain.entity.Review; @@ -14,7 +15,11 @@ import cotato.bookitlist.review.repository.ReviewRepository; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,6 +28,9 @@ @RequiredArgsConstructor public class ReviewService { + @Value("${recommend.count.review}") + private int recommendCount; + private final BookService bookService; private final MemberRepository memberRepository; private final BookRepository bookRepository; @@ -83,4 +91,25 @@ public void deleteReview(Long reviewId, Long memberId) { review.deleteReview(); } + + public ReviewListResponse getRecommendReviews(RecommendType type, int start, Long memberId) { + return switch (type) { + case LIKE -> getMostLikeReviews(start, memberId); + case NEW -> getNewReviews(start, memberId); + }; + } + + public ReviewListResponse getMostLikeReviews(int start, Long memberId) { + Pageable pageable = PageRequest.of(start, recommendCount, Sort.by("likeCount").descending()); + Page reviewPage = reviewRepository.findPublicReviewAll(pageable); + + return ReviewListResponse.from(reviewPage, memberId); + } + + public ReviewListResponse getNewReviews(int start, Long memberId) { + Pageable pageable = PageRequest.of(start, recommendCount, Sort.by("createdAt").descending()); + Page reviewPage = reviewRepository.findPublicReviewAll(pageable); + + return ReviewListResponse.from(reviewPage, memberId); + } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 193f474..11326d5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -53,3 +53,10 @@ cloud: profile: default: url: ${PROFILE_DEFAULT_URL} + +recommend: + count: + book: 3 + post: 3 + review: 4 + member: 3 diff --git a/src/test/java/cotato/bookitlist/book/controller/BookControllerTest.java b/src/test/java/cotato/bookitlist/book/controller/BookControllerTest.java index 3b26ec3..d3d1603 100644 --- a/src/test/java/cotato/bookitlist/book/controller/BookControllerTest.java +++ b/src/test/java/cotato/bookitlist/book/controller/BookControllerTest.java @@ -268,4 +268,16 @@ void givenNonExistedId_whenGettingBook_thenReturnErrorResponse() throws Exceptio .andExpect(jsonPath("$.message").value("책을 찾을 수 없습니다.")) ; } + + @Test + @DisplayName("[DB] 추천할 책을 랜덤으로 3개 고른다.") + void givenRecommendCount_whenGettingRecommendBook_thenReturnRecommendBook() throws Exception { + //given + + //when & then + mockMvc.perform(get("/books/recommend"). + contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.bookList.length()").value(3)); + } } diff --git a/src/test/java/cotato/bookitlist/member/controller/MemberControllerTest.java b/src/test/java/cotato/bookitlist/member/controller/MemberControllerTest.java index 9f3ea78..1c985b3 100644 --- a/src/test/java/cotato/bookitlist/member/controller/MemberControllerTest.java +++ b/src/test/java/cotato/bookitlist/member/controller/MemberControllerTest.java @@ -92,7 +92,7 @@ void givenPrivateMemberIdWithLogin_whenGettingMember_thenReturnErrorResponse() t @Test @WithCustomMockUser @DisplayName("profileStatus를 변경하는 요청을 한다.") - void givenLoginMember_whenChangingProfileStatus_thenChangeProfileStatus() throws Exception{ + void givenLoginMember_whenChangingProfileStatus_thenChangeProfileStatus() throws Exception { //given //when & then @@ -131,4 +131,19 @@ void givenNonLogin_whenChangingName_thenReturnErrorResponse() throws Exception { ; } + @Test + @DisplayName("최신 순으로 멤버를 3명 반환한다.") + void givenMembers_whenGettingNewMembers_thenReturnNewMembers() throws Exception { + //given + + //when & then + mockMvc.perform(get("/members/recommend/new") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.memberList.length()").value(3)) + .andExpect(jsonPath("$.memberList[0].memberId").value(1)) + .andExpect(jsonPath("$.memberList[1].memberId").value(2)) + ; + } + } diff --git a/src/test/java/cotato/bookitlist/member/service/MemberServiceTest.java b/src/test/java/cotato/bookitlist/member/service/MemberServiceTest.java index 6c6adad..212bb41 100644 --- a/src/test/java/cotato/bookitlist/member/service/MemberServiceTest.java +++ b/src/test/java/cotato/bookitlist/member/service/MemberServiceTest.java @@ -38,7 +38,7 @@ void givenMemberId_whenChangingProfileStatus_thenChangeProfileStatus() throws Ex //then then(memberRepository).should().getReferenceById(memberId); - assertThat(member.getProfileStatus()).isEqualTo(ProfileStatus.PRIVATE); + assertThat(member.getStatus()).isEqualTo(ProfileStatus.PRIVATE); } @Test diff --git a/src/test/java/cotato/bookitlist/post/controller/PostControllerTest.java b/src/test/java/cotato/bookitlist/post/controller/PostControllerTest.java index a61446a..950af3b 100644 --- a/src/test/java/cotato/bookitlist/post/controller/PostControllerTest.java +++ b/src/test/java/cotato/bookitlist/post/controller/PostControllerTest.java @@ -517,5 +517,42 @@ void givenNonLogin_whenGettingMyPost_thenReturnErrorResponse() throws Exception ; } + @Test + @DisplayName("좋아요가 많은 순으로 게시글을 3개 반환한다.") + void givenPageStartAndRecommendType_whenGettingMostLikePosts_thenReturnMostLikePosts() throws Exception { + //given + int start = 0; + String type = "LIKE"; + + //when & then + mockMvc.perform(get("/posts/recommend") + .param("start", String.valueOf(start)) + .param("type", type) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.postList.length()").value(3)) + .andExpect(jsonPath("$.postList[0].likeCount").value(3)) + .andExpect(jsonPath("$.postList[1].likeCount").value(2)) + ; + } + + @Test + @DisplayName("최신 순으로 게시글을 3개 반환한다") + void givenPageStartAndRecommendType_whenGettingNewPosts_thenReturnNewPosts() throws Exception { + //given + int start = 0; + String type = "NEW"; + + //when & then + mockMvc.perform(get("/posts/recommend") + .param("start", String.valueOf(start)) + .param("type", type) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.postList.length()").value(3)) + .andExpect(jsonPath("$.postList[0].postId").value(1)) + .andExpect(jsonPath("$.postList[1].postId").value(2)) + ; + } } diff --git a/src/test/java/cotato/bookitlist/review/controller/ReviewControllerTest.java b/src/test/java/cotato/bookitlist/review/controller/ReviewControllerTest.java index 5651af0..17bb957 100644 --- a/src/test/java/cotato/bookitlist/review/controller/ReviewControllerTest.java +++ b/src/test/java/cotato/bookitlist/review/controller/ReviewControllerTest.java @@ -343,4 +343,42 @@ void givenPrivateReviewId_whenDeletingReview_thenDeleteReview() throws Exception .andExpect(status().isNoContent()) ; } + + @Test + @DisplayName("좋아요가 많은 순으로 한줄요약을 4개 반환한다.") + void givenPageStartAndRecommendType_whenGettingMostLikeReviews_thenReturnMostLikeReviews() throws Exception { + //given + int start = 0; + String type = "LIKE"; + + //when & then + mockMvc.perform(get("/reviews/recommend") + .param("start", String.valueOf(start)) + .param("type", type) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.reviewList.length()").value(4)) + .andExpect(jsonPath("$.reviewList[0].likeCount").value(3)) + .andExpect(jsonPath("$.reviewList[1].likeCount").value(2)) + ; + } + + @Test + @DisplayName("최신 순으로 한줄요약을 4개 반환한다.") + void givenPageStartAndRecommendType_whenGettingNewReviews_thenReturnNewReviews() throws Exception { + //given + int start = 0; + String type = "NEW"; + + //when & then + mockMvc.perform(get("/reviews/recommend") + .param("start", String.valueOf(start)) + .param("type", type) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.reviewList.length()").value(4)) + .andExpect(jsonPath("$.reviewList[0].reviewId").value(1)) + .andExpect(jsonPath("$.reviewList[1].reviewId").value(2)) + ; + } } diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 03cafeb..4e03427 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -18,43 +18,47 @@ VALUES ('Aladdin and His Lamp (반양장) - and the Other Stories', 'Harriette T 'http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=209468603&partner=openAPI&start=api', '9791187824824', 14400, 'https://image.aladin.co.kr/product/20946/86/cover500sum/k822636271_1.jpg', 0, false); -INSERT INTO member (email, name, oauth2Id, auth_provider, profile_status, profile_link, deleted, created_at, +INSERT INTO member (email, name, oauth2Id, auth_provider, status, profile_link, deleted, created_at, modified_at) -VALUES ('test@gmail.com', 'test', 'test', 'KAKAO', 'PUBLIC', 'profile', false, CURRENT_TIMESTAMP, +VALUES ('test@gmail.com', 'test', 'test', 'KAKAO', 'PUBLIC', 'profile', false, TIMESTAMP '2024-02-15 00:00:00', CURRENT_TIMESTAMP), - ('test2@gmail.com', 'test2', 'test2', 'KAKAO', 'PUBLIC', 'profile', false, CURRENT_TIMESTAMP, + ('test2@gmail.com', 'test2', 'test2', 'KAKAO', 'PUBLIC', 'profile', false, TIMESTAMP '2024-02-14 00:00:00', CURRENT_TIMESTAMP), - ('test2@gmail.com', 'test2', 'test3', 'KAKAO', 'PUBLIC', 'profile', false, CURRENT_TIMESTAMP, + ('test2@gmail.com', 'test2', 'test3', 'KAKAO', 'PUBLIC', 'profile', false, TIMESTAMP '2024-02-13 00:00:00', CURRENT_TIMESTAMP), - ('test2@gmail.com', 'test2', 'test4', 'KAKAO', 'PRIVATE', 'profile', false, CURRENT_TIMESTAMP, + ('test2@gmail.com', 'test2', 'test4', 'KAKAO', 'PRIVATE', 'profile', false, TIMESTAMP '2024-02-12 00:00:00', + CURRENT_TIMESTAMP), + ('test2@gmail.com', 'test2', 'test4', 'KAKAO', 'PRIVATE', 'profile', false, TIMESTAMP '2024-02-11 00:00:00', CURRENT_TIMESTAMP); -INSERT INTO post (member_id, book_id, title, content, status, template, like_count, view_count, deleted) -values (1, 1, 'posttitle', 'postcontent', 'PUBLIC', 'NON', 0, 0, false), - (2, 1, 'postTitle1', 'Content', 'PUBLIC', 'NON', 2, 0, false), - (2, 1, 'postTitle2', '제목', 'PUBLIC', 'NON', 0, 0, false), - (2, 1, 'postTitle3', 'post', 'PUBLIC', 'NON', 0, 0, false), - (2, 1, 'privateTitle', 'privateContent', 'PRIVATE', 'NON', 0, 0, false), - (2, 2, 'posTitle', 'ptent', 'PUBLIC', 'NON', 1, 0, false), - (2, 2, 'positle', 'postent', 'PUBLIC', 'NON', 1, 0, false), - (2, 2, 'privateTitle', 'privateContent', 'PRIVATE', 'NON', 0, 0, false), +INSERT INTO post (member_id, book_id, title, content, status, template, like_count, view_count, deleted, created_at, modified_at) +VALUES (1, 1, 'posttitle', 'postcontent', 'PUBLIC', 'NON', 2, 0, false, TIMESTAMP '2024-02-15 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'postTitle1', 'Content', 'PUBLIC', 'NON', 3, 0, false, TIMESTAMP '2024-02-14 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'postTitle2', '제목', 'PUBLIC', 'NON', 0, 0, false, TIMESTAMP '2024-02-13 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'postTitle3', 'post', 'PUBLIC', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'privateTitle', 'privateContent', 'PRIVATE', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (2, 2, 'posTitle', 'ptent', 'PUBLIC', 'NON', 1, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (2, 2, 'positle', 'postent', 'PUBLIC', 'NON', 1, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (2, 2, 'privateTitle', 'privateContent', 'PRIVATE', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), (2, 2, 'privateTitle', 'first<============================>second<============================>third<============================>fourth', - 'PUBLIC', 'TEMPLATE', 0, 0, false), - (1, 2, 'posttitle', 'postcontent', 'PUBLIC', 'NON', 0, 0, false), - (1, 2, 'posttitle', 'postcontent', 'PRIVATE', 'NON', 0, 0, false), - (4, 2, 'posttitle', 'postcontent', 'PUBLIC', 'NON', 0, 0, false), - (4, 2, 'posttitle', 'postcontent', 'PRIVATE', 'NON', 0, 0, false); + 'PUBLIC', 'TEMPLATE', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (1, 2, 'posttitle', 'postcontent', 'PUBLIC', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (1, 2, 'posttitle', 'postcontent', 'PRIVATE', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (4, 2, 'posttitle', 'postcontent', 'PUBLIC', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (4, 2, 'posttitle', 'postcontent', 'PRIVATE', 'NON', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP); + + +INSERT INTO review (member_id, book_id, content, status, like_count, view_count, deleted, created_at, modified_at) +VALUES (1, 1, 'reviewContent', 'PUBLIC', 2, 0, false, TIMESTAMP '2024-02-15 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'reviewContent1', 'PUBLIC', 3, 0, false, TIMESTAMP '2024-02-14 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'rContent', 'PUBLIC', 0, 0, false, TIMESTAMP '2024-02-13 00:00:00', CURRENT_TIMESTAMP), + (2, 1, 'rContent2', 'PUBLIC', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (2, 2, 'rContent3', 'PUBLIC', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (2, 2, 'reContent2', 'PUBLIC', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (1, 2, 'rContent', 'PUBLIC', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP), + (1, 2, 'rContent', 'PRIVATE', 0, 0, false, TIMESTAMP '2024-02-12 00:00:00', CURRENT_TIMESTAMP); -INSERT INTO review (member_id, book_id, content, status, like_count, view_count, deleted) -VALUES (1, 1, 'reviewContent', 'PUBLIC', 2, 0, false), - (2, 1, 'reviewContent1', 'PUBLIC', 2, 0, false), - (2, 1, 'rContent', 'PUBLIC', 0, 0, false), - (2, 1, 'rContent2', 'PUBLIC', 0, 0, false), - (2, 2, 'rContent3', 'PUBLIC', 0, 0, false), - (2, 2, 'reContent2', 'PUBLIC', 0, 0, false), - (1, 2, 'rContent', 'PUBLIC', 0, 0, false), - (1, 2, 'rContent', 'PRIVATE', 0, 0, false); INSERT INTO post_like (member_id, post_id) VALUES (2, 1), @@ -62,13 +66,15 @@ VALUES (2, 1), (1, 2), (2, 2), (1, 6), - (1, 7); + (1, 7), + (5, 2); INSERT INTO review_like (member_id, review_id) VALUES (2, 1), (3, 1), (1, 2), - (2, 2); + (2, 2), + (5, 2); INSERT INTO book_like (book_id, member_id) VALUES (1, 1),