diff --git a/src/main/java/com/moongeul/backend/api/book/entity/Book.java b/src/main/java/com/moongeul/backend/api/book/entity/Book.java index ecda1cc..7dfc10b 100644 --- a/src/main/java/com/moongeul/backend/api/book/entity/Book.java +++ b/src/main/java/com/moongeul/backend/api/book/entity/Book.java @@ -39,5 +39,9 @@ public void update(String title, String author, String bookImage, String publish this.description = description; this.pubdate = pubdate; } -} + public void updateRatingStats(Double ratingAverage, Integer ratingCount) { + this.ratingAverage = ratingAverage; + this.ratingCount = ratingCount; + } +} diff --git a/src/main/java/com/moongeul/backend/api/bookshelf/controller/DoneReadBookshelfController.java b/src/main/java/com/moongeul/backend/api/bookshelf/controller/DoneReadBookshelfController.java index 39ec957..937c6c1 100644 --- a/src/main/java/com/moongeul/backend/api/bookshelf/controller/DoneReadBookshelfController.java +++ b/src/main/java/com/moongeul/backend/api/bookshelf/controller/DoneReadBookshelfController.java @@ -47,7 +47,7 @@ public ResponseEntity> getDoneReadBook @RequestParam(required = false, defaultValue = "10") @Min(value = 1, message = "한 페이지당 개수는 1 이상이어야 합니다.") Integer size) { DoneReadBookshelfResponseDTO doneReadBookshelfResponseDTO = - doneReadBookshelfService.getDoneReadBooks(userDetails.getUsername(), userId, page, size); + doneReadBookshelfService.getDoneReadBooks(resolveUsername(userDetails), userId, page, size); return ApiResponse.success(SuccessStatus.GET_DONE_READ_BOOKS_SUCCESS, doneReadBookshelfResponseDTO); } @@ -71,7 +71,7 @@ public ResponseEntity> getDoneReadB @RequestParam(defaultValue = "10") @Min(value = 1, message = "한 페이지당 개수는 1 이상이어야 합니다.") Integer size) { DoneReadBookPostListResponseDTO doneReadBookPostListResponseDTO = - doneReadBookshelfService.getDoneReadBookPosts(userDetails.getUsername(), userId, isbn, page, size); + doneReadBookshelfService.getDoneReadBookPosts(resolveUsername(userDetails), userId, isbn, page, size); return ApiResponse.success(SuccessStatus.GET_DONE_READ_BOOK_POSTS_SUCCESS, doneReadBookPostListResponseDTO); } @@ -94,7 +94,7 @@ public ResponseEntity> getDoneReadCalen @RequestParam @Min(value = 1, message = "월은 1 이상이어야 합니다.") @Max(value = 12, message = "월은 12 이하여야 합니다.") Integer month) { DoneReadCalendarResponseDTO doneReadCalendar = doneReadBookshelfService.getDoneReadCalendar( - userDetails.getUsername(), userId, year, month); + resolveUsername(userDetails), userId, year, month); return ApiResponse.success(SuccessStatus.GET_DONE_READ_CALENDAR_SUCCESS, doneReadCalendar); } @@ -113,7 +113,7 @@ public ResponseEntity> getDoneRead @RequestParam(required = false) Long userId) { DoneReadRatingSummaryResponseDTO doneReadRatingSummaryResponseDTO = - doneReadBookshelfService.getDoneReadRatingSummary(userDetails.getUsername(), userId); + doneReadBookshelfService.getDoneReadRatingSummary(resolveUsername(userDetails), userId); return ApiResponse.success(SuccessStatus.GET_DONE_READ_RATING_SUMMARY_SUCCESS, doneReadRatingSummaryResponseDTO); } @@ -144,7 +144,11 @@ public ResponseEntity> getDoneReadRatin @RequestParam(defaultValue = "10") @Min(value = 1, message = "한 페이지당 개수는 1 이상이어야 합니다.") Integer size) { CategoryPostListResponseDTO categoryPostListResponseDTO = - doneReadBookshelfService.getDoneReadRatingDetail(userDetails.getUsername(), userId, range, sortBy, page, size); + doneReadBookshelfService.getDoneReadRatingDetail(resolveUsername(userDetails), userId, range, sortBy, page, size); return ApiResponse.success(SuccessStatus.GET_DONE_READ_RATING_DETAIL_SUCCESS, categoryPostListResponseDTO); } + + private String resolveUsername(UserDetails userDetails) { + return (userDetails != null) ? userDetails.getUsername() : "anonymousUser"; + } } diff --git a/src/main/java/com/moongeul/backend/api/bookshelf/service/DoneReadBookshelfService.java b/src/main/java/com/moongeul/backend/api/bookshelf/service/DoneReadBookshelfService.java index 8796085..9cafb8e 100644 --- a/src/main/java/com/moongeul/backend/api/bookshelf/service/DoneReadBookshelfService.java +++ b/src/main/java/com/moongeul/backend/api/bookshelf/service/DoneReadBookshelfService.java @@ -203,7 +203,7 @@ public DoneReadRatingSummaryResponseDTO getDoneReadRatingSummary(String email, L // 읽은 책별 기록 리스트 조회 @Transactional(readOnly = true) public DoneReadBookPostListResponseDTO getDoneReadBookPosts(String email, Long userId, String isbn, Integer page, Integer size) { - Member currentMember = getCurrentMember(email); + Member currentMember = getCurrentMemberOrNull(email); Member targetMember = getTargetMember(currentMember, userId); Book book = bookRepository.findByIsbn(isbn) .orElseThrow(() -> new NotFoundException(ErrorStatus.BOOK_NOTFOUND_EXCEPTION.getMessage())); @@ -230,7 +230,7 @@ public DoneReadBookPostListResponseDTO getDoneReadBookPosts(String email, Long u // 읽은 책 별점 구간 상세 조회 @Transactional(readOnly = true) public CategoryPostListResponseDTO getDoneReadRatingDetail(String email, Long userId, String range, String sortBy, Integer page, Integer size) { - Member currentMember = getCurrentMember(email); + Member currentMember = getCurrentMemberOrNull(email); Member targetMember = getTargetMember(currentMember, userId); int rangeIndex = findRangeIndex(range); @@ -380,27 +380,36 @@ private int findRangeIndex(String range) { } private Member getTargetMember(String email, Long userId) { - Member currentMember = getCurrentMember(email); + Member currentMember = getCurrentMemberOrNull(email); return getTargetMember(currentMember, userId); } - private Member getCurrentMember(String email) { + private Member getCurrentMemberOrNull(String email) { + if (email == null || "anonymousUser".equals(email)) { + return null; + } + return memberRepository.findByEmail(email) .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); } private Member getTargetMember(Member currentMember, Long userId) { - Member targetMember = (userId == null) - ? currentMember - : memberRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + Member targetMember; + if (userId != null) { + targetMember = memberRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + } else if (currentMember != null) { + targetMember = currentMember; + } else { + throw new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()); + } validatePrivacyAccess(currentMember, targetMember); return targetMember; } private void validatePrivacyAccess(Member currentMember, Member targetMember) { - if (currentMember.getId().equals(targetMember.getId())) { + if (currentMember != null && currentMember.getId().equals(targetMember.getId())) { return; } @@ -415,6 +424,10 @@ private void validatePrivacyAccess(Member currentMember, Member targetMember) { } if (privacyLevel == PrivacyLevel.FOLLOWER_ONLY) { + if (currentMember == null) { + throw new ForbiddenException(ErrorStatus.PRIVACY_FORBIDDEN_EXCEPTION.getMessage()); + } + FollowStatus status = followRepository.findByFollowingIdAndFollowerId(targetMember.getId(), currentMember.getId()) .map(Follow::getFollowStatus) .orElse(FollowStatus.NONE); diff --git a/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java b/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java index f3cc028..a680e0b 100644 --- a/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java +++ b/src/main/java/com/moongeul/backend/api/member/controller/MemberController.java @@ -106,7 +106,7 @@ public ResponseEntity> loginWithKakao(@Valid @Requ public ResponseEntity> getUserInfo( @AuthenticationPrincipal UserDetails userDetails, @RequestParam(required = false) Long userId){ - UserInfoDTO response = memberService.getUserInfo(userDetails.getUsername(), userId); + UserInfoDTO response = memberService.getUserInfo(resolveUsername(userDetails), userId); return ApiResponse.success(SuccessStatus.GET_USERINFO_SUCCESS, response); } @@ -187,7 +187,7 @@ public ResponseEntity> withdraw(@AuthenticationPrincipal UserD public ResponseEntity> getPostStats( @AuthenticationPrincipal UserDetails userDetails, @RequestParam(required = false) Long userId){ - PostStatsResponseDTO response = memberService.getPostStats(userDetails.getUsername(), userId); + PostStatsResponseDTO response = memberService.getPostStats(resolveUsername(userDetails), userId); return ApiResponse.success(SuccessStatus.GET_POST_STATS_SUCCESS, response); } @@ -217,7 +217,7 @@ public ResponseEntity> getCategoryPostL @RequestParam(defaultValue = "10") Integer size) { CategoryPostListResponseDTO categoryPostListResponseDTO = - memberService.getCategoryPostList(userDetails.getUsername(), userId, categoryId, sortBy, page, size); + memberService.getCategoryPostList(resolveUsername(userDetails), userId, categoryId, sortBy, page, size); return ApiResponse.success(SuccessStatus.GET_CATEGORY_POST_LIST_SUCCESS, categoryPostListResponseDTO); } @@ -245,7 +245,7 @@ public ResponseEntity> getLikedPostList @RequestParam(defaultValue = "10") Integer size) { CategoryPostListResponseDTO likedPostListResponseDTO = - memberService.getLikedPostList(userDetails.getUsername(), userId, sortBy, page, size); + memberService.getLikedPostList(resolveUsername(userDetails), userId, sortBy, page, size); return ApiResponse.success(SuccessStatus.GET_LIKED_POST_LIST_SUCCESS, likedPostListResponseDTO); } @@ -265,10 +265,14 @@ public ResponseEntity> getMyQuestionList( @RequestParam(defaultValue = "10") Integer size) { QuestionListResponseDTO questionListResponseDTO = - questionService.getMyQuestionList(page, size, userDetails.getUsername(), userId); + questionService.getMyQuestionList(page, size, resolveUsername(userDetails), userId); return ApiResponse.success(SuccessStatus.GET_MY_QUESTION_LIST_SUCCESS, questionListResponseDTO); } + private String resolveUsername(UserDetails userDetails) { + return (userDetails != null) ? userDetails.getUsername() : "anonymousUser"; + } + /* * * 팔로잉/팔로우 API diff --git a/src/main/java/com/moongeul/backend/api/member/service/MemberService.java b/src/main/java/com/moongeul/backend/api/member/service/MemberService.java index 987ebf8..22adffc 100644 --- a/src/main/java/com/moongeul/backend/api/member/service/MemberService.java +++ b/src/main/java/com/moongeul/backend/api/member/service/MemberService.java @@ -169,13 +169,10 @@ private Member signUp(String socialId, String email, String name, String picture @Transactional(readOnly = true) public UserInfoDTO getUserInfo(String email, Long userId){ - Member currentMember = getMemberByEmail(email); + Member currentMember = getCurrentMemberOrNull(email); // userId가 null이면 본인 정보 조회, 있으면 타 사용자 조회 - Member member = (userId == null) - ? currentMember - : memberRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + Member member = getTargetMember(currentMember, userId); // 팔로워 수 계산 (나를 팔로우하는 사람들 중 승인된 경우) int followerCount = followRepository.findByFollowers(member.getId()).size(); @@ -185,7 +182,7 @@ public UserInfoDTO getUserInfo(String email, Long userId){ // 내가 해당 사용자를 팔로우했는지 여부 FollowStatus myFollowStatus = FollowStatus.NONE; - if (!currentMember.getId().equals(member.getId())) { + if (currentMember != null && !currentMember.getId().equals(member.getId())) { myFollowStatus = followRepository.findByFollowingIdAndFollowerId(member.getId(), currentMember.getId()) .map(Follow::getFollowStatus) .orElse(FollowStatus.NONE); @@ -285,11 +282,8 @@ public void withdraw(String email, WithdrawalRequestDTO withdrawalRequestDTO) { @Transactional(readOnly = true) public PostStatsResponseDTO getPostStats(String email, Long userId) { - Member currentMember = getMemberByEmail(email); - Member targetMember = (userId == null) - ? currentMember - : memberRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + Member currentMember = getCurrentMemberOrNull(email); + Member targetMember = getTargetMember(currentMember, userId); validatePrivacyAccess(currentMember, targetMember); @@ -436,11 +430,8 @@ public void updateProfileImage(String email, MultipartFile profileImage) { @Transactional(readOnly = true) public CategoryPostListResponseDTO getCategoryPostList(String email, Long userId, Long categoryId, String sortBy, Integer page, Integer size) { - Member currentMember = getMemberByEmail(email); - Member targetMember = (userId == null) - ? currentMember - : memberRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + Member currentMember = getCurrentMemberOrNull(email); + Member targetMember = getTargetMember(currentMember, userId); validatePrivacyAccess(currentMember, targetMember); @@ -507,11 +498,8 @@ public CategoryPostListResponseDTO getCategoryPostList(String email, Long userId @Transactional(readOnly = true) public CategoryPostListResponseDTO getLikedPostList(String email, Long userId, String sortBy, Integer page, Integer size) { - Member currentMember = getMemberByEmail(email); - Member targetMember = (userId == null) - ? currentMember - : memberRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + Member currentMember = getCurrentMemberOrNull(email); + Member targetMember = getTargetMember(currentMember, userId); validatePrivacyAccess(currentMember, targetMember); @@ -627,7 +615,7 @@ private PostDTO.MyLikesStatus convertToMyLikesStatus(Member currentMember, Long } private void validatePrivacyAccess(Member currentMember, Member targetMember) { - if (currentMember.getId().equals(targetMember.getId())) { + if (currentMember != null && currentMember.getId().equals(targetMember.getId())) { return; } @@ -642,6 +630,10 @@ private void validatePrivacyAccess(Member currentMember, Member targetMember) { } if (privacyLevel == PrivacyLevel.FOLLOWER_ONLY) { + if (currentMember == null) { + throw new ForbiddenException(ErrorStatus.PRIVACY_FORBIDDEN_EXCEPTION.getMessage()); + } + FollowStatus status = followRepository.findByFollowingIdAndFollowerId(targetMember.getId(), currentMember.getId()) .map(Follow::getFollowStatus) .orElse(FollowStatus.NONE); @@ -652,6 +644,27 @@ private void validatePrivacyAccess(Member currentMember, Member targetMember) { } } + private Member getCurrentMemberOrNull(String email) { + if (email == null || "anonymousUser".equals(email)) { + return null; + } + + return getMemberByEmail(email); + } + + private Member getTargetMember(Member currentMember, Long userId) { + if (userId != null) { + return memberRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + } + + if (currentMember != null) { + return currentMember; + } + + throw new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()); + } + /* 스토리 보관함 조회 API */ @Transactional(readOnly = true) public MyStoryResponseDTO getMyStories(String email, Integer page, Integer size) { diff --git a/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java b/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java index 910e703..70a5806 100644 --- a/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java +++ b/src/main/java/com/moongeul/backend/api/post/repository/PostRepository.java @@ -142,6 +142,11 @@ List findCalendarPostsByMemberAndReadDateBetweenOrderByReadDateAscCreatedA @Query("SELECT p.rating FROM Post p WHERE p.member = :member AND p.rating IS NOT NULL") List findRatingsByMember(@Param("member") Member member); + @Query("SELECT COALESCE(AVG(p.rating), 0.0) FROM Post p WHERE p.book = :book AND p.rating IS NOT NULL") + Double findAverageRatingByBook(@Param("book") Book book); + + long countByBookAndRatingIsNotNull(Book book); + Page findByMemberAndRatingBetween(Member member, Double startRating, Double endRating, Pageable pageable); @Query("SELECT p FROM Post p WHERE p.member = :member AND p.book.isbn = :isbn ORDER BY p.createdAt DESC") diff --git a/src/main/java/com/moongeul/backend/api/post/service/PostService.java b/src/main/java/com/moongeul/backend/api/post/service/PostService.java index d30d78e..5d27ddd 100644 --- a/src/main/java/com/moongeul/backend/api/post/service/PostService.java +++ b/src/main/java/com/moongeul/backend/api/post/service/PostService.java @@ -103,6 +103,8 @@ public PostIdResponseDTO createPost(PostRequestDTO postRequestDTO, String email) doneReadBookshelfRepository.save(doneReadBookshelf); } + updateBookRatingStats(book); + return PostIdResponseDTO.builder() .postId(savedPost.getId()) .build(); @@ -284,8 +286,8 @@ private PostDTO.MyLikesStatus convertToMyLikesStatus(String email, Long postId){ public PostIdResponseDTO updatePost(Long postId, String email, PostRequestDTO postRequestDTO){ Post post = getPost(postId); + Book targetBook = post.getBook(); Category category = getCategory(postRequestDTO.getCategoryId()); - Book book = getBook(post.getBook().getIsbn()); // 예외처리: 수정하는 사람과 게시글 주인이 같은지 확인 (본인의 게시글인지) if (!post.getMember().getEmail().equals(email)) { @@ -320,9 +322,11 @@ public PostIdResponseDTO updatePost(Long postId, String email, PostRequestDTO po postRequestDTO.getContent(), postRequestDTO.getPostVisibility(), category, - book + targetBook ); + updateBookRatingStats(targetBook); + return PostIdResponseDTO.builder() .postId(post.getId()) .build(); @@ -333,6 +337,7 @@ public PostIdResponseDTO updatePost(Long postId, String email, PostRequestDTO po public void deletePost(Long postId, String email){ Post post = getPost(postId); + Book book = post.getBook(); // 예외처리: 수정하는 사람과 게시글 주인이 같은지 확인 (본인의 게시글인지) if (!post.getMember().getEmail().equals(email)) { @@ -344,6 +349,8 @@ public void deletePost(Long postId, String email){ // 게시글 삭제 postRepository.delete(post); + + updateBookRatingStats(book); } @@ -415,6 +422,15 @@ private void decrementLikeCount(Post post, LikeType likeType) { } } + // 책 평점 수정 메서드 + private void updateBookRatingStats(Book book) { + long ratingCount = postRepository.countByBookAndRatingIsNotNull(book); + Double ratingAverage = postRepository.findAverageRatingByBook(book); + + double finalAverage = (ratingAverage == null) ? 0.0 : Math.round(ratingAverage * 10) / 10.0; + book.updateRatingStats(finalAverage, (int) ratingCount); + } + /* * 추천 */ diff --git a/src/main/java/com/moongeul/backend/api/question/service/QuestionService.java b/src/main/java/com/moongeul/backend/api/question/service/QuestionService.java index c47c69b..ddd2d9b 100644 --- a/src/main/java/com/moongeul/backend/api/question/service/QuestionService.java +++ b/src/main/java/com/moongeul/backend/api/question/service/QuestionService.java @@ -85,13 +85,8 @@ public QuestionListResponseDTO getQuestionList(Integer page, Integer size, Strin // 마이페이지 질문 리스트 조회 public QuestionListResponseDTO getMyQuestionList(Integer page, Integer size, String email, Long userId) { - Member currentMember = memberRepository.findByEmail(email) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); - - Member targetMember = (userId == null) - ? currentMember - : memberRepository.findById(userId) - .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + Member currentMember = getCurrentMemberOrNull(email); + Member targetMember = getTargetMember(currentMember, userId); validatePrivacyAccess(currentMember, targetMember); @@ -113,7 +108,7 @@ public QuestionListResponseDTO getMyQuestionList(Integer page, Integer size, Str } private void validatePrivacyAccess(Member currentMember, Member targetMember) { - if (currentMember.getId().equals(targetMember.getId())) { + if (currentMember != null && currentMember.getId().equals(targetMember.getId())) { return; } @@ -128,6 +123,10 @@ private void validatePrivacyAccess(Member currentMember, Member targetMember) { } if (privacyLevel == PrivacyLevel.FOLLOWER_ONLY) { + if (currentMember == null) { + throw new ForbiddenException(ErrorStatus.PRIVACY_FORBIDDEN_EXCEPTION.getMessage()); + } + FollowStatus status = followRepository.findByFollowingIdAndFollowerId(targetMember.getId(), currentMember.getId()) .map(Follow::getFollowStatus) .orElse(FollowStatus.NONE); @@ -237,4 +236,26 @@ private QuestionDTO convertToQuestionDTO(Question question, String email) { .participantProfileImages(participantProfileImages) .build(); } + + private Member getCurrentMemberOrNull(String email) { + if (email == null || "anonymousUser".equals(email)) { + return null; + } + + return memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + } + + private Member getTargetMember(Member currentMember, Long userId) { + if (userId != null) { + return memberRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage())); + } + + if (currentMember != null) { + return currentMember; + } + + throw new NotFoundException(ErrorStatus.USER_NOTFOUND_EXCEPTION.getMessage()); + } } diff --git a/src/main/java/com/moongeul/backend/common/config/security/SecurityConfig.java b/src/main/java/com/moongeul/backend/common/config/security/SecurityConfig.java index d534cbc..ae6d8c2 100644 --- a/src/main/java/com/moongeul/backend/common/config/security/SecurityConfig.java +++ b/src/main/java/com/moongeul/backend/common/config/security/SecurityConfig.java @@ -39,6 +39,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/v2/reading-taste", "/api/v2/reading-taste/total-count").permitAll() .requestMatchers("/api/v2/book/bestseller").permitAll() .requestMatchers(HttpMethod.GET, "/api/v2/post/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v2/bookshelf/done-read", "/api/v2/bookshelf/done-read/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/v2/member/user-info", "/api/v2/member/post-stats", "/api/v2/member/post-stats/**", "/api/v2/member/liked-posts", "/api/v2/member/question-list").permitAll() .anyRequest().authenticated() );