Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ dependencies {

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// AOP 사용을 위한 스타터 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class Post extends BaseTimeEntity {
private LocalDate readDate; // 읽은날짜
private Double rating; // 평점
private Integer page; // 페이지 수
@Column(columnDefinition = "TEXT")
private String content; // 감상평

@Enumerated(EnumType.STRING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface LikeRepository extends JpaRepository<Likes, Long> {

List<Likes> findByPostIdAndMemberId(Long postId, Long memberId);

List<Likes> findAllByMemberEmailAndPostIdIn(String email, List<Long> postIds);

Optional<Likes> findByPostIdAndMemberIdAndLikeType(Long postId, Long memberId, LikeType likeType);

// 특정 게시글의 모든 공감 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public interface QuoteRepository extends JpaRepository<Quote, Long> {

List<Quote> findByPostId(Long postId);

List<Quote> findAllByPostIdIn(List<Long> postIds);

void deleteAllByPostId(Long postId);
}
145 changes: 88 additions & 57 deletions src/main/java/com/moongeul/backend/api/post/service/PostService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.moongeul.backend.api.post.repository.PostRepository;
import com.moongeul.backend.api.post.repository.QuoteRepository;
import com.moongeul.backend.api.post.util.WritingGuideGenerator;
import com.moongeul.backend.common.annotation.Timer;
import com.moongeul.backend.common.exception.NotFoundException;
import com.moongeul.backend.common.exception.UnauthorizedException;
import com.moongeul.backend.common.response.ErrorStatus;
Expand All @@ -31,10 +32,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
Expand Down Expand Up @@ -116,6 +114,7 @@ public String writingGuide(){
}

/* 기록(게시글) 전체 조회 */
@Timer
@Transactional
public PostAllResponseDTO getPostAll(PostAllRequestDTO postAllRequestDTO, String email){

Expand All @@ -137,16 +136,55 @@ public PostAllResponseDTO getPostAll(PostAllRequestDTO postAllRequestDTO, String
postPage = postRepository.findAllByFollower(email, pageable);
}

List<PostDTO> postDTOList = new ArrayList<>();
if (!postPage.isEmpty()) {
for(Post post : postPage.getContent()){
postDTOList.add(getPostDetail(post.getId(), email));
}
if (postPage.isEmpty()) {
return PostAllResponseDTO.builder().data(new ArrayList<>()).build();
}

// Batch 준비: 현재 페이지의 게시글 ID 리스트 추출
List<Long> postIds = postPage.getContent().stream()
.map(Post::getId)
.toList();

// Batch 조회 & Map 매핑: 내가 누른 공감들 한꺼번에 가져오기
Map<Long, Set<LikeType>> myLikesMap;
if (!isAnonymous) {
List<Likes> allMyLikes = likeRepository.findAllByMemberEmailAndPostIdIn(email, postIds);
myLikesMap = allMyLikes.stream().collect(Collectors.groupingBy(
like -> like.getPost().getId(),
Collectors.mapping(Likes::getLikeType, Collectors.toSet())
));
} else {
myLikesMap = new HashMap<>();
}

// Batch 조회 & Map 매핑: 인상 깊은 구절들 한꺼번에 가져오기
List<Quote> allQuotes = quoteRepository.findAllByPostIdIn(postIds);
Map<Long, List<Quote>> quotesMap = allQuotes.stream()
.collect(Collectors.groupingBy(quote -> quote.getPost().getId()));

// 메모리 매핑: 루프를 돌며 Map에서 데이터를 꺼내 DTO 조립 (getPostDetail 호출 안 함 X)
List<PostDTO> postDTOList = postPage.getContent().stream().map(post -> {

// 해당 게시글의 공감/구절 데이터를 Map에서 즉시 꺼냄 (DB 조회 0번)
Set<LikeType> myTypes = myLikesMap.getOrDefault(post.getId(), Collections.emptySet());
List<Quote> postQuotes = quotesMap.getOrDefault(post.getId(), Collections.emptyList());

// MyLikesStatus 객체 조립
PostDTO.MyLikesStatus myLikesStatus = PostDTO.MyLikesStatus.builder()
.relatableCount(myTypes.contains(LikeType.RELATABLE))
.sameTasteCount(myTypes.contains(LikeType.SAME_TASTE))
.impressiveExpressionCount(myTypes.contains(LikeType.IMPRESSIVE_EXPRESSION))
.wantToReadCount(myTypes.contains(LikeType.WANT_TO_READ))
.helpfulCount(myTypes.contains(LikeType.HELPFUL))
.build();

// 최종 PostDTO 조립 (기존 getPostDetail에 있던 변환 로직만 활용)
return convertToPostDTO(post, myLikesStatus, postQuotes);
}).toList();

return PostAllResponseDTO.builder()
.total(postPage.getTotalElements())
.page(postPage.getNumber() + 1) // 페이지 1부터 시작(임의 지정)
.page(postPage.getNumber() + 1)
.size(postPage.getSize())
.totalPages(postPage.getTotalPages())
.isLast(postPage.isLast())
Expand All @@ -155,68 +193,60 @@ public PostAllResponseDTO getPostAll(PostAllRequestDTO postAllRequestDTO, String
}

/* 기록(게시글) 상세 조회 */
@Transactional
public PostDTO getPostDetail(Long postId, String email){
@Transactional(readOnly = true)
public PostDTO getPostDetail(Long postId, String email) {

Post post = getPost(postId);
Book book = getBook(post.getBook().getIsbn());

// 멤버 정보(필요 정보만) DTO
PostDTO.MemberInfo memberInfo = PostDTO.MemberInfo.builder()
.memberId(post.getMember().getId())
.nickname(post.getMember().getNickname())
.profileImage(post.getMember().getProfileImage())
.readingTasteType(post.getMember().getReadingTasteType())
.build();
List<Quote> quotes = quoteRepository.findByPostId(postId);

// 책 정보(필요 정보만) DTO
PostDTO.BookInfo bookInfo = PostDTO.BookInfo.builder()
.isbn(book.getIsbn())
.bookImage(book.getBookImage())
.title(book.getTitle())
.author(book.getAuthor())
.publisher(book.getPublisher())
.pubdate(book.getPubdate())
.ratingAverage(book.getRatingAverage())
.build();

// 인상깊은구절 조회
List<Quote> quotes = quoteRepository.findByPostId(postId); // 리스트 반환이기에 `orElseThrow()` 사용 x
List<PostDTO.QuoteDTO> quoteDTOList = new ArrayList<>();
for(Quote quote : quotes){
PostDTO.QuoteDTO quoteDTO = PostDTO.QuoteDTO.builder()
.quoteContent(quote.getQuoteContent())
.pageNumber(quote.getPageNumber())
.build();
quoteDTOList.add(quoteDTO);
}

// 공감 개수 DTO
PostDTO.LikesCnt likesCnt = PostDTO.LikesCnt.builder()
.relatableCount(post.getRelatableCount())
.sameTasteCount(post.getSameTasteCount())
.impressiveExpressionCount(post.getImpressiveExpressionCount())
.wantToReadCount(post.getWantToReadCount())
.helpfulCount(post.getHelpfulCount())
.build();

/* 내가 누른 공감 유형 정보 DTO */
boolean isAnonymous = (email == null || "anonymousUser".equals(email));
PostDTO.MyLikesStatus myLikesStatus = isAnonymous
? PostDTO.MyLikesStatus.empty()
: convertToMyLikesStatus(email, postId);

return convertToPostDTO(post, myLikesStatus, quotes);
}

// 메서드: PostDTO 조립 전용 Helper 메서드
private PostDTO convertToPostDTO(Post post, PostDTO.MyLikesStatus myLikesStatus, List<Quote> quotes) {

List<PostDTO.QuoteDTO> quoteDTOList = quotes.stream()
.map(q -> PostDTO.QuoteDTO.builder()
.quoteContent(q.getQuoteContent())
.pageNumber(q.getPageNumber())
.build())
.toList();

return PostDTO.builder()
.postId(postId)
.memberInfo(memberInfo)
.postId(post.getId())
.memberInfo(PostDTO.MemberInfo.builder()
.memberId(post.getMember().getId())
.nickname(post.getMember().getNickname())
.profileImage(post.getMember().getProfileImage())
.readingTasteType(post.getMember().getReadingTasteType())
.build())
.bookInfo(PostDTO.BookInfo.builder()
.isbn(post.getBook().getIsbn())
.bookImage(post.getBook().getBookImage())
.title(post.getBook().getTitle())
.author(post.getBook().getAuthor())
.publisher(post.getBook().getPublisher())
.pubdate(post.getBook().getPubdate())
.ratingAverage(post.getBook().getRatingAverage())
.build())
.created(post.getCreatedAt())
.bookInfo(bookInfo)
.rating(post.getRating())
.content(post.getContent())
.readDate(post.getReadDate())
.quotesCnt(quoteDTOList.size())
.quotes(quoteDTOList)
.likesCnt(likesCnt)
.likesCnt(PostDTO.LikesCnt.builder()
.relatableCount(post.getRelatableCount())
.sameTasteCount(post.getSameTasteCount())
.impressiveExpressionCount(post.getImpressiveExpressionCount())
.wantToReadCount(post.getWantToReadCount())
.helpfulCount(post.getHelpfulCount())
.build())
.myLikesStatus(myLikesStatus)
.build();
}
Expand Down Expand Up @@ -335,6 +365,7 @@ private void saveQuotes(PostRequestDTO postRequestDTO, Post post){
*/

/* 공감 토글 */
@Timer
@Transactional
public void likePost(Long postId, String email, LikeDTO likeDTO){

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.moongeul.backend.api.member.entity.Member;
import com.moongeul.backend.api.member.entity.Role;
import com.moongeul.backend.api.member.repository.MemberRepository;
import com.moongeul.backend.api.setting.dto.NoticeAllRequestDTO;
import com.moongeul.backend.api.setting.dto.NoticeAllResponseDTO;
import com.moongeul.backend.api.setting.dto.NoticeDTO;
import com.moongeul.backend.api.setting.dto.NoticeRequestDTO;
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/moongeul/backend/common/annotation/Timer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.moongeul.backend.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
29 changes: 29 additions & 0 deletions src/main/java/com/moongeul/backend/common/aspect/TimerAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.moongeul.backend.common.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Aspect
@Component
@Slf4j
public class TimerAspect {

@Around("@annotation(com.moongeul.backend.common.annotation.Timer)")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();

try {
stopWatch.start();
return joinPoint.proceed();
} finally {
stopWatch.stop();
log.info("메서드 실행 시간 [{}]: {}ms",
joinPoint.getSignature().getName(),
stopWatch.getTotalTimeMillis());
}
}
}