diff --git a/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleCustomRepository.java b/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleCustomRepository.java new file mode 100644 index 00000000..38d57c14 --- /dev/null +++ b/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleCustomRepository.java @@ -0,0 +1,9 @@ +package com.aliens.db.communityarticle.repository; + +import java.time.Instant; +import java.util.Optional; + +public interface CommunityArticleCustomRepository { + Optional findLatestArticleTimeByMemberId(Long memberId); + +} diff --git a/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepository.java b/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepository.java index 1e389f9f..f2b399f0 100644 --- a/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepository.java +++ b/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepository.java @@ -7,14 +7,20 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.Instant; import java.util.List; +import java.util.Optional; public interface CommunityArticleRepository - extends JpaRepository { + extends JpaRepository, CommunityArticleCustomRepository { Page findAllByCategory(ArticleCategory category, Pageable pageable); List findAllByTitleContainingOrContentContaining(String title, String content); + List findAllByMember(MemberEntity member); + Page findAllByCategoryAndTitleContainingOrContentContaining(ArticleCategory category, String title, String content, Pageable pageable); + Optional findLatestArticleTimeByMemberId(Long memberId); + } \ No newline at end of file diff --git a/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepositoryImpl.java b/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepositoryImpl.java new file mode 100644 index 00000000..ceb2c070 --- /dev/null +++ b/db/src/main/java/com/aliens/db/communityarticle/repository/CommunityArticleRepositoryImpl.java @@ -0,0 +1,27 @@ +package com.aliens.db.communityarticle.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; + +import javax.persistence.EntityManager; +import java.time.Instant; +import java.util.Optional; + +import static com.aliens.db.communityarticle.entity.QCommunityArticleEntity.communityArticleEntity; + +@RequiredArgsConstructor + +public class CommunityArticleRepositoryImpl implements CommunityArticleCustomRepository { + private final EntityManager entityManager; + + @Override + public Optional findLatestArticleTimeByMemberId(Long memberId) { + JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); + + return Optional.ofNullable(queryFactory + .select(communityArticleEntity.createdAt.max()) + .from(communityArticleEntity) + .where(communityArticleEntity.member.id.eq(memberId)) + .fetchOne()); + } +} diff --git a/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleCustomRepository.java b/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleCustomRepository.java new file mode 100644 index 00000000..0fa1b711 --- /dev/null +++ b/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleCustomRepository.java @@ -0,0 +1,17 @@ +package com.aliens.db.marketarticle.repository; + +import com.aliens.db.marketarticle.entity.MarketArticleEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.time.Instant; +import java.util.Optional; + +public interface MarketArticleCustomRepository { + + Page findAllWithFetchJoin(Pageable pageable); + + Page findAllByTitleContainingOrContentContainingByFetchJoin(String title, String content, Pageable pageable); + + Optional findLatestArticleTimeByMemberId(Long memberId); +} diff --git a/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepository.java b/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepository.java index 55631fb4..d800fada 100644 --- a/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepository.java +++ b/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepository.java @@ -6,11 +6,17 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.Instant; import java.util.List; +import java.util.Optional; public interface MarketArticleRepository extends - JpaRepository { + JpaRepository, MarketArticleCustomRepository { List findAllByTitleContainingOrContentContaining(String title, String content); + List findAllByMember(MemberEntity member); + Page findAllByTitleContainingOrContentContaining(String title, String content, Pageable pageable); + + Optional findLatestArticleTimeByMemberId(Long memberId); } \ No newline at end of file diff --git a/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepositoryImpl.java b/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepositoryImpl.java new file mode 100644 index 00000000..40775ae4 --- /dev/null +++ b/db/src/main/java/com/aliens/db/marketarticle/repository/MarketArticleRepositoryImpl.java @@ -0,0 +1,70 @@ +package com.aliens.db.marketarticle.repository; + +import com.aliens.db.marketarticle.entity.MarketArticleEntity; +import com.aliens.db.marketarticlecomment.entity.QMarketArticleCommentEntity; +import com.aliens.db.marketbookmark.entity.QMarketBookmarkEntity; +import com.querydsl.jpa.JPQLQuery; +import com.querydsl.jpa.JPQLQueryFactory; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.support.Querydsl; + +import javax.persistence.EntityManager; +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import static com.aliens.db.marketarticle.entity.QMarketArticleEntity.marketArticleEntity; +import static com.aliens.db.marketarticlecomment.entity.QMarketArticleCommentEntity.*; +import static com.aliens.db.marketbookmark.entity.QMarketBookmarkEntity.*; + +@RequiredArgsConstructor +public class MarketArticleRepositoryImpl implements MarketArticleCustomRepository { + private final EntityManager entityManager; + + @Override + public Page findAllWithFetchJoin(Pageable pageable) { + JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); + + List result = queryFactory + .selectFrom(marketArticleEntity) + .leftJoin(marketArticleEntity.likes, marketBookmarkEntity) + .leftJoin(marketArticleEntity.comments, marketArticleCommentEntity) + .fetchJoin() + .fetch(); + + return new PageImpl<>(result, pageable, result.size()); + } + + @Override + public Page findAllByTitleContainingOrContentContainingByFetchJoin(String title, String content, Pageable pageable) { + JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); + + List result = queryFactory + .selectFrom(marketArticleEntity) + .leftJoin(marketArticleEntity.likes, marketBookmarkEntity) + .leftJoin(marketArticleEntity.comments, marketArticleCommentEntity) + .fetchJoin() + .where( + marketArticleEntity.title.containsIgnoreCase(title) + .or(marketArticleEntity.content.containsIgnoreCase(content)) + ) + .fetch(); + + return new PageImpl<>(result, pageable, result.size()); + } + + @Override + public Optional findLatestArticleTimeByMemberId(Long memberId) { + JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager); + + return Optional.ofNullable(queryFactory + .select(marketArticleEntity.createdAt.max()) + .from(marketArticleEntity) + .where(marketArticleEntity.member.id.eq(memberId)) + .fetchOne()); + } +} diff --git a/membership/src/main/java/com/aliens/friendship/domain/article/community/service/CommunityArticleService.java b/membership/src/main/java/com/aliens/friendship/domain/article/community/service/CommunityArticleService.java index d3a8da1c..8dec215e 100644 --- a/membership/src/main/java/com/aliens/friendship/domain/article/community/service/CommunityArticleService.java +++ b/membership/src/main/java/com/aliens/friendship/domain/article/community/service/CommunityArticleService.java @@ -13,6 +13,7 @@ import com.aliens.friendship.domain.article.community.dto.CreateCommunityArticleRequest; import com.aliens.friendship.domain.article.community.dto.UpdateCommunityArticleRequest; import com.aliens.friendship.domain.article.dto.ArticleDto; +import com.aliens.friendship.domain.article.exception.ArticleCreationNotAllowedException; import com.aliens.friendship.domain.article.service.ArticleImageService; import com.aliens.friendship.global.error.InvalidResourceOwnerException; import com.aliens.friendship.global.error.ResourceNotFoundException; @@ -25,6 +26,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -102,22 +105,33 @@ public Long saveCommunityArticle( CreateCommunityArticleRequest request, UserDetails principal ) throws Exception { - CommunityArticleEntity communityArticle = communityArticleRepository.save( - request.toEntity(getMemberEntity(principal.getUsername())) - ); + MemberEntity member = getMemberEntity(principal.getUsername()); + Optional latestArticleTime = communityArticleRepository.findLatestArticleTimeByMemberId(member.getId()); - List imageUrls = request.getImageUrls(); - if (imageUrls != null && !imageUrls.isEmpty()) { - for (MultipartFile imageUrl : imageUrls) { - CommunityArticleImageEntity communityArticleImage = CommunityArticleImageEntity.of( - articleImageService.uploadProfileImage(imageUrl), - communityArticle - ); - communityArticleImageRepository.save(communityArticleImage); + boolean canCreateArticle = latestArticleTime.map( + time -> Duration.between(time, Instant.now()).toMinutes() >= 5 + ).orElse(true); + + if (canCreateArticle) { + CommunityArticleEntity communityArticle = communityArticleRepository.save( + request.toEntity(getMemberEntity(principal.getUsername())) + ); + + List imageUrls = request.getImageUrls(); + if (imageUrls != null && !imageUrls.isEmpty()) { + for (MultipartFile imageUrl : imageUrls) { + CommunityArticleImageEntity communityArticleImage = CommunityArticleImageEntity.of( + articleImageService.uploadProfileImage(imageUrl), + communityArticle + ); + communityArticleImageRepository.save(communityArticleImage); + } } - } - return communityArticle.getId(); + return communityArticle.getId(); + } else { + throw new ArticleCreationNotAllowedException(); + } } public void updateCommunityArticle( diff --git a/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleCreationNotAllowedException.java b/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleCreationNotAllowedException.java new file mode 100644 index 00000000..800f40ad --- /dev/null +++ b/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleCreationNotAllowedException.java @@ -0,0 +1,20 @@ +package com.aliens.friendship.domain.article.exception; + +import com.aliens.friendship.global.error.ExceptionCode; +import com.aliens.friendship.global.error.ResourceNotFoundException; + +public class ArticleCreationNotAllowedException + extends ResourceNotFoundException { + + private ExceptionCode exceptionCode; + + public ArticleCreationNotAllowedException() { + super(ArticleExceptionCode.ARTICLE_CREATION_NOT_ALLOWED); + this.exceptionCode = ArticleExceptionCode.ARTICLE_CREATION_NOT_ALLOWED; + } + + @Override + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} \ No newline at end of file diff --git a/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionCode.java b/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionCode.java index ee02be4a..4eea110e 100644 --- a/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionCode.java +++ b/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionCode.java @@ -5,6 +5,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.NOT_FOUND; @Getter @@ -13,7 +14,8 @@ public enum ArticleExceptionCode implements ExceptionCode { ARTICLE_NOT_FOUND(NOT_FOUND, "BA-C-001", "존재하지 않는 게시글입니다."), - ARTICLE_COMMENT_NOT_FOUND(NOT_FOUND, "BA-C-001", "존재하지 않는 게시글 댓글입니다."); + ARTICLE_COMMENT_NOT_FOUND(NOT_FOUND, "BA-C-001", "존재하지 않는 게시글 댓글입니다."), + ARTICLE_CREATION_NOT_ALLOWED(BAD_REQUEST, "BA-C-003", "게시글 생성 후 5분 후에 재생성이 가능합니다."); private final HttpStatus httpStatus; private final String code; diff --git a/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionHandler.java b/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionHandler.java new file mode 100644 index 00000000..1306cdc7 --- /dev/null +++ b/membership/src/main/java/com/aliens/friendship/domain/article/exception/ArticleExceptionHandler.java @@ -0,0 +1,28 @@ +package com.aliens.friendship.domain.article.exception; + +import com.aliens.friendship.global.error.ErrorResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ArticleExceptionHandler { + + /** + * ArticleCreationNotAllowedException 핸들링 + * Custom Exception + */ + @ExceptionHandler(ArticleCreationNotAllowedException.class) + protected ResponseEntity handlingArticleCreationNotAllowedException( + ArticleCreationNotAllowedException e + ) { + log.error("[handling ArticleCreationNotAllowedException] {}", e.getExceptionCode().getMessage()); + return new ResponseEntity<>( + ErrorResponse.of(e.getExceptionCode()), + HttpStatus.valueOf(e.getExceptionCode().getHttpStatus().value()) + ); + } +} \ No newline at end of file diff --git a/membership/src/main/java/com/aliens/friendship/domain/article/market/service/MarketArticleService.java b/membership/src/main/java/com/aliens/friendship/domain/article/market/service/MarketArticleService.java index eb29b65a..ae93cc8b 100644 --- a/membership/src/main/java/com/aliens/friendship/domain/article/market/service/MarketArticleService.java +++ b/membership/src/main/java/com/aliens/friendship/domain/article/market/service/MarketArticleService.java @@ -12,6 +12,7 @@ import com.aliens.db.productimage.entity.ProductImageEntity; import com.aliens.db.productimage.repsitory.ProductImageRepository; import com.aliens.friendship.domain.article.dto.ArticleDto; +import com.aliens.friendship.domain.article.exception.ArticleCreationNotAllowedException; import com.aliens.friendship.domain.article.market.dto.CreateMarketArticleRequest; import com.aliens.friendship.domain.article.market.dto.MarketArticleDto; import com.aliens.friendship.domain.article.market.dto.UpdateMarketArticleRequest; @@ -27,6 +28,8 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -83,6 +86,36 @@ public Page searchMarketArticles( )); } + @Transactional(readOnly = true) + public Page searchMarketArticlesWithFetchJoin( + Pageable pageable, + String searchKeyword + ) { + if (searchKeyword == null || searchKeyword.isBlank()) { + return marketArticleRepository.findAllWithFetchJoin(pageable) + .map(article -> + MarketArticleDto.from( + article, + article.getLikes().size(), + article.getComments().size(), + getMarketArticleImages(article) + ) + ); + } + + return marketArticleRepository.findAllByTitleContainingOrContentContainingByFetchJoin( + searchKeyword, + searchKeyword, + pageable + ) + .map(article -> MarketArticleDto.from( + article, + article.getLikes().size(), + article.getComments().size(), + getMarketArticleImages(article) + )); + } + /** * 장터 게시글 상세 조회 */ @@ -109,20 +142,30 @@ public Long saveMarketArticle( CreateMarketArticleRequest request, UserDetails userPrincipal ) throws Exception { + MemberEntity member = getMemberEntity(userPrincipal.getUsername()); + Optional latestArticleTime = marketArticleRepository.findLatestArticleTimeByMemberId(member.getId()); - MarketArticleEntity savedMarketArticle = marketArticleRepository.save(request.toEntity( - getMemberEntity(userPrincipal.getUsername()) - )); + boolean canCreateArticle = latestArticleTime.map( + time -> Duration.between(time, Instant.now()).toMinutes() >= 5 + ).orElse(true); - for (MultipartFile imageUrl : request.getImageUrls()) { - ProductImageEntity productImage = ProductImageEntity.of( - articleImageService.uploadProfileImage(imageUrl), - savedMarketArticle - ); - productImageRepository.save(productImage); - } + if (canCreateArticle) { + MarketArticleEntity savedMarketArticle = marketArticleRepository.save(request.toEntity( + getMemberEntity(userPrincipal.getUsername()) + )); - return savedMarketArticle.getId(); + for (MultipartFile imageUrl : request.getImageUrls()) { + ProductImageEntity productImage = ProductImageEntity.of( + articleImageService.uploadProfileImage(imageUrl), + savedMarketArticle + ); + productImageRepository.save(productImage); + } + + return savedMarketArticle.getId(); + } else { + throw new ArticleCreationNotAllowedException(); + } } /** @@ -182,10 +225,10 @@ public Optional updateArticleLike( MarketArticleEntity marketArticle = getMarketArticleEntity(articleId); MemberEntity member = getMemberEntity(principal.getUsername()); Optional marketBookmark = marketBookmarkRepository.findByMarketArticleAndMemberEntity(marketArticle, member); - if(marketBookmark.isPresent()){ + if (marketBookmark.isPresent()) { deleteBookmark(articleId, principal); return Optional.empty(); - } else{ + } else { return Optional.of(createBookmark(articleId, principal)); } } @@ -224,8 +267,8 @@ private void deleteBookmark( } @Transactional(readOnly = true) - public List getAllBookmarks( MemberEntity loginMemberEntity - ) { + public List getAllBookmarks(MemberEntity loginMemberEntity + ) { List marketArticles = marketBookmarkRepository.findAllByMemberEntity( loginMemberEntity ) diff --git a/membership/src/test/java/com/aliens/friendship/domain/article/market/service/MarketArticleServiceTest.java b/membership/src/test/java/com/aliens/friendship/domain/article/market/service/MarketArticleServiceTest.java new file mode 100644 index 00000000..9fe6cff1 --- /dev/null +++ b/membership/src/test/java/com/aliens/friendship/domain/article/market/service/MarketArticleServiceTest.java @@ -0,0 +1,46 @@ +package com.aliens.friendship.domain.article.market.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import javax.transaction.Transactional; + +@SpringBootTest +class MarketArticleServiceTest { + + @Autowired + private MarketArticleService marketArticleService; + + @Test + @Transactional + public void testSearchMarketArticles() { + String searchKeyword = ""; + Pageable pageable = PageRequest.of(0, 10); // Set the page and size as needed + + long startTime = System.currentTimeMillis(); + marketArticleService.searchMarketArticles(pageable, searchKeyword); + long endTime = System.currentTimeMillis(); + + System.out.println("Search time: " + (endTime - startTime) + "ms"); + + // Perform assertions on the result as needed + } + + @Test + @Transactional + public void testSearchMarketArticlesWithFetchJoin() { + String searchKeyword = ""; + Pageable pageable = PageRequest.of(0, 10); // Set the page and size as needed + + long startTime = System.currentTimeMillis(); + marketArticleService.searchMarketArticlesWithFetchJoin(pageable, searchKeyword); + long endTime = System.currentTimeMillis(); + + System.out.println("Search with fetch join time: " + (endTime - startTime) + "ms"); + + // Perform assertions on the result as needed + } +} \ No newline at end of file