From 0417d341ac7f418607deeb63b7e4254a87ade623 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Tue, 31 Oct 2023 23:14:42 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20#56=20=ED=95=99=EC=8B=9D=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=EC=9D=84=20=EC=9C=84=ED=95=9C=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../everymeal/server/meal/entity/Meal.java | 6 ++ .../review/controller/ReviewController.java | 3 + .../Review.java => review/entity/Image.java} | 18 ++--- .../server/review/entity/MarkId.java | 18 +++++ .../server/review/entity/Review.java | 68 +++++++++++++++++++ .../server/review/entity/ReviewMark.java | 40 +++++++++++ .../server/review/service/ReviewService.java | 3 + .../everymeal/server/user/entity/User.java | 12 ++++ 8 files changed, 159 insertions(+), 9 deletions(-) create mode 100644 src/main/java/everymeal/server/review/controller/ReviewController.java rename src/main/java/everymeal/server/{user/entity/Review.java => review/entity/Image.java} (62%) create mode 100644 src/main/java/everymeal/server/review/entity/MarkId.java create mode 100644 src/main/java/everymeal/server/review/entity/Review.java create mode 100644 src/main/java/everymeal/server/review/entity/ReviewMark.java create mode 100644 src/main/java/everymeal/server/review/service/ReviewService.java diff --git a/src/main/java/everymeal/server/meal/entity/Meal.java b/src/main/java/everymeal/server/meal/entity/Meal.java index e959d66..212d6a5 100644 --- a/src/main/java/everymeal/server/meal/entity/Meal.java +++ b/src/main/java/everymeal/server/meal/entity/Meal.java @@ -1,6 +1,7 @@ package everymeal.server.meal.entity; +import everymeal.server.review.entity.Review; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; @@ -9,8 +10,10 @@ import jakarta.persistence.Id; import jakarta.persistence.Index; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDate; +import java.util.Set; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -42,6 +45,9 @@ public class Meal { @ManyToOne private Restaurant restaurant; + @OneToMany(mappedBy = "meal") + private Set reviews; + @Builder public Meal( String menu, diff --git a/src/main/java/everymeal/server/review/controller/ReviewController.java b/src/main/java/everymeal/server/review/controller/ReviewController.java new file mode 100644 index 0000000..74224b0 --- /dev/null +++ b/src/main/java/everymeal/server/review/controller/ReviewController.java @@ -0,0 +1,3 @@ +package everymeal.server.review.controller; + +public class ReviewController {} diff --git a/src/main/java/everymeal/server/user/entity/Review.java b/src/main/java/everymeal/server/review/entity/Image.java similarity index 62% rename from src/main/java/everymeal/server/user/entity/Review.java rename to src/main/java/everymeal/server/review/entity/Image.java index 661e2d9..e3be927 100644 --- a/src/main/java/everymeal/server/user/entity/Review.java +++ b/src/main/java/everymeal/server/review/entity/Image.java @@ -1,30 +1,30 @@ -package everymeal.server.user.entity; +package everymeal.server.review.entity; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @Table -@Entity +@Entity(name = "images") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Review { +public class Image { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long idx; - private String photoList; - private String content; - private Double grade; - private Integer awesomeCount; + private String imageUrl; - @ManyToOne private User user; + @Builder + public Image(String imageUrl) { + this.imageUrl = imageUrl; + } } diff --git a/src/main/java/everymeal/server/review/entity/MarkId.java b/src/main/java/everymeal/server/review/entity/MarkId.java new file mode 100644 index 0000000..ae0766a --- /dev/null +++ b/src/main/java/everymeal/server/review/entity/MarkId.java @@ -0,0 +1,18 @@ +package everymeal.server.review.entity; + + +import java.io.Serializable; + +public class MarkId implements Serializable { + + private Long user; + private Long review; + + public MarkId() {} + + public MarkId(Long user, Long review) { + super(); + this.user = user; + this.review = review; + } +} diff --git a/src/main/java/everymeal/server/review/entity/Review.java b/src/main/java/everymeal/server/review/entity/Review.java new file mode 100644 index 0000000..057c7ce --- /dev/null +++ b/src/main/java/everymeal/server/review/entity/Review.java @@ -0,0 +1,68 @@ +package everymeal.server.review.entity; + + +import everymeal.server.global.entity.BaseEntity; +import everymeal.server.meal.entity.Meal; +import everymeal.server.user.entity.User; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table +@Entity(name = "reviews") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long idx; + + @Column(length = Integer.MAX_VALUE, columnDefinition = "리뷰 내용") + private String content; + + @Column(nullable = false, columnDefinition = "별점 1~5점") + private int grade; + + private boolean isDeleted; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_idx") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meal_idx") + private Meal meal; + + @OneToMany(mappedBy = "review", cascade = CascadeType.ALL) + private Set reviewMarks = new HashSet<>(); + + @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "review_idx") + private List images = new ArrayList<>(); + + @Builder + public Review(String content, int grade, List images, User user, Meal meal) { + this.content = content; + this.grade = grade; + this.images = images; + this.user = user; + this.meal = meal; + } +} diff --git a/src/main/java/everymeal/server/review/entity/ReviewMark.java b/src/main/java/everymeal/server/review/entity/ReviewMark.java new file mode 100644 index 0000000..8f94b17 --- /dev/null +++ b/src/main/java/everymeal/server/review/entity/ReviewMark.java @@ -0,0 +1,40 @@ +package everymeal.server.review.entity; + + +import everymeal.server.user.entity.User; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.io.Serializable; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Table +@Entity(name = "review_marks") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@IdClass(MarkId.class) +public class ReviewMark implements Serializable { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_idx") + private User user; + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_idx") + private Review review; + + @Builder + public ReviewMark(User user, Review review) { + this.user = user; + this.review = review; + } +} diff --git a/src/main/java/everymeal/server/review/service/ReviewService.java b/src/main/java/everymeal/server/review/service/ReviewService.java new file mode 100644 index 0000000..13659c5 --- /dev/null +++ b/src/main/java/everymeal/server/review/service/ReviewService.java @@ -0,0 +1,3 @@ +package everymeal.server.review.service; + +public interface ReviewService {} diff --git a/src/main/java/everymeal/server/user/entity/User.java b/src/main/java/everymeal/server/user/entity/User.java index 48bf807..fa06001 100644 --- a/src/main/java/everymeal/server/user/entity/User.java +++ b/src/main/java/everymeal/server/user/entity/User.java @@ -2,13 +2,19 @@ import everymeal.server.global.entity.BaseEntity; +import everymeal.server.review.entity.Review; +import everymeal.server.review.entity.ReviewMark; import everymeal.server.university.entity.University; +import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.util.HashSet; +import java.util.Set; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -33,6 +39,12 @@ public class User extends BaseEntity { @ManyToOne private University university; + @OneToMany(mappedBy = "user") + private Set reviews; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) + Set reviewMarks = new HashSet<>(); + @Builder public User(String deviceId, String nickName, String email, University university) { this.deviceId = deviceId; From 28595f00607485ce83a5e61a4aaed550783b33b7 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 11 Nov 2023 21:39:03 +0900 Subject: [PATCH 02/12] =?UTF-8?q?fix:=20#56=20=ED=95=99=EC=8B=9D=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20pk=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/everymeal/server/meal/repository/MealRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/everymeal/server/meal/repository/MealRepository.java b/src/main/java/everymeal/server/meal/repository/MealRepository.java index 57cd038..bdb6299 100644 --- a/src/main/java/everymeal/server/meal/repository/MealRepository.java +++ b/src/main/java/everymeal/server/meal/repository/MealRepository.java @@ -4,4 +4,4 @@ import everymeal.server.meal.entity.Meal; import org.springframework.data.jpa.repository.JpaRepository; -public interface MealRepository extends JpaRepository {} +public interface MealRepository extends JpaRepository {} From d41effd51385de83e8faea7780eb167cdf65a416 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 11 Nov 2023 21:40:58 +0900 Subject: [PATCH 03/12] =?UTF-8?q?fix:=20#56=20=ED=95=99=EC=83=9D=EC=8B=9D?= =?UTF-8?q?=EB=8B=B9=20=EC=82=AC=EC=9A=A9=EC=97=AC=EB=B6=80=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 코드 통일화를 위해 변수명을 변경 - 컬럼명 변경에 따른 레포지토리 함수명 변경 --- .../java/everymeal/server/meal/entity/Restaurant.java | 9 ++++++--- .../server/meal/repository/RestaurantRepository.java | 2 +- .../everymeal/server/meal/service/MealServiceImpl.java | 2 +- .../server/meal/service/MealServiceImplTest.java | 5 +++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/everymeal/server/meal/entity/Restaurant.java b/src/main/java/everymeal/server/meal/entity/Restaurant.java index 5db70b9..bc54f51 100644 --- a/src/main/java/everymeal/server/meal/entity/Restaurant.java +++ b/src/main/java/everymeal/server/meal/entity/Restaurant.java @@ -2,6 +2,7 @@ import everymeal.server.university.entity.University; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -22,9 +23,11 @@ public class Restaurant { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long idx; + @Column(length = 20, nullable = false) private String name; + private String address; - private Boolean useYn; + private Boolean isDeleted; @ManyToOne private University university; @@ -32,12 +35,12 @@ public class Restaurant { public Restaurant(String name, String address, University university) { this.name = name; this.address = address; - this.useYn = Boolean.TRUE; + this.isDeleted = Boolean.TRUE; this.university = university; } /** 학생식당 미운영 상태로 변경 폐업, 업체 변경 등의 이유일 경우, 해당 함수를 통해 상태 변경 */ public void updateUseYn() { - this.useYn = false; + this.isDeleted = false; } } diff --git a/src/main/java/everymeal/server/meal/repository/RestaurantRepository.java b/src/main/java/everymeal/server/meal/repository/RestaurantRepository.java index 8c53ca2..4cb31cb 100644 --- a/src/main/java/everymeal/server/meal/repository/RestaurantRepository.java +++ b/src/main/java/everymeal/server/meal/repository/RestaurantRepository.java @@ -8,7 +8,7 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface RestaurantRepository extends JpaRepository { - List findAllByUniversityAndUseYnTrue(University university); + List findAllByUniversityAndIsDeletedFalse(University university); Optional findByName(String name); } diff --git a/src/main/java/everymeal/server/meal/service/MealServiceImpl.java b/src/main/java/everymeal/server/meal/service/MealServiceImpl.java index c49ee72..84a46d0 100644 --- a/src/main/java/everymeal/server/meal/service/MealServiceImpl.java +++ b/src/main/java/everymeal/server/meal/service/MealServiceImpl.java @@ -158,7 +158,7 @@ public List getRestaurantList(String universityName, Strin () -> new ApplicationException(ExceptionList.UNIVERSITY_NOT_FOUND)); // 학교와 식당 폐업 여부를 키로 조회 List restaurants = - restaurantRepository.findAllByUniversityAndUseYnTrue(university); + restaurantRepository.findAllByUniversityAndIsDeletedFalse(university); return RestaurantListGetRes.of(restaurants); } /** diff --git a/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java b/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java index 5327a1e..fe3f32a 100644 --- a/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java +++ b/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java @@ -1,6 +1,7 @@ package everymeal.server.meal.service; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -233,9 +234,9 @@ void getRestaurantList() throws Exception { // when List response = mealService.getRestaurantList(universityName, campusName); - + var result = restaurantRepository.findAllByUniversityAndIsDeletedFalse(university); // then - assertEquals(response.get(0).restaurantIdx(), restaurant.getIdx()); + assertEquals(response.size(), result.size()); } @Test From 1f2739838a2e2a0a715726c41de5e8c6a9c928aa Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 11 Nov 2023 21:41:42 +0900 Subject: [PATCH 04/12] =?UTF-8?q?fix:=20#56=20=ED=95=99=EC=8B=9D=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EB=93=B1=EB=A1=9D=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=ED=95=99=EC=8B=9D=20pk=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 리뷰 등록을 위한 참조 값이 필요해짐에 따라 학식 조회 DTO에 PK 값을 추가 --- .../meal/controller/dto/response/DayMealListGetRes.java | 1 + .../everymeal/server/meal/repository/MealRepositoryImpl.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java b/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java index 79e0cce..5998f70 100644 --- a/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java +++ b/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java @@ -8,6 +8,7 @@ import java.time.LocalDate; public record DayMealListGetRes( + Long mealIdx, String menu, MealType mealType, MealStatus mealStatus, diff --git a/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java b/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java index 646a581..7ae8345 100644 --- a/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java +++ b/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java @@ -77,6 +77,7 @@ public List findAllByOfferedAtOnDate( GroupBy.list( Projections.constructor( DayMealListGetRes.class, + qMeal.idx.as("mealIdx"), qMeal.menu.as("menu"), qMeal.mealType.as("mealType"), qMeal.mealStatus.as("mealStatus"), @@ -92,6 +93,7 @@ public List findAllByOfferedAtOnDate( if (dayMeals == null || dayMeals.isEmpty()) { resultList.add( new DayMealListGetRes( + 0L, UN_REGISTERED_MEAL, mealType, MealStatus.CLOSED, @@ -132,6 +134,7 @@ public List getWeekMealList( GroupBy.list( Projections.constructor( DayMealListGetRes.class, + qMeal.idx.as("mealIdx"), qMeal.menu.as("menu"), qMeal.mealType.as("mealType"), qMeal.mealStatus.as("mealStatus"), @@ -150,6 +153,7 @@ public List getWeekMealList( if (dayMeals == null || dayMeals.isEmpty()) { dayMealListGetResList.add( new DayMealListGetRes( + 0L, UN_REGISTERED_MEAL, mealType, MealStatus.CLOSED, From 04c58fd48280de9565086ffc41573cd015ecf17e Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 11 Nov 2023 21:42:22 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20#56=20=ED=95=99=EC=8B=9D=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20API=20=EA=B0=9C=EB=B0=9C=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/ExceptionList.java | 2 + .../review/controller/ReviewController.java | 118 ++++++++- .../server/review/dto/ReviewCreateReq.java | 24 ++ .../server/review/dto/ReviewGetRes.java | 11 + .../server/review/dto/ReviewPaging.java | 18 ++ .../server/review/dto/ReviewPagingVO.java | 24 ++ .../review/dto/ReviewPagingVOWithCnt.java | 12 + .../server/review/entity/Review.java | 17 +- .../review/repository/ReviewRepository.java | 12 + .../repository/ReviewRepositoryCustom.java | 10 + .../repository/ReviewRepositoryImpl.java | 109 +++++++++ .../server/review/service/ReviewService.java | 15 +- .../review/service/ReviewServiceImpl.java | 119 +++++++++ .../everymeal/server/user/entity/User.java | 6 + .../server/global/ControllerTestSupport.java | 7 +- .../controller/ReviewControllerTest.java | 108 +++++++++ .../review/service/ReviewServiceImplTest.java | 228 ++++++++++++++++++ 17 files changed, 834 insertions(+), 6 deletions(-) create mode 100644 src/main/java/everymeal/server/review/dto/ReviewCreateReq.java create mode 100644 src/main/java/everymeal/server/review/dto/ReviewGetRes.java create mode 100644 src/main/java/everymeal/server/review/dto/ReviewPaging.java create mode 100644 src/main/java/everymeal/server/review/dto/ReviewPagingVO.java create mode 100644 src/main/java/everymeal/server/review/dto/ReviewPagingVOWithCnt.java create mode 100644 src/main/java/everymeal/server/review/repository/ReviewRepository.java create mode 100644 src/main/java/everymeal/server/review/repository/ReviewRepositoryCustom.java create mode 100644 src/main/java/everymeal/server/review/repository/ReviewRepositoryImpl.java create mode 100644 src/main/java/everymeal/server/review/service/ReviewServiceImpl.java create mode 100644 src/test/java/everymeal/server/review/controller/ReviewControllerTest.java create mode 100644 src/test/java/everymeal/server/review/service/ReviewServiceImplTest.java diff --git a/src/main/java/everymeal/server/global/exception/ExceptionList.java b/src/main/java/everymeal/server/global/exception/ExceptionList.java index 2c23dcd..4ce8c83 100644 --- a/src/main/java/everymeal/server/global/exception/ExceptionList.java +++ b/src/main/java/everymeal/server/global/exception/ExceptionList.java @@ -23,6 +23,8 @@ public enum ExceptionList { TOKEN_NOT_VALID("T0001", HttpStatus.NOT_ACCEPTABLE, "해당 토큰은 유효하지 않습니다."), TOKEN_EXPIRATION("T0002", HttpStatus.FORBIDDEN, "토큰이 만료되었습니다."), + + REVIEW_NOT_FOUND("R0001", HttpStatus.NOT_FOUND, "등록된 리뷰가 아닙니다."), ; public final String CODE; diff --git a/src/main/java/everymeal/server/review/controller/ReviewController.java b/src/main/java/everymeal/server/review/controller/ReviewController.java index 74224b0..de5cbd0 100644 --- a/src/main/java/everymeal/server/review/controller/ReviewController.java +++ b/src/main/java/everymeal/server/review/controller/ReviewController.java @@ -1,3 +1,119 @@ package everymeal.server.review.controller; -public class ReviewController {} + +import everymeal.server.global.dto.response.ApplicationResponse; +import everymeal.server.global.util.authresolver.Auth; +import everymeal.server.global.util.authresolver.AuthUser; +import everymeal.server.review.dto.ReviewCreateReq; +import everymeal.server.review.dto.ReviewGetRes; +import everymeal.server.review.service.ReviewService; +import everymeal.server.user.entity.User; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RequestMapping("/api/v1/reviews") +@RestController +@RequiredArgsConstructor +@Tag(name = "Review API", description = "리뷰 관련 API 입니다.") +public class ReviewController { + private final ReviewService reviewService; + + @Operation( + summary = "학식 리뷰 작성", + description = """ + 학식 리뷰 작성을 진행합니다.
+ 로그인이 필요한 기능입니다. + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "리뷰 등록 성공"), + }) + @Auth + @PostMapping + public ApplicationResponse createReview( + @RequestBody ReviewCreateReq request, @Schema(hidden = true) @AuthUser User user) { + return ApplicationResponse.create(reviewService.createReview(request, user)); + } + + @Operation( + summary = "학식 리뷰 수정", + description = """ + 학식 리뷰 수정을 진행합니다.
+ 로그인이 필요한 기능입니다. + """) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "리뷰 수정 성공"), + @ApiResponse( + responseCode = "404", + description = """ + (R0001)등록된 리뷰가 아닙니다.
+ """), + }) + @Auth + @PutMapping("/{reviewIdx}") + public ApplicationResponse updateReview( + @Schema(description = "리뷰 PK", defaultValue = "1") @PathVariable Long reviewIdx, + @RequestBody ReviewCreateReq request, + @Schema(hidden = true) @AuthUser User user) { + return ApplicationResponse.ok(reviewService.updateReview(request, user, reviewIdx)); + } + + @Operation( + summary = "학식 리뷰 삭제", + description = """ + 학식 리뷰 삭제를 진행합니다.
+ 로그인이 필요한 기능입니다. + """) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "리뷰 삭제 성공", + content = @Content(schema = @Schema())), + @ApiResponse( + responseCode = "404", + description = """ + (R0001)등록된 리뷰가 아닙니다.
+ """, + content = @Content(schema = @Schema())), + }) + @Auth + @DeleteMapping("/{reviewIdx}") + public ApplicationResponse deleteReview( + @Schema(description = "리뷰 PK", defaultValue = "1") @PathVariable Long reviewIdx, + @Schema(hidden = true) @AuthUser User user) { + return ApplicationResponse.ok(reviewService.deleteReview(user, reviewIdx)); + } + + @Operation(summary = "학식 리뷰 페이징 조회", description = """ + 학식 리뷰 조회를 진행합니다.
+ """) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "리뷰 조회 성공", + content = @Content(schema = @Schema(implementation = ReviewGetRes.class))), + }) + @GetMapping + public ApplicationResponse getReviewWithNoOffSetPaging( + @Schema(description = "조회하고자 하는 데이터의 시작점 idx", example = "1") @RequestParam + Long cursorIdx, + @Schema(description = "학식 idx", example = "1") @RequestParam Long mealIdx, + @Schema(description = "한 페이지에서 보고자 하는 데이터의 개수", example = "8") @RequestParam + int pageSize) { + return ApplicationResponse.ok( + reviewService.getReviewWithNoOffSetPaging(cursorIdx, mealIdx, pageSize)); + } +} diff --git a/src/main/java/everymeal/server/review/dto/ReviewCreateReq.java b/src/main/java/everymeal/server/review/dto/ReviewCreateReq.java new file mode 100644 index 0000000..df68f56 --- /dev/null +++ b/src/main/java/everymeal/server/review/dto/ReviewCreateReq.java @@ -0,0 +1,24 @@ +package everymeal.server.review.dto; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import java.util.List; + +public record ReviewCreateReq( + @Schema(description = "리뷰를 남기고자 하는 학식의 idx를 입력해주세요.", defaultValue = "1") @NotBlank + Long mealIdx, + @Schema(description = "학식에 대한 리뷰 평가 점수를 정수형 최소 1 ~ 최대 5점까지로 입력해주세요.", defaultValue = "5") + @NotBlank + int grade, + @Schema( + description = "학식에 대한 리뷰 내용을 1글자 이상 300자 내로 입력해주세요.", + defaultValue = "오늘 학식 진짜 미침...안먹으면 땅을 치고 후회함") + @NotBlank + @Size(min = 1, max = 300) + String content, + @Schema( + description = "학식이 보이는 사진 이미지 주소를 String 리스트 형태로 입력해주세요.", + defaultValue = "['이미지 주소']") + List imageList) {} diff --git a/src/main/java/everymeal/server/review/dto/ReviewGetRes.java b/src/main/java/everymeal/server/review/dto/ReviewGetRes.java new file mode 100644 index 0000000..0f0757e --- /dev/null +++ b/src/main/java/everymeal/server/review/dto/ReviewGetRes.java @@ -0,0 +1,11 @@ +package everymeal.server.review.dto; + + +import java.util.List; + +/** + * ============================================================================================ 커서 + * 기반 리뷰 목록 조회 시, Response DTO + * ============================================================================================= + */ +public record ReviewGetRes(int reviewTotalCnt, List reviewPagingList) {} diff --git a/src/main/java/everymeal/server/review/dto/ReviewPaging.java b/src/main/java/everymeal/server/review/dto/ReviewPaging.java new file mode 100644 index 0000000..8ac8597 --- /dev/null +++ b/src/main/java/everymeal/server/review/dto/ReviewPaging.java @@ -0,0 +1,18 @@ +package everymeal.server.review.dto; + + +import java.util.List; + +/** + * ============================================================================================ 커서 + * 기반 리뷰 목록 조회 시, Response에 사용되는 하위 DTO + * ============================================================================================= + */ +public record ReviewPaging( + Long reviewIdx, + String restaurantName, + String mealType, + int grade, + String content, + List imageList, + int reviewMarksCnt) {} diff --git a/src/main/java/everymeal/server/review/dto/ReviewPagingVO.java b/src/main/java/everymeal/server/review/dto/ReviewPagingVO.java new file mode 100644 index 0000000..9bfeb24 --- /dev/null +++ b/src/main/java/everymeal/server/review/dto/ReviewPagingVO.java @@ -0,0 +1,24 @@ +package everymeal.server.review.dto; + + +import com.querydsl.core.annotations.QueryProjection; +import everymeal.server.review.entity.Image; +import java.util.List; + +/** + * ============================================================================================ 커서 + * 기반 리뷰 목록 조회 시, select에 사용하는 VO + * ============================================================================================= + */ +public record ReviewPagingVO( + Long reviewIdx, + String restaurantName, + String mealType, + int grade, + String content, + List imageList, + int reviewMarksCnt) { + + @QueryProjection + public ReviewPagingVO {} +} diff --git a/src/main/java/everymeal/server/review/dto/ReviewPagingVOWithCnt.java b/src/main/java/everymeal/server/review/dto/ReviewPagingVOWithCnt.java new file mode 100644 index 0000000..9af4348 --- /dev/null +++ b/src/main/java/everymeal/server/review/dto/ReviewPagingVOWithCnt.java @@ -0,0 +1,12 @@ +package everymeal.server.review.dto; + + +import everymeal.server.review.entity.Review; +import java.util.List; + +/** + * ============================================================================================ 커서 + * 기반 리뷰 목록 조회 시, Repository에서 contents와 count를 함께 받아오는 VO + * ============================================================================================= + */ +public record ReviewPagingVOWithCnt(int reviewTotalCnt, List reviewList) {} diff --git a/src/main/java/everymeal/server/review/entity/Review.java b/src/main/java/everymeal/server/review/entity/Review.java index 057c7ce..03f732a 100644 --- a/src/main/java/everymeal/server/review/entity/Review.java +++ b/src/main/java/everymeal/server/review/entity/Review.java @@ -34,10 +34,10 @@ public class Review extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long idx; - @Column(length = Integer.MAX_VALUE, columnDefinition = "리뷰 내용") + @Column(length = 300) private String content; - @Column(nullable = false, columnDefinition = "별점 1~5점") + @Column(nullable = false, length = 1) private int grade; private boolean isDeleted; @@ -53,7 +53,7 @@ public class Review extends BaseEntity { @OneToMany(mappedBy = "review", cascade = CascadeType.ALL) private Set reviewMarks = new HashSet<>(); - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "review_idx") private List images = new ArrayList<>(); @@ -64,5 +64,16 @@ public Review(String content, int grade, List images, User user, Meal mea this.images = images; this.user = user; this.meal = meal; + this.isDeleted = Boolean.FALSE; + } + + public void updateEntity(String content, int grade, List images) { + this.content = content; + this.grade = grade; + this.images = images; + } + + public void deleteEntity() { + this.isDeleted = Boolean.TRUE; } } diff --git a/src/main/java/everymeal/server/review/repository/ReviewRepository.java b/src/main/java/everymeal/server/review/repository/ReviewRepository.java new file mode 100644 index 0000000..1f79f67 --- /dev/null +++ b/src/main/java/everymeal/server/review/repository/ReviewRepository.java @@ -0,0 +1,12 @@ +package everymeal.server.review.repository; + + +import everymeal.server.review.entity.Review; +import everymeal.server.user.entity.User; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository, ReviewRepositoryCustom { + + Optional findByIdxAndUser(Long id, User user); +} diff --git a/src/main/java/everymeal/server/review/repository/ReviewRepositoryCustom.java b/src/main/java/everymeal/server/review/repository/ReviewRepositoryCustom.java new file mode 100644 index 0000000..0fafecc --- /dev/null +++ b/src/main/java/everymeal/server/review/repository/ReviewRepositoryCustom.java @@ -0,0 +1,10 @@ +package everymeal.server.review.repository; + + +import everymeal.server.review.dto.ReviewPagingVOWithCnt; + +public interface ReviewRepositoryCustom { + ReviewPagingVOWithCnt getReview(Long cursorIdx, Long mealIdx, int pageSize); + + ReviewPagingVOWithCnt getReviewWithNoOffSetPaging(Long cursorIdx, Long mealIdx, int pageSize); +} diff --git a/src/main/java/everymeal/server/review/repository/ReviewRepositoryImpl.java b/src/main/java/everymeal/server/review/repository/ReviewRepositoryImpl.java new file mode 100644 index 0000000..bf2fe6d --- /dev/null +++ b/src/main/java/everymeal/server/review/repository/ReviewRepositoryImpl.java @@ -0,0 +1,109 @@ +package everymeal.server.review.repository; + +import static everymeal.server.meal.entity.QMeal.meal; +import static everymeal.server.meal.entity.QRestaurant.restaurant; +import static everymeal.server.review.entity.QImage.image; +import static everymeal.server.review.entity.QReview.review; +import static everymeal.server.review.entity.QReviewMark.reviewMark; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import everymeal.server.review.dto.QReviewPagingVO; +import everymeal.server.review.dto.ReviewPagingVOWithCnt; +import java.util.Objects; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ReviewRepositoryImpl implements ReviewRepositoryCustom { + private final JPAQueryFactory jpaQueryFactory; + + /** + * ============================================================================================ + * 커서 기반 페이징으로 리뷰 목록 조회 VO 형식으로 필요 데이터만 받기 + * ============================================================================================= + */ + @Override + public ReviewPagingVOWithCnt getReviewWithNoOffSetPaging( + Long cursorIdx, Long mealIdx, int pageSize) { + var queryResult = + jpaQueryFactory + .select( + new QReviewPagingVO( + review.idx.as("reviewIdx"), + restaurant.name.as("restaurantName"), + meal.mealType.stringValue().as("mealType"), + review.grade, + review.content, + review.images.as("imageList"), + review.reviewMarks.size().as("reviewMarksCnt"))) + .from(review) + .leftJoin(review.images, image) + .leftJoin(review.reviewMarks, reviewMark) + .leftJoin(review.meal, meal) + .leftJoin(meal.restaurant, restaurant) + .where(gtReviewIdx(cursorIdx), eqMealIdx(mealIdx), isDeleted()) + .groupBy(review.idx) + .orderBy(review.idx.desc()) + .limit(pageSize) + .fetch(); + var countResult = + Objects.requireNonNull( + jpaQueryFactory + .select(review.idx.count()) + .from(review) + .where(isDeleted()) + .fetchOne()) + .intValue(); + // return new ReviewPagingVOWithCnt(countResult, queryResult); + return null; + } + /** + * ============================================================================================ + * 커서 기반 페이징으로 리뷰 목록 조회 + * ============================================================================================= + */ + @Override + public ReviewPagingVOWithCnt getReview(Long cursorIdx, Long mealIdx, int pageSize) { + var queryResult = + jpaQueryFactory + .select(review) + .from(review) + .leftJoin(review.images, image) + .leftJoin(review.reviewMarks, reviewMark) + .leftJoin(review.meal, meal) + .leftJoin(meal.restaurant, restaurant) + .where(gtReviewIdx(cursorIdx), eqMealIdx(mealIdx), isDeleted()) + .groupBy(review.idx) + .orderBy(review.idx.desc()) + .limit(pageSize) + .fetch(); + var countResult = + Objects.requireNonNull( + jpaQueryFactory + .select(review.idx.count()) + .from(review) + .where(isDeleted()) + .fetchOne()) + .intValue(); + return new ReviewPagingVOWithCnt(countResult, queryResult); + } + /** + * ============================================================================================ + * PRIVATE FUNCTION --BooleanExpression + * ============================================================================================= + */ + private BooleanExpression isDeleted() { + return review.isDeleted.eq(Boolean.FALSE); + } + + private BooleanExpression eqMealIdx(Long mealIdx) { + return mealIdx == null ? null : meal.idx.eq(mealIdx); + } + + private BooleanExpression gtReviewIdx(Long cursorIdx) { + if (cursorIdx == null) { + return null; + } + return review.idx.gt(cursorIdx); + } +} diff --git a/src/main/java/everymeal/server/review/service/ReviewService.java b/src/main/java/everymeal/server/review/service/ReviewService.java index 13659c5..21de23b 100644 --- a/src/main/java/everymeal/server/review/service/ReviewService.java +++ b/src/main/java/everymeal/server/review/service/ReviewService.java @@ -1,3 +1,16 @@ package everymeal.server.review.service; -public interface ReviewService {} + +import everymeal.server.review.dto.ReviewCreateReq; +import everymeal.server.review.dto.ReviewGetRes; +import everymeal.server.user.entity.User; + +public interface ReviewService { + Boolean createReview(ReviewCreateReq request, User user); + + Boolean updateReview(ReviewCreateReq request, User user, Long reviewIdx); + + Boolean deleteReview(User user, Long reviewIdx); + + ReviewGetRes getReviewWithNoOffSetPaging(Long cursorIdx, Long mealIdx, int pageSize); +} diff --git a/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java b/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java new file mode 100644 index 0000000..9177188 --- /dev/null +++ b/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java @@ -0,0 +1,119 @@ +package everymeal.server.review.service; + +import static everymeal.server.global.exception.ExceptionList.MEAL_NOT_FOUND; +import static everymeal.server.global.exception.ExceptionList.REVIEW_NOT_FOUND; + +import everymeal.server.global.exception.ApplicationException; +import everymeal.server.meal.entity.Meal; +import everymeal.server.meal.repository.MealRepository; +import everymeal.server.review.dto.ReviewCreateReq; +import everymeal.server.review.dto.ReviewGetRes; +import everymeal.server.review.dto.ReviewPaging; +import everymeal.server.review.entity.Image; +import everymeal.server.review.entity.Review; +import everymeal.server.review.repository.ReviewRepository; +import everymeal.server.user.entity.User; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewServiceImpl implements ReviewService { + + private final MealRepository mealRepository; + private final ReviewRepository reviewRepository; + private final Logger logger = LoggerFactory.getLogger(ReviewServiceImpl.class); + + @Override + @Transactional + public Boolean createReview(ReviewCreateReq request, User user) { + // (1) Meal 객체 조회 + Optional meal = mealRepository.findById(request.mealIdx()); + if (meal.isEmpty()) { + logger.info(MEAL_NOT_FOUND.MESSAGE, new ApplicationException(MEAL_NOT_FOUND)); + return false; + } + // (2) 이미지 주소 <> 이미지 객체 치환 + List imageList = getImageFromString(request.imageList()); + // (3) Entity 생성 + Review review = + Review.builder() + .content(request.content()) + .images(imageList) + .grade(request.grade()) + .meal(meal.get()) + .user(user) + .build(); + // (4) 저장 + reviewRepository.save(review); + return true; + } + + @Override + @Transactional + public Boolean updateReview(ReviewCreateReq request, User user, Long reviewIdx) { + // (1) 기존 리뷰 조회 + Review review = + reviewRepository + .findById(reviewIdx) + .orElseThrow(() -> new ApplicationException(REVIEW_NOT_FOUND)); + // (2) 이미지 주소 <> 이미지 객체 치환 + List imageList = getImageFromString(request.imageList()); + // (3) 기존 데이터 수정 + review.updateEntity(request.content(), request.grade(), imageList); + return true; + } + + @Override + @Transactional + public Boolean deleteReview(User user, Long reviewIdx) { + // (1) 기존 리뷰 조회 + Review review = + reviewRepository + .findByIdxAndUser(reviewIdx, user) + .orElseThrow(() -> new ApplicationException(REVIEW_NOT_FOUND)); + // (2) 기존 데이터 삭제 + review.deleteEntity(); + return true; + } + + public ReviewGetRes getReviewWithNoOffSetPaging(Long cursorIdx, Long mealIdx, int pageSize) { + var result = reviewRepository.getReview(cursorIdx, mealIdx, pageSize); + List reviewPagingList = new ArrayList<>(); + for (Review vo : result.reviewList()) { + List strImgList = new ArrayList<>(); + vo.getImages().forEach(img -> strImgList.add(img.getImageUrl())); + reviewPagingList.add( + new ReviewPaging( + vo.getIdx(), + vo.getMeal().getRestaurant().getName(), + vo.getMeal().getMealType().name(), + vo.getGrade(), + vo.getContent(), + strImgList, + vo.getReviewMarks().size())); + } + + return new ReviewGetRes(result.reviewTotalCnt(), reviewPagingList); + } + + /** + * ============================================================================================ + * PRIVATE FUNCTION + * ============================================================================================= + */ + private List getImageFromString(List reqImgList) { + List imageList = new ArrayList<>(); + if (reqImgList.size() > 0) { + reqImgList.forEach(img -> imageList.add(Image.builder().imageUrl(img).build())); + } + return imageList; + } +} diff --git a/src/main/java/everymeal/server/user/entity/User.java b/src/main/java/everymeal/server/user/entity/User.java index 4eb70ab..f4f427b 100644 --- a/src/main/java/everymeal/server/user/entity/User.java +++ b/src/main/java/everymeal/server/user/entity/User.java @@ -2,12 +2,18 @@ import everymeal.server.global.entity.BaseEntity; +import everymeal.server.review.entity.Review; +import everymeal.server.review.entity.ReviewMark; import everymeal.server.university.entity.University; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.util.HashSet; import java.util.Set; diff --git a/src/test/java/everymeal/server/global/ControllerTestSupport.java b/src/test/java/everymeal/server/global/ControllerTestSupport.java index e6e350c..e42e6e8 100644 --- a/src/test/java/everymeal/server/global/ControllerTestSupport.java +++ b/src/test/java/everymeal/server/global/ControllerTestSupport.java @@ -7,6 +7,8 @@ import everymeal.server.infra.HealthCheckController; import everymeal.server.meal.controller.MealController; import everymeal.server.meal.service.MealService; +import everymeal.server.review.controller.ReviewController; +import everymeal.server.review.service.ReviewService; import everymeal.server.store.controller.StoreController; import everymeal.server.store.service.StoreService; import everymeal.server.university.controller.UniversityController; @@ -24,7 +26,8 @@ MealController.class, UniversityController.class, HealthCheckController.class, - StoreController.class + StoreController.class, + ReviewController.class }) public abstract class ControllerTestSupport { @@ -43,4 +46,6 @@ public abstract class ControllerTestSupport { @MockBean protected StoreService storeService; @MockBean protected UniversityService universityService; + + @MockBean protected ReviewService reviewService; } diff --git a/src/test/java/everymeal/server/review/controller/ReviewControllerTest.java b/src/test/java/everymeal/server/review/controller/ReviewControllerTest.java new file mode 100644 index 0000000..2b1d693 --- /dev/null +++ b/src/test/java/everymeal/server/review/controller/ReviewControllerTest.java @@ -0,0 +1,108 @@ +package everymeal.server.review.controller; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import everymeal.server.global.ControllerTestSupport; +import everymeal.server.review.dto.ReviewCreateReq; +import everymeal.server.user.controller.dto.request.UserEmailSingReq; +import everymeal.server.user.controller.dto.response.UserLoginRes; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; + +class ReviewControllerTest extends ControllerTestSupport { + private String ACCESS_TOKEN = "accessToken"; + + @BeforeEach + void 로그인() { + UserEmailSingReq singReq = + new UserEmailSingReq("testNickname", "Token", "sendValue", 1L, "testImageKey"); + + given(userService.signUp(any())) + .willReturn( + new UserLoginRes( + "accessToken", "testNickname", "testImageKey", "refreshToken")); + + given(userService.login(any())) + .willReturn( + new UserLoginRes( + "accessToken", "testNickname", "testImageKey", "refreshToken")); + } + + @DisplayName("리뷰 등록") + @Test + void createReview() throws Exception { + // given + ReviewCreateReq req = new ReviewCreateReq(1L, 5, "오늘 학식 진짜 미침", List.of()); + + // when-then + mockMvc.perform( + post("/api/v1/reviews") + .content(objectMapper.writeValueAsString(req)) + .contentType(MediaType.APPLICATION_JSON) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()); + } + + @DisplayName("리뷰 수정") + @Test + void updateReview() throws Exception { + // given + ReviewCreateReq req = new ReviewCreateReq(1L, 5, "오늘 학식 진짜 미침", List.of()); + Long reviewIdx = 1L; + // when-then + mockMvc.perform( + put("/api/v1/reviews/{reviewIdx}", reviewIdx) + .content(objectMapper.writeValueAsString(req)) + .contentType(MediaType.APPLICATION_JSON) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()); + } + + @DisplayName("리뷰 삭제") + @Test + void deleteReview() throws Exception { + // given + Long reviewIdx = 1L; + // when-then + mockMvc.perform( + delete("/api/v1/reviews/{reviewIdx}", reviewIdx) + .contentType(MediaType.APPLICATION_JSON) + .header(AUTHORIZATION, ACCESS_TOKEN)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()); + } + + @DisplayName("리뷰 커서 기반 페이징 조회") + @Test + void getReviewWithNoOffSetPaging() throws Exception { + // given + Long cursorIdx = 1L; + Long mealIdx = 1L; + int pageSize = 8; + // when-then + mockMvc.perform( + get("/api/v1/reviews?cursorIdx=" + + cursorIdx + + "&mealIdx=" + + mealIdx + + "&pageSize=" + + pageSize) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/everymeal/server/review/service/ReviewServiceImplTest.java b/src/test/java/everymeal/server/review/service/ReviewServiceImplTest.java new file mode 100644 index 0000000..947acda --- /dev/null +++ b/src/test/java/everymeal/server/review/service/ReviewServiceImplTest.java @@ -0,0 +1,228 @@ +package everymeal.server.review.service; + +import static org.junit.jupiter.api.Assertions.*; + +import everymeal.server.global.IntegrationTestSupport; +import everymeal.server.global.exception.ApplicationException; +import everymeal.server.global.exception.ExceptionList; +import everymeal.server.meal.controller.dto.request.RestaurantRegisterReq; +import everymeal.server.meal.entity.Meal; +import everymeal.server.meal.entity.MealCategory; +import everymeal.server.meal.entity.MealStatus; +import everymeal.server.meal.entity.MealType; +import everymeal.server.meal.entity.Restaurant; +import everymeal.server.meal.repository.MealRepository; +import everymeal.server.meal.repository.RestaurantRepository; +import everymeal.server.review.dto.ReviewCreateReq; +import everymeal.server.review.entity.Review; +import everymeal.server.review.repository.ReviewRepository; +import everymeal.server.university.entity.University; +import everymeal.server.university.repository.UniversityRepository; +import everymeal.server.user.entity.User; +import everymeal.server.user.repository.UserRepository; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +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; + +class ReviewServiceImplTest extends IntegrationTestSupport { + + @Autowired private ReviewService reviewService; + @Autowired private ReviewRepository reviewRepository; + @Autowired private MealRepository mealRepository; + @Autowired private UniversityRepository universityRepository; + @Autowired private RestaurantRepository restaurantRepository; + @Autowired private UserRepository userRepository; + + /** + * ============================================================================================ + * PRIVATE VARIABLE FOR TEST + * ============================================================================================= + */ + private University university; + + private Meal meal; + private User user; + private Review review; + + @BeforeEach + void createDummyForTest() { + RestaurantRegisterReq restaurantRegisterReq = getRestaurantRegisterReq(); + university = + universityRepository.save( + getUniversity( + restaurantRegisterReq.universityName(), + restaurantRegisterReq.campusName())); + Restaurant restaurant = + restaurantRepository.save( + getRestaurant( + university, + restaurantRegisterReq.address(), + restaurantRegisterReq.restaurantName())); + meal = mealRepository.save(getMeal(restaurant)); + user = userRepository.save(getUser(university, 1)); + review = reviewRepository.save(getReview(user, meal)); + } + + @AfterEach + void tearDown() { + reviewRepository.deleteAllInBatch(); + mealRepository.deleteAllInBatch(); + restaurantRepository.deleteAllInBatch(); + userRepository.deleteAllInBatch(); + universityRepository.deleteAllInBatch(); + } + + @DisplayName("리뷰를 작성에 성공한다.") + @Test + void createReview() { + // given + ReviewCreateReq req = new ReviewCreateReq(meal.getIdx(), 5, "오늘 학식 진짜 미침", List.of()); + // when + var result = reviewService.createReview(req, user); + + // then + assertEquals(result, Boolean.TRUE); + } + + @DisplayName("등록되지 않은 학식에 대한 리뷰를 작성할 경우, 실패한다.") + @Test + void createReview_failed() { + // given + ReviewCreateReq req = new ReviewCreateReq(0L, 5, "오늘 학식 진짜 미침", List.of()); + // when + var result = reviewService.createReview(req, user); + + // then + assertEquals(result, Boolean.FALSE); + } + + @DisplayName("학식 리뷰를 수정한다.") + @Test + void updateReview() { + // given + ReviewCreateReq req = new ReviewCreateReq(meal.getIdx(), 5, "오늘 학식 진짜 미침", List.of()); + // when + var result = reviewService.updateReview(req, user, review.getIdx()); + var updated = reviewRepository.findById(review.getIdx()); + // then + assertEquals(result, Boolean.TRUE); + assertEquals(updated.get().getContent(), req.content()); + } + + @DisplayName("등록되지 않은 리뷰를 수정할 경우, 실패한다.") + @Test + void updateReview_failed() { + // given + ReviewCreateReq req = new ReviewCreateReq(meal.getIdx(), 5, "오늘 학식 진짜 미침", List.of()); + // when then + ApplicationException applicationException = + assertThrows( + ApplicationException.class, + () -> reviewService.updateReview(req, user, 0L)); + + // then + assertEquals(applicationException.getErrorCode(), ExceptionList.REVIEW_NOT_FOUND.getCODE()); + } + + @DisplayName("리뷰를 삭제한다.") + @Test + void deleteReview() { + // given + + // when + var result = reviewService.deleteReview(user, review.getIdx()); + var deleted = reviewRepository.findById(review.getIdx()); + // then + assertEquals(result, Boolean.TRUE); + assertEquals(deleted.get().isDeleted(), Boolean.TRUE); + } + + @DisplayName("등록되지 않은 리뷰를 삭제할 경우, 실패한다.") + @Test + void deleteReview_failed() { + // given + + // when then + ApplicationException applicationException = + assertThrows( + ApplicationException.class, () -> reviewService.deleteReview(user, 0L)); + + // then + assertEquals(applicationException.getErrorCode(), ExceptionList.REVIEW_NOT_FOUND.getCODE()); + } + + @DisplayName("학식에 대한 리뷰를 커서 기반 페이징으로 조회합니다.") + @Test + void getReviewWithNoOffSetPaging() { + // given + createDummy(); + Long cursorIdx = 1L; + Long mealIdx = meal.getIdx(); + int pageSize = 8; + // when + var result = reviewService.getReviewWithNoOffSetPaging(cursorIdx, mealIdx, pageSize); + // then + assertEquals(result.reviewPagingList().size(), pageSize); + } + /** + * ============================================================================================ + * PRIVATE FUNCTION --더미 데이터 생성용 + * ============================================================================================= + */ + private void createDummy() { + List users = new ArrayList<>(); + for (int i = 2; i < 12; i++) { + users.add(userRepository.save(getUser(university, i))); + } + for (User user : users) { + reviewRepository.save(getReview(user, meal)); + } + } + + private Review getReview(User user, Meal meal) { + return Review.builder() + .user(user) + .meal(meal) + .grade(5) + .images(List.of()) + .content("Good") + .build(); + } + + private User getUser(University university, int uniqueIdx) { + return User.builder() + .email(uniqueIdx + "test@gmail.com") + .university(university) + .nickname(uniqueIdx + "띵랑이") + .profileImgUrl("img.url") + .build(); + } + + private Meal getMeal(Restaurant restaurant) { + return Meal.builder() + .mealType(MealType.BREAKFAST) + .mealStatus(MealStatus.OPEN) + .offeredAt(LocalDate.now()) + .price(10000.0) + .category(MealCategory.DEFAULT) + .restaurant(restaurant) + .build(); + } + + private RestaurantRegisterReq getRestaurantRegisterReq() { + return new RestaurantRegisterReq("명지대학교", "인문캠퍼스", "서울시 서대문구 남가좌동 거북골로 34", "MCC 식당"); + } + + private University getUniversity(String universityName, String campusName) { + return University.builder().name(universityName).campusName(campusName).build(); + } + + private Restaurant getRestaurant(University university, String address, String name) { + return Restaurant.builder().university(university).address(address).name(name).build(); + } +} From a50ada708d506d8e1f96e13777bc2b5938e43241 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 11 Nov 2023 22:06:39 +0900 Subject: [PATCH 06/12] =?UTF-8?q?fix:=20#56=20=ED=95=99=EC=8B=9D=20?= =?UTF-8?q?=EB=A6=AC=EB=B7=B0=20=EC=9D=91=EB=8B=B5=EA=B0=92=EC=97=90=20?= =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/everymeal/server/review/dto/ReviewPaging.java | 1 + .../everymeal/server/review/service/ReviewServiceImpl.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/everymeal/server/review/dto/ReviewPaging.java b/src/main/java/everymeal/server/review/dto/ReviewPaging.java index 8ac8597..e9c1c01 100644 --- a/src/main/java/everymeal/server/review/dto/ReviewPaging.java +++ b/src/main/java/everymeal/server/review/dto/ReviewPaging.java @@ -12,6 +12,7 @@ public record ReviewPaging( Long reviewIdx, String restaurantName, String mealType, + String mealCategory, int grade, String content, List imageList, diff --git a/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java b/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java index 9177188..88d2493 100644 --- a/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java +++ b/src/main/java/everymeal/server/review/service/ReviewServiceImpl.java @@ -94,7 +94,8 @@ public ReviewGetRes getReviewWithNoOffSetPaging(Long cursorIdx, Long mealIdx, in new ReviewPaging( vo.getIdx(), vo.getMeal().getRestaurant().getName(), - vo.getMeal().getMealType().name(), + vo.getMeal().getMealType().getValue(), + vo.getMeal().getCategory().getValue(), vo.getGrade(), vo.getContent(), strImgList, From 8a9d5691f810b1d15ebae5613ee40b8e31c37049 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sat, 11 Nov 2023 22:12:31 +0900 Subject: [PATCH 07/12] style: #56 SpotlessApply --- .../java/everymeal/server/meal/service/MealServiceImplTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java b/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java index fe3f32a..9c6c9df 100644 --- a/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java +++ b/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java @@ -1,7 +1,6 @@ package everymeal.server.meal.service; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; From f66f5adbf712d5a07e3d731f205383268fae5cf2 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 16 Nov 2023 22:39:05 +0900 Subject: [PATCH 08/12] =?UTF-8?q?chore:=20#56=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=BD=98=EC=86=94=20=EC=B6=9C=EB=A0=A5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 콘솔창에 실행 쿼리의 param이 포함된 채 출력되도록 해줍니다. --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index d744db3..897c29f 100644 --- a/build.gradle +++ b/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation 'org.hibernate.orm:hibernate-core:6.2.5.Final' //QueryDSL + implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" From d9142efbc5df72487dd2143245829ca2871cd04f Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 16 Nov 2023 22:40:56 +0900 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20#56=20=ED=95=98=EB=A3=A8=20?= =?UTF-8?q?=ED=95=99=EC=8B=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=ED=8A=9C=EB=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기존) 날짜로 그룹핑 해서 리스트 형태로 반환 - 변경) 학생식당명을 기준으로 그룹핑해서 MAP 형태로 반환 --- .../meal/controller/MealController.java | 48 ++++++------ .../dto/request/WeekMealRegisterReq.java | 4 +- .../dto/response/DayMealListGetRes.java | 9 ++- .../server/meal/repository/MealDao.java | 64 ++++++++++++++++ .../meal/repository/MealRepository.java | 30 +++++++- .../meal/repository/MealRepositoryCustom.java | 9 +-- .../meal/repository/MealRepositoryImpl.java | 71 ++++++++++-------- .../server/meal/service/MealService.java | 6 +- .../server/meal/service/MealServiceImpl.java | 74 +++++++++---------- .../meal/controller/MealControllerTest.java | 12 +-- .../meal/service/MealServiceImplTest.java | 48 ++++++------ 11 files changed, 239 insertions(+), 136 deletions(-) create mode 100644 src/main/java/everymeal/server/meal/repository/MealDao.java diff --git a/src/main/java/everymeal/server/meal/controller/MealController.java b/src/main/java/everymeal/server/meal/controller/MealController.java index a59ccbb..57b3b99 100644 --- a/src/main/java/everymeal/server/meal/controller/MealController.java +++ b/src/main/java/everymeal/server/meal/controller/MealController.java @@ -13,6 +13,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -30,11 +31,9 @@ public class MealController { private final MealService mealService; /** + * ============================================================================================ * 학생식당 등록 API - * - * @param restaurantRegisterReq RestaurantRegisterReq 식당 정보 입력 - * @return restID String 학생식당 고유 번호 - * @author dldmsql + * ============================================================================================ */ @PostMapping("/restaurant") @Operation(summary = "학생식당 추가") @@ -44,12 +43,9 @@ public ApplicationResponse createRestaurant( } /** + * ============================================================================================ * 학생식당 조회 API - * - * @param universityName String 학교 이름 - * @param campusName String 캠퍼스 이름 - * @return List 학교+캠퍼스로 등록된 식당 리스트 - * @author dldmsql + * ============================================================================================ */ @GetMapping("/restaurant") @Operation(summary = "학교별 학생 식당 목록 조회") @@ -64,11 +60,9 @@ public ApplicationResponse> getRestaurants( } /** + * ============================================================================================ * 주간 단위 식단 등록 API - * - * @param weekMealRegisterReq WeekMealRegisterReq 식당 정보 입력 - * @return boolean 성공 - * @author dldmsql + * ============================================================================================ */ @PostMapping("/week") @Operation(summary = "주간 단위 식단 추가") @@ -78,38 +72,38 @@ public ApplicationResponse createWeekMeal( } /** + * ============================================================================================ * 하루 식단 조회 API - * - * @param restaurantIdx Long - * @return - * @author dldmsql + * ============================================================================================ */ @GetMapping("/day") @Operation(summary = "당일 식단 조회") - public ApplicationResponse> getDayMeal( - @RequestParam @Schema(description = "학생식당 PK", defaultValue = "1") Long restaurantIdx, + public ApplicationResponse>> getDayMeal( + @RequestParam @Schema(description = "학교 이름", defaultValue = "명지대학교") + String universityName, + @RequestParam @Schema(description = "캠퍼스 이름", defaultValue = "인문캠퍼스") String campusName, @RequestParam @Schema(description = "조회하고자 하는 날짜 ( yyyy-MM-dd )", defaultValue = "2023-10-01") String offeredAt) { - return ApplicationResponse.ok(mealService.getDayMealList(restaurantIdx, offeredAt)); + return ApplicationResponse.ok( + mealService.getDayMealList(universityName, campusName, offeredAt)); } /** - * 주간 단위 식단 조회 API - * - * @param restaurantIdx 식당 아이디 - * @param offeredAt 조회날짜 - * @author dldmsql + * ============================================================================================ + * 주간 단위 식단 조회 API ( 리펙토링 이전 코드입니다. ) + * ============================================================================================ */ @GetMapping("/week") @Operation(summary = "주간 식단 조회") public ApplicationResponse> getWeekMeal( - @RequestParam @Schema(description = "학생식당 PK", defaultValue = "1") Long restaurantIdx, + @RequestParam @Schema(description = "학교 이름", defaultValue = "명지대학교") + String universityName, @RequestParam @Schema( description = "조회하고자 하는 시작 날짜 ( yyyy-MM-dd )", defaultValue = "2023-10-01") String offeredAt) { - return ApplicationResponse.ok(mealService.getWeekMealListTest(restaurantIdx, offeredAt)); + return ApplicationResponse.ok(mealService.getWeekMealListTest(universityName, offeredAt)); } } diff --git a/src/main/java/everymeal/server/meal/controller/dto/request/WeekMealRegisterReq.java b/src/main/java/everymeal/server/meal/controller/dto/request/WeekMealRegisterReq.java index ea23943..864b632 100644 --- a/src/main/java/everymeal/server/meal/controller/dto/request/WeekMealRegisterReq.java +++ b/src/main/java/everymeal/server/meal/controller/dto/request/WeekMealRegisterReq.java @@ -6,4 +6,6 @@ public record WeekMealRegisterReq( @Schema(description = "등록하고자 하는 식단 데이터 객체") List registerReqList, - @Schema(description = "학생식당 PK", defaultValue = "1") Long restaurantIdx) {} + @Schema(description = "학생식당 PK", defaultValue = "1") Long restaurantIdx, + @Schema(description = "학교 이름", defaultValue = "명지대학교 인문캠퍼스") String universityName, + @Schema(description = "학생식당 이름", defaultValue = "MCC 식당") String restaurantName) {} diff --git a/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java b/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java index 5998f70..d274fc3 100644 --- a/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java +++ b/src/main/java/everymeal/server/meal/controller/dto/response/DayMealListGetRes.java @@ -9,14 +9,15 @@ public record DayMealListGetRes( Long mealIdx, - String menu, - MealType mealType, - MealStatus mealStatus, @JsonFormat( shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "Asia/Seoul") LocalDate offeredAt, + MealStatus mealStatus, + MealType mealType, + String menu, Double price, MealCategory category, - String restaurantName) {} + String restaurantName, + String universityName) {} diff --git a/src/main/java/everymeal/server/meal/repository/MealDao.java b/src/main/java/everymeal/server/meal/repository/MealDao.java new file mode 100644 index 0000000..350fe3b --- /dev/null +++ b/src/main/java/everymeal/server/meal/repository/MealDao.java @@ -0,0 +1,64 @@ +package everymeal.server.meal.repository; + + +import everymeal.server.meal.controller.dto.response.DayMealListGetRes; +import jakarta.persistence.EntityManager; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MealDao { + private final EntityManager em; + + public Map> findDayListByOfferedAtAndUniversity( + Optional offeredAt, + Optional universityName, + Optional campusName) { + // 쿼리 생성 + String query = + "SELECT NEW everymeal.server.meal.controller.dto.response.DayMealListGetRes(" + + " COALESCE(m.idx, 0) AS mealIdx," + + " COALESCE(m.offeredAt, :offeredAt) AS offeredAt," + + " COALESCE(m.mealStatus, 'OPEN') AS mealStatus," + + " meal_types.mealType," + + " COALESCE(m.menu, '등록된 식단이 없습니다.') AS menu," + + " COALESCE(m.price, 0) AS price," + + " COALESCE(m.category, 'DEFAULT') AS category," + + " r.name AS restaurantName," + + " CONCAT(u.name, ' ', u.campusName) AS universityName) " + + "FROM Restaurant r " + + "JOIN (SELECT DISTINCT mealType AS mealType FROM Meal) AS meal_types ON 1=1 " + + "LEFT JOIN Meal m ON m.restaurant = r AND m.offeredAt = DATE(:offeredAt) AND m.mealType = meal_types.mealType " + + "INNER JOIN University u ON r.university = u " + + "WHERE " + + " u.name = :universityName AND u.campusName = :campusName " + + "ORDER BY r.name ASC"; + // 쿼리 옵션 추가 및 실행 + List result = + em.createQuery(query, DayMealListGetRes.class) + .setParameter("offeredAt", offeredAt.orElse(null)) + .setParameter("universityName", universityName.orElse(null)) + .setParameter("campusName", campusName.orElse(null)) + .getResultList(); + // 결과 반환용 변수 선언 + Map> map = new HashMap<>(); + for (DayMealListGetRes meal : result) { + String restaurantName = meal.restaurantName(); + + // 해당 레스토랑 이름으로 맵에서 리스트를 가져오거나 없으면 새 리스트 생성 + List restaurantList = + map.computeIfAbsent(restaurantName, k -> new ArrayList<>()); + + // 현재 Meal을 레스토랑 리스트에 추가 + restaurantList.add(meal); + } + + return map; + } +} diff --git a/src/main/java/everymeal/server/meal/repository/MealRepository.java b/src/main/java/everymeal/server/meal/repository/MealRepository.java index bdb6299..e8e911c 100644 --- a/src/main/java/everymeal/server/meal/repository/MealRepository.java +++ b/src/main/java/everymeal/server/meal/repository/MealRepository.java @@ -1,7 +1,35 @@ package everymeal.server.meal.repository; +import everymeal.server.meal.controller.dto.response.DayMealListGetRes; import everymeal.server.meal.entity.Meal; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; -public interface MealRepository extends JpaRepository {} +public interface MealRepository extends JpaRepository { + @Query( + value = + "SELECT new everymeal.server.meal.controller.dto.response.DayMealListGetRes(" + + " COALESCE(m.idx, 0) AS mealIdx," + + " COALESCE(m.offeredAt, :offeredAt) AS offeredAt," + + " COALESCE(m.mealStatus, 'OPEN') AS mealStatus," + + " meal_types.mealType," + + " COALESCE(m.menu, '등록된 식단이 없습니다.') AS menu," + + " COALESCE(m.price, 0) AS price," + + " COALESCE(m.category, 'DEFAULT') AS category," + + " r.name AS restaurantName," + + " CONCAT(u.name, ' ', u.campusName) AS universityName) " + + "FROM Restaurant r " + + "JOIN (SELECT DISTINCT mealType AS mealType FROM Meal) AS meal_types ON 1=1 " + + "LEFT JOIN Meal m ON m.restaurant = r AND m.offeredAt = DATE(:offeredAt) AND m.mealType = meal_types.mealType " + + "INNER JOIN University u ON r.university = u " + + "WHERE u.name = :universityName AND u.campusName = :campusName " + + "ORDER BY r.name ASC") + List findDayListByOfferedAtAndUniversity( + @Param("offeredAt") Optional offeredAt, + @Param("universityName") Optional universityName, + @Param("campusName") Optional campusName); +} diff --git a/src/main/java/everymeal/server/meal/repository/MealRepositoryCustom.java b/src/main/java/everymeal/server/meal/repository/MealRepositoryCustom.java index cc116e3..589f81e 100644 --- a/src/main/java/everymeal/server/meal/repository/MealRepositoryCustom.java +++ b/src/main/java/everymeal/server/meal/repository/MealRepositoryCustom.java @@ -5,7 +5,6 @@ import everymeal.server.meal.controller.dto.response.WeekMealListGetRes; import everymeal.server.meal.entity.Meal; import everymeal.server.meal.entity.MealType; -import everymeal.server.meal.entity.Restaurant; import java.time.LocalDate; import java.util.List; import org.springframework.stereotype.Repository; @@ -13,10 +12,10 @@ @Repository public interface MealRepositoryCustom { List findAllByOfferedAtOnDateAndMealType( - LocalDate offeredAt, MealType mealType, Restaurant restaurant); - - List findAllByOfferedAtOnDate(LocalDate offeredAt, Restaurant restaurant); + LocalDate offeredAt, MealType mealType, String universityName); + /** 23.11.16 기준 미사용 쿼리 */ + List findAllByOfferedAtOnDate(LocalDate offeredAt, String universityName); List getWeekMealList( - Restaurant restaurant, LocalDate mondayInstant, LocalDate sundayInstant); + String universityName, LocalDate mondayInstant, LocalDate sundayInstant); } diff --git a/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java b/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java index 7ae8345..add87a1 100644 --- a/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java +++ b/src/main/java/everymeal/server/meal/repository/MealRepositoryImpl.java @@ -14,11 +14,11 @@ import everymeal.server.meal.entity.MealStatus; import everymeal.server.meal.entity.MealType; import everymeal.server.meal.entity.QMeal; -import everymeal.server.meal.entity.Restaurant; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; +import org.h2.util.StringUtils; @RequiredArgsConstructor public class MealRepositoryImpl implements MealRepositoryCustom { @@ -35,19 +35,19 @@ public class MealRepositoryImpl implements MealRepositoryCustom { * * @param offeredAt 제공일자 * @param mealType 식사구분 ( 조식/중식/석식 ) - * @param restaurant 학생식당 + * @param universityName 학교 * @return List * ========================================================================================= */ @Override public List findAllByOfferedAtOnDateAndMealType( - LocalDate offeredAt, MealType mealType, Restaurant restaurant) { + LocalDate offeredAt, MealType mealType, String universityName) { QMeal qMeal = meal; var queryResult = jpaQueryFactory .selectFrom(qMeal) .where( - qMeal.restaurant.eq(restaurant), + isEqUniversityName(universityName), isEqOfferedAt(offeredAt), isEqMealType(mealType)) .fetch(); @@ -59,33 +59,35 @@ public List findAllByOfferedAtOnDateAndMealType( * 일별 식사구분에 따른 식사 데이터 조회
* * @param offeredAt 제공일자 - * @param restaurant 식당 + * @param universityName 식당 * @return List * ========================================================================================= */ @Override public List findAllByOfferedAtOnDate( - LocalDate offeredAt, Restaurant restaurant) { + LocalDate offeredAt, String universityName) { QMeal qMeal = meal; var queryResult = jpaQueryFactory .selectFrom(qMeal) - .where(qMeal.restaurant.eq(restaurant), isEqOfferedAt(offeredAt)) + .where(isEqUniversityName(universityName), isEqOfferedAt(offeredAt)) .transform( groupBy(qMeal.mealType) - .as( + .as( // groupby 식당으로! GroupBy.list( Projections.constructor( DayMealListGetRes.class, qMeal.idx.as("mealIdx"), - qMeal.menu.as("menu"), - qMeal.mealType.as("mealType"), - qMeal.mealStatus.as("mealStatus"), qMeal.offeredAt.as("offeredAt"), + qMeal.mealStatus.as("mealStatus"), + qMeal.mealType.as("mealType"), + qMeal.menu.as("menu"), qMeal.price.as("price"), qMeal.category.as("category"), qMeal.restaurant.name.as( - "restaurantName"))))); + "restaurantName"), + qMeal.restaurant.university.name.as( + "universityName"))))); List resultList = new ArrayList<>(); for (MealType mealType : MealType.values()) { @@ -94,13 +96,14 @@ public List findAllByOfferedAtOnDate( resultList.add( new DayMealListGetRes( 0L, - UN_REGISTERED_MEAL, - mealType, - MealStatus.CLOSED, offeredAt, + MealStatus.CLOSED, + mealType, + UN_REGISTERED_MEAL, 0.0, MealCategory.DEFAULT, - restaurant.getName())); + universityName, + universityName)); } else { resultList.addAll(dayMeals); } @@ -111,7 +114,7 @@ public List findAllByOfferedAtOnDate( * ============================================================================================ * 주간 식사 데이터 조회
* - * @param restaurant 식당 + * @param universityName * @param monday 월요일 * @param sunday 일요일 * @return List @@ -119,15 +122,12 @@ public List findAllByOfferedAtOnDate( */ @Override public List getWeekMealList( - Restaurant restaurant, LocalDate monday, LocalDate sunday) { + String universityName, LocalDate monday, LocalDate sunday) { QMeal qMeal = meal; var transform = jpaQueryFactory .selectFrom(qMeal) - .where( - qMeal.restaurant - .eq(restaurant) - .and(qMeal.offeredAt.between(monday, sunday))) + .where(qMeal.offeredAt.between(monday, sunday)) .transform( groupBy(qMeal.offeredAt, qMeal.mealType) .as( @@ -135,14 +135,17 @@ public List getWeekMealList( Projections.constructor( DayMealListGetRes.class, qMeal.idx.as("mealIdx"), - qMeal.menu.as("menu"), - qMeal.mealType.as("mealType"), - qMeal.mealStatus.as("mealStatus"), qMeal.offeredAt.as("offeredAt"), + qMeal.mealStatus.as("mealStatus"), + qMeal.mealType.as("mealType"), + qMeal.menu.as("menu"), qMeal.price.as("price"), qMeal.category.as("category"), qMeal.restaurant.name.as( - "restaurantName"))))); + "restaurantName"), + qMeal.restaurant.university.name.as( + "universityName"))))); + LocalDate currentInstant = monday; List result = new ArrayList<>(); while (!currentInstant.isAfter(sunday)) { @@ -154,13 +157,14 @@ public List getWeekMealList( dayMealListGetResList.add( new DayMealListGetRes( 0L, - UN_REGISTERED_MEAL, - mealType, - MealStatus.CLOSED, currentInstant, + MealStatus.CLOSED, + mealType, + UN_REGISTERED_MEAL, 0.0, MealCategory.DEFAULT, - restaurant.getName())); + universityName, + universityName)); } else { dayMealListGetResList.addAll(dayMeals); } @@ -184,4 +188,11 @@ private BooleanExpression isEqOfferedAt(LocalDate offeredAt) { private BooleanExpression isEqMealType(MealType mealType) { return meal.mealType.eq(mealType); } + + /** universityName와 동일한 경우 */ + private BooleanExpression isEqUniversityName(String universityName) { + if (StringUtils.isNullOrEmpty(universityName)) { + return null; + } else return meal.restaurant.university.name.eq(universityName); + } } diff --git a/src/main/java/everymeal/server/meal/service/MealService.java b/src/main/java/everymeal/server/meal/service/MealService.java index cc28cc9..3e59d9d 100644 --- a/src/main/java/everymeal/server/meal/service/MealService.java +++ b/src/main/java/everymeal/server/meal/service/MealService.java @@ -7,6 +7,7 @@ import everymeal.server.meal.controller.dto.response.RestaurantListGetRes; import everymeal.server.meal.controller.dto.response.WeekMealListGetRes; import java.util.List; +import java.util.Map; public interface MealService { @@ -16,7 +17,8 @@ public interface MealService { List getRestaurantList(String universityName, String campusName); - List getDayMealList(Long restaurantIdx, String offeredAt); + Map> getDayMealList( + String universityName, String campusName, String offeredAt); - List getWeekMealListTest(Long restaurantIdx, String offeredAt); + List getWeekMealListTest(String universityName, String offeredAt); } diff --git a/src/main/java/everymeal/server/meal/service/MealServiceImpl.java b/src/main/java/everymeal/server/meal/service/MealServiceImpl.java index 84a46d0..6ff82a9 100644 --- a/src/main/java/everymeal/server/meal/service/MealServiceImpl.java +++ b/src/main/java/everymeal/server/meal/service/MealServiceImpl.java @@ -14,6 +14,7 @@ import everymeal.server.meal.entity.MealStatus; import everymeal.server.meal.entity.MealType; import everymeal.server.meal.entity.Restaurant; +import everymeal.server.meal.repository.MealDao; import everymeal.server.meal.repository.MealRepository; import everymeal.server.meal.repository.MealRepositoryCustom; import everymeal.server.meal.repository.RestaurantRepository; @@ -24,6 +25,8 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -33,16 +36,17 @@ @Service public class MealServiceImpl implements MealService { - private final MealRepository mealRepository; - private final MealRepositoryCustom mealRepositoryCustom; - private final UniversityRepository universityRepository; - private final RestaurantRepository restaurantRepository; /** * ============================================================================================ - * GLOBAL STATIC CONSTANTS - * ============================================================================================= + * DI + * ============================================================================================ */ - private final String TIME_PARSING_INFO = "T00:00:00"; + private final MealRepository mealRepository; // 기본 JPA 제공 DAO + + private final MealRepositoryCustom mealRepositoryCustom; // QueryDsl DAO + private final MealDao mealDao; // JPQL DAO + private final UniversityRepository universityRepository; + private final RestaurantRepository restaurantRepository; /** * ============================================================================================ @@ -82,7 +86,7 @@ public Boolean createRestaurant(RestaurantRegisterReq restaurantRegisterReq) { * ============================================================================================ * 학식 식단 등록 ( 주간, 하루 모두 등록 가능 ) * - * @param weekMealRegisterReq 식단 등록 요청 DTO + * @param request 식단 등록 요청 DTO * @return true *

식당이 없는 경우, * @throws ApplicationException 404 존재하지 않는 식당입니다.
@@ -92,33 +96,35 @@ public Boolean createRestaurant(RestaurantRegisterReq restaurantRegisterReq) { */ @Override @Transactional - public Boolean createWeekMeal(WeekMealRegisterReq weekMealRegisterReq) { + public Boolean createWeekMeal(WeekMealRegisterReq request) { // 식당 조회 Restaurant restaurant = restaurantRepository - .findById(weekMealRegisterReq.restaurantIdx()) + .findById(request.restaurantIdx()) .orElseThrow( () -> new ApplicationException(ExceptionList.RESTAURANT_NOT_FOUND)); // REQ 데이터 제공 날짜 기준 오름차순 정렬 - weekMealRegisterReq - .registerReqList() - .sort(Comparator.comparing(MealRegisterReq::offeredAt)); + request.registerReqList().sort(Comparator.comparing(MealRegisterReq::offeredAt)); // 식단 등록 List mealList = new ArrayList<>(); - for (MealRegisterReq req : weekMealRegisterReq.registerReqList()) { + for (MealRegisterReq req : request.registerReqList()) { // 제공날짜, 학생식당, 식사분류가 동일한 데이터가 이미 존재하면, 덮어쓰기 불가능 오류 if (!mealRepositoryCustom .findAllByOfferedAtOnDateAndMealType( - req.offeredAt(), MealType.valueOf(req.mealType()), restaurant) + req.offeredAt(), + MealType.valueOf(req.mealType()), + request.universityName()) .isEmpty()) { throw new ApplicationException(ExceptionList.INVALID_MEAL_OFFEREDAT_REQUEST); } else { LocalDate iOfferedAt = LocalDate.from(req.offeredAt()); + // nullable 데이터 기본값 바인딩 MealStatus mealStatus = req.mealStatus() == null ? MealStatus.OPEN : MealStatus.valueOf(req.mealStatus()); Double price = req.price() == null ? 0.0 : req.price(); + // 데이터 생성 Meal meal = Meal.builder() .mealStatus(mealStatus) @@ -166,33 +172,26 @@ public List getRestaurantList(String universityName, Strin * 학식 식단 Day 조회
* 등록되지 않은 식단 데이터는 아침/점심/저녁 포맷팅에 맞게 응답 데이터를 생성해서 반환합니다. * - * @param restaurantIdx 식당 아이디 - * @param offeredAt 제공 일자 --- yyyy-MM-dd - * @return List - *

식당이 없는 경우, - * @throws ApplicationException 404 존재하지 않는 식당입니다.
+ * @param universityName + * @param campusName + * @param offeredAt 제공 일자 --yyyy-MM-dd + * @return Map> * ========================================================================================= */ @Override - public List getDayMealList(Long restaurantIdx, String offeredAt) { - // offeredAt을 LocalDate로 바꿉니다. - LocalDate ldOfferedAt = LocalDate.parse(offeredAt); - - // 학생 식당 등록 여부 판단 - Restaurant restaurant = - restaurantRepository - .findById(restaurantIdx) - .orElseThrow( - () -> new ApplicationException(ExceptionList.RESTAURANT_NOT_FOUND)); - - return mealRepositoryCustom.findAllByOfferedAtOnDate(ldOfferedAt, restaurant); + public Map> getDayMealList( + String universityName, String campusName, String offeredAt) { + return mealDao.findDayListByOfferedAtAndUniversity( + Optional.of(offeredAt), + Optional.ofNullable(universityName), + Optional.ofNullable(campusName)); } /** * ============================================================================================ * 학식 식단 Week 조회
* 등록되지 않은 식단 데이터는 아침/점심/저녁 포맷팅에 맞게 응답 데이터를 생성해서 반환합니다. * - * @param restaurantIdx 식당 아이디 + * @param universityName 식당 아이디 * @param offeredAt 제공 일자 --- yyyy-MM-dd * @return List *

식당이 없는 경우, @@ -200,12 +199,7 @@ public List getDayMealList(Long restaurantIdx, String offered * ========================================================================================= */ @Override - public List getWeekMealListTest(Long restaurantIdx, String offeredAt) { - Restaurant restaurant = - restaurantRepository - .findById(restaurantIdx) - .orElseThrow( - () -> new ApplicationException(ExceptionList.RESTAURANT_NOT_FOUND)); + public List getWeekMealListTest(String universityName, String offeredAt) { // 현재 날짜와 시간을 가져옵니다. LocalDate ldOfferedAt = LocalDate.parse(offeredAt); @@ -231,6 +225,6 @@ public List getWeekMealListTest(Long restaurantIdx, String o DayOfWeek.SUNDAY.getValue() - (long) currentDayOfWeek.getValue()); } - return mealRepositoryCustom.getWeekMealList(restaurant, monday, sunday); + return mealRepositoryCustom.getWeekMealList(universityName, monday, sunday); } } diff --git a/src/test/java/everymeal/server/meal/controller/MealControllerTest.java b/src/test/java/everymeal/server/meal/controller/MealControllerTest.java index 1fb94ae..3cfad85 100644 --- a/src/test/java/everymeal/server/meal/controller/MealControllerTest.java +++ b/src/test/java/everymeal/server/meal/controller/MealControllerTest.java @@ -37,7 +37,7 @@ void createWeekMeal() throws Exception { MealCategory.DEFAULT.name()); list.add(mealReq); } - WeekMealRegisterReq req = new WeekMealRegisterReq(list, 1L); + WeekMealRegisterReq req = new WeekMealRegisterReq(list, 1L, "명지대학교", "MCC 식당"); // when-then mockMvc.perform( @@ -84,13 +84,15 @@ void getRestaurants() throws Exception { @Test void getDayMeal() throws Exception { // given - Long restaurantIdx = 1L; + String universityName = "명지대학교"; + String campusName = "인문캠퍼스"; String offeredAt = "2023-10-01"; // when-then mockMvc.perform( get("/api/v1/meals/day") - .param("restaurantIdx", String.valueOf(restaurantIdx)) + .param("universityName", universityName) + .param("campusName", campusName) .param("offeredAt", offeredAt) .contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()) @@ -101,13 +103,13 @@ void getDayMeal() throws Exception { @Test void getWeekMeal() throws Exception { // given - Long restaurantIdx = 1L; + String universityName = "명지대학교"; String offeredAt = "2023-10-01"; // when-then mockMvc.perform( get("/api/v1/meals/week") - .param("restaurantIdx", String.valueOf(restaurantIdx)) + .param("universityName", universityName) .param("offeredAt", offeredAt) .contentType(MediaType.APPLICATION_JSON)) .andDo(MockMvcResultHandlers.print()) diff --git a/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java b/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java index 9c6c9df..6ccf7b7 100644 --- a/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java +++ b/src/test/java/everymeal/server/meal/service/MealServiceImplTest.java @@ -26,6 +26,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -100,7 +101,9 @@ void createWeekMeal() throws Exception { MealCategory.DEFAULT.name()); list.add(mealReq); } - WeekMealRegisterReq req = new WeekMealRegisterReq(list, restaurant.getIdx()); + WeekMealRegisterReq req = + new WeekMealRegisterReq( + list, restaurant.getIdx(), university.getName(), restaurant.getName()); // when Boolean response = mealService.createWeekMeal(req); @@ -158,13 +161,15 @@ void getWeekMealList() throws Exception { list.add(lunch); list.add(dinner); } - WeekMealRegisterReq req = new WeekMealRegisterReq(list, restaurant.getIdx()); + WeekMealRegisterReq req = + new WeekMealRegisterReq( + list, restaurant.getIdx(), university.getName(), restaurant.getName()); mealService.createWeekMeal(req); // when String offeredAt = today.toString().split("T")[0]; List response = - mealService.getWeekMealListTest(restaurant.getIdx(), offeredAt); + mealService.getWeekMealListTest(req.universityName(), offeredAt); // then assertEquals(response.size(), 7); @@ -186,28 +191,27 @@ void getDayMealList() throws Exception { university, restaurantRegisterReq.address(), restaurantRegisterReq.restaurantName())); - List list = new ArrayList<>(); + LocalDate today = LocalDate.now(); - MealRegisterReq mealReq = - new MealRegisterReq( - "갈비탕, 깍두기, 흰쌀밥", - MealType.BREAKFAST.name(), - MealStatus.OPEN.name(), - today, - 10000.0, - MealCategory.DEFAULT.name()); - list.add(mealReq); - WeekMealRegisterReq req = new WeekMealRegisterReq(list, restaurant.getIdx()); - mealService.createWeekMeal(req); + + mealRepository.save( + Meal.builder() + .mealType(MealType.BREAKFAST) + .mealStatus(MealStatus.OPEN) + .menu("갈비탕, 깍두기, 흰쌀밥") + .offeredAt(today) + .price(0.0) + .category(MealCategory.DEFAULT) + .build()); // when String offeredAt = LocalDate.now().toString().split("T")[0]; - List response = - mealService.getDayMealList(restaurant.getIdx(), offeredAt); + Map> response = + mealService.getDayMealList( + university.getName(), university.getCampusName(), offeredAt); // then - assertEquals(response.size(), 3); - assertEquals(response.get(1).menu(), "등록된 식단이 없습니다."); + assertEquals(response.size(), 1); } @DisplayName("학교별 학생 식당 조회") @@ -270,7 +274,7 @@ void createWeekMealWhenRestaurantIsNotFound() throws Exception { MealCategory.DEFAULT.name()); list.add(mealReq); } - WeekMealRegisterReq invalidReq = new WeekMealRegisterReq(list, 9999L); + WeekMealRegisterReq invalidReq = new WeekMealRegisterReq(list, 9999L, "명지대학교", "없는식당"); // when-then ApplicationException applicationException = @@ -322,7 +326,9 @@ void createWeekMealBeforeLastMealOfferedAt() throws Exception { MealCategory.DEFAULT.name()); list.add(mealReq); } - WeekMealRegisterReq invalidReq = new WeekMealRegisterReq(list, restaurant.getIdx()); + WeekMealRegisterReq invalidReq = + new WeekMealRegisterReq( + list, restaurant.getIdx(), university.getName(), restaurant.getName()); ApplicationException applicationException = assertThrows( From a29e4c84f7d996612944a52cf6cd46ce49b92f89 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 16 Nov 2023 23:03:15 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20#56=20dao=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=95=EC=97=90=EC=84=9C=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 897c29f..b71abe6 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ sonar { property 'sonar.language', 'java' property 'sonar.sourceEncoding', 'UTF-8' property("sonar.test.inclusions", "**/*Test.java") - property "sonar.exclusions", "**/test/**, **/*Application*.java, **/dto/**, **/entity/**, **/*Exception*.java, **/*RepositoryImpl.java, **/global/**, **/resources/**" + property "sonar.exclusions", "**/test/**, **/*Application*.java, **/dto/**, **/entity/**, **/*Exception*.java, **/*RepositoryImpl.java, **/global/**, **/resources/**, **/*Dao*.java" property "sonar.java.coveragePlugin", "jacoco" property 'sonar.coverage.jacoco.xmlReportPaths', 'build/reports/jacoco/test/jacocoTestReport.xml' } @@ -122,6 +122,7 @@ jacocoTestReport { "**/exception/**", "**/repository/**", "**/global/*", + "**/*Dao*", ]) })) } @@ -155,6 +156,7 @@ jacocoTestCoverageVerification { "**/exception/**", "**/repository/**", "**/global/*", + "**/*Dao*", ]) })) } From 752b21ff48be21650f6ead790c785494accccc6e Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 16 Nov 2023 23:16:02 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20#56=20=EC=A3=BC=EA=B0=84=20?= =?UTF-8?q?=EC=8B=9D=EB=8B=B9=20=EB=93=B1=EB=A1=9D=20bulk=20insert?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/meal/repository/MealDao.java | 62 ++++++++++++++----- .../server/meal/service/MealServiceImpl.java | 2 +- src/main/resources/application-local.yml | 2 +- 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/main/java/everymeal/server/meal/repository/MealDao.java b/src/main/java/everymeal/server/meal/repository/MealDao.java index 350fe3b..57cdaf2 100644 --- a/src/main/java/everymeal/server/meal/repository/MealDao.java +++ b/src/main/java/everymeal/server/meal/repository/MealDao.java @@ -2,43 +2,71 @@ import everymeal.server.meal.controller.dto.response.DayMealListGetRes; +import everymeal.server.meal.entity.Meal; import jakarta.persistence.EntityManager; +import java.sql.PreparedStatement; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository @RequiredArgsConstructor public class MealDao { private final EntityManager em; + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void saveAll(List mealList){ + String sql = "INSERT INTO meal ( meal_status, meal_type, menu, offered_at, price, restaurant_idx, category) " + + "VALUES ( ?, ?, ?, ?, ?, ?, ?)"; + + jdbcTemplate.batchUpdate(sql, + mealList, + mealList.size(), + (PreparedStatement ps, Meal meal) -> { + ps.setString(1, meal.getMealStatus().toString()); + ps.setString(2, meal.getMealType().toString()); + ps.setString(3, meal.getMenu()); + ps.setString(4, meal.getOfferedAt().toString()); + ps.setDouble(5, meal.getPrice()); + ps.setLong(6, meal.getRestaurant().getIdx()); + ps.setString(7, meal.getCategory().toString()); + + }); + } + public Map> findDayListByOfferedAtAndUniversity( Optional offeredAt, Optional universityName, Optional campusName) { // 쿼리 생성 String query = - "SELECT NEW everymeal.server.meal.controller.dto.response.DayMealListGetRes(" - + " COALESCE(m.idx, 0) AS mealIdx," - + " COALESCE(m.offeredAt, :offeredAt) AS offeredAt," - + " COALESCE(m.mealStatus, 'OPEN') AS mealStatus," - + " meal_types.mealType," - + " COALESCE(m.menu, '등록된 식단이 없습니다.') AS menu," - + " COALESCE(m.price, 0) AS price," - + " COALESCE(m.category, 'DEFAULT') AS category," - + " r.name AS restaurantName," - + " CONCAT(u.name, ' ', u.campusName) AS universityName) " - + "FROM Restaurant r " - + "JOIN (SELECT DISTINCT mealType AS mealType FROM Meal) AS meal_types ON 1=1 " - + "LEFT JOIN Meal m ON m.restaurant = r AND m.offeredAt = DATE(:offeredAt) AND m.mealType = meal_types.mealType " - + "INNER JOIN University u ON r.university = u " - + "WHERE " - + " u.name = :universityName AND u.campusName = :campusName " - + "ORDER BY r.name ASC"; + """ + SELECT NEW everymeal.server.meal.controller.dto.response.DayMealListGetRes( + COALESCE(m.idx, 0) AS mealIdx, + COALESCE(m.offeredAt, :offeredAt) AS offeredAt, + COALESCE(m.mealStatus, 'OPEN') AS mealStatus, + meal_types.mealType, + COALESCE(m.menu, '등록된 식단이 없습니다.') AS menu, + COALESCE(m.price, 0) AS price, + COALESCE(m.category, 'DEFAULT') AS category, + r.name AS restaurantName, + CONCAT(u.name, ' ', u.campusName) AS universityName) + FROM Restaurant r + JOIN (SELECT DISTINCT mealType AS mealType FROM Meal) AS meal_types ON 1=1 + LEFT JOIN Meal m ON m.restaurant = r AND m.offeredAt = DATE(:offeredAt) AND m.mealType = meal_types.mealType + INNER JOIN University u ON r.university = u + WHERE + u.name = :universityName AND u.campusName = :campusName + ORDER BY r.name ASC + """; // 쿼리 옵션 추가 및 실행 List result = em.createQuery(query, DayMealListGetRes.class) diff --git a/src/main/java/everymeal/server/meal/service/MealServiceImpl.java b/src/main/java/everymeal/server/meal/service/MealServiceImpl.java index 6ff82a9..9536942 100644 --- a/src/main/java/everymeal/server/meal/service/MealServiceImpl.java +++ b/src/main/java/everymeal/server/meal/service/MealServiceImpl.java @@ -138,7 +138,7 @@ public Boolean createWeekMeal(WeekMealRegisterReq request) { mealList.add(meal); } } - mealRepository.saveAll(mealList); + mealDao.saveAll(mealList); return true; } /** diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 3d3b77f..9bb3638 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -1,6 +1,6 @@ spring: datasource: - url: jdbc:mysql://localhost:3306/everymeal?characterEncoding=UTF-8&useUnicode=true&serverTimezone=Asia/Seoul + url: jdbc:mysql://localhost:3306/everymeal?characterEncoding=UTF-8&useUnicode=true&serverTimezone=Asia/Seoul&rewriteBatchedStatements=true username: root password: 1q2w3e4r! driver-class-name: com.mysql.cj.jdbc.Driver From ea07745362fe1f6efb476ec5b07cb1456aa49dbf Mon Sep 17 00:00:00 2001 From: dldmsql Date: Thu, 16 Nov 2023 23:16:47 +0900 Subject: [PATCH 12/12] chore: #56 spotless --- .../server/meal/repository/MealDao.java | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/main/java/everymeal/server/meal/repository/MealDao.java b/src/main/java/everymeal/server/meal/repository/MealDao.java index 57cdaf2..fcc3b9a 100644 --- a/src/main/java/everymeal/server/meal/repository/MealDao.java +++ b/src/main/java/everymeal/server/meal/repository/MealDao.java @@ -23,23 +23,24 @@ public class MealDao { private final JdbcTemplate jdbcTemplate; @Transactional - public void saveAll(List mealList){ - String sql = "INSERT INTO meal ( meal_status, meal_type, menu, offered_at, price, restaurant_idx, category) " - + "VALUES ( ?, ?, ?, ?, ?, ?, ?)"; + public void saveAll(List mealList) { + String sql = + "INSERT INTO meal ( meal_status, meal_type, menu, offered_at, price, restaurant_idx, category) " + + "VALUES ( ?, ?, ?, ?, ?, ?, ?)"; - jdbcTemplate.batchUpdate(sql, - mealList, - mealList.size(), - (PreparedStatement ps, Meal meal) -> { - ps.setString(1, meal.getMealStatus().toString()); - ps.setString(2, meal.getMealType().toString()); - ps.setString(3, meal.getMenu()); - ps.setString(4, meal.getOfferedAt().toString()); - ps.setDouble(5, meal.getPrice()); - ps.setLong(6, meal.getRestaurant().getIdx()); - ps.setString(7, meal.getCategory().toString()); - - }); + jdbcTemplate.batchUpdate( + sql, + mealList, + mealList.size(), + (PreparedStatement ps, Meal meal) -> { + ps.setString(1, meal.getMealStatus().toString()); + ps.setString(2, meal.getMealType().toString()); + ps.setString(3, meal.getMenu()); + ps.setString(4, meal.getOfferedAt().toString()); + ps.setDouble(5, meal.getPrice()); + ps.setLong(6, meal.getRestaurant().getIdx()); + ps.setString(7, meal.getCategory().toString()); + }); } public Map> findDayListByOfferedAtAndUniversity( @@ -48,7 +49,7 @@ public Map> findDayListByOfferedAtAndUniversity( Optional campusName) { // 쿼리 생성 String query = - """ + """ SELECT NEW everymeal.server.meal.controller.dto.response.DayMealListGetRes( COALESCE(m.idx, 0) AS mealIdx, COALESCE(m.offeredAt, :offeredAt) AS offeredAt, @@ -58,14 +59,14 @@ public Map> findDayListByOfferedAtAndUniversity( COALESCE(m.price, 0) AS price, COALESCE(m.category, 'DEFAULT') AS category, r.name AS restaurantName, - CONCAT(u.name, ' ', u.campusName) AS universityName) - FROM Restaurant r - JOIN (SELECT DISTINCT mealType AS mealType FROM Meal) AS meal_types ON 1=1 - LEFT JOIN Meal m ON m.restaurant = r AND m.offeredAt = DATE(:offeredAt) AND m.mealType = meal_types.mealType - INNER JOIN University u ON r.university = u - WHERE - u.name = :universityName AND u.campusName = :campusName - ORDER BY r.name ASC + CONCAT(u.name, ' ', u.campusName) AS universityName) + FROM Restaurant r + JOIN (SELECT DISTINCT mealType AS mealType FROM Meal) AS meal_types ON 1=1 + LEFT JOIN Meal m ON m.restaurant = r AND m.offeredAt = DATE(:offeredAt) AND m.mealType = meal_types.mealType + INNER JOIN University u ON r.university = u + WHERE + u.name = :universityName AND u.campusName = :campusName + ORDER BY r.name ASC """; // 쿼리 옵션 추가 및 실행 List result =