diff --git a/build.gradle b/build.gradle index ee064e0..4eb072b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,8 +32,34 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' 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() +} 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 new file mode 100644 index 0000000..ea05b91 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -0,0 +1,34 @@ +package com.example.umc9th.domain.review.controller; + +import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; +import com.example.umc9th.domain.review.service.ReviewService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +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/v1") +public class ReviewController { + + private final ReviewService reviewService; + + @GetMapping("/users/{userId}/reviews") + public ResponseEntity> getMyReviews(@PathVariable Long userId, + @RequestParam(required = false) String restaurantName, + @RequestParam(required = false) Integer ratingFloor, + @PageableDefault(size = 10) Pageable pageable) { + Page response = reviewService.getMyReviews(userId, restaurantName, ratingFloor, pageable); + return ResponseEntity.ok(response); + } +} + + + 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 new file mode 100644 index 0000000..8ef617b --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java @@ -0,0 +1,21 @@ +package com.example.umc9th.domain.review.converter; + +import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; + +public final class ReviewConverter { + + private ReviewConverter() { + } + + public static ReviewMyReviewResponse toMyReviewResponse(ReviewSummaryProjection projection) { + return new ReviewMyReviewResponse( + projection.reviewId(), + projection.restaurantName(), + projection.reviewStar(), + projection.body(), + projection.createdAt() + ); + } +} + diff --git a/src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java b/src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java new file mode 100644 index 0000000..a9ccb52 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java @@ -0,0 +1,13 @@ +package com.example.umc9th.domain.review.dto; + +import java.time.LocalDateTime; + +public record ReviewMyReviewResponse( + Long reviewId, + String restaurantName, + Integer reviewStar, + String body, + LocalDateTime createdAt +) { +} + diff --git a/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java new file mode 100644 index 0000000..715d221 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java @@ -0,0 +1,41 @@ +package com.example.umc9th.domain.review.enums; + +import java.util.Arrays; + +public enum ReviewRatingGroup { + + FIVE(5, 5), + FOUR(4, 4), + THREE(3, 3), + TWO(2, 2), + ONE(1, 1); + + private final int minInclusive; + private final int maxInclusive; + + ReviewRatingGroup(int minInclusive, int maxInclusive) { + this.minInclusive = minInclusive; + this.maxInclusive = maxInclusive; + } + + public int getMinInclusive() { + return minInclusive; + } + + public int getMaxInclusive() { + return maxInclusive; + } + + public static ReviewRatingGroup fromValue(Integer value) { + if (value == null) { + return null; + } + return Arrays.stream(values()) + .filter(group -> group.minInclusive == value) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unsupported rating group value: " + value)); + } +} + + + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java new file mode 100644 index 0000000..7f9b99e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java @@ -0,0 +1,17 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.enums.ReviewRatingGroup; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ReviewQueryRepository { + + Page findMyReviews(Long userId, + String restaurantName, + ReviewRatingGroup ratingGroup, + Pageable pageable); +} + + + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java new file mode 100644 index 0000000..740c450 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java @@ -0,0 +1,80 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.restaurant.entity.QRestaurant; +import com.example.umc9th.domain.review.entity.QReview; +import com.example.umc9th.domain.review.enums.ReviewRatingGroup; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +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.stereotype.Repository; +import org.springframework.util.StringUtils; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ReviewQueryRepositoryImpl implements ReviewQueryRepository { + + private final JPAQueryFactory queryFactory; + + private static final QReview review = QReview.review; + private static final QRestaurant restaurant = QRestaurant.restaurant; + + @Override + public Page findMyReviews(Long userId, + String restaurantName, + ReviewRatingGroup ratingGroup, + Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder() + .and(review.user.id.eq(userId)); + + if (StringUtils.hasText(restaurantName)) { + builder.and(restaurant.name.eq(restaurantName)); + } + + BooleanExpression ratingCondition = ratingGroupCondition(ratingGroup); + if (ratingCondition != null) { + builder.and(ratingCondition); + } + + List content = queryFactory + .select(Projections.constructor(ReviewSummaryProjection.class, + review.id, + restaurant.name, + review.reviewStar, + review.body, + review.createdAt + )) + .from(review) + .join(review.restaurant, restaurant) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(review.createdAt.desc()) + .fetch(); + + Long total = queryFactory + .select(review.count()) + .from(review) + .join(review.restaurant, restaurant) + .where(builder) + .fetchOne(); + + long totalElements = total != null ? total : 0L; + return new PageImpl<>(content, pageable, totalElements); + } + + private BooleanExpression ratingGroupCondition(ReviewRatingGroup ratingGroup) { + if (ratingGroup == null) { + return null; + } + return review.reviewStar.between(ratingGroup.getMinInclusive(), ratingGroup.getMaxInclusive()); + } +} + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java index 7a7a4ac..c2d2221 100644 --- a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java @@ -3,6 +3,6 @@ import com.example.umc9th.domain.review.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; -public interface ReviewRepository extends JpaRepository { +public interface ReviewRepository extends JpaRepository, ReviewQueryRepository { } diff --git a/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java new file mode 100644 index 0000000..505f89e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java @@ -0,0 +1,15 @@ +package com.example.umc9th.domain.review.repository.result; + +import java.time.LocalDateTime; + +public record ReviewSummaryProjection( + Long reviewId, + String restaurantName, + Integer reviewStar, + String body, + LocalDateTime createdAt +) { +} + + + 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 new file mode 100644 index 0000000..335939d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -0,0 +1,39 @@ +package com.example.umc9th.domain.review.service; + +import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; +import com.example.umc9th.domain.review.enums.ReviewRatingGroup; +import com.example.umc9th.domain.review.repository.ReviewRepository; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewService { + + private final ReviewRepository reviewRepository; + + public Page getMyReviews(Long userId, + String restaurantName, + Integer ratingFloor, + Pageable pageable) { + ReviewRatingGroup ratingGroup = ReviewRatingGroup.fromValue(ratingFloor); + + Page projectionPage = reviewRepository.findMyReviews( + userId, + restaurantName, + ratingGroup, + pageable + ); + + return projectionPage.map(ReviewConverter::toMyReviewResponse); + } +} + + + 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..3041f90 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java @@ -0,0 +1,22 @@ +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 entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} + + +