diff --git a/build.gradle b/build.gradle index 6e48c05..c621df3 100644 --- a/build.gradle +++ b/build.gradle @@ -44,8 +44,34 @@ dependencies { // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // QueryDSL : OpenFeign + implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" + implementation "io.github.openfeign.querydsl:querydsl-core:7.0" + annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" } tasks.named('test') { useJUnitPlatform() } + +// QueryDSL 관련 설정 +// generated/querydsl 폴더 생성 & 삽입 +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +// 소스 세트에 생성 경로 추가 (구체적인 경로 지정) +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +// 컴파일 시 생성 경로 지정 +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory.set(querydslDir) +} + +// clean 태스크에 생성 폴더 삭제 로직 추가 +clean.doLast { + file(querydslDir).deleteDir() +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index ec1045b..bb40ab4 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -4,7 +4,10 @@ import com.example.umc9th.domain.review.dto.ReviewResponse; import com.example.umc9th.domain.review.service.ReviewService; import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.*; @RestController @@ -15,6 +18,7 @@ public class ReviewController { private final ReviewService reviewService; @PostMapping + @Operation(summary = "리뷰 등록 API") public ApiResponse createReview( @RequestBody ReviewRequest.ReviewCreateDTO request ){ @@ -22,4 +26,18 @@ public ApiResponse createReview( return ApiResponse.onSuccess(response); } + @GetMapping("/my") + @Operation(summary = "나의 리뷰 조회 API", description = "가게별, 별점별(소수점 제외 몇점대)로 나의 리뷰를 조회할 수 있습니다.") + public ApiResponse getMyReviews( + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) Integer rate, + @RequestParam(required = false) Long cursorId){ + + ReviewResponse.ReviewListDTO reviews = + reviewService.getMyReviews(1L, storeId, rate, cursorId); + + return ApiResponse.onSuccess(reviews); + + } + } diff --git a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java index e40d2ee..5ed1aad 100644 --- a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java +++ b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java @@ -6,6 +6,10 @@ import com.example.umc9th.domain.review.entity.Review; import com.example.umc9th.domain.store.entity.Store; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + public class ReviewConverter { // ReviewCreateDTO -> Review 엔터티 @@ -18,10 +22,29 @@ public static Review toReview (ReviewRequest.ReviewCreateDTO dto, Member member, .build(); } - // Review 엔터티 -> ReviewResultDTO - public static ReviewResponse.ReviewResultDTO toReviewResultDTO (Review review) { + // 단일 리뷰 엔티티 → ReviewDetailDTO + public static ReviewResponse.ReviewResultDTO toReviewResultDTO(Review review, List images) { return ReviewResponse.ReviewResultDTO.builder() .reviewId(review.getId()) + .storeName(review.getStore().getName()) + .rate(review.getRate()) + .content(review.getContent()) + .reviewImages(images) + .createdAt(review.getCreatedAt()) .build(); } + + // 리뷰 목록 변환 + public static List toReviewResultDTOList( + List reviews, + Map> reviewImageMap + ) { + return reviews.stream() + .map(review -> toReviewResultDTO( + review, + reviewImageMap.getOrDefault(review.getId(), List.of()) + )) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/com/example/umc9th/domain/review/dto/ReviewResponse.java b/src/main/java/com/example/umc9th/domain/review/dto/ReviewResponse.java index 0d2f5c6..2f423e8 100644 --- a/src/main/java/com/example/umc9th/domain/review/dto/ReviewResponse.java +++ b/src/main/java/com/example/umc9th/domain/review/dto/ReviewResponse.java @@ -5,13 +5,33 @@ import lombok.Builder; import lombok.Getter; +import java.time.LocalDateTime; +import java.util.List; + public class ReviewResponse { @Getter @Builder @AllArgsConstructor - @Schema(description = "리뷰 등록 응답 정보") + @Schema(description = "리뷰 응답 정보") public static class ReviewResultDTO { private Long reviewId; + private String storeName; + private Float rate; + private String content; + private List reviewImages; + private LocalDateTime createdAt; + } + + @Getter + @Builder + @AllArgsConstructor + @Schema(description = "리뷰 목록 조회 응답 정보") + public static class ReviewListDTO { + + private List reviewList; // 리뷰 리스트 + private Boolean hasNext; // 다음 페이지 존재 여부 + private Long nextCursorId; // 다음 커서 ID (다음 요청에 사용) } + } diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepositoryCustom.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepositoryCustom.java new file mode 100644 index 0000000..9f50627 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepositoryCustom.java @@ -0,0 +1,23 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.dto.ReviewResponse; +import com.example.umc9th.domain.review.entity.Review; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import java.util.List; +import java.util.Map; + +public interface ReviewRepositoryCustom { + + Slice findMyReviewsByFilter( + Long memberId, + Long storeId, + Integer rate, // 정수로 (3.x -> 3점대) + Long cursorId); + + // ReviewImage N + 1 해결용 + Map> findReviewImages(List reviewIds); + + +} diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepositoryImpl.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepositoryImpl.java new file mode 100644 index 0000000..13fe49e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepositoryImpl.java @@ -0,0 +1,90 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.entity.Review; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.group.GroupBy; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.example.umc9th.domain.review.entity.QReview.review; +import static com.example.umc9th.domain.review.entity.QReviewImage.reviewImage; +import static com.example.umc9th.domain.store.entity.QStore.store; + +@Repository +@RequiredArgsConstructor +public class ReviewRepositoryImpl implements ReviewRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + private static final int PAGE_SIZE = 10; + + @Override + public Slice findMyReviewsByFilter( + Long memberId, + Long storeId, + Integer rate, + Long cursorId) { + + + BooleanBuilder builder = new BooleanBuilder(); + + // 1. 내가 쓴 리뷰 + builder.and(review.member.id.eq(memberId)); + + // 2. 특정 가게 필터링 + if(storeId != null) { + builder.and(review.store.id.eq(storeId)); + } + + // 3. 별점 필터링 (정수부분 기준 - 3점대, 4점대...) + // goe : >= (이상), lt : < (미만) + if(rate != null) { + builder.and(review.rate.goe(rate).and(review.rate.lt(rate + 1))); + } + + // 4. 커서 기반 무한스크롤 + if(cursorId != null) { + builder.and(review.id.lt(cursorId)); + } + + // 5. query 실행 + List results = jpaQueryFactory + .selectFrom(review) + .leftJoin(review.store, store).fetchJoin() // store N+1 방지 + .where(builder) + .orderBy(review.id.desc()) + .limit(PAGE_SIZE + 1) // 다음페이지 존재 여부 확인 + .fetch(); + + // 6. slice 반환 + boolean hasNext = results.size() > PAGE_SIZE; + if (hasNext) { + results.remove(PAGE_SIZE); // +1 로 가져온건 삭제 - 다음 페이지 존재 여부 확인용 이니까 + } + + return new SliceImpl<>(results, PageRequest.of(0, PAGE_SIZE), hasNext); + } + + // 리뷰 이미지 조회 + @Override + public Map> findReviewImages(List reviewIds) { + if(reviewIds == null || reviewIds.isEmpty()) { + return Collections.emptyMap(); + } + + // reviewId를 Key로, imageUrl 리스트를 value로 묶어서 반환 + return jpaQueryFactory + .from(reviewImage) + .where(reviewImage.review.id.in(reviewIds)) + .transform(GroupBy.groupBy(reviewImage.review.id) + .as(GroupBy.list(reviewImage.imgUrl))); + } +} diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java index 77a9999..915e1b8 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -2,8 +2,17 @@ import com.example.umc9th.domain.review.dto.ReviewRequest; import com.example.umc9th.domain.review.dto.ReviewResponse; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; public interface ReviewService { ReviewResponse.ReviewResultDTO createReview(Long memberId, Long storeId, ReviewRequest.ReviewCreateDTO request); + + ReviewResponse.ReviewListDTO getMyReviews( + Long memberId, + Long storeId, + Integer rate, + Long cursorId + ); } diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewServiceImpl.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewServiceImpl.java index 51a0bb9..b666676 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/ReviewServiceImpl.java +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewServiceImpl.java @@ -7,15 +7,20 @@ import com.example.umc9th.domain.review.dto.ReviewResponse; import com.example.umc9th.domain.review.entity.Review; import com.example.umc9th.domain.review.repository.ReviewRepository; +import com.example.umc9th.domain.review.repository.ReviewRepositoryCustom; import com.example.umc9th.domain.store.entity.Store; import com.example.umc9th.domain.store.repository.StoreRepository; import com.example.umc9th.global.apiPayload.code.status.ErrorStatus; import com.example.umc9th.global.apiPayload.exception.handler.ErrorHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Map; + @Slf4j @Service @Transactional(readOnly = true) @@ -25,7 +30,9 @@ public class ReviewServiceImpl implements ReviewService { private final MemberRepository memberRepository; private final StoreRepository storeRepository; private final ReviewRepository reviewRepository; + private final ReviewRepositoryCustom reviewRepositoryCustom; + // 리뷰 작성 @Transactional @Override public ReviewResponse.ReviewResultDTO createReview(Long memberId, Long storeId, ReviewRequest.ReviewCreateDTO request){ @@ -45,7 +52,42 @@ public ReviewResponse.ReviewResultDTO createReview(Long memberId, Long storeId, reviewRepository.save(review); // 5. 응답 DTO로 변환하여 반환 - return ReviewConverter.toReviewResultDTO(review); + return ReviewConverter.toReviewResultDTO(review, List.of()); } + // 나의 리뷰 조회 + @Override + public ReviewResponse.ReviewListDTO getMyReviews( + Long memberId, Long storeId, Integer rate, Long cursorId) { + + // 1. Repository 호출 → DB 조회 + Slice reviewSlice = reviewRepositoryCustom.findMyReviewsByFilter( + memberId, storeId, rate, cursorId); + + // 2. 리뷰 ID 리스트 추출 + List reviewIds = reviewSlice.getContent().stream() + .map(Review::getId) + .toList(); + + // 3. 리뷰별 이미지 Map 조회 + Map> reviewImageMap = reviewRepositoryCustom.findReviewImages(reviewIds); + + // 4. Converter로 DTO 변환 + List dtoList = + ReviewConverter.toReviewResultDTOList(reviewSlice.getContent(), reviewImageMap); + + // 5. 마지막 커서 아이디 확인 + Long nextCursorId = reviewSlice.hasNext() + ? dtoList.get(dtoList.size() - 1).getReviewId() + : null; + + // 6. 응답 dto 반환 + return ReviewResponse.ReviewListDTO.builder() + .reviewList(dtoList) + .hasNext(reviewSlice.hasNext()) + .nextCursorId(nextCursorId) + .build(); + } + + } diff --git a/src/main/java/com/example/umc9th/domain/store/controller/StoreController.java b/src/main/java/com/example/umc9th/domain/store/controller/StoreController.java new file mode 100644 index 0000000..b658052 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/controller/StoreController.java @@ -0,0 +1,41 @@ +package com.example.umc9th.domain.store.controller; + +import com.example.umc9th.domain.store.dto.StoreResponse; +import com.example.umc9th.domain.store.entity.enums.StoreSortType; +import com.example.umc9th.domain.store.service.StoreService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/stores") +public class StoreController { + + private final StoreService storeService; + + @Operation( + summary = "커서 기반 가게 검색", + description = """ + 지역(region), 이름(keyword), 정렬(sort) 조건으로 가게를 검색합니다. + - region: 지역 필터 (예: 강남구) + - keyword: 검색어 (공백 포함 시 단어별 OR 검색) + - sort: latest(최신순), name(이름순) + - cursorId: 이전 요청의 마지막 storeId + """ + ) + @GetMapping("/search") + public ApiResponse searchStores( + @RequestParam(required = false) String region, + @RequestParam(required = false) String keyword, + @RequestParam(defaultValue = "LATEST") StoreSortType sortType, + @RequestParam(required = false) Long cursorId + ) { + StoreResponse.StoreListDTO response = storeService.searchStores(region, keyword, sortType, cursorId); + return ApiResponse.onSuccess(response); + } +} diff --git a/src/main/java/com/example/umc9th/domain/store/converter/StoreConverter.java b/src/main/java/com/example/umc9th/domain/store/converter/StoreConverter.java new file mode 100644 index 0000000..214bf4b --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/converter/StoreConverter.java @@ -0,0 +1,39 @@ +package com.example.umc9th.domain.store.converter; + +import com.example.umc9th.domain.store.dto.StoreResponse; +import com.example.umc9th.domain.store.entity.Store; +import org.springframework.data.domain.Slice; + +import java.util.List; +import java.util.stream.Collectors; + +public class StoreConverter { + + public static StoreResponse.StoreResultDTO toStoreResultDTO(Store store) { + return StoreResponse.StoreResultDTO.builder() + .storeId(store.getId()) + .storeName(store.getName()) + .region(store.getRegion().getName()) + .address(store.getAddress()) + .rateAvg(store.getRateAvg()) + .openTime(store.getOpenTime()) + .closeTime(store.getCloseTime()) + .build(); + } + + public static StoreResponse.StoreListDTO toStoreListDTO(Slice storeSlice) { + List dtoList = storeSlice.getContent().stream() + .map(StoreConverter::toStoreResultDTO) + .collect(Collectors.toList()); + + Long nextCursorId = storeSlice.hasNext() + ? dtoList.get(dtoList.size() - 1).getStoreId() + : null; + + return StoreResponse.StoreListDTO.builder() + .storeList(dtoList) + .hasNext(storeSlice.hasNext()) + .nextCursorId(nextCursorId) + .build(); + } +} diff --git a/src/main/java/com/example/umc9th/domain/store/dto/StoreResponse.java b/src/main/java/com/example/umc9th/domain/store/dto/StoreResponse.java new file mode 100644 index 0000000..c17196e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/dto/StoreResponse.java @@ -0,0 +1,36 @@ +package com.example.umc9th.domain.store.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +public class StoreResponse { + + @Getter + @Builder + @AllArgsConstructor + @Schema(description = "가게 검색 개별 결과 DTO") + public static class StoreResultDTO { + private Long storeId; + private String storeName; + private String region; + private String address; + private Float rateAvg; + private LocalDateTime openTime; + private LocalDateTime closeTime; + } + + @Getter + @Builder + @AllArgsConstructor + @Schema(description = "커서 기반 가게 검색 결과 DTO") + public static class StoreListDTO { + private List storeList; + private Boolean hasNext; + private Long nextCursorId; // 다음 요청 시 사용할 커서 ID + } +} diff --git a/src/main/java/com/example/umc9th/domain/store/entity/enums/StoreSortType.java b/src/main/java/com/example/umc9th/domain/store/entity/enums/StoreSortType.java new file mode 100644 index 0000000..46e6f4e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/entity/enums/StoreSortType.java @@ -0,0 +1,6 @@ +package com.example.umc9th.domain.store.entity.enums; + +public enum StoreSortType { + NAME, + LATEST; +} diff --git a/src/main/java/com/example/umc9th/domain/store/repository/StoreRepositoryCustom.java b/src/main/java/com/example/umc9th/domain/store/repository/StoreRepositoryCustom.java new file mode 100644 index 0000000..9048e50 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/repository/StoreRepositoryCustom.java @@ -0,0 +1,12 @@ +package com.example.umc9th.domain.store.repository; + +import com.example.umc9th.domain.store.entity.Store; +import com.example.umc9th.domain.store.entity.enums.StoreSortType; +import org.springframework.data.domain.Slice; + +public interface StoreRepositoryCustom { + + // 가게 검색 + Slice searchStores(String region, String keyword, StoreSortType sortType, Long cursorId); + +} diff --git a/src/main/java/com/example/umc9th/domain/store/repository/StoreRepositoryImpl.java b/src/main/java/com/example/umc9th/domain/store/repository/StoreRepositoryImpl.java new file mode 100644 index 0000000..754b585 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/repository/StoreRepositoryImpl.java @@ -0,0 +1,100 @@ +package com.example.umc9th.domain.store.repository; + +import com.example.umc9th.domain.region.entity.QRegion; +import com.example.umc9th.domain.store.entity.Store; +import com.example.umc9th.domain.store.entity.enums.StoreSortType; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Predicate; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.example.umc9th.domain.store.entity.QStore.store; + +@Repository +@RequiredArgsConstructor +public class StoreRepositoryImpl implements StoreRepositoryCustom { + + private final JPAQueryFactory queryFactory; + private static final int PAGE_SIZE = 10; + + @Override + public Slice searchStores( + String region, String keyword, StoreSortType sortType, Long cursorId + ) { + + QRegion qRegion = QRegion.region; + + // 1. Predicate 조립 + BooleanExpression predicate = createPredicate(region, keyword); + + // 2. 커서 조건 추가 (cursorId보다 작은 id만) + if (cursorId != null) { + predicate = Expressions.allOf(predicate, store.id.lt(cursorId)); + } + + // 3. 정렬 조건 + OrderSpecifier orderSpecifier = + (sortType == StoreSortType.NAME) + ? store.name.asc() + : store.createdAt.desc(); + + // 4. 쿼리 실행 (limit + 1로 다음 페이지 존재 여부 확인) + List results = queryFactory + .selectFrom(store) + .leftJoin(store.region, qRegion).fetchJoin() + .where(predicate) + .orderBy(orderSpecifier, store.createdAt.desc()) + .limit(PAGE_SIZE + 1) + .fetch(); + + // 5. hasNext 계산 + boolean hasNext = results.size() > PAGE_SIZE; + if (hasNext) { + results.remove(PAGE_SIZE); // +1 개 데이터 제거 + } + + return new SliceImpl<>(results, PageRequest.of(0, PAGE_SIZE), hasNext); + + } + + // Predicate 생성 메서드 + private BooleanExpression createPredicate(String region, String keyword) { + + // 기본값 : true (처음에는 아무 조건이 없는 상태) + BooleanExpression condition = Expressions.TRUE.isTrue(); + + // 지역 필터 + if (region != null && !region.isBlank()) { + condition = condition.and(store.region.name.eq(region)); + } + + // 이름 검색 + if (keyword != null && !keyword.isBlank()) { + if (keyword.contains(" ")) { + // 공백 포함 → 단어별 OR 검색 + String[] words = keyword.split("\\s+"); // 공백을 기준으로 문자열 분리 + BooleanExpression keywordCondition = null; // 단어별 조건을 조립하기 위한 임시 변수 - 초기: null로 시작 + for (String word : words) { + BooleanExpression expr = store.name.containsIgnoreCase(word); //조건을 하나씩 붙힘 + keywordCondition = (keywordCondition == null) + ? expr // 첫번째 단어 - 초기화 + : keywordCondition.or(expr); // 두번째 부터는 or 조건으로 붙힘 + } + condition = condition.and(keywordCondition); // 최종적으로 조립된을 전체 조건에 and로 연결 + } else { + // 공백 없음 → 전체 단어 포함 + condition = condition.and(store.name.containsIgnoreCase(keyword)); + } + } + + return condition; + } +} diff --git a/src/main/java/com/example/umc9th/domain/store/service/StoreService.java b/src/main/java/com/example/umc9th/domain/store/service/StoreService.java new file mode 100644 index 0000000..3f17b2d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/service/StoreService.java @@ -0,0 +1,14 @@ +package com.example.umc9th.domain.store.service; + +import com.example.umc9th.domain.store.dto.StoreResponse; +import com.example.umc9th.domain.store.entity.enums.StoreSortType; + +public interface StoreService { + + StoreResponse.StoreListDTO searchStores( + String region, + String keyword, + StoreSortType sortType, + Long cursorId + ); +} diff --git a/src/main/java/com/example/umc9th/domain/store/service/StoreServiceImpl.java b/src/main/java/com/example/umc9th/domain/store/service/StoreServiceImpl.java new file mode 100644 index 0000000..477a020 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/store/service/StoreServiceImpl.java @@ -0,0 +1,27 @@ +package com.example.umc9th.domain.store.service; + +import com.example.umc9th.domain.store.converter.StoreConverter; +import com.example.umc9th.domain.store.dto.StoreResponse; +import com.example.umc9th.domain.store.entity.Store; +import com.example.umc9th.domain.store.entity.enums.StoreSortType; +import com.example.umc9th.domain.store.repository.StoreRepositoryCustom; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StoreServiceImpl implements StoreService { + + private final StoreRepositoryCustom storeRepositoryCustom; + + @Override + public StoreResponse.StoreListDTO searchStores( + String region, String keyword, StoreSortType sortType, Long cursorId + ) { + Slice storeSlice = storeRepositoryCustom.searchStores(region, keyword, sortType, cursorId); + return StoreConverter.toStoreListDTO(storeSlice); + } +} diff --git a/src/main/java/com/example/umc9th/global/config/QueryDslConfig.java b/src/main/java/com/example/umc9th/global/config/QueryDslConfig.java new file mode 100644 index 0000000..bb674e4 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/QueryDslConfig.java @@ -0,0 +1,19 @@ +package com.example.umc9th.global.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 em; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(em); + } +}