diff --git a/server/build.gradle b/server/build.gradle index 6dae4d74..1a7ec81c 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -51,6 +51,26 @@ dependencies { //Flyway implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' + + //QueryDsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" +} + +def generatedQueryDsl = 'build/generated/querydsl' + +sourceSets { + main.java.srcDirs += [generatedQueryDsl] +} + +tasks.withType(JavaCompile).configureEach { + options.getGeneratedSourceOutputDirectory().set(file(generatedQueryDsl)) +} + +clean.doLast { + file(generatedQueryDsl).deleteDir() } ext { diff --git a/server/src/main/java/com/project/yozmcafe/config/QueryDslConfig.java b/server/src/main/java/com/project/yozmcafe/config/QueryDslConfig.java new file mode 100644 index 00000000..903a96fd --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.project.yozmcafe.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QueryDslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} diff --git a/server/src/main/java/com/project/yozmcafe/controller/CafeController.java b/server/src/main/java/com/project/yozmcafe/controller/CafeController.java index a477f93d..9da7c6e0 100644 --- a/server/src/main/java/com/project/yozmcafe/controller/CafeController.java +++ b/server/src/main/java/com/project/yozmcafe/controller/CafeController.java @@ -2,6 +2,8 @@ import com.project.yozmcafe.controller.dto.cafe.CafeRankResponse; import com.project.yozmcafe.controller.dto.cafe.CafeResponse; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchRequest; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchResponse; import com.project.yozmcafe.service.CafeService; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -17,8 +19,7 @@ @RequestMapping("/cafes") public class CafeController { - private static final int PAGE_SIZE = 5; - private static final int RANKING_PAGE_SIZE = 10; + private static final int CAFE_PAGE_SIZE = 5; private final CafeService cafeService; @@ -28,27 +29,34 @@ public CafeController(final CafeService cafeService) { @GetMapping public ResponseEntity> getCafesForLoggedInMember(@LoginUser final String memberId) { - List cafeResponses = cafeService.getCafesForLoginMember(memberId, PAGE_SIZE); + final List cafeResponses = cafeService.getCafesForLoginMember(memberId, CAFE_PAGE_SIZE); return ResponseEntity.ok(cafeResponses); } @GetMapping("/guest") - public ResponseEntity> getCafesForUnLoggedInMember(@PageableDefault(size = PAGE_SIZE) final Pageable pageable) { - List cafeResponses = cafeService.getCafesForUnLoginMember(pageable); + public ResponseEntity> getCafesForUnLoggedInMember( + @PageableDefault(size = CAFE_PAGE_SIZE) final Pageable pageable) { + final List cafeResponses = cafeService.getCafesForUnLoginMember(pageable); return ResponseEntity.ok(cafeResponses); } @GetMapping("/ranks") - public ResponseEntity> getCafesOrderByLikeCount(@PageableDefault(size = RANKING_PAGE_SIZE) final Pageable pageable) { + public ResponseEntity> getCafesOrderByLikeCount(@PageableDefault final Pageable pageable) { final List cafeRankResponses = cafeService.getCafesOrderByLikeCount(pageable); return ResponseEntity.ok(cafeRankResponses); } @GetMapping("/{cafeId}") public ResponseEntity getCafeById(@PathVariable("cafeId") final long cafeId) { - CafeResponse cafeResponse = cafeService.getCafeById(cafeId); + final CafeResponse cafeResponse = cafeService.getCafeByIdOrThrow(cafeId); return ResponseEntity.ok(cafeResponse); } + + @GetMapping("/search") + public ResponseEntity> getCafeBySearch(final CafeSearchRequest cafeSearchRequest) { + final List cafeSearchResponses = cafeService.getCafesBySearch(cafeSearchRequest); + return ResponseEntity.ok(cafeSearchResponses); + } } diff --git a/server/src/main/java/com/project/yozmcafe/controller/LikedCafeController.java b/server/src/main/java/com/project/yozmcafe/controller/LikedCafeController.java index d6594220..bee40229 100644 --- a/server/src/main/java/com/project/yozmcafe/controller/LikedCafeController.java +++ b/server/src/main/java/com/project/yozmcafe/controller/LikedCafeController.java @@ -1,7 +1,7 @@ package com.project.yozmcafe.controller; +import com.project.yozmcafe.controller.dto.cafe.CafeThumbnailResponse; import com.project.yozmcafe.controller.dto.cafe.LikedCafeResponse; -import com.project.yozmcafe.controller.dto.cafe.LikedCafeThumbnailResponse; import com.project.yozmcafe.service.LikedCafeService; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; @@ -26,9 +26,9 @@ public LikedCafeController(final LikedCafeService likedCafeService) { } @GetMapping("/members/{memberId}/liked-cafes") - public ResponseEntity> getLikedCafeThumbnails(@PathVariable("memberId") final String memberId, - @PageableDefault(size = PAGE_SIZE) final Pageable pageable) { - final List likedCafes = likedCafeService.findLikedCafeThumbnailsByMemberId(memberId, pageable); + public ResponseEntity> getLikedCafeThumbnails(@PathVariable("memberId") final String memberId, + @PageableDefault(size = PAGE_SIZE) final Pageable pageable) { + final List likedCafes = likedCafeService.findLikedCafeThumbnailsByMemberId(memberId, pageable); return ResponseEntity.ok(likedCafes); } diff --git a/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeSearchRequest.java b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeSearchRequest.java new file mode 100644 index 00000000..47e25ce8 --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeSearchRequest.java @@ -0,0 +1,4 @@ +package com.project.yozmcafe.controller.dto.cafe; + +public record CafeSearchRequest(String cafeName, String menu, String address) { +} diff --git a/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeSearchResponse.java b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeSearchResponse.java new file mode 100644 index 00000000..714e5d46 --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeSearchResponse.java @@ -0,0 +1,15 @@ +package com.project.yozmcafe.controller.dto.cafe; + +import com.project.yozmcafe.domain.cafe.Cafe; + +public record CafeSearchResponse(Long id, String name, String address, String image, int likeCount) { + public static CafeSearchResponse from(final Cafe cafe) { + return new CafeSearchResponse( + cafe.getId(), + cafe.getName(), + cafe.getAddress(), + cafe.getRepresentativeImage(), + cafe.getLikeCount() + ); + } +} diff --git a/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeThumbnailResponse.java b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeThumbnailResponse.java new file mode 100644 index 00000000..501ce11d --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/CafeThumbnailResponse.java @@ -0,0 +1,10 @@ +package com.project.yozmcafe.controller.dto.cafe; + +import com.project.yozmcafe.domain.cafe.Cafe; + +public record CafeThumbnailResponse(Long cafeId, String imageUrl) { + + public static CafeThumbnailResponse from(final Cafe cafe) { + return new CafeThumbnailResponse(cafe.getId(), cafe.getRepresentativeImage()); + } +} diff --git a/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/LikedCafeThumbnailResponse.java b/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/LikedCafeThumbnailResponse.java deleted file mode 100644 index e5a585bf..00000000 --- a/server/src/main/java/com/project/yozmcafe/controller/dto/cafe/LikedCafeThumbnailResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.project.yozmcafe.controller.dto.cafe; - -import com.project.yozmcafe.domain.cafe.Cafe; -import com.project.yozmcafe.domain.cafe.LikedCafe; - -public record LikedCafeThumbnailResponse(Long cafeId, String imageUrl) { - - public static LikedCafeThumbnailResponse from(LikedCafe likedCafe) { - final Cafe cafe = likedCafe.getCafe(); - return new LikedCafeThumbnailResponse(cafe.getId(), cafe.getRepresentativeImage()); - } -} diff --git a/server/src/main/java/com/project/yozmcafe/domain/CafeRankGenerator.java b/server/src/main/java/com/project/yozmcafe/domain/CafeRankGenerator.java deleted file mode 100644 index c15efba8..00000000 --- a/server/src/main/java/com/project/yozmcafe/domain/CafeRankGenerator.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.project.yozmcafe.domain; - -import com.project.yozmcafe.exception.BadRequestException; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; - -import static com.project.yozmcafe.exception.ErrorCode.RANK_OUT_OF_BOUNDS; - -@Component -public class CafeRankGenerator { - - public static final int MAX_RANK = 30; - - public int makeRank(final int index, final Pageable pageable) { - validatePage(pageable); - return (pageable.getPageSize() * pageable.getPageNumber()) + index + 1; - } - - public void validatePage(final Pageable pageable) { - final int maxPage = MAX_RANK / pageable.getPageSize(); - if (maxPage <= pageable.getPageNumber()) { - throw new BadRequestException(RANK_OUT_OF_BOUNDS); - } - } -} diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/Cafe.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/Cafe.java index 9ee84839..91cf2a96 100644 --- a/server/src/main/java/com/project/yozmcafe/domain/cafe/Cafe.java +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/Cafe.java @@ -1,7 +1,12 @@ package com.project.yozmcafe.domain.cafe; import com.project.yozmcafe.exception.BadRequestException; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.util.Objects; @@ -71,7 +76,7 @@ private void validate(final String name, final String address, final Images imag } } - public Cafe(String name, String address, Images images, Detail detail) { + public Cafe(final String name, final String address, final Images images, final Detail detail) { this(null, name, address, images, detail, 0); } diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeCustomRepository.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeCustomRepository.java new file mode 100644 index 00000000..b7a0f962 --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeCustomRepository.java @@ -0,0 +1,10 @@ +package com.project.yozmcafe.domain.cafe; + +import java.util.List; + +public interface CafeCustomRepository { + + List findAllBy(final String cafeName, final String menu, final String address); + + List findAllBy(final String cafeName, final String address); +} diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeCustomRepositoryImpl.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeCustomRepositoryImpl.java new file mode 100644 index 00000000..512583dc --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeCustomRepositoryImpl.java @@ -0,0 +1,52 @@ +package com.project.yozmcafe.domain.cafe; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.StringPath; +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.project.yozmcafe.domain.cafe.QCafe.cafe; +import static com.project.yozmcafe.domain.menu.QMenu.menu; +import static com.querydsl.core.types.dsl.Expressions.numberTemplate; +import static io.micrometer.common.util.StringUtils.isBlank; + +@Repository +public class CafeCustomRepositoryImpl extends QuerydslRepositorySupport implements CafeCustomRepository { + + private static final double MATCH_THRESHOLD = 0.0; + + public CafeCustomRepositoryImpl() { + super(Cafe.class); + } + + public List findAllBy(final String cafeNameWord, final String menuWord, final String addressWord) { + return from(cafe) + .innerJoin(menu).on(menu.cafe.eq(cafe)) + .where( + contains(cafe.name, cafeNameWord), + contains(menu.name, menuWord), + contains(cafe.address, addressWord)) + .fetch(); + } + + public List findAllBy(final String cafeNameWord, final String addressWord) { + return from(cafe) + .where( + contains(cafe.name, cafeNameWord), + contains(cafe.address, addressWord)) + .fetch(); + } + + private BooleanExpression contains(final StringPath target, final String searchWord) { + if (isBlank(searchWord)) { + return null; + } + + final String formattedSearchWord = "\"" + searchWord + "\""; + return numberTemplate(Double.class, "function('match_against', {0}, {1})", + target, formattedSearchWord) + .gt(MATCH_THRESHOLD); + } +} diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeRepository.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeRepository.java index 50d1993b..ca8feada 100644 --- a/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeRepository.java +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/CafeRepository.java @@ -9,30 +9,30 @@ import java.util.List; import java.util.Optional; -public interface CafeRepository extends JpaRepository { +public interface CafeRepository extends JpaRepository, CafeCustomRepository { Slice findSliceBy(Pageable pageable); @Override @Query(""" - select c - from Cafe c - left join fetch c.images.urls - where c.id = :cafeId + SELECT c + FROM Cafe c + JOIN FETCH c.images.urls + WHERE c.id = :cafeId """) Optional findById(@Param("cafeId") Long cafeId); @Override @Query(""" - select c - from Cafe c - left join fetch c.images.urls + SELECT c + FROM Cafe c + JOIN FETCH c.images.urls """) List findAll(); @Query(""" - SELECT c.id - FROM Cafe c + SELECT c.id + FROM Cafe c ORDER BY c.likeCount DESC """) List findCafeIdsOrderByLikeCount(Pageable pageable); diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/CustomFunctionContributor.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/CustomFunctionContributor.java new file mode 100644 index 00000000..d9b92936 --- /dev/null +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/CustomFunctionContributor.java @@ -0,0 +1,19 @@ +package com.project.yozmcafe.domain.cafe; + +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.FunctionContributor; + +import static org.hibernate.type.StandardBasicTypes.DOUBLE; + +public class CustomFunctionContributor implements FunctionContributor { + + private static final String FUNCTION_NAME = "match_against"; + private static final String FUNCTION_PATTERN = "match (?1) against (?2 in boolean mode)"; + + @Override + public void contributeFunctions(final FunctionContributions functionContributions) { + functionContributions.getFunctionRegistry() + .registerPattern(FUNCTION_NAME, FUNCTION_PATTERN, + functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve(DOUBLE)); + } +} diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/Detail.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/Detail.java index 319c298e..37c1c9a3 100644 --- a/server/src/main/java/com/project/yozmcafe/domain/cafe/Detail.java +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/Detail.java @@ -6,8 +6,8 @@ import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; -import jakarta.persistence.FetchType; +import java.util.ArrayList; import java.util.List; import static com.project.yozmcafe.exception.ErrorCode.DUPLICATED_CAFE_AVAILABLE_TIMES; @@ -21,8 +21,8 @@ public class Detail { public static final int MAP_URL_MAX_LENGTH = 512; public static final int PHONE_MAX_LENGTH = 20; - @ElementCollection(fetch = FetchType.LAZY) - private List availableTimes; + @ElementCollection + private List availableTimes = new ArrayList<>(); @Column(nullable = false) private String mapUrl; @Column(columnDefinition = "text") diff --git a/server/src/main/java/com/project/yozmcafe/domain/cafe/Images.java b/server/src/main/java/com/project/yozmcafe/domain/cafe/Images.java index afc629f3..c617d1c0 100644 --- a/server/src/main/java/com/project/yozmcafe/domain/cafe/Images.java +++ b/server/src/main/java/com/project/yozmcafe/domain/cafe/Images.java @@ -1,14 +1,15 @@ package com.project.yozmcafe.domain.cafe; -import java.util.List; - +import com.project.yozmcafe.exception.BadRequestException; import jakarta.persistence.CollectionTable; import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; +import org.apache.logging.log4j.util.Strings; import java.util.ArrayList; import java.util.List; +import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_CAFE_IMAGE; @Embeddable public class Images { @@ -17,12 +18,16 @@ public class Images { @ElementCollection @CollectionTable(name = "image") - private List urls; + private List urls = new ArrayList<>(); protected Images() { } - public Images(List urls) { + public Images(final List urls) { + if (urls.isEmpty()) { + throw new BadRequestException(NOT_EXISTED_CAFE_IMAGE); + } + this.urls = urls; } @@ -31,6 +36,10 @@ public List getUrls() { } public String getRepresentativeImage() { + if (urls.isEmpty()) { + return Strings.EMPTY; + } + return urls.get(REPRESENTATIVE_INDEX); } } diff --git a/server/src/main/java/com/project/yozmcafe/domain/member/Member.java b/server/src/main/java/com/project/yozmcafe/domain/member/Member.java index dbfb840d..2505eea2 100644 --- a/server/src/main/java/com/project/yozmcafe/domain/member/Member.java +++ b/server/src/main/java/com/project/yozmcafe/domain/member/Member.java @@ -11,13 +11,13 @@ import jakarta.persistence.OrderBy; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_LIKED_CAFE; import static jakarta.persistence.CascadeType.MERGE; import static jakarta.persistence.CascadeType.PERSIST; import static java.lang.Math.min; +import static java.util.Collections.emptyList; @Entity public class Member { @@ -100,14 +100,17 @@ public List getNextUnViewedCafes(final int size) { return result; } - public List getLikedCafesSection(final int startIndex, final int endIndex) { + public List getLikedCafes(final int startIndex, final int amount) { if (startIndex >= likedCafes.size()) { - return Collections.emptyList(); + return emptyList(); } - final List reverseLikedCafes = new ArrayList<>(likedCafes); + final List cafes = likedCafes.stream() + .map(LikedCafe::getCafe) + .toList(); - return reverseLikedCafes.subList(startIndex, min(endIndex, likedCafes.size())); + final int endIndex = startIndex + amount; + return cafes.subList(startIndex, min(endIndex, cafes.size())); } public String getId() { diff --git a/server/src/main/java/com/project/yozmcafe/domain/member/MemberRepository.java b/server/src/main/java/com/project/yozmcafe/domain/member/MemberRepository.java index 4a086e64..60442b18 100644 --- a/server/src/main/java/com/project/yozmcafe/domain/member/MemberRepository.java +++ b/server/src/main/java/com/project/yozmcafe/domain/member/MemberRepository.java @@ -5,28 +5,17 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; import java.util.Optional; @Repository public interface MemberRepository extends JpaRepository { - @Override @Query(""" - select m - from Member m - left join fetch m.unViewedCafes uvc - left join fetch uvc.cafe - where m.id = :id + SELECT m + FROM Member m + JOIN FETCH m.unViewedCafes uvc + JOIN FETCH uvc.cafe + WHERE m.id = :id """) - Optional findById(@Param("id") String id); - - @Override - @Query(""" - select m - from Member m - left join fetch m.unViewedCafes uvc - left join fetch uvc.cafe - """) - List findAll(); + Optional findWithUnViewedCafesById(@Param("id") String id); } diff --git a/server/src/main/java/com/project/yozmcafe/exception/ErrorCode.java b/server/src/main/java/com/project/yozmcafe/exception/ErrorCode.java index 5aca72ba..0d173a92 100644 --- a/server/src/main/java/com/project/yozmcafe/exception/ErrorCode.java +++ b/server/src/main/java/com/project/yozmcafe/exception/ErrorCode.java @@ -1,14 +1,11 @@ package com.project.yozmcafe.exception; -import static java.lang.String.format; - import com.project.yozmcafe.domain.cafe.Cafe; import com.project.yozmcafe.domain.cafe.Detail; - -import static com.project.yozmcafe.domain.CafeRankGenerator.MAX_RANK; - import com.project.yozmcafe.domain.resizedimage.ImageResizer; +import static java.lang.String.format; + public enum ErrorCode { TOKEN_NOT_EXIST("T1", "토큰이 존재하지 않습니다."), TOKEN_IS_EXPIRED("T2", "만료된 토큰입니다."), @@ -35,8 +32,6 @@ public enum ErrorCode { NOT_EXISTED_OAUTH_PROVIDER("O2", "잘못된 Provider Name 입니다."), NOT_EXISTED_OAUTH_CLIENT("O3", "일치하는 OAuthClient가 존재하지 않습니다."), - RANK_OUT_OF_BOUNDS("R1", format("좋아요 개수 랭킹 범위를 벗어나는 요청입니다. 좋아요 랭킹은 %d위까지 가능합니다. 요청의 페이지 확인이 필요합니다.", MAX_RANK)), - NOT_IMAGE("I1", "이미지 형식만 받을 수 있습니다."), INVALID_IMAGE_SIZE("I2", format("이미지는 최대 %d Byte까지만 가능합니다.", ImageResizer.MAX_IMAGE_SIZE)), diff --git a/server/src/main/java/com/project/yozmcafe/service/CafeService.java b/server/src/main/java/com/project/yozmcafe/service/CafeService.java index 61efd700..45d745f8 100644 --- a/server/src/main/java/com/project/yozmcafe/service/CafeService.java +++ b/server/src/main/java/com/project/yozmcafe/service/CafeService.java @@ -2,35 +2,38 @@ import com.project.yozmcafe.controller.dto.cafe.CafeRankResponse; import com.project.yozmcafe.controller.dto.cafe.CafeResponse; -import com.project.yozmcafe.domain.CafeRankGenerator; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchRequest; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchResponse; import com.project.yozmcafe.domain.cafe.Cafe; import com.project.yozmcafe.domain.cafe.CafeRepository; import com.project.yozmcafe.domain.cafe.UnViewedCafe; import com.project.yozmcafe.domain.member.Member; +import com.project.yozmcafe.domain.member.MemberRepository; import com.project.yozmcafe.exception.BadRequestException; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_CAFE; +import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_MEMBER; +import static io.micrometer.common.util.StringUtils.isBlank; @Service @Transactional(readOnly = true) public class CafeService { private final CafeRepository cafeRepository; - private final MemberService memberService; + private final MemberRepository memberRepository; private final UnViewedCafeService unViewedCafeService; - private final CafeRankGenerator cafeRankGenerator; - public CafeService(final CafeRepository cafeRepository, final MemberService memberService, - final UnViewedCafeService unViewedCafeService, final CafeRankGenerator cafeRankGenerator) { + public CafeService(final CafeRepository cafeRepository, final MemberRepository memberRepository, + final UnViewedCafeService unViewedCafeService) { this.cafeRepository = cafeRepository; - this.memberService = memberService; + this.memberRepository = memberRepository; this.unViewedCafeService = unViewedCafeService; - this.cafeRankGenerator = cafeRankGenerator; } public List getCafesForUnLoginMember(final Pageable pageable) { @@ -42,19 +45,22 @@ public List getCafesForUnLoginMember(final Pageable pageable) { } public List getCafesOrderByLikeCount(final Pageable pageable) { - cafeRankGenerator.validatePage(pageable); - final List ids = cafeRepository.findCafeIdsOrderByLikeCount(pageable); - final List foundCafes = cafeRepository.findCafesByIdsOrderByLikeCount(ids); + final List cafes = cafeRepository.findCafesByIdsOrderByLikeCount(ids); - return foundCafes.stream() - .map(cafe -> CafeRankResponse.of(cafeRankGenerator.makeRank(foundCafes.indexOf(cafe), pageable), cafe)) - .toList(); + final List response = new ArrayList<>(); + + int rank = (int) pageable.getOffset(); + for (final Cafe cafe : cafes) { + response.add(CafeRankResponse.of(++rank, cafe)); + } + + return response; } @Transactional public List getCafesForLoginMember(final String memberId, final int size) { - final Member member = memberService.findMemberByIdOrElseThrow(memberId); + final Member member = getMemberByIdOrThrow(memberId); final List cafes = member.getNextUnViewedCafes(size); unViewedCafeService.refillWhenUnViewedCafesSizeUnderTwenty(member); @@ -64,10 +70,30 @@ public List getCafesForLoginMember(final String memberId, final in .toList(); } - public CafeResponse getCafeById(final long cafeId) { + private Member getMemberByIdOrThrow(final String memberId) { + return memberRepository.findWithUnViewedCafesById(memberId) + .orElseThrow(() -> new BadRequestException(NOT_EXISTED_MEMBER)); + } + + public CafeResponse getCafeByIdOrThrow(final long cafeId) { final Cafe foundCafe = cafeRepository.findById(cafeId) .orElseThrow(() -> new BadRequestException(NOT_EXISTED_CAFE)); return CafeResponse.fromUnLoggedInUser(foundCafe); } + + public List getCafesBySearch(final CafeSearchRequest searchRequest) { + final List cafes = searchWith(searchRequest); + + return cafes.stream() + .map(CafeSearchResponse::from) + .toList(); + } + + private List searchWith(final CafeSearchRequest cafeSearchRequest) { + if (isBlank(cafeSearchRequest.menu())) { + return cafeRepository.findAllBy(cafeSearchRequest.cafeName(), cafeSearchRequest.address()); + } + return cafeRepository.findAllBy(cafeSearchRequest.cafeName(), cafeSearchRequest.menu(), cafeSearchRequest.address()); + } } diff --git a/server/src/main/java/com/project/yozmcafe/service/LikedCafeService.java b/server/src/main/java/com/project/yozmcafe/service/LikedCafeService.java index 132db1f2..eaa6fed4 100644 --- a/server/src/main/java/com/project/yozmcafe/service/LikedCafeService.java +++ b/server/src/main/java/com/project/yozmcafe/service/LikedCafeService.java @@ -1,7 +1,7 @@ package com.project.yozmcafe.service; +import com.project.yozmcafe.controller.dto.cafe.CafeThumbnailResponse; import com.project.yozmcafe.controller.dto.cafe.LikedCafeResponse; -import com.project.yozmcafe.controller.dto.cafe.LikedCafeThumbnailResponse; import com.project.yozmcafe.domain.cafe.Cafe; import com.project.yozmcafe.domain.cafe.CafeRepository; import com.project.yozmcafe.domain.cafe.LikedCafe; @@ -26,23 +26,16 @@ public LikedCafeService(final CafeRepository cafeRepository, final MemberService this.memberService = memberService; } - public List findLikedCafeThumbnailsByMemberId(final String memberId, final Pageable pageable) { + public List findLikedCafeThumbnailsByMemberId(final String memberId, final Pageable pageable) { final Member member = memberService.findMemberByIdOrElseThrow(memberId); - final List likedCafes = getLikedCafes(pageable, member); + final List likedCafes = member.getLikedCafes((int) pageable.getOffset(), pageable.getPageSize()); return likedCafes.stream() - .map(LikedCafeThumbnailResponse::from) + .map(CafeThumbnailResponse::from) .toList(); } - private List getLikedCafes(final Pageable pageable, final Member member) { - final int startIndex = pageable.getPageNumber() * pageable.getPageSize(); - final int endIndex = startIndex + pageable.getPageSize(); - - return member.getLikedCafesSection(startIndex, endIndex); - } - public List findLikedCafesByMemberId(final String memberId) { final Member member = memberService.findMemberByIdOrElseThrow(memberId); diff --git a/server/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor b/server/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor new file mode 100644 index 00000000..a8dcca7a --- /dev/null +++ b/server/src/main/resources/META-INF/services/org.hibernate.boot.model.FunctionContributor @@ -0,0 +1 @@ +com.project.yozmcafe.domain.cafe.CustomFunctionContributor \ No newline at end of file diff --git a/server/src/main/resources/application-test.properties b/server/src/main/resources/application-test.properties index beb366c1..ea37a068 100644 --- a/server/src/main/resources/application-test.properties +++ b/server/src/main/resources/application-test.properties @@ -22,3 +22,4 @@ spring.flyway.enabled=true spring.flyway.baseline-version=20230901153300 spring.flyway.baseline-on-migrate=true spring.flyway.out-of-order=true +spring.jpa.properties.hibernate.default_batch_fetch_size=1000 diff --git a/server/src/main/resources/db/migration/V20230925125500__full_text_idx.sql b/server/src/main/resources/db/migration/V20230925125500__full_text_idx.sql new file mode 100644 index 00000000..2bfd26c9 --- /dev/null +++ b/server/src/main/resources/db/migration/V20230925125500__full_text_idx.sql @@ -0,0 +1,5 @@ +ALTER TABLE cafe ADD FULLTEXT INDEX cafe_name_idx (name) WITH PARSER NGRAM; + +ALTER TABLE cafe ADD FULLTEXT INDEX address_idx (address) WITH PARSER NGRAM; + +ALTER TABLE menu ADD FULLTEXT INDEX menu_name_idx (name) WITH PARSER NGRAM; \ No newline at end of file diff --git a/server/src/test/java/com/project/yozmcafe/controller/CafeControllerTest.java b/server/src/test/java/com/project/yozmcafe/controller/CafeControllerTest.java index e6d8a998..09ba3a7d 100644 --- a/server/src/test/java/com/project/yozmcafe/controller/CafeControllerTest.java +++ b/server/src/test/java/com/project/yozmcafe/controller/CafeControllerTest.java @@ -2,10 +2,12 @@ import com.project.yozmcafe.controller.dto.cafe.CafeRankResponse; import com.project.yozmcafe.controller.dto.cafe.CafeResponse; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchResponse; import com.project.yozmcafe.domain.cafe.Cafe; import com.project.yozmcafe.domain.cafe.CafeRepository; import com.project.yozmcafe.domain.member.Member; import com.project.yozmcafe.domain.member.MemberRepository; +import com.project.yozmcafe.domain.menu.MenuRepository; import com.project.yozmcafe.fixture.Fixture; import com.project.yozmcafe.service.auth.JwtTokenProvider; import io.restassured.response.Response; @@ -43,6 +45,8 @@ class CafeControllerTest extends BaseControllerTest { private MemberRepository memberRepository; @Autowired private CafeRepository cafeRepository; + @Autowired + private MenuRepository menuRepository; @MockBean private JwtTokenProvider jwtTokenProvider; private Cafe cafe1, cafe2, cafe3, cafe4, cafe5; @@ -115,7 +119,7 @@ void updateLikes() { assertAll( () -> assertThat(likeResponse.getStatusCode()).isEqualTo(200), () -> assertThat(cafeResponses).hasSize(4), - () -> assertThat(cafeResponse.likeCount()).isEqualTo(0), + () -> assertThat(cafeResponse.likeCount()).isZero(), () -> assertThat(cafeResponse.isLiked()).isFalse() ); } @@ -321,16 +325,43 @@ void getCafesOrderByLikeCountWhenNotExist() { } @Test - @DisplayName("/cafes/rank?page=? 요청을 보낼 때, 순위 범위를 초과하는 요청이면 statusCode 400을 응답한다") - void getCafesOrderByLikeCountWhenRankOutBoundFail() { + @DisplayName("사용자가 검색 요청을 보내면, 검색어와 기준에 맞는 카페를 응답한다.") + void getCafesBySearch() { + //given + menuRepository.save(Fixture.getMenu(cafe1, 1, "요즘커피")); + //when final Response response = given(spec).log().all() - .filter(document(CAFE_API + "좋아요 개수 순위에 따라 카페정보 조회 - 범위 초과 예외", getPageRequestParam())) + .filter(document(CAFE_API + "카페 검색", + getSearchRequestParam(), + getCafeSearchResponseFields())) .when() - .get("/cafes/ranks?page=4"); + .get("/cafes/search?cafeName=n1&menu=요즘커피&address=address"); //then - assertThat(response.statusCode()).isEqualTo(400); + final List cafeSearchResponses = getCafeSearchResponses(response); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(200), + () -> assertThat(cafeSearchResponses).extracting("id", "name") + .containsOnly(tuple(cafe1.getId(), "n1")) + ); + } + + @Test + @DisplayName("사용자가 검색 요청을 보내면, 검색어와 기준에 맞는 카페를 응답한다. - 메뉴 검색하지 않을 경우") + void getCafesBySearchWhenNotSearchMenu() { + //given, when + final Response response = given().log().all() + .when() + .get("/cafes/search?cafeName=n2&address=addr"); + + //then + final List cafeSearchResponses = getCafeSearchResponses(response); + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(200), + () -> assertThat(cafeSearchResponses).extracting("id", "name") + .containsOnly(tuple(cafe2.getId(), "n2")) + ); } private Member saveMemberAndUnViewedCafes() { @@ -401,6 +432,11 @@ private List getCafeResponses(final Response response) { .extract().jsonPath().getList(".", CafeResponse.class); } + private List getCafeSearchResponses(final Response response) { + return response.then().log().all() + .extract().jsonPath().getList(".", CafeSearchResponse.class); + } + private List getCafeRankResponses(final Response response) { return response.then().log().all() .extract().jsonPath().getList(".", CafeRankResponse.class); @@ -409,4 +445,22 @@ private List getCafeRankResponses(final Response response) { private QueryParametersSnippet getPageRequestParam() { return queryParameters(parameterWithName("page").description("페이지 번호")); } + + private QueryParametersSnippet getSearchRequestParam() { + return queryParameters( + parameterWithName("cafeName").description("카페명 검색어"), + parameterWithName("menu").description("카페의 메뉴 검색어"), + parameterWithName("address").description("카페의 주소 검색어") + ); + } + + private ResponseFieldsSnippet getCafeSearchResponseFields() { + return responseFields( + fieldWithPath("[].id").description("카페 아이디"), + fieldWithPath("[].name").description("카페 이름"), + fieldWithPath("[].address").description("카페 주소"), + fieldWithPath("[].image").description("카페 대표 이미지 url"), + fieldWithPath("[].likeCount").description("카페의 좋아요 갯수") + ); + } } diff --git a/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeCustomRepositoryImplTest.java b/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeCustomRepositoryImplTest.java new file mode 100644 index 00000000..b0436886 --- /dev/null +++ b/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeCustomRepositoryImplTest.java @@ -0,0 +1,183 @@ +package com.project.yozmcafe.domain.cafe; + +import com.project.yozmcafe.BaseTest; +import com.project.yozmcafe.domain.menu.MenuRepository; +import com.project.yozmcafe.fixture.Fixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +class CafeCustomRepositoryImplTest extends BaseTest { + + @Autowired + private CafeCustomRepositoryImpl cafeCustomRepositoryImpl; + @Autowired + private MenuRepository menuRepository; + @Autowired + private CafeRepository cafeRepository; + + private Cafe cafe1, cafe2, cafe3, cafe4; + + @BeforeEach + void setUp() { + cafe1 = cafeRepository.save(Fixture.getCafe("카페1", "도치 주소1", 1)); + cafe2 = cafeRepository.save(Fixture.getCafe("카페2", "카페 주소2", 2)); + cafe3 = cafeRepository.save(Fixture.getCafe("도치 카페3", "카페 주소3", 3)); + cafe4 = cafeRepository.save(Fixture.getCafe("도치 카페4", "도치 카페 주소4", 4)); + } + + @Test + @DisplayName("카페 이름으로 카페를 조회한다") + void findByCafeName() { + // given + final String cafeName = "도치 카페"; + final String address = null; + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, address); + + // then + assertAll( + () -> assertThat(cafes).extracting(Cafe::getName).containsOnly("도치 카페3", "도치 카페4"), + () -> assertThat(cafes).hasSize(2) + ); + } + + @Test + @DisplayName("메뉴 이름으로 카페를 조회한다") + void findByMenuName() { + // given + final String cafeName = null; + final String menu = "도치 음료"; + final String address = null; + + menuRepository.save(Fixture.getMenu(cafe1, 1, "도치 음료1")); + menuRepository.save(Fixture.getMenu(cafe1, 2, "도치 음료2")); + menuRepository.save(Fixture.getMenu(cafe1, 3, "아무 음료")); + menuRepository.save(Fixture.getMenu(cafe2, 2, "도치 음료")); + menuRepository.save(Fixture.getMenu(cafe3, 1, "도치")); + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, menu, address); + + // then + assertAll( + () -> assertThat(cafes).extracting(Cafe::getName).containsOnly(cafe1.getName(), cafe2.getName()), + () -> assertThat(cafes).hasSize(2) + ); + } + + @Test + @DisplayName("주소로 카페를 조회한다") + void findByAddress() { + // given + final String cafeName = null; + final String address = "카페 주소"; + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, address); + + // then + assertAll( + () -> assertThat(cafes).extracting(Cafe::getAddress).containsOnly("카페 주소2", "카페 주소3", "도치 카페 주소4"), + () -> assertThat(cafes).extracting(Cafe::getName).containsOnly(cafe2.getName(), cafe3.getName(), cafe4.getName()), + () -> assertThat(cafes).hasSize(3) + ); + } + + @Test + @DisplayName("카페 이름,메뉴로 카페를 조회한다") + void findByNameAndMenu() { + // given + final String cafeName = "카페1"; + final String menu = "음료"; + final String address = null; + + menuRepository.save(Fixture.getMenu(cafe1, 1, "음료1")); + menuRepository.save(Fixture.getMenu(cafe2, 1, "음료2")); + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, menu, address); + + // then + assertThat(cafes).extracting(Cafe::getName).containsOnly(cafe1.getName()); + } + + @Test + @DisplayName("카페 이름,주소로 카페를 조회한다") + void findByNameAndAddress() { + // given + final String cafeName = "카페"; + final String address = "주소3"; + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, address); + + // then + assertThat(cafes).extracting(Cafe::getName).containsOnly(cafe3.getName()); + } + + @Test + @DisplayName("카페 메뉴,주소로 카페를 조회한다") + void findByMenuAndAddress() { + // given + final String cafeName = null; + final String menu = "음료"; + final String address = "카페 주소"; + + menuRepository.save(Fixture.getMenu(cafe2, 1, "음료2")); + menuRepository.save(Fixture.getMenu(cafe3, 1, "음료3")); + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, menu, address); + + // then + assertThat(cafes).extracting(Cafe::getName).containsOnly(cafe2.getName(), cafe3.getName()); + } + + @Test + @DisplayName("카페 이름,메뉴,주소로 카페를 조회한다") + void findByAllFilters() { + // given + final String cafeName = "카페"; + final String menu = "도치 음료"; + final String address = "주소"; + + menuRepository.save(Fixture.getMenu(cafe1, 1, "도치 음료")); + menuRepository.save(Fixture.getMenu(cafe2, 1, "도치 음료")); + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, menu, address); + + // then + assertAll( + () -> assertThat(cafes).extracting(Cafe::getName).containsOnly(cafe1.getName(), cafe2.getName()), + () -> assertThat(cafes).hasSize(2) + ); + } + + @Test + @DisplayName("아무 조건을 걸지 않고 카페를 조회한다") + void findByNoFilters() { + // given + final String cafeName = null; + final String address = null; + + menuRepository.save(Fixture.getMenu(cafe2, 1, "음료2")); + menuRepository.save(Fixture.getMenu(cafe3, 1, "음료3")); + + // when + final List cafes = cafeCustomRepositoryImpl.findAllBy(cafeName, address); + + // then + assertThat(cafes).extracting(Cafe::getName) + .containsOnly(cafe1.getName(), cafe2.getName(), cafe3.getName(), cafe4.getName()); + } +} + diff --git a/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeTest.java b/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeTest.java index b7f787bb..52443c5b 100644 --- a/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeTest.java +++ b/server/src/test/java/com/project/yozmcafe/domain/cafe/CafeTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.params.provider.NullAndEmptySource; import java.time.LocalTime; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -66,9 +67,9 @@ void invalidAddress2() { } @Test - @DisplayName("카페의 이미지 정보가 없으면 예외가 발생한다") + @DisplayName("이미지 정보가 하나도 없으면 예외가 발생한다") void invalidImages() { - assertThatThrownBy(() -> new Cafe("연어카페", "광안리", null, detail())) + assertThatThrownBy(() -> new Images(Collections.emptyList())) .isInstanceOf(BadRequestException.class) .hasMessage(NOT_EXISTED_CAFE_IMAGE.getMessage()); } @@ -117,7 +118,7 @@ void subtractLikeCount2() { cafe.subtractLikeCount(); //then - assertThat(cafe.getLikeCount()).isEqualTo(0); + assertThat(cafe.getLikeCount()).isZero(); } @Nested diff --git a/server/src/test/java/com/project/yozmcafe/domain/member/MemberTest.java b/server/src/test/java/com/project/yozmcafe/domain/member/MemberTest.java index 233ab547..11525d9b 100644 --- a/server/src/test/java/com/project/yozmcafe/domain/member/MemberTest.java +++ b/server/src/test/java/com/project/yozmcafe/domain/member/MemberTest.java @@ -1,7 +1,6 @@ package com.project.yozmcafe.domain.member; import com.project.yozmcafe.domain.cafe.Cafe; -import com.project.yozmcafe.domain.cafe.LikedCafe; import com.project.yozmcafe.fixture.Fixture; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -108,7 +107,7 @@ void updateLikedCafesBy() { //then assertAll( - () -> assertThat(member.getLikedCafes()).hasSize(0), + () -> assertThat(member.getLikedCafes()).isEmpty(), () -> assertThat(member.isLike(cafe)).isFalse(), () -> assertThat(cafe.getLikeCount()).isEqualTo(10)); } @@ -195,7 +194,7 @@ public static Stream provideMemberAndIsLiked() { @Test @DisplayName("좋아요한 카페 목록을 페이지 처리한다") - void getLikedCafesSection() { + void getLikedCafes() { //given final Member member = new Member("1234", "오션", "오션사진"); final Cafe cafe1 = Fixture.getCafe(1L, "카페1", "주소1", 3); @@ -208,9 +207,39 @@ void getLikedCafesSection() { member.updateLikedCafesBy(cafe4, true); //when - final List likedCafes = member.getLikedCafesSection(0, 2); + final List likedCafes = member.getLikedCafes(0, 2); //then - assertThat(likedCafes).map(LikedCafe::getCafe).containsExactly(cafe1, cafe2); + assertThat(likedCafes).containsExactly(cafe1, cafe2); + } + + @Test + @DisplayName("좋아요한 카페 목록을 조회할 때 없는 페이지를 요청하면 빈 리스트를 반환한다") + void getLikedCafes_empty() { + //given + final Member member = new Member("1234", "연어", "딱새우회_먹고싶다"); + + //when + final List likedCafes = member.getLikedCafes(0, 5); + + //then + assertThat(likedCafes).isEmpty(); + } + + @Test + @DisplayName("좋아요한 카페 목록을 조회할 때 요청한 사이즈보다 적으면 있는 만큼만 반환한다") + void getLikedCafes_amount() { + //given + final Member member = new Member("1234", "연어", "딱새우회_먹고싶다"); + final Cafe cafe1 = Fixture.getCafe(1L, "카페1", "주소1", 3); + final Cafe cafe2 = Fixture.getCafe(2L, "카페2", "주소2", 3); + member.updateLikedCafesBy(cafe1, true); + member.updateLikedCafesBy(cafe2, true); + + //when + final List likedCafes = member.getLikedCafes(0, 5); + + //then + assertThat(likedCafes).hasSize(2); } } diff --git a/server/src/test/java/com/project/yozmcafe/fixture/Fixture.java b/server/src/test/java/com/project/yozmcafe/fixture/Fixture.java index a9489291..31244eba 100644 --- a/server/src/test/java/com/project/yozmcafe/fixture/Fixture.java +++ b/server/src/test/java/com/project/yozmcafe/fixture/Fixture.java @@ -5,6 +5,7 @@ import com.project.yozmcafe.domain.cafe.Images; import com.project.yozmcafe.domain.cafe.available.AvailableTime; import com.project.yozmcafe.domain.cafe.available.Days; +import com.project.yozmcafe.domain.menu.Menu; import java.time.LocalTime; import java.util.List; @@ -33,4 +34,9 @@ public static Cafe getCafe(String name, String address, int likeCount) { public static Cafe getCafe(Long id, String name, String address, int likeCount) { return new Cafe(id, name, address, getImages(), getDetail(), likeCount); } + + public static Menu getMenu(final Cafe cafe, final int priority, final String name) { + return new Menu(null, cafe, priority, name, "image", "description", + "1000원", false); + } } diff --git a/server/src/test/java/com/project/yozmcafe/service/CafeRankGeneratorTest.java b/server/src/test/java/com/project/yozmcafe/service/CafeRankGeneratorTest.java deleted file mode 100644 index 040a6483..00000000 --- a/server/src/test/java/com/project/yozmcafe/service/CafeRankGeneratorTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.project.yozmcafe.service; - -import com.project.yozmcafe.domain.CafeRankGenerator; -import com.project.yozmcafe.domain.cafe.Cafe; -import com.project.yozmcafe.exception.BadRequestException; -import com.project.yozmcafe.exception.ErrorCode; -import com.project.yozmcafe.fixture.Fixture; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class CafeRankGeneratorTest { - - private final CafeRankGenerator cafeRankGenerator = new CafeRankGenerator(); - - @Test - @DisplayName("카페의 순위를 계산한다.") - void getRank() { - //given - final Cafe cafe6 = Fixture.getCafe("cafe6", "address6", 2); - final List cafes = List.of(Fixture.getCafe("cafe1", "address1", 7), - Fixture.getCafe("cafe2", "address2", 6), - Fixture.getCafe("cafe3", "address3", 5), - Fixture.getCafe("cafe4", "address4", 4), - Fixture.getCafe("cafe5", "address5", 3), - cafe6, - Fixture.getCafe("cafe7", "address7", 1) - ); - final Pageable pageRequest = PageRequest.of(1, 5); - - //when - final int rank = cafeRankGenerator.makeRank(cafes.indexOf(cafe6), pageRequest); - - //then - assertThat(rank).isEqualTo(6); - } - - @Test - @DisplayName("카페의 순위를 계산할 때, 순위 범위를 벗어나면 예외를 발생시킨다.") - void getRankFailWhenOutOfBounds() { - //given - final Cafe cafe1 = Fixture.getCafe("cafe1", "address1", 5); - final List cafes = List.of(cafe1, - Fixture.getCafe("cafe2", "address2", 4), - Fixture.getCafe("cafe3", "address3", 3), - Fixture.getCafe("cafe4", "address4", 2), - Fixture.getCafe("cafe5", "address5", 1) - ); - final Pageable pageRequest = PageRequest.of(3, 10); - - //when, then - assertThatThrownBy(() -> cafeRankGenerator.makeRank(cafes.indexOf(cafe1), pageRequest)) - .isInstanceOf(BadRequestException.class) - .hasMessage(ErrorCode.RANK_OUT_OF_BOUNDS.getMessage()); - } -} diff --git a/server/src/test/java/com/project/yozmcafe/service/CafeServiceTest.java b/server/src/test/java/com/project/yozmcafe/service/CafeServiceTest.java index 06336f1d..e46d455e 100644 --- a/server/src/test/java/com/project/yozmcafe/service/CafeServiceTest.java +++ b/server/src/test/java/com/project/yozmcafe/service/CafeServiceTest.java @@ -3,10 +3,13 @@ import com.project.yozmcafe.BaseTest; import com.project.yozmcafe.controller.dto.cafe.CafeRankResponse; import com.project.yozmcafe.controller.dto.cafe.CafeResponse; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchRequest; +import com.project.yozmcafe.controller.dto.cafe.CafeSearchResponse; import com.project.yozmcafe.domain.cafe.Cafe; import com.project.yozmcafe.domain.cafe.CafeRepository; import com.project.yozmcafe.domain.member.Member; import com.project.yozmcafe.domain.member.MemberRepository; +import com.project.yozmcafe.domain.menu.MenuRepository; import com.project.yozmcafe.exception.BadRequestException; import com.project.yozmcafe.fixture.Fixture; import org.junit.jupiter.api.DisplayName; @@ -18,7 +21,6 @@ import java.util.List; import static com.project.yozmcafe.exception.ErrorCode.NOT_EXISTED_CAFE; -import static com.project.yozmcafe.exception.ErrorCode.RANK_OUT_OF_BOUNDS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.AssertionsForClassTypes.tuple; @@ -32,6 +34,8 @@ class CafeServiceTest extends BaseTest { private CafeRepository cafeRepository; @Autowired private MemberRepository memberRepository; + @Autowired + private MenuRepository menuRepository; @Test @DisplayName("로그인 된 사용자의 안본 카페 목록을 조회한다.") @@ -78,51 +82,20 @@ void getCafesForUnLoginMember() { @DisplayName("cafe를 likeCount가 많은 순으로 조회한다.") void getCafesOrderByLikeCount() { //given - final PageRequest pageRequest = PageRequest.of(0, 5); - cafeRepository.save(Fixture.getCafe("카페1", "주소1", 11)); - cafeRepository.save(Fixture.getCafe("카페2", "주소2", 12)); - cafeRepository.save(Fixture.getCafe("카페3", "주소3", 13)); - final Cafe savedCafe4 = cafeRepository.save(Fixture.getCafe("카페4", "주소4", 14)); - final Cafe savedCafe5 = cafeRepository.save(Fixture.getCafe("카페5", "주소5", 15)); - final Cafe savedCafe6 = cafeRepository.save(Fixture.getCafe("카페6", "주소6", 16)); - final Cafe savedCafe7 = cafeRepository.save(Fixture.getCafe("카페7", "주소7", 17)); - final Cafe savedCafe8 = cafeRepository.save(Fixture.getCafe("카페8", "주소8", 18)); - - //when - final List result = cafeService.getCafesOrderByLikeCount(pageRequest); - - //then - assertThat(result).extracting("id", "rank", "likeCount") - .containsExactly(tuple(savedCafe8.getId(), 1, savedCafe8.getLikeCount()), - tuple(savedCafe7.getId(), 2, savedCafe7.getLikeCount()), - tuple(savedCafe6.getId(), 3, savedCafe6.getLikeCount()), - tuple(savedCafe5.getId(), 4, savedCafe5.getLikeCount()), - tuple(savedCafe4.getId(), 5, savedCafe4.getLikeCount()) - ); - } - - @Test - @DisplayName("cafe를 likeCount가 많은 순으로 조회할 때, 페이지에 맞는 카페들을 응답한다.") - void getCafesOrderByLikeCountWhenNotFirstPage() { - //given - final PageRequest pageRequest = PageRequest.of(1, 5); + final PageRequest pageRequest = PageRequest.of(0, 3); final Cafe savedCafe1 = cafeRepository.save(Fixture.getCafe("카페1", "주소1", 11)); final Cafe savedCafe2 = cafeRepository.save(Fixture.getCafe("카페2", "주소2", 12)); final Cafe savedCafe3 = cafeRepository.save(Fixture.getCafe("카페3", "주소3", 13)); - cafeRepository.save(Fixture.getCafe("카페4", "주소4", 14)); - cafeRepository.save(Fixture.getCafe("카페5", "주소5", 15)); - cafeRepository.save(Fixture.getCafe("카페6", "주소6", 16)); - cafeRepository.save(Fixture.getCafe("카페7", "주소7", 17)); - cafeRepository.save(Fixture.getCafe("카페8", "주소8", 18)); //when final List result = cafeService.getCafesOrderByLikeCount(pageRequest); //then assertThat(result).extracting("id", "rank", "likeCount") - .containsExactly(tuple(savedCafe3.getId(), 6, savedCafe3.getLikeCount()), - tuple(savedCafe2.getId(), 7, savedCafe2.getLikeCount()), - tuple(savedCafe1.getId(), 8, savedCafe1.getLikeCount()) + .containsExactly( + tuple(savedCafe3.getId(), 1, savedCafe3.getLikeCount()), + tuple(savedCafe2.getId(), 2, savedCafe2.getLikeCount()), + tuple(savedCafe1.getId(), 3, savedCafe1.getLikeCount()) ); } @@ -130,13 +103,10 @@ void getCafesOrderByLikeCountWhenNotFirstPage() { @DisplayName("cafe를 likeCount가 많은 순으로 조회할 때, 해당 순위 페이지에 카페가 존재하지 않으면 빈 배열을 반환한다.") void getCafesOrderByLikeCountWhenNotExist() { //given - final PageRequest pageRequest = PageRequest.of(3, 5); + final PageRequest pageRequest = PageRequest.of(1, 3); cafeRepository.save(Fixture.getCafe("카페1", "주소1", 11)); cafeRepository.save(Fixture.getCafe("카페2", "주소2", 12)); cafeRepository.save(Fixture.getCafe("카페3", "주소3", 13)); - cafeRepository.save(Fixture.getCafe("카페4", "주소4", 14)); - cafeRepository.save(Fixture.getCafe("카페5", "주소5", 15)); - cafeRepository.save(Fixture.getCafe("카페6", "주소6", 16)); //when final List result = cafeService.getCafesOrderByLikeCount(pageRequest); @@ -145,44 +115,55 @@ void getCafesOrderByLikeCountWhenNotExist() { assertThat(result).isEmpty(); } - @Test - @DisplayName("cafe를 likeCount가 많은 순으로 조회할 때, 최대 순위 페이지를 초과하는 요청이면 예외가 발생한다.") - void getCafesOrderByLikeCountWhenPageOutFail() { - //given - final PageRequest pageRequest = PageRequest.of(4, 10); - cafeRepository.save(Fixture.getCafe("카페1", "주소1", 11)); - cafeRepository.save(Fixture.getCafe("카페2", "주소2", 12)); - cafeRepository.save(Fixture.getCafe("카페3", "주소3", 13)); - - //when,then - assertThatThrownBy(() -> cafeService.getCafesOrderByLikeCount(pageRequest)) - .isInstanceOf(BadRequestException.class) - .hasMessage(RANK_OUT_OF_BOUNDS.getMessage()); - } - @DisplayName("id로 카페를 단 건 조회한다.") void getCafeById() { //given - final Cafe savedCafe1 = cafeRepository.save(Fixture.getCafe("카페1", "주소1", 10)); + final Cafe savedCafe = cafeRepository.save(Fixture.getCafe("카페1", "주소1", 10)); //when - final CafeResponse result = cafeService.getCafeById(savedCafe1.getId()); + final CafeResponse result = cafeService.getCafeByIdOrThrow(savedCafe.getId()); //then - assertThat(result.id()).isEqualTo(savedCafe1.getId()); + assertThat(result.id()).isEqualTo(savedCafe.getId()); } @Test @DisplayName("id로 카페를 단 건 조회할 때, 존재하지 않는 카페이면 예외가 발생한다.") void getCafeByIdFailWhenNotExist() { //given - cafeRepository.save(Fixture.getCafe("카페1", "주소1", 10)); - cafeRepository.save(Fixture.getCafe("카페2", "주소2", 11)); final long notExistCafeId = 9999L; //when, then - assertThatThrownBy(() -> cafeService.getCafeById(notExistCafeId)) + assertThatThrownBy(() -> cafeService.getCafeByIdOrThrow(notExistCafeId)) .isInstanceOf(BadRequestException.class) .hasMessage(NOT_EXISTED_CAFE.getMessage()); } + + @Test + @DisplayName("메뉴가 검색 조건에 있을 때 카페를 검색해 조회한다.") + void getCafesByKeyWordWhenMenuExist() { + final Cafe savedCafe1 = cafeRepository.save(Fixture.getCafe("카페1", "주소1", 10)); + menuRepository.save(Fixture.getMenu(savedCafe1, 1, "카페 메뉴")); + + final CafeSearchRequest cafeSearchRequest = new CafeSearchRequest("카페", "카페 메뉴", "주소"); + List cafeResponse = cafeService.getCafesBySearch(cafeSearchRequest); + assertThat(cafeResponse).extracting("id", "name") + .containsOnly(tuple(savedCafe1.getId(), savedCafe1.getName())); + } + + @Test + @DisplayName("메뉴가 검색 조건에 없을 때 카페를 검색해 조회한다.") + void getCafesByKeyWordWhenMenuNotExist() { + final Cafe savedCafe1 = cafeRepository.save(Fixture.getCafe("카페1", "주소1", 10)); + final Cafe savedCafe2 = cafeRepository.save(Fixture.getCafe("카페2", "주소2", 11)); + final Cafe savedCafe3 = cafeRepository.save(Fixture.getCafe("카페3", "주소3", 11)); + menuRepository.save(Fixture.getMenu(savedCafe1, 1, "카페 메뉴1")); + menuRepository.save(Fixture.getMenu(savedCafe2, 1, "카페 메뉴2")); + menuRepository.save(Fixture.getMenu(savedCafe3, 1, "카페 메뉴3")); + + final CafeSearchRequest cafeSearchRequest = new CafeSearchRequest("카페1", null, "주소"); + List cafeResponse = cafeService.getCafesBySearch(cafeSearchRequest); + assertThat(cafeResponse).extracting("id", "name") + .containsOnly(tuple(savedCafe1.getId(), savedCafe1.getName())); + } } diff --git a/server/src/test/java/com/project/yozmcafe/service/LikedCafeServiceTest.java b/server/src/test/java/com/project/yozmcafe/service/LikedCafeServiceTest.java index e275525d..04d644f1 100644 --- a/server/src/test/java/com/project/yozmcafe/service/LikedCafeServiceTest.java +++ b/server/src/test/java/com/project/yozmcafe/service/LikedCafeServiceTest.java @@ -1,8 +1,8 @@ package com.project.yozmcafe.service; import com.project.yozmcafe.BaseTest; +import com.project.yozmcafe.controller.dto.cafe.CafeThumbnailResponse; import com.project.yozmcafe.controller.dto.cafe.LikedCafeResponse; -import com.project.yozmcafe.controller.dto.cafe.LikedCafeThumbnailResponse; import com.project.yozmcafe.domain.cafe.Cafe; import com.project.yozmcafe.domain.cafe.CafeRepository; import com.project.yozmcafe.domain.member.Member; @@ -42,7 +42,7 @@ void findLikedCafesById() { memberRepository.save(member); //when - final List likedCafes = likedCafeService.findLikedCafeThumbnailsByMemberId(member.getId(), pageRequest); + final List likedCafes = likedCafeService.findLikedCafeThumbnailsByMemberId(member.getId(), pageRequest); //then assertThat(likedCafes.get(0).cafeId()).isEqualTo(savedCafe.getId()); @@ -65,15 +65,18 @@ void findLikedCafesById_fail() { @DisplayName("좋아요 목록 수를 초과한 page 요청 시 빈 list를 반환한다.") void findLikedCafesById_empty() { //given - final Member member = memberRepository.save(new Member("1234", "오션", "오션사진")); final Cafe cafe1 = Fixture.getCafe(1L, "카페1", "주소1", 3); final Cafe cafe2 = Fixture.getCafe(2L, "카페2", "주소2", 3); - final PageRequest pageRequest = PageRequest.of(1, 2); + cafeRepository.save(cafe1); + cafeRepository.save(cafe2); + Member member = new Member("1234", "오션", "오션사진"); member.updateLikedCafesBy(cafe1, true); member.updateLikedCafesBy(cafe2, true); + memberRepository.save(member); + final PageRequest pageRequest = PageRequest.of(1, 2); //when - final List likedCafesById = likedCafeService.findLikedCafeThumbnailsByMemberId(member.getId(), pageRequest); + final List likedCafesById = likedCafeService.findLikedCafeThumbnailsByMemberId(member.getId(), pageRequest); //then assertThat(likedCafesById).isEmpty(); diff --git a/server/src/test/java/com/project/yozmcafe/service/auth/AuthServiceTest.java b/server/src/test/java/com/project/yozmcafe/service/auth/AuthServiceTest.java index df89dbe2..b736c0b4 100644 --- a/server/src/test/java/com/project/yozmcafe/service/auth/AuthServiceTest.java +++ b/server/src/test/java/com/project/yozmcafe/service/auth/AuthServiceTest.java @@ -27,10 +27,6 @@ class AuthServiceTest extends BaseTest { @SpyBean private GoogleOAuthClient googleOAuthClient; - @SpyBean - private KakaoOAuthClient kakaoOAuthClient; - @Autowired - private JwtTokenProvider jwtTokenProvider; @Autowired private MemberRepository memberRepository;