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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,35 @@ 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"
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}

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()
}
3 changes: 3 additions & 0 deletions src/main/java/com/example/umc9th/Umc9thApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@SpringBootApplication
@EnableJpaAuditing
@EnableWebMvc
public class Umc9thApplication {

public static void main(String[] args) {
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/example/umc9th/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.umc9th.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);
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/example/umc9th/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.umc9th.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components())
.info(apiInfo());
}

private Info apiInfo() {
return new Info()
.title("Umc 9th API")
.description("Umc 9th API ๋ช…์„ธ์„œ")
.version("1.0.0");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.umc9th.domain.review.controller;

import com.example.umc9th.domain.review.dto.ReviewResponseDto;
import com.example.umc9th.domain.review.service.ReviewService;
import com.example.umc9th.global.dto.CursorResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
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/reviews")
@Tag(name = "๋ฆฌ๋ทฐ")
public class ReviewController {

private final ReviewService reviewService;

@Operation(
summary = "๋ฆฌ๋ทฐ ์กฐํšŒ",
description = "๊ฐ€๊ฒŒ๋ณ„, ๋ณ„์ ๋ณ„๋กœ ๋ฆฌ๋ทฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.")
@GetMapping
public CursorResponseDto<ReviewResponseDto.Review> getReviews(
@RequestParam(required = false) String storeName,
@RequestParam(required = false, defaultValue = "0.0") Float minStar,
@RequestParam(required = false, defaultValue = "5.0") Float maxStar,
@RequestParam(required = false) Long cursorId,
@RequestParam(required = false, defaultValue = "10") Integer size) {

return reviewService.getReviews(storeName,
minStar,
maxStar,
cursorId,
size);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.umc9th.domain.review.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

public class ReviewResponseDto {
// ๊ฐ์ฒด ์ƒ์„ฑ ๋ฐฉ์ง€
private ReviewResponseDto() {}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Review{
private Long id;
private String nickname;
private Float star;
private String content;
private String createdAt;
private List<String> reviewImages;

private String reviewReplyContent;
private String reviewReplyCreatedAt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.example.umc9th.domain.user.entity.User;
import com.example.umc9th.global.entity.BaseEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

Expand Down Expand Up @@ -36,6 +38,11 @@ public class Review extends BaseEntity {
@Column(nullable = false)
private String content;

@Column(nullable = false)
@Min(value = 0, message = "๋ณ„์ ์€ 0์  ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
@Max(value = 5, message = "๋ณ„์ ์€ 5์ ์„ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
private Float star;

@OneToMany(mappedBy = "review", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<ReviewImage> reviewImages = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.umc9th.domain.review.repository;

import com.querydsl.core.types.dsl.BooleanExpression;
import org.springframework.util.StringUtils;

import static com.example.umc9th.domain.review.entity.QReview.review;
import static com.example.umc9th.domain.store.entity.QStore.store;

public class ReviewPredicate {

private ReviewPredicate() {}

public static BooleanExpression storeNameContains(String storeName) {
return StringUtils.hasText(storeName) ? store.name.contains(storeName) : null;
}

public static BooleanExpression starRange(Float minStar, Float maxStar) {
if (minStar != null && maxStar != null) {
return review.star.between(minStar, maxStar);
} else if (minStar != null) {
return review.star.goe(minStar); // goe: great or equal
} else if (maxStar != null) {
return review.star.loe(maxStar); // loe: low or equal
}

return null;
}

public static BooleanExpression userIdEquals(Long userId) {
return userId != null ? review.user.id.eq(userId) : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.umc9th.domain.review.repository;

import com.example.umc9th.domain.review.dto.ReviewResponseDto;
import com.querydsl.core.types.Predicate;
import org.springframework.data.domain.Slice;

import org.springframework.data.domain.Pageable;

public interface ReviewQueryDsl {
Slice<ReviewResponseDto.Review> getReviews(Predicate predicate,
Long cursorId,
Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

public interface ReviewRepository extends JpaRepository<Review, Long> {
public interface ReviewRepository extends JpaRepository<Review, Long>, ReviewQueryDsl {
@Modifying
@Query("DELETE FROM Review r WHERE r.user.id = :userId")
void deleteByUserId(@Param("userId") Long userId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.example.umc9th.domain.review.repository;

import com.example.umc9th.domain.review.dto.ReviewResponseDto;
import com.example.umc9th.domain.review.entity.QReviewImage;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.group.GroupBy;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.Projections;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.List;

import static com.example.umc9th.domain.review.entity.QReview.review;
import static com.example.umc9th.domain.store.entity.QStore.store;
import static com.example.umc9th.domain.user.entity.QUser.user;
import static com.example.umc9th.domain.review.entity.QReviewImage.reviewImage;
import static com.example.umc9th.domain.review.entity.QReviewReply.reviewReply;

@Slf4j
@RequiredArgsConstructor
public class ReviewRepositoryImpl implements ReviewQueryDsl {

private final JPAQueryFactory queryFactory;

@Override
public Slice<ReviewResponseDto.Review> getReviews(Predicate predicate,
Long cursorId,
Pageable pageable) {

BooleanBuilder builder = new BooleanBuilder(predicate);

// ์ปค์„œ๊ฐ€ ์žˆ์œผ๋ฉด
if (cursorId != null) {
builder.and(review.id.lt(cursorId)); // lt: less than
}

// 'size + 1' ๋งŒํผ ์กฐํšŒ (๋‹ค์Œ ํŽ˜์ด์ง€ ์œ ๋ฌด ํ™•์ธ์šฉ)
int pageSize = pageable.getPageSize();

List<ReviewResponseDto.Review> content = queryFactory
.from(review)
.leftJoin(review.user, user)
.leftJoin(review.reviewImages, reviewImage)
.leftJoin(review.reviewReply, reviewReply)
.where(builder)
.orderBy(review.id.desc())
.limit(pageSize + 1)
.transform( // transform์€ ์ž๋ฐ” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ฉ”๋ชจ๋ฆฌ์—์„œ sql ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ฐ€๊ณต
GroupBy.groupBy(review.id).list(
Projections.constructor(ReviewResponseDto.Review.class,
review.id,
user.nickname,
review.star,
review.content,
review.createdAt.stringValue(),
GroupBy.list(reviewImage.imageUrl),
reviewReply.content,
reviewReply.createdAt.stringValue()
)
)
);

// hasNext (๋‹ค์Œ ํŽ˜์ด์ง€ ์กด์žฌ ์—ฌ๋ถ€) ํ™•์ธ
boolean hasNext = false;
if (content.size() > pageSize) {
content.remove(pageSize);
hasNext = true;
}

return new SliceImpl<>(content, pageable, hasNext);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package com.example.umc9th.domain.review.service;

import com.example.umc9th.domain.review.dto.ReviewResponseDto;
import com.example.umc9th.global.dto.CursorResponseDto;

public interface ReviewService {
void createReview(String content, Long userId, Long storeId);

CursorResponseDto<ReviewResponseDto.Review> getReviews(String storeName,
Float minStar,
Float maxStar,
Long cursorId,
int size);
}
Loading
Loading