From 2c0613bb40bcfa4443a55c9a62e73c06a008a3a3 Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Tue, 4 Feb 2025 21:51:08 +0000 Subject: [PATCH 1/7] feat: Added requests, responses, and search conditions --- .../foods/domain/FoodSearchCondition.java | 20 ++++++++ .../dto/request/CreateFoodRequestDTO.java | 50 +++++++++++++++++++ .../dto/request/UpdateFoodRequestDTO.java | 37 ++++++++++++++ .../foods/dto/response/FoodDTO.java | 46 +++++++++++++++-- 4 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java create mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java create mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/UpdateFoodRequestDTO.java diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java new file mode 100644 index 0000000..e2d15a9 --- /dev/null +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java @@ -0,0 +1,20 @@ +package com.f_lab.joyeuse_planete.foods.domain; + + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class FoodSearchCondition { + // TODO 거리 추가 구현 + // 위도 경도 DEFAULT SET TO LONDON + Double lat = 51.5072; + Double lon = -0.118092; + String search; + int page = 0; + int size = 25; + List sortBy = List.of("RATE_HIGH"); +} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java new file mode 100644 index 0000000..87441e4 --- /dev/null +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java @@ -0,0 +1,50 @@ +package com.f_lab.joyeuse_planete.foods.dto.request; + +import com.f_lab.joyeuse_planete.core.domain.Currency; +import com.f_lab.joyeuse_planete.core.domain.Food; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CreateFoodRequestDTO { + + @JsonProperty("food_name") + private String foodName; + + @JsonProperty("price") + private BigDecimal price; + + @JsonProperty("total_quantity") + private int totalQuantity; + + @JsonProperty("currency_code") + private String currencyCode; + + @JsonProperty("collection_start") + private LocalDateTime collectionStartTime; + + @JsonProperty("collection_end") + private LocalDateTime collectionEndTime; + + public Food toEntity(Currency currency) { + return Food.builder() + .foodName(foodName) + .price(price) + .totalQuantity(totalQuantity) + .currency(currency) + .collectionStartTime(collectionStartTime) + .collectionEndTime(collectionEndTime) + .build(); + } +} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/UpdateFoodRequestDTO.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/UpdateFoodRequestDTO.java new file mode 100644 index 0000000..f4e1da6 --- /dev/null +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/UpdateFoodRequestDTO.java @@ -0,0 +1,37 @@ +package com.f_lab.joyeuse_planete.foods.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + + +@Builder +@Getter @Setter +@NoArgsConstructor +@AllArgsConstructor +public class UpdateFoodRequestDTO { + + @JsonProperty("food_name") + private String foodName; + + @JsonProperty("price") + private BigDecimal price; + + @JsonProperty("total_quantity") + private int totalQuantity; + + @JsonProperty("currency_code") + private String currencyCode; + + @JsonProperty("collection_start") + private LocalDateTime collectionStartTime; + + @JsonProperty("collection_end") + private LocalDateTime collectionEndTime; +} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java index c05c60c..a6b90ea 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java @@ -2,19 +2,17 @@ import com.f_lab.joyeuse_planete.core.domain.Food; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.AllArgsConstructor; +import com.querydsl.core.annotations.QueryProjection; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; - +import java.time.LocalDateTime; @Data -@Builder @NoArgsConstructor -@AllArgsConstructor public class FoodDTO { @JsonProperty("food_id") @@ -41,16 +39,56 @@ public class FoodDTO { @JsonProperty("currency_symbol") private String currencySymbol; + @JsonProperty("rate") + private double rate; + + @JsonProperty("collection_start") + private LocalDateTime collectionStartTime; + + @JsonProperty("collection_end") + private LocalDateTime collectionEndTime; + + @Builder + @QueryProjection + public FoodDTO( + Long foodId, + Long storeId, + Long currencyId, + String foodName, + BigDecimal price, + int totalQuantity, + String currencyCode, + String currencySymbol, + double rate, + LocalDateTime collectionStartTime, + LocalDateTime collectionEndTime + ) { + this.foodId = foodId; + this.storeId = storeId; + this.currencyId = currencyId; + this.foodName = foodName; + this.price = price; + this.totalQuantity = totalQuantity; + this.currencyCode = currencyCode; + this.currencySymbol = currencySymbol; + this.rate = rate; + this.collectionStartTime = collectionStartTime; + this.collectionEndTime = collectionEndTime; + } + public static FoodDTO from(Food food) { return FoodDTO.builder() .foodId(food.getId()) .storeId(food.getStore().getId()) .currencyId(food.getCurrency().getId()) .foodName(food.getFoodName()) + .rate(food.getRate()) .price(food.getPrice()) .totalQuantity(food.getTotalQuantity()) .currencyCode(food.getCurrency().getCurrencyCode()) .currencySymbol(food.getCurrency().getCurrencySymbol()) + .collectionStartTime(food.getCollectionStartTime()) + .collectionEndTime(food.getCollectionEndTime()) .build(); } } From 23d004be0aec7e22f5578b0d3f25cefb99fbaaaa Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Tue, 4 Feb 2025 21:59:32 +0000 Subject: [PATCH 2/7] fix: Added all changes on the controller, services, repository and tests --- .../foods/controller/FoodController.java | 59 ++- .../foods/repository/CurrencyRepository.java | 11 + .../repository/FoodCustomRepository.java | 11 + .../repository/FoodCustomRepositoryImpl.java | 100 ++++ .../foods/repository/FoodRepository.java | 2 +- .../foods/service/FoodService.java | 42 ++ .../foods/repository/FoodRepositoryTest.java | 470 ++++++++++++++++++ .../foods/service/FoodServiceTest.java | 129 ++++- 8 files changed, 793 insertions(+), 31 deletions(-) create mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java create mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepository.java create mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java create mode 100644 foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/controller/FoodController.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/controller/FoodController.java index 8c07267..274c0f7 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/controller/FoodController.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/controller/FoodController.java @@ -1,11 +1,29 @@ package com.f_lab.joyeuse_planete.foods.controller; +import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; +import com.f_lab.joyeuse_planete.foods.dto.request.CreateFoodRequestDTO; +import com.f_lab.joyeuse_planete.foods.dto.request.UpdateFoodRequestDTO; import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; import com.f_lab.joyeuse_planete.foods.service.FoodService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import static com.f_lab.joyeuse_planete.foods.controller.FoodController.ResponseUtil.FOOD_CREATE_SUCCESS; + @RestController @RequiredArgsConstructor @@ -14,6 +32,13 @@ public class FoodController { private final FoodService foodService; + @GetMapping + @ResponseStatus(HttpStatus.OK) + public Page getFoodList(@ModelAttribute FoodSearchCondition condition) { + Pageable pageable = PageRequest.of(condition.getPage(), condition.getSize()); + return foodService.getFoodList(condition, pageable); + } + @GetMapping("/{foodId}") public ResponseEntity getFood(@PathVariable("foodId") Long foodId) { return ResponseEntity @@ -21,10 +46,38 @@ public ResponseEntity getFood(@PathVariable("foodId") Long foodId) { .body(foodService.getFood(foodId)); } + @PostMapping + public ResponseEntity createFood(@RequestBody CreateFoodRequestDTO request) { + foodService.createFood(request); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(FOOD_CREATE_SUCCESS); + } + @PutMapping("/{foodId}") - public ResponseEntity updateFood(@PathVariable("foodId") Long foodId) { + public ResponseEntity updateFood(@PathVariable Long foodId, + @RequestBody UpdateFoodRequestDTO request) { + foodService.updateFood(foodId, request); + return ResponseEntity .status(HttpStatus.OK) - .body(foodService.getFood(foodId)); + .body(ResponseUtil.FOOD_UPDATE_SUCCESS); + } + + @DeleteMapping("/{foodId}") + public ResponseEntity deleteFood(@PathVariable Long foodId) { + foodService.deleteFood(foodId); + + return ResponseEntity + .status(HttpStatus.OK) + .body(ResponseUtil.FOOD_DELETE_SUCCESS); + } + + static abstract class ResponseUtil { + + public static final String FOOD_CREATE_SUCCESS = "성공적으로 생성되었습니다."; + public static final String FOOD_UPDATE_SUCCESS = "업데이트 요청이 완료되었습니다."; + public static final String FOOD_DELETE_SUCCESS = "삭제 요청이 완료되었습니다."; } } diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java new file mode 100644 index 0000000..1fccc22 --- /dev/null +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java @@ -0,0 +1,11 @@ +package com.f_lab.joyeuse_planete.foods.repository; + +import com.f_lab.joyeuse_planete.core.domain.Currency; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CurrencyRepository extends JpaRepository { + + Optional findByCurrencyCode(String currencyCode); +} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepository.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepository.java new file mode 100644 index 0000000..39f5d0d --- /dev/null +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepository.java @@ -0,0 +1,11 @@ +package com.f_lab.joyeuse_planete.foods.repository; + +import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; +import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FoodCustomRepository { + + Page getFoodList(FoodSearchCondition condition, Pageable pageable); +} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java new file mode 100644 index 0000000..8af8de7 --- /dev/null +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java @@ -0,0 +1,100 @@ +package com.f_lab.joyeuse_planete.foods.repository; + +import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; +import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; +import com.f_lab.joyeuse_planete.foods.dto.response.QFoodDTO; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.f_lab.joyeuse_planete.core.domain.QCurrency.currency; +import static com.f_lab.joyeuse_planete.core.domain.QFood.food; +import static com.f_lab.joyeuse_planete.core.domain.QStore.store; + + +public class FoodCustomRepositoryImpl implements FoodCustomRepository { + + private final JPAQueryFactory queryFactory; + private Map sortByMap = new HashMap<>(); + + public FoodCustomRepositoryImpl(EntityManager em) { + queryFactory = new JPAQueryFactory(em); + } + + @PostConstruct + public void init() { + sortByMap.put("RATE_HIGH", food.rate.desc()); + } + + @Override + public Page getFoodList(FoodSearchCondition condition, Pageable pageable) { + List result = queryFactory + .select(new QFoodDTO( + food.id.as("foodId"), + food.store.id.as("storeId"), + food.currency.id.as("currencyId"), + food.foodName.as("foodName"), + food.price, + food.totalQuantity.as("totalQuantity"), + food.currency.currencyCode.as("currencyCode"), + food.currency.currencySymbol.as("currencySymbol"), + food.rate, + food.collectionStartTime, + food.collectionEndTime + )) + .from(food) + .innerJoin(food.currency, currency) + .innerJoin(food.store, store) + .where(Expressions.anyOf( + eqFoodName(condition.getSearch()), + eqFoodTag(condition.getSearch()), + eqStoreName(condition.getSearch())) + ) + .orderBy(getOrders(condition.getSortBy())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long count = queryFactory + .select(food.count()) + .from(food) + .fetch() + .get(0); + + return new PageImpl<>(result, pageable, count); + } + + private BooleanExpression eqFoodName(String search) { + return (search != null) ? food.foodName.containsIgnoreCase(search) : null; + } + + private BooleanExpression eqFoodTag(String search) { + return (search != null) ? food.tags.containsIgnoreCase(search) : null; + } + + private BooleanExpression eqStoreName(String search) { + return (search != null) ? food.store.name.containsIgnoreCase(search) : null; + } + + private OrderSpecifier[] getOrders(List sortBy) { + List list = new ArrayList<>(); + + for (String sort : sortBy) { + if (sortByMap.containsKey(sort)) + list.add(sortByMap.get(sort)); + } + + return list.toArray(OrderSpecifier[]::new); + } +} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepository.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepository.java index 7c24a90..b9fea4b 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepository.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepository.java @@ -16,7 +16,7 @@ import static com.f_lab.joyeuse_planete.core.util.time.TimeConstantsString.FIVE_SECONDS; -public interface FoodRepository extends JpaRepository { +public interface FoodRepository extends JpaRepository, FoodCustomRepository { @RetryOnLockFailure @Lock(LockModeType.PESSIMISTIC_WRITE) diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java index a5450d8..aa477a4 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java @@ -1,12 +1,19 @@ package com.f_lab.joyeuse_planete.foods.service; +import com.f_lab.joyeuse_planete.core.domain.Currency; import com.f_lab.joyeuse_planete.core.domain.Food; import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode; import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; +import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; +import com.f_lab.joyeuse_planete.foods.dto.request.CreateFoodRequestDTO; +import com.f_lab.joyeuse_planete.foods.dto.request.UpdateFoodRequestDTO; import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; +import com.f_lab.joyeuse_planete.foods.repository.CurrencyRepository; import com.f_lab.joyeuse_planete.foods.repository.FoodRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,16 +25,46 @@ public class FoodService { private final FoodRepository foodRepository; + private final CurrencyRepository currencyRepository; public FoodDTO getFood(Long foodId) { return FoodDTO.from(findFood(foodId)); } + public Page getFoodList(FoodSearchCondition condition, Pageable pageable) { + return foodRepository.getFoodList(condition, pageable); + } + + @Transactional + public void createFood(CreateFoodRequestDTO request) { + Currency currency = findCurrency(request.getCurrencyCode()); + Food food = request.toEntity(currency); + + foodRepository.save(food); + } + @Transactional public void deleteFood(Long foodId) { foodRepository.delete(findFood(foodId)); } + @Transactional + public void updateFood(Long foodId, UpdateFoodRequestDTO request) { + Food food = findFood(foodId); + Currency currency = findCurrency(request.getCurrencyCode()); + + food.update( + request.getFoodName(), + request.getPrice(), + request.getTotalQuantity(), + currency, + request.getCollectionStartTime(), + request.getCollectionEndTime() + ); + + foodRepository.save(food); + } + @Transactional public void reserve(Long foodId, int quantity) { Food food = findFoodWithLock(foodId); @@ -51,4 +88,9 @@ private Food findFoodWithLock(Long foodId) { return foodRepository.findFoodByFoodIdWithPessimisticLock(foodId) .orElseThrow(() -> new JoyeusePlaneteApplicationException(ErrorCode.FOOD_NOT_EXIST_EXCEPTION)); } + + private Currency findCurrency(String currencyCode) { + return currencyRepository.findByCurrencyCode(currencyCode) + .orElseThrow(() -> new JoyeusePlaneteApplicationException(ErrorCode.CURRENCY_NOT_EXIST_EXCEPTION)); + } } diff --git a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java new file mode 100644 index 0000000..60ace58 --- /dev/null +++ b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java @@ -0,0 +1,470 @@ +package com.f_lab.joyeuse_planete.foods.repository; + +import com.f_lab.joyeuse_planete.core.domain.Currency; +import com.f_lab.joyeuse_planete.core.domain.Food; +import com.f_lab.joyeuse_planete.core.domain.Store; +import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; +import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; +import jakarta.annotation.Nullable; +import jakarta.persistence.EntityManager; +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; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.Comparator; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DirtiesContext +@Transactional +@SpringBootTest +class FoodRepositoryTest { + + @Autowired + FoodRepository foodRepository; + + @Autowired + private EntityManager em; + + @BeforeEach + void beforeEach() { + foodRepository.saveAll(getFoodList()); + } + + @AfterEach + void afterEach() { + em.createQuery("DELETE FROM Food").executeUpdate(); + em.createQuery("DELETE FROM Store").executeUpdate(); + em.createQuery("DELETE FROM Currency").executeUpdate(); + em.flush(); + } + + @Test + @DisplayName("기본 조건으로 검색했을 때 성공하는 것을 확인") + void testDefaultFoodSearchConditionSuccess() { + // given + FoodSearchCondition condition = createDefaultSearchCondition(); + Pageable pageable = PageRequest.of(condition.getPage(), condition.getSize()); + + List expected = getFoodList().stream() + .sorted(Comparator.comparing(Food::getRate).reversed()) + .map(FoodDTO::from) + .limit(condition.getSize()) + .toList(); + + // when + Page result = foodRepository.getFoodList(condition, pageable); + + // then + assertTrue(result, expected); + } + + @Test + @DisplayName("음식 검색 조건을 변경하였을 때 작동하는 것을 확인") + void testFoodSearchConditionOnSearchSuccess() { + // given + String search = "Asian"; + List sortBy = List.of("RATE_HIGH"); + + FoodSearchCondition condition = createSearchCondition(null, null, search, sortBy); + Pageable pageable = PageRequest.of(condition.getPage(), condition.getSize()); + + List expected = getFoodList().stream() + .filter(f -> + f.getFoodName().toLowerCase().contains(search.toLowerCase()) || + f.getTags().stream().anyMatch(tag -> tag.toLowerCase().contains(search.toLowerCase())) || + f.getStore().getName().toLowerCase().contains(search.toLowerCase())) + .sorted(Comparator.comparing(Food::getRate).reversed()) + .map(FoodDTO::from) + .limit(condition.getSize()) + .toList(); + + // when + Page result = foodRepository.getFoodList(condition, pageable); + + for (FoodDTO foodDTO : result) { + System.out.println("foodDTO = " + foodDTO); + } + + // then + assertTrue(result, expected); + } + + @Test + @DisplayName("페이지 조건을 변경하였을 때 작동하는 것을 확인") + void testFoodSearchConditionOnPageSuccess() { + // given + List sortBy = List.of("RATE_HIGH"); + int page = 1; + int size = 10; + + FoodSearchCondition condition = createSearchCondition(null, null, null, sortBy); + Pageable pageable = PageRequest.of(page, size); + + List expected = getFoodList().stream() + .sorted(Comparator.comparing(Food::getRate).reversed()) + .skip(page * size) + .limit(size) + .map(FoodDTO::from) + .toList(); + + // when + Page result = foodRepository.getFoodList(condition, pageable); + + // then + assertTrue(result, expected); + } + + private void assertTrue(Page result, List expected) { + assertThat(result.getContent()) + .usingRecursiveComparison() + .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) + .withComparatorForType(Double::compare, Double.class) + .comparingOnlyFields("rate") + .isEqualTo(expected); + } + + private FoodSearchCondition createDefaultSearchCondition() { + return new FoodSearchCondition(); + } + + private FoodSearchCondition createSearchCondition( + @Nullable Double lat, + @Nullable Double lon, + @Nullable String search, + @Nullable List sortBy + ) { + FoodSearchCondition condition = new FoodSearchCondition(); + condition.setLat(lat); + condition.setLon(lon); + condition.setSearch(search); + condition.setSortBy(sortBy); + return condition; + } + + private List getFoodList() { + Store store1 = Store.builder().name("Store A").build(); + Store store2 = Store.builder().name("Store B").build(); + Store store3 = Store.builder().name("Store C").build(); + Store store4 = Store.builder().name("Store Asian").build(); + Store store5 = Store.builder().name("Store Asian").build(); + + em.persist(store1); + em.persist(store2); + em.persist(store3); + em.persist(store4); + em.persist(store5); + em.flush(); + + Currency currency = Currency.builder() + .currencyCode("KRW") + .currencySymbol("₩") + .roundingScale(2) + .build(); + + em.persist(currency); + em.flush(); + + return List.of( + Food.builder() + .foodName("Chicken Burger") + .price(BigDecimal.valueOf(1000)) + .rate(4.5) + .store(store1) + .currency(currency) + .totalQuantity(100) + .tags("fast-food,burger,chicken") + .build(), + + Food.builder() + .foodName("Spaghetti Carbonara") + .price(BigDecimal.valueOf(1200)) + .rate(4.2) + .store(store2) + .currency(currency) + .totalQuantity(80) + .tags("pasta,creamy,Italian") + .build(), + + Food.builder() + .foodName("Caesar Salad") + .price(BigDecimal.valueOf(900)) + .rate(4.8) + .store(store3) + .currency(currency) + .totalQuantity(90) + .tags("healthy,salad,fresh") + .build(), + + Food.builder() + .foodName("Pepperoni Pizza") + .price(BigDecimal.valueOf(1500)) + .rate(4.7) + .store(store1) + .currency(currency) + .totalQuantity(60) + .tags("pizza,cheesy,Italian") + .build(), + + Food.builder() + .foodName("Grilled Steak") + .price(BigDecimal.valueOf(2500)) + .rate(4.9) + .store(store2) + .currency(currency) + .totalQuantity(50) + .tags("meat,grill,protein") + .build(), + + Food.builder() + .foodName("Tuna Sandwich") + .price(BigDecimal.valueOf(800)) + .rate(3.9) + .store(store3) + .currency(currency) + .totalQuantity(110) + .tags("sandwich,fish,healthy") + .build(), + + Food.builder() + .foodName("Vegetable Stir Fry") + .price(BigDecimal.valueOf(950)) + .rate(4.1) + .store(store1) + .currency(currency) + .totalQuantity(85) + .tags("vegetarian,stir-fry,Asian") + .build(), + + Food.builder() + .foodName("Beef Tacos") + .price(BigDecimal.valueOf(1100)) + .rate(4.3) + .store(store2) + .currency(currency) + .totalQuantity(70) + .tags("Mexican,taco,spicy") + .build(), + + Food.builder() + .foodName("Chicken Noodles") + .price(BigDecimal.valueOf(1050)) + .rate(4.2) + .store(store3) + .currency(currency) + .totalQuantity(75) + .tags("asian,noodles,chicken") + .build(), + + Food.builder() + .foodName("Margarita Pizza") + .price(BigDecimal.valueOf(1400)) + .rate(4.6) + .store(store1) + .currency(currency) + .totalQuantity(65) + .tags("pizza,tomato,cheese") + .build(), + + Food.builder() + .foodName("Sushi Rolls") + .price(BigDecimal.valueOf(1600)) + .rate(4.8) + .store(store2) + .currency(currency) + .totalQuantity(55) + .tags("sushi,fish,Japanese") + .build(), + + Food.builder() + .foodName("Pancakes with Maple Syrup") + .price(BigDecimal.valueOf(700)) + .rate(4.4) + .store(store3) + .currency(currency) + .totalQuantity(120) + .tags("breakfast,sweet,fluffy") + .build(), + + Food.builder() + .foodName("Lasagna") + .price(BigDecimal.valueOf(1300)) + .rate(4.7) + .store(store1) + .currency(currency) + .totalQuantity(72) + .tags("pasta,Italian,cheese") + .build(), + + Food.builder() + .foodName("Teriyaki Chicken") + .price(BigDecimal.valueOf(1150)) + .rate(4.5) + .store(store2) + .currency(currency) + .totalQuantity(80) + .tags("Asian,sweet,chicken") + .build(), + + Food.builder() + .foodName("Salmon Fillet") + .price(BigDecimal.valueOf(2000)) + .rate(4.9) + .store(store3) + .currency(currency) + .totalQuantity(40) + .tags("fish,protein,healthy") + .build(), + + Food.builder() + .foodName("Crispy French Fries") + .price(BigDecimal.valueOf(600)) + .rate(3.8) + .store(store1) + .currency(currency) + .totalQuantity(150) + .tags("fast-food,potato,fried") + .build(), + + Food.builder() + .foodName("BBQ Ribs") + .price(BigDecimal.valueOf(1800)) + .rate(4.7) + .store(store2) + .currency(currency) + .totalQuantity(50) + .tags("grill,meat,BBQ") + .build(), + + Food.builder() + .foodName("Falafel Wrap") + .price(BigDecimal.valueOf(900)) + .rate(4.0) + .store(store3) + .currency(currency) + .totalQuantity(95) + .tags("vegan,wrap,Mediterranean") + .build(), + + Food.builder() + .foodName("Egg Fried Rice") + .price(BigDecimal.valueOf(850)) + .rate(4.2) + .store(store1) + .currency(currency) + .totalQuantity(105) + .tags("rice,Asian,egg") + .build(), + + Food.builder() + .foodName("Chocolate Cake") + .price(BigDecimal.valueOf(950)) + .rate(4.8) + .store(store2) + .currency(currency) + .totalQuantity(65) + .tags("dessert,chocolate,sweet") + .build(), + + Food.builder() + .foodName("Vanilla Ice Cream") + .price(BigDecimal.valueOf(500)) + .rate(4.6) + .store(store3) + .currency(currency) + .totalQuantity(130) + .tags("dessert,ice-cream,cold") + .build(), + + Food.builder() + .foodName("Strawberry Cheesecake") + .price(BigDecimal.valueOf(1100)) + .rate(4.7) + .store(store1) + .currency(currency) + .totalQuantity(70) + .tags("dessert,cake,strawberry") + .build(), + + Food.builder() + .foodName("Grilled Chicken Breast") + .price(BigDecimal.valueOf(1350)) + .rate(4.5) + .store(store2) + .currency(currency) + .totalQuantity(55) + .tags("protein,chicken,healthy") + .build(), + + Food.builder() + .foodName("Mushroom Soup") + .price(BigDecimal.valueOf(900)) + .rate(4.3) + .store(store3) + .currency(currency) + .totalQuantity(95) + .tags("soup,warm,healthy") + .build(), + + Food.builder() + .foodName("Butter Chicken") + .price(BigDecimal.valueOf(1250)) + .rate(4.6) + .store(store1) + .currency(currency) + .totalQuantity(85) + .tags("Indian,spicy,curry") + .build(), + + Food.builder() + .foodName("Garlic Bread") + .price(BigDecimal.valueOf(700)) + .rate(4.2) + .store(store2) + .currency(currency) + .totalQuantity(120) + .tags("bread,garlic,side-dish") + .build(), + + Food.builder() + .foodName("Asian Bread") + .price(BigDecimal.valueOf(700)) + .rate(4.2) + .store(store2) + .currency(currency) + .totalQuantity(120) + .tags("bread,garlic,side-dish") + .build(), + + Food.builder() + .foodName("Garlic Bread") + .price(BigDecimal.valueOf(700)) + .rate(4.2) + .store(store4) + .currency(currency) + .totalQuantity(120) + .tags("bread,garlic,side-dish") + .build(), + + Food.builder() + .foodName("Garlic Bread") + .price(BigDecimal.valueOf(700)) + .rate(4.2) + .store(store5) + .currency(currency) + .totalQuantity(120) + .tags("bread,garlic,side-dish") + .build() + ); + } +} \ No newline at end of file diff --git a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java index 0a03888..020b864 100644 --- a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java +++ b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java @@ -5,7 +5,10 @@ import com.f_lab.joyeuse_planete.core.domain.Store; import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode; import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; +import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; +import com.f_lab.joyeuse_planete.foods.dto.request.UpdateFoodRequestDTO; import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; +import com.f_lab.joyeuse_planete.foods.repository.CurrencyRepository; import com.f_lab.joyeuse_planete.foods.repository.FoodRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -13,15 +16,18 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import java.math.BigDecimal; + import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class FoodServiceTest { @@ -30,9 +36,11 @@ class FoodServiceTest { FoodService foodService; @Mock FoodRepository foodRepository; + @Mock + CurrencyRepository currencyRepository; @Test - @DisplayName("getFood 호출시 성공") + @DisplayName("getFood() 호출시 성공") void testGetFoodSuccess() { Long foodId = 1L; Food food = createFood(foodId); @@ -111,45 +119,112 @@ void testReleaseSuccess() { assertThat(food.getTotalQuantity()).isEqualTo(totalQuantity + quantity); } + @Test + @DisplayName("updateFood() 호출시 성공") + void testUpdateFoodSuccess() { + // given + Long foodId = 1L; + Food food = createFood(foodId); + String expectedCurrencyCode = "USD"; + Currency currency = createCurrency(); + String originalName = food.getFoodName(); + + UpdateFoodRequestDTO request = createExpectedUpdateFoodRequestDTO(food); + + // when + when(foodRepository.findById(anyLong())).thenReturn(Optional.of(food)); + when(currencyRepository.findByCurrencyCode(anyString())).thenReturn(Optional.of(currency)); + foodService.updateFood(foodId, request); + + // then + assertThat(food.getFoodName()).isEqualTo(originalName + "test"); + assertThat(food.getCurrency().getCurrencyCode()).isEqualTo(expectedCurrencyCode); + } + + @Test + @DisplayName("updateFood() 호출시 실패") + void testUpdateFoodOnNotExistingCurrencyFail() { + // given + Long foodId = 1L; + Food food = createFood(foodId); + UpdateFoodRequestDTO request = createExpectedUpdateFoodRequestDTO(food); + + // when + when(foodRepository.findById(anyLong())).thenReturn(Optional.of(food)); + when(currencyRepository.findByCurrencyCode(anyString())).thenReturn(Optional.empty()); + + // then + assertThatThrownBy(() -> foodService.updateFood(foodId, request)) + .isInstanceOf(JoyeusePlaneteApplicationException.class) + .hasMessage(ErrorCode.CURRENCY_NOT_EXIST_EXCEPTION.getDescription()); + } + + @Test + @DisplayName("foodService 가 올바로 foodRepository 호출하고 Page를 return 하는 것을 확인") + void testGetFoodListSuccess() { + // given + Page expected = Page.empty(); + FoodSearchCondition condition = new FoodSearchCondition(); + Pageable pageable = PageRequest.of(0, 10); + + // when + when(foodRepository.getFoodList(any(), any())).thenReturn(expected); + Page result = foodService.getFoodList(condition, pageable); + + // then + assertThat(result).isEqualTo(expected); + verify(foodRepository, times(1)).getFoodList(condition, pageable); + } + private Food createFood(Long foodId) { - Long storeId = 1L; - Long currencyId = 100L; String foodName = "Pizza"; BigDecimal price = new BigDecimal("9.99"); int totalQuantity = 50; - String currencyCode = "USD"; - String currencySymbol = "$"; - Store store = Store.builder() + return Food.builder() + .id(foodId) + .store(createStore()) + .currency(createCurrency()) + .foodName(foodName) + .rate(4.5) + .price(price) + .totalQuantity(totalQuantity) + .build(); + } + + private Store createStore() { + Long storeId = 1L; + + return Store.builder() .id(storeId) .build(); + } + + private Currency createCurrency() { + Long currencyId = 100L; + String currencyCode = "USD"; + String currencySymbol = "$"; - Currency currency = Currency.builder() + return Currency.builder() .id(currencyId) .currencyCode(currencyCode) .currencySymbol(currencySymbol) .build(); - - return Food.builder() - .id(foodId) - .store(store) - .currency(currency) - .foodName(foodName) - .price(price) - .totalQuantity(totalQuantity) - .build(); } - private FoodDTO createExpectedFoodDTO(Food food) { - return FoodDTO.builder() - .foodId(food.getId()) - .storeId(food.getStore().getId()) - .currencyId(food.getCurrency().getId()) - .foodName(food.getFoodName()) + private UpdateFoodRequestDTO createExpectedUpdateFoodRequestDTO(Food food) { + String TEST_SUFFIX = "test"; + String currencyCode = "USD"; + + return UpdateFoodRequestDTO.builder() + .foodName(food.getFoodName() + TEST_SUFFIX) + .currencyCode(currencyCode) .price(food.getPrice()) .totalQuantity(food.getTotalQuantity()) - .currencyCode(food.getCurrency().getCurrencyCode()) - .currencySymbol(food.getCurrency().getCurrencySymbol()) .build(); } + + private FoodDTO createExpectedFoodDTO(Food food) { + return FoodDTO.from(food); + } } \ No newline at end of file From e0b3199842d43c04177afb9c95b34c9ba485dda6 Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Tue, 4 Feb 2025 22:05:14 +0000 Subject: [PATCH 3/7] update: Updated Order MVC --- .../joyeuse_planete/core/domain/Order.java | 4 +-- .../dto/request/OrderCreateRequestDTO.java | 3 ++ .../orders/dto/response/OrderDTO.java | 10 +++---- .../repository/OrderRepositoryCustom.java | 3 ++ .../repository/OrderRepositoryCustomImpl.java | 29 ++++++++++++++++++- .../orders/service/OrderService.java | 4 +-- .../repository/OrderRepositoryTest.java | 1 - .../orders/service/OrderServiceKafkaTest.java | 15 ++++++---- 8 files changed, 53 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Order.java b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Order.java index 30d2210..8f95fed 100644 --- a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Order.java +++ b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Order.java @@ -45,6 +45,8 @@ public class Order extends BaseTimeEntity { private int quantity; + private double rate; + @Enumerated(EnumType.STRING) private OrderStatus status; @@ -56,8 +58,6 @@ public class Order extends BaseTimeEntity { @JoinColumn(name = "voucher_id") private Voucher voucher; - private LocalDateTime collectionTime; - public BigDecimal calculateTotalCost() { return (voucher != null) ? voucher.apply(food.calculateCost(quantity), food.getCurrency()) diff --git a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/request/OrderCreateRequestDTO.java b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/request/OrderCreateRequestDTO.java index e748e92..22e754a 100644 --- a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/request/OrderCreateRequestDTO.java +++ b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/request/OrderCreateRequestDTO.java @@ -27,6 +27,9 @@ public class OrderCreateRequestDTO { @JsonProperty("store_id") private Long storeId; + @JsonProperty("currency_id") + private Long currencyId; + @JsonProperty("total_cost") private BigDecimal totalCost; diff --git a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/response/OrderDTO.java b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/response/OrderDTO.java index 584053e..a3491f1 100644 --- a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/response/OrderDTO.java +++ b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/dto/response/OrderDTO.java @@ -32,6 +32,9 @@ public class OrderDTO { @JsonProperty("quantity") private int quantity; + @JsonProperty("rate") + private double rate; + @JsonProperty("status") private String status; @@ -44,9 +47,6 @@ public class OrderDTO { @JsonProperty("created_at") private LocalDateTime createdAt; - @JsonProperty("collection_time") - private LocalDateTime collectionTime; - @Builder @QueryProjection public OrderDTO( @@ -56,10 +56,10 @@ public OrderDTO( String currencyCode, String currencySymbol, int quantity, + double rate, String status, Long payment, Long voucher, - LocalDateTime collectionTime, LocalDateTime createdAt ) { @@ -69,10 +69,10 @@ public OrderDTO( this.currencyCode = currencyCode; this.currencySymbol = currencySymbol; this.quantity = quantity; + this.rate = rate; this.status = status; this.payment = payment; this.voucher = voucher; - this.collectionTime = collectionTime; this.createdAt = createdAt; } } diff --git a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustom.java b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustom.java index 312b57b..319a0b8 100644 --- a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustom.java +++ b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustom.java @@ -1,6 +1,8 @@ package com.f_lab.joyeuse_planete.orders.repository; +import com.f_lab.joyeuse_planete.core.domain.Order; import com.f_lab.joyeuse_planete.orders.domain.OrderSearchCondition; +import com.f_lab.joyeuse_planete.orders.dto.request.OrderCreateRequestDTO; import com.f_lab.joyeuse_planete.orders.dto.response.OrderDTO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -10,4 +12,5 @@ public interface OrderRepositoryCustom { // Page findOrders(Long memberId, OrderSearchCondition condition, Pageable pageable); Page findOrders(OrderSearchCondition condition, Pageable pageable); + Order saveOrder(OrderCreateRequestDTO request); } diff --git a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustomImpl.java b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustomImpl.java index 3531598..6def745 100644 --- a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustomImpl.java +++ b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryCustomImpl.java @@ -1,7 +1,10 @@ package com.f_lab.joyeuse_planete.orders.repository; +import com.f_lab.joyeuse_planete.core.domain.Order; import com.f_lab.joyeuse_planete.core.domain.OrderStatus; + import com.f_lab.joyeuse_planete.orders.domain.OrderSearchCondition; +import com.f_lab.joyeuse_planete.orders.dto.request.OrderCreateRequestDTO; import com.f_lab.joyeuse_planete.orders.dto.response.OrderDTO; import com.f_lab.joyeuse_planete.orders.dto.response.QOrderDTO; import com.querydsl.core.types.OrderSpecifier; @@ -9,6 +12,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -44,6 +48,29 @@ public OrderRepositoryCustomImpl(EntityManager em) { this.queryFactory = new JPAQueryFactory(em); } + @Override + public Order saveOrder(OrderCreateRequestDTO request) { + long orderId = queryFactory + .insert(order) + .columns( + order.food.id, + order.totalCost, + order.quantity, + order.status, + order.voucher.id + ) + .values( + request.getFoodId(), + request.getTotalCost(), + request.getQuantity(), + OrderStatus.READY, + request.getVoucherId() + ) + .execute(); + + return queryFactory.selectFrom(order).where(order.id.eq(orderId)).fetchFirst(); + } + @Override public Page findOrders(OrderSearchCondition condition, Pageable pageable) { List results = queryFactory @@ -54,10 +81,10 @@ public Page findOrders(OrderSearchCondition condition, Pageable pageab food.currency.currencyCode, food.currency.currencySymbol, order.quantity, + order.rate, order.status.stringValue(), order.payment.id.as("paymentId"), order.voucher.id.as("voucherId"), - order.collectionTime.as("collectionTime"), order.createdAt.as("createdAt") )) .from(order) diff --git a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/service/OrderService.java b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/service/OrderService.java index 5ee8548..8d218eb 100644 --- a/orders/src/main/java/com/f_lab/joyeuse_planete/orders/service/OrderService.java +++ b/orders/src/main/java/com/f_lab/joyeuse_planete/orders/service/OrderService.java @@ -47,9 +47,9 @@ public void updateOrderStatus(Long orderId, OrderStatus status) { @Transactional public OrderCreateResponseDTO createFoodOrder(OrderCreateRequestDTO request) { - Order order = request.toEntity(); + Order order; try { - orderRepository.save(order); + order = orderRepository.saveOrder(request); } catch (JoyeusePlaneteApplicationException e) { LogUtil.exception("OrderService.createFoodOrder", e); diff --git a/orders/src/test/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryTest.java b/orders/src/test/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryTest.java index ba6165f..f537cea 100644 --- a/orders/src/test/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryTest.java +++ b/orders/src/test/java/com/f_lab/joyeuse_planete/orders/repository/OrderRepositoryTest.java @@ -206,7 +206,6 @@ private OrderDTO from(Order order) { .status(order.getStatus().name()) .payment(order.getPayment() != null ? order.getPayment().getId() : null) .voucher(order.getVoucher() != null ? order.getVoucher().getId() : null) - .collectionTime(order.getCollectionTime()) .createdAt(order.getCreatedAt()) .build(); } diff --git a/orders/src/test/java/com/f_lab/joyeuse_planete/orders/service/OrderServiceKafkaTest.java b/orders/src/test/java/com/f_lab/joyeuse_planete/orders/service/OrderServiceKafkaTest.java index b3ebb78..9aa2339 100644 --- a/orders/src/test/java/com/f_lab/joyeuse_planete/orders/service/OrderServiceKafkaTest.java +++ b/orders/src/test/java/com/f_lab/joyeuse_planete/orders/service/OrderServiceKafkaTest.java @@ -3,7 +3,6 @@ import com.f_lab.joyeuse_planete.core.domain.Order; import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; -import com.f_lab.joyeuse_planete.core.kafka.exceptions.RetryableException; import com.f_lab.joyeuse_planete.core.kafka.service.KafkaService; import com.f_lab.joyeuse_planete.orders.dto.request.OrderCreateRequestDTO; import com.f_lab.joyeuse_planete.orders.dto.response.OrderCreateResponseDTO; @@ -43,9 +42,10 @@ void testCreateOrderSuccess() { // given OrderCreateResponseDTO expected = createOrderCreateResponseDTO("PROCESSING"); OrderCreateRequestDTO request = createOrderCreateRequestDTO(); + Order order = createOrder(); // when - when(orderRepository.save(any(Order.class))).thenReturn(null); + when(orderRepository.saveOrder(any())).thenReturn(order); doNothing().when(kafkaService).sendKafkaEvent(anyString(), any(Object.class)); OrderCreateResponseDTO response = orderService.createFoodOrder(request); @@ -60,7 +60,7 @@ void testCreateOrderRepositoryThrowExceptionFail1() { OrderCreateRequestDTO request = createOrderCreateRequestDTO(); // when - when(orderRepository.save(any(Order.class))).thenThrow(new RuntimeException()); + when(orderRepository.saveOrder(any())).thenThrow(new RuntimeException()); // then assertThatThrownBy(() -> orderService.createFoodOrder(request)) @@ -74,7 +74,7 @@ void testCreateOrderRepositoryThrowExceptionFail2() { OrderCreateRequestDTO request = createOrderCreateRequestDTO(); // when - when(orderRepository.save(any(Order.class))).thenThrow(new JoyeusePlaneteApplicationException()); + when(orderRepository.saveOrder(any())).thenThrow(new JoyeusePlaneteApplicationException()); // then assertThatThrownBy(() -> orderService.createFoodOrder(request)) @@ -86,9 +86,10 @@ void testCreateOrderRepositoryThrowExceptionFail2() { void testCreateOrderRepositoryKafkaServiceFail() { // given OrderCreateRequestDTO request = createOrderCreateRequestDTO(); + Order order = createOrder(); // when - when(orderRepository.save(any(Order.class))).thenReturn(null); + when(orderRepository.saveOrder(any())).thenReturn(order); doThrow(JoyeusePlaneteApplicationException.class).when(kafkaService).sendKafkaEvent(any(), any()); // then @@ -113,6 +114,10 @@ private OrderCreateRequestDTO createOrderCreateRequestDTO() { .build(); } + private Order createOrder() { + return Order.builder().id(1L).build(); + } + private OrderCreateResponseDTO createOrderCreateResponseDTO(String message) { return new OrderCreateResponseDTO(message); } From a6e09890583de6f9b0e90d4f13837b45fd676d4f Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Thu, 6 Feb 2025 11:24:26 +0000 Subject: [PATCH 4/7] feat: Added Dockerfile to projects --- Dockerfile | 11 ----------- foods/Dockerfile | 11 +++++++++++ notifications/Dockerfile | 11 +++++++++++ orders/Dockerfile | 11 +++++++++++ payment/Dockerfile | 11 +++++++++++ 5 files changed, 44 insertions(+), 11 deletions(-) delete mode 100644 Dockerfile create mode 100644 foods/Dockerfile create mode 100644 notifications/Dockerfile create mode 100644 orders/Dockerfile create mode 100644 payment/Dockerfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 747503b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM openjdk:17 - -ARG JAVA_FILE=build/libs/*.jar - -WORKDIR usr/src/app - -COPY ${JAVA_FILE} la_planete-0.0.1-SNAPSHOT.jar - -EXPOSE 8080 - -CMD [ "java", "-jar", "la_planete-0.0.1-SNAPSHOT.jar" ] \ No newline at end of file diff --git a/foods/Dockerfile b/foods/Dockerfile new file mode 100644 index 0000000..bd0f2dc --- /dev/null +++ b/foods/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:17 + +ARG JAVA_FILE=build/libs/*.jar + +WORKDIR /usr/src/app + +COPY ${JAVA_FILE} joyeuse_planete-foods.jar + +EXPOSE 8080 + +CMD [ "java", "-jar", "joyeuse_planete-foods.jar" ] \ No newline at end of file diff --git a/notifications/Dockerfile b/notifications/Dockerfile new file mode 100644 index 0000000..72c39ba --- /dev/null +++ b/notifications/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:17 + +ARG JAVA_FILE=build/libs/*.jar + +WORKDIR /usr/src/app + +COPY ${JAVA_FILE} joyeuse_planete-notifications.jar + +EXPOSE 8080 + +CMD [ "java", "-jar", "joyeuse_planete-notifications.jar" ] \ No newline at end of file diff --git a/orders/Dockerfile b/orders/Dockerfile new file mode 100644 index 0000000..c09da4f --- /dev/null +++ b/orders/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:17 + +ARG JAVA_FILE=build/libs/*.jar + +WORKDIR /usr/src/app + +COPY ${JAVA_FILE} joyeuse_planete-orders.jar + +EXPOSE 8080 + +CMD [ "java", "-jar", "joyeuse_planete-orders.jar" ] \ No newline at end of file diff --git a/payment/Dockerfile b/payment/Dockerfile new file mode 100644 index 0000000..ec4d083 --- /dev/null +++ b/payment/Dockerfile @@ -0,0 +1,11 @@ +FROM openjdk:17 + +ARG JAVA_FILE=build/libs/*.jar + +WORKDIR /usr/src/app + +COPY ${JAVA_FILE} joyeuse_planete-payment.jar + +EXPOSE 8080 + +CMD [ "java", "-jar", "joyeuse_planete-payment.jar" ] \ No newline at end of file From 49d3a020c2fb127ffbec54fb543ed58d7e13dc1a Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Thu, 6 Feb 2025 11:25:48 +0000 Subject: [PATCH 5/7] feat: Added GitHub Actions for Docker Terraform --- .github/workflows/ci-gradle.yml | 2 +- .github/workflows/docker.yml | 56 ++++++++++++++++++ .github/workflows/terraform-apply.yml | 49 ++++++++++++++++ .github/workflows/terraform-plan.yml | 83 +++++++++++++++++++++++++++ infrastructure/main.tf | 16 ++++++ 5 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker.yml create mode 100644 .github/workflows/terraform-apply.yml create mode 100644 .github/workflows/terraform-plan.yml create mode 100644 infrastructure/main.tf diff --git a/.github/workflows/ci-gradle.yml b/.github/workflows/ci-gradle.yml index 0b6cc7c..945ab81 100644 --- a/.github/workflows/ci-gradle.yml +++ b/.github/workflows/ci-gradle.yml @@ -1,6 +1,6 @@ name: Java CI with Gradle on: - [ push, pull_request ] + workflow_call: jobs: build_on_test: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..46cc624 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,56 @@ +name: Docker build and push +on: + workflow_call: + secrets: + DOCKERHUB_USERNAME: + required: true + + DOCKERHUB_TOKEN: + required: true + +jobs: + docker: + strategy: + matrix: + project: [ foods, orders, notifications, payment ] + + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Java 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Test and Build + run: | + chmod +x gradlew + ./gradlew clean build + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker BuildX + uses: docker/setup-buildx-action@v3 + + - name: Build and Push for ${{ matrix.project }} + uses: docker/build-push-action@v6 + with: + push: true + context: ./${{ matrix.project }} + file: ./${{ matrix.project }}/Dockerfile + platforms: linux/arm64,linux/amd64 + tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.project }}:latest diff --git a/.github/workflows/terraform-apply.yml b/.github/workflows/terraform-apply.yml new file mode 100644 index 0000000..ac54a91 --- /dev/null +++ b/.github/workflows/terraform-apply.yml @@ -0,0 +1,49 @@ +name: 'Terraform Apply' + +on: + push: + branches: + - main + +env: + TF_CLOUD_ORGANIZATION: "${{ secrets.TERRAFORM_ORGANISATION }}" + TF_API_TOKEN: "${{ secrets.TF_API_TOKEN }}" + TF_WORKSPACE: "${{ secrets.TF_WORKSPACE }}" + CONFIG_DIRECTORY: "./infrastructure" + +jobs: + docker-build: + uses: ./.github/workflows/docker.yml + secrets: + DOCKERHUB_USERNAME: "${{ secrets.DOCKERHUB_USERNAME }}" + DOCKERHUB_TOKEN: "${{ secrets.DOCKERHUB_TOKEN }}" + + terraform: + name: "Terraform Apply" + needs: [ docker-build ] + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Upload Configuration + uses: hashicorp/tfc-workflows-github/actions/upload-configuration@v1.0.0 + id: apply-upload + with: + workspace: ${{ env.TF_WORKSPACE }} + directory: ${{ env.CONFIG_DIRECTORY }} + + - name: Create Apply Run + uses: hashicorp/tfc-workflows-github/actions/create-run@v1.0.0 + id: apply-run + with: + workspace: ${{ env.TF_WORKSPACE }} + configuration_version: ${{ steps.apply-upload.outputs.configuration_version_id }} + + - name: Wait for Manual Approval + run: | + echo "Terraform apply requires manual approval in Terraform Cloud." + echo "Go to Terraform Cloud UI to approve the apply: ${{ steps.apply-run.outputs.run_link }}" \ No newline at end of file diff --git a/.github/workflows/terraform-plan.yml b/.github/workflows/terraform-plan.yml new file mode 100644 index 0000000..bc75a4c --- /dev/null +++ b/.github/workflows/terraform-plan.yml @@ -0,0 +1,83 @@ +name: Terraform Plan +on: + [ pull_request ] + +env: + TF_CLOUD_ORGANIZATION: "${{ secrets.TERRAFORM_ORGANISATION }}" + TF_API_TOKEN: "${{ secrets.TF_API_TOKEN }}" + TF_WORKSPACE: "${{ secrets.TF_WORKSPACE }}" + CONFIG_DIRECTORY: "./infrastructure" + +jobs: + ci-gradle: + uses: ./.github/workflows/ci-gradle.yml + + terraform: + needs: [ ci-gradle ] + name: "Terraform Plan" + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Upload Configuration + uses: hashicorp/tfc-workflows-github/actions/upload-configuration@v1.0.0 + id: plan-upload + with: + workspace: ${{ env.TF_WORKSPACE }} + directory: ${{ env.CONFIG_DIRECTORY }} + speculative: true + + - name: Create Plan Run + uses: hashicorp/tfc-workflows-github/actions/create-run@v1.0.0 + id: plan-run + with: + workspace: ${{ env.TF_WORKSPACE }} + configuration_version: ${{ steps.plan-upload.outputs.configuration_version_id }} + plan_only: true + + - name: Get Plan Output + uses: hashicorp/tfc-workflows-github/actions/plan-output@v1.0.0 + id: plan-output + with: + plan: ${{ fromJSON(steps.plan-run.outputs.payload).data.relationships.plan.data.id }} + + - name: Update PR + uses: actions/github-script@v6 + id: plan-comment + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + // 1. Retrieve existing bot comments for the PR + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const botComment = comments.find(comment => { + return comment.user.type === 'Bot' && comment.body.includes('HCP Terraform Plan Output') + }); + const output = `#### HCP Terraform Plan Output + \`\`\` + Plan: ${{ steps.plan-output.outputs.add }} to add, ${{ steps.plan-output.outputs.change }} to change, ${{ steps.plan-output.outputs.destroy }} to destroy. + \`\`\` + [HCP Terraform Plan](${{ steps.plan-run.outputs.run_link }}) + `; + // 3. Delete previous comment so PR timeline makes sense + if (botComment) { + github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + }); + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: output + }); \ No newline at end of file diff --git a/infrastructure/main.tf b/infrastructure/main.tf new file mode 100644 index 0000000..24ed1f5 --- /dev/null +++ b/infrastructure/main.tf @@ -0,0 +1,16 @@ +resource "aws_iam_group" "developers" { + name = "developers" +} + +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = "eu-west-2" +} \ No newline at end of file From ef7b66e6ca7ff4bffcc6a687f645e358a84e7999 Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Sun, 9 Feb 2025 14:28:52 +0000 Subject: [PATCH 6/7] fix: Fixed the comments --- .../joyeuse_planete/core/domain/Currency.java | 10 +------ .../joyeuse_planete/core/domain/Food.java | 17 ++++-------- .../domain/converter/StringListConverter.java | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 core/src/main/java/com/f_lab/joyeuse_planete/core/domain/converter/StringListConverter.java diff --git a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Currency.java b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Currency.java index 730f62b..6cd59ce 100644 --- a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Currency.java +++ b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Currency.java @@ -7,7 +7,6 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; import jakarta.persistence.Table; import lombok.AllArgsConstructor; import lombok.Builder; @@ -39,13 +38,6 @@ public class Currency extends BaseTimeEntity { private int roundingScale; @Enumerated(EnumType.STRING) - @Column(nullable = false) + @Column(nullable = false, columnDefinition = "VARCHAR(20) NOT NULL DEFAULT 'FLOOR'") private RoundingMode roundingMode; - - @PrePersist - public void prePersist() { - if (this.roundingMode == null) { - this.roundingMode = RoundingMode.FLOOR; - } - } } diff --git a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java index 9dba811..ebcb7e7 100644 --- a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java +++ b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java @@ -1,8 +1,10 @@ package com.f_lab.joyeuse_planete.core.domain; import com.f_lab.joyeuse_planete.core.domain.base.BaseEntity; +import com.f_lab.joyeuse_planete.core.domain.converter.StringListConverter; import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode; import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; +import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -19,8 +21,6 @@ import java.math.BigDecimal; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import static jakarta.persistence.FetchType.LAZY; @@ -55,9 +55,10 @@ public class Food extends BaseEntity { @JoinColumn(name = "currency_id") private Currency currency; - private Double rate; + private BigDecimal rate; - private String tags; + @Convert(converter = StringListConverter.class) + private List tags; private LocalDateTime collectionStartTime; @@ -97,12 +98,4 @@ public void update( this.collectionStartTime = collectionStartTime; this.collectionEndTime = collectionEndTime; } - - public List getTags() { - return tags == null ? new ArrayList<>() : Arrays.asList(tags.split(",")); - } - - public void setTags(List tags) { - this.tags = String.join(",", tags); - } } diff --git a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/converter/StringListConverter.java b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/converter/StringListConverter.java new file mode 100644 index 0000000..3cd04c7 --- /dev/null +++ b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/converter/StringListConverter.java @@ -0,0 +1,26 @@ +package com.f_lab.joyeuse_planete.core.domain.converter; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import org.springframework.util.ObjectUtils; + +import java.util.Arrays; +import java.util.List; + +import static java.util.Collections.emptyList; + +@Converter +public class StringListConverter implements AttributeConverter, String> { + + private static final String SPLIT_CHAR = ";"; + + @Override + public String convertToDatabaseColumn(List attribute) { + return (attribute != null) ? String.join(SPLIT_CHAR, attribute) : ""; + } + + @Override + public List convertToEntityAttribute(String dbData) { + return (!ObjectUtils.isEmpty(dbData)) ? Arrays.asList(dbData.split(SPLIT_CHAR)) : emptyList(); + } +} From 7fc2a840beee54e89e319c2ba1bbd3c968debd2e Mon Sep 17 00:00:00 2001 From: koreanMike513 Date: Sun, 9 Feb 2025 15:35:35 +0000 Subject: [PATCH 7/7] fix: Fixed comments --- .../joyeuse_planete/core/domain/Food.java | 2 - .../foods/domain/FoodSearchCondition.java | 12 +- .../dto/request/CreateFoodRequestDTO.java | 3 +- .../foods/dto/response/FoodDTO.java | 4 +- .../foods/repository/CurrencyRepository.java | 11 - .../repository/FoodCustomRepositoryImpl.java | 41 +-- .../foods/service/FoodService.java | 13 +- .../foods/repository/FoodRepositoryTest.java | 240 +++++++----------- .../foods/service/FoodServiceTest.java | 23 +- 9 files changed, 127 insertions(+), 222 deletions(-) delete mode 100644 foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java diff --git a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java index ebcb7e7..64dc63e 100644 --- a/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java +++ b/core/src/main/java/com/f_lab/joyeuse_planete/core/domain/Food.java @@ -86,7 +86,6 @@ public void update( String foodName, BigDecimal price, int totalQuantity, - Currency currency, LocalDateTime collectionStartTime, LocalDateTime collectionEndTime ) { @@ -94,7 +93,6 @@ public void update( this.foodName = foodName; this.price = price; this.totalQuantity = totalQuantity; - this.currency = currency; this.collectionStartTime = collectionStartTime; this.collectionEndTime = collectionEndTime; } diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java index e2d15a9..f6d6b1e 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/domain/FoodSearchCondition.java @@ -10,11 +10,17 @@ @NoArgsConstructor public class FoodSearchCondition { // TODO 거리 추가 구현 - // 위도 경도 DEFAULT SET TO LONDON - Double lat = 51.5072; - Double lon = -0.118092; + + Double lat = London.lat; + Double lon = London.lon; String search; int page = 0; int size = 25; List sortBy = List.of("RATE_HIGH"); + + // 위도 경도 DEFAULT SET TO LONDON + static class London { + private static final Double lat = 51.5072; + private static final Double lon = -0.118092; + } } diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java index 87441e4..ba0f76c 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/request/CreateFoodRequestDTO.java @@ -37,12 +37,11 @@ public class CreateFoodRequestDTO { @JsonProperty("collection_end") private LocalDateTime collectionEndTime; - public Food toEntity(Currency currency) { + public Food toEntity() { return Food.builder() .foodName(foodName) .price(price) .totalQuantity(totalQuantity) - .currency(currency) .collectionStartTime(collectionStartTime) .collectionEndTime(collectionEndTime) .build(); diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java index a6b90ea..5518699 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/dto/response/FoodDTO.java @@ -40,7 +40,7 @@ public class FoodDTO { private String currencySymbol; @JsonProperty("rate") - private double rate; + private BigDecimal rate; @JsonProperty("collection_start") private LocalDateTime collectionStartTime; @@ -59,7 +59,7 @@ public FoodDTO( int totalQuantity, String currencyCode, String currencySymbol, - double rate, + BigDecimal rate, LocalDateTime collectionStartTime, LocalDateTime collectionEndTime ) { diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java deleted file mode 100644 index 1fccc22..0000000 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/CurrencyRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.f_lab.joyeuse_planete.foods.repository; - -import com.f_lab.joyeuse_planete.core.domain.Currency; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - -public interface CurrencyRepository extends JpaRepository { - - Optional findByCurrencyCode(String currencyCode); -} diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java index 8af8de7..217760e 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/repository/FoodCustomRepositoryImpl.java @@ -13,7 +13,6 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,11 +55,7 @@ public Page getFoodList(FoodSearchCondition condition, Pageable pageabl .from(food) .innerJoin(food.currency, currency) .innerJoin(food.store, store) - .where(Expressions.anyOf( - eqFoodName(condition.getSearch()), - eqFoodTag(condition.getSearch()), - eqStoreName(condition.getSearch())) - ) + .where(eqFoodNameTagsAndStoreName(condition.getSearch())) .orderBy(getOrders(condition.getSortBy())) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) @@ -75,26 +70,36 @@ public Page getFoodList(FoodSearchCondition condition, Pageable pageabl return new PageImpl<>(result, pageable, count); } - private BooleanExpression eqFoodName(String search) { - return (search != null) ? food.foodName.containsIgnoreCase(search) : null; - } - - private BooleanExpression eqFoodTag(String search) { - return (search != null) ? food.tags.containsIgnoreCase(search) : null; - } + private BooleanExpression eqFoodNameTagsAndStoreName(String search) { + return (search != null) + ? Expressions.anyOf( + food.foodName.containsIgnoreCase(search), + Expressions.booleanTemplate( + "LOWER({0}) LIKE LOWER(CONCAT('%', {1}, '%'))", + food.tags, search), + food.store.name.containsIgnoreCase(search)) - private BooleanExpression eqStoreName(String search) { - return (search != null) ? food.store.name.containsIgnoreCase(search) : null; + : null; } private OrderSpecifier[] getOrders(List sortBy) { - List list = new ArrayList<>(); + int size = 0, idx = 0; + + for (String sort : sortBy) { + if (sortByMap.containsKey(sort)) + size++; + } + + if (size == 0) + return new OrderSpecifier[]{ food.rate.desc() }; + + OrderSpecifier[] list = new OrderSpecifier[size]; for (String sort : sortBy) { if (sortByMap.containsKey(sort)) - list.add(sortByMap.get(sort)); + list[idx++] = sortByMap.get(sort); } - return list.toArray(OrderSpecifier[]::new); + return list; } } diff --git a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java index aa477a4..d6fdb6c 100644 --- a/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java +++ b/foods/src/main/java/com/f_lab/joyeuse_planete/foods/service/FoodService.java @@ -1,6 +1,5 @@ package com.f_lab.joyeuse_planete.foods.service; -import com.f_lab.joyeuse_planete.core.domain.Currency; import com.f_lab.joyeuse_planete.core.domain.Food; import com.f_lab.joyeuse_planete.core.exceptions.ErrorCode; import com.f_lab.joyeuse_planete.core.exceptions.JoyeusePlaneteApplicationException; @@ -8,7 +7,6 @@ import com.f_lab.joyeuse_planete.foods.dto.request.CreateFoodRequestDTO; import com.f_lab.joyeuse_planete.foods.dto.request.UpdateFoodRequestDTO; import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; -import com.f_lab.joyeuse_planete.foods.repository.CurrencyRepository; import com.f_lab.joyeuse_planete.foods.repository.FoodRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -25,7 +23,6 @@ public class FoodService { private final FoodRepository foodRepository; - private final CurrencyRepository currencyRepository; public FoodDTO getFood(Long foodId) { return FoodDTO.from(findFood(foodId)); @@ -37,8 +34,7 @@ public Page getFoodList(FoodSearchCondition condition, Pageable pageabl @Transactional public void createFood(CreateFoodRequestDTO request) { - Currency currency = findCurrency(request.getCurrencyCode()); - Food food = request.toEntity(currency); + Food food = request.toEntity(); foodRepository.save(food); } @@ -51,13 +47,11 @@ public void deleteFood(Long foodId) { @Transactional public void updateFood(Long foodId, UpdateFoodRequestDTO request) { Food food = findFood(foodId); - Currency currency = findCurrency(request.getCurrencyCode()); food.update( request.getFoodName(), request.getPrice(), request.getTotalQuantity(), - currency, request.getCollectionStartTime(), request.getCollectionEndTime() ); @@ -88,9 +82,4 @@ private Food findFoodWithLock(Long foodId) { return foodRepository.findFoodByFoodIdWithPessimisticLock(foodId) .orElseThrow(() -> new JoyeusePlaneteApplicationException(ErrorCode.FOOD_NOT_EXIST_EXCEPTION)); } - - private Currency findCurrency(String currencyCode) { - return currencyRepository.findByCurrencyCode(currencyCode) - .orElseThrow(() -> new JoyeusePlaneteApplicationException(ErrorCode.CURRENCY_NOT_EXIST_EXCEPTION)); - } } diff --git a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java index 60ace58..a62662b 100644 --- a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java +++ b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/repository/FoodRepositoryTest.java @@ -20,6 +20,7 @@ import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Comparator; import java.util.List; @@ -92,10 +93,6 @@ void testFoodSearchConditionOnSearchSuccess() { // when Page result = foodRepository.getFoodList(condition, pageable); - for (FoodDTO foodDTO : result) { - System.out.println("foodDTO = " + foodDTO); - } - // then assertTrue(result, expected); } @@ -125,6 +122,58 @@ void testFoodSearchConditionOnPageSuccess() { assertTrue(result, expected); } + @Test + @DisplayName("가짜 + 진짜 정렬 조건을 부여했을 때 무시 후 정상 작동") + void testNonDefinedSortBySuccess() { + // given + List sortBy = List.of("FAKE_SORT_BY", "RATE_HIGH", "NO_RATE"); + int page = 1; + int size = 10; + + FoodSearchCondition condition = createSearchCondition(null, null, null, sortBy); + Pageable pageable = PageRequest.of(page, size); + + List expected = getFoodList().stream() + .sorted(Comparator.comparing(Food::getRate).reversed()) + .skip(page * size) + .limit(size) + .map(FoodDTO::from) + .toList(); + + // when + Page result = foodRepository.getFoodList(condition, pageable); + + // then + assertTrue(result, expected); + } + + @Test + @DisplayName("가짜 정렬 조건만을 부여했을 때 무시 후 정상 작동") + void testNonDefinedSortByOnlySuccess() { + // given + String search = "Asian"; + List sortBy = List.of("RATE_NO", "NO_RATE"); + + FoodSearchCondition condition = createSearchCondition(null, null, search, sortBy); + Pageable pageable = PageRequest.of(condition.getPage(), condition.getSize()); + + List expected = getFoodList().stream() + .filter(f -> + f.getFoodName().toLowerCase().contains(search.toLowerCase()) || + f.getTags().stream().anyMatch(tag -> tag.toLowerCase().contains(search.toLowerCase())) || + f.getStore().getName().toLowerCase().contains(search.toLowerCase())) + .sorted(Comparator.comparing(Food::getRate).reversed()) + .map(FoodDTO::from) + .limit(condition.getSize()) + .toList(); + + // when + Page result = foodRepository.getFoodList(condition, pageable); + + // then + assertTrue(result, expected); + } + private void assertTrue(Page result, List expected) { assertThat(result.getContent()) .usingRecursiveComparison() @@ -170,6 +219,7 @@ private List getFoodList() { .currencyCode("KRW") .currencySymbol("₩") .roundingScale(2) + .roundingMode(RoundingMode.FLOOR) .build(); em.persist(currency); @@ -179,291 +229,181 @@ private List getFoodList() { Food.builder() .foodName("Chicken Burger") .price(BigDecimal.valueOf(1000)) - .rate(4.5) + .rate(BigDecimal.valueOf(4.5)) .store(store1) .currency(currency) .totalQuantity(100) - .tags("fast-food,burger,chicken") + .tags(List.of("fast-food", "burger", "chicken")) .build(), Food.builder() .foodName("Spaghetti Carbonara") .price(BigDecimal.valueOf(1200)) - .rate(4.2) + .rate(BigDecimal.valueOf(4.2)) .store(store2) .currency(currency) .totalQuantity(80) - .tags("pasta,creamy,Italian") + .tags(List.of("pasta", "creamy", "Italian")) .build(), Food.builder() .foodName("Caesar Salad") .price(BigDecimal.valueOf(900)) - .rate(4.8) + .rate(BigDecimal.valueOf(4.8)) .store(store3) .currency(currency) .totalQuantity(90) - .tags("healthy,salad,fresh") + .tags(List.of("healthy", "salad", "fresh")) .build(), Food.builder() .foodName("Pepperoni Pizza") .price(BigDecimal.valueOf(1500)) - .rate(4.7) + .rate(BigDecimal.valueOf(4.7)) .store(store1) .currency(currency) .totalQuantity(60) - .tags("pizza,cheesy,Italian") + .tags(List.of("pizza", "cheesy", "Italian")) .build(), Food.builder() .foodName("Grilled Steak") .price(BigDecimal.valueOf(2500)) - .rate(4.9) + .rate(BigDecimal.valueOf(4.9)) .store(store2) .currency(currency) .totalQuantity(50) - .tags("meat,grill,protein") + .tags(List.of("meat", "grill", "protein")) .build(), Food.builder() .foodName("Tuna Sandwich") .price(BigDecimal.valueOf(800)) - .rate(3.9) + .rate(BigDecimal.valueOf(3.9)) .store(store3) .currency(currency) .totalQuantity(110) - .tags("sandwich,fish,healthy") + .tags(List.of("sandwich", "fish", "healthy")) .build(), Food.builder() .foodName("Vegetable Stir Fry") .price(BigDecimal.valueOf(950)) - .rate(4.1) + .rate(BigDecimal.valueOf(4.1)) .store(store1) .currency(currency) .totalQuantity(85) - .tags("vegetarian,stir-fry,Asian") + .tags(List.of("vegetarian", "stir-fry", "Asian")) .build(), Food.builder() .foodName("Beef Tacos") .price(BigDecimal.valueOf(1100)) - .rate(4.3) + .rate(BigDecimal.valueOf(4.3)) .store(store2) .currency(currency) .totalQuantity(70) - .tags("Mexican,taco,spicy") + .tags(List.of("Mexican", "taco", "spicy")) .build(), Food.builder() .foodName("Chicken Noodles") .price(BigDecimal.valueOf(1050)) - .rate(4.2) + .rate(BigDecimal.valueOf(4.2)) .store(store3) .currency(currency) .totalQuantity(75) - .tags("asian,noodles,chicken") + .tags(List.of("Asian", "noodles", "chicken")) .build(), Food.builder() .foodName("Margarita Pizza") .price(BigDecimal.valueOf(1400)) - .rate(4.6) + .rate(BigDecimal.valueOf(4.6)) .store(store1) .currency(currency) .totalQuantity(65) - .tags("pizza,tomato,cheese") + .tags(List.of("pizza", "tomato", "cheese")) .build(), Food.builder() .foodName("Sushi Rolls") .price(BigDecimal.valueOf(1600)) - .rate(4.8) + .rate(BigDecimal.valueOf(4.8)) .store(store2) .currency(currency) .totalQuantity(55) - .tags("sushi,fish,Japanese") - .build(), - - Food.builder() - .foodName("Pancakes with Maple Syrup") - .price(BigDecimal.valueOf(700)) - .rate(4.4) - .store(store3) - .currency(currency) - .totalQuantity(120) - .tags("breakfast,sweet,fluffy") - .build(), - - Food.builder() - .foodName("Lasagna") - .price(BigDecimal.valueOf(1300)) - .rate(4.7) - .store(store1) - .currency(currency) - .totalQuantity(72) - .tags("pasta,Italian,cheese") - .build(), - - Food.builder() - .foodName("Teriyaki Chicken") - .price(BigDecimal.valueOf(1150)) - .rate(4.5) - .store(store2) - .currency(currency) - .totalQuantity(80) - .tags("Asian,sweet,chicken") - .build(), - - Food.builder() - .foodName("Salmon Fillet") - .price(BigDecimal.valueOf(2000)) - .rate(4.9) - .store(store3) - .currency(currency) - .totalQuantity(40) - .tags("fish,protein,healthy") - .build(), - - Food.builder() - .foodName("Crispy French Fries") - .price(BigDecimal.valueOf(600)) - .rate(3.8) - .store(store1) - .currency(currency) - .totalQuantity(150) - .tags("fast-food,potato,fried") - .build(), - - Food.builder() - .foodName("BBQ Ribs") - .price(BigDecimal.valueOf(1800)) - .rate(4.7) - .store(store2) - .currency(currency) - .totalQuantity(50) - .tags("grill,meat,BBQ") - .build(), - - Food.builder() - .foodName("Falafel Wrap") - .price(BigDecimal.valueOf(900)) - .rate(4.0) - .store(store3) - .currency(currency) - .totalQuantity(95) - .tags("vegan,wrap,Mediterranean") - .build(), - - Food.builder() - .foodName("Egg Fried Rice") - .price(BigDecimal.valueOf(850)) - .rate(4.2) - .store(store1) - .currency(currency) - .totalQuantity(105) - .tags("rice,Asian,egg") + .tags(List.of("sushi", "fish", "Japanese")) .build(), Food.builder() .foodName("Chocolate Cake") .price(BigDecimal.valueOf(950)) - .rate(4.8) + .rate(BigDecimal.valueOf(4.8)) .store(store2) .currency(currency) .totalQuantity(65) - .tags("dessert,chocolate,sweet") + .tags(List.of("dessert", "chocolate", "sweet")) .build(), Food.builder() .foodName("Vanilla Ice Cream") .price(BigDecimal.valueOf(500)) - .rate(4.6) + .rate(BigDecimal.valueOf(4.6)) .store(store3) .currency(currency) .totalQuantity(130) - .tags("dessert,ice-cream,cold") + .tags(List.of("dessert", "ice-cream", "cold")) .build(), Food.builder() .foodName("Strawberry Cheesecake") .price(BigDecimal.valueOf(1100)) - .rate(4.7) + .rate(BigDecimal.valueOf(4.7)) .store(store1) .currency(currency) .totalQuantity(70) - .tags("dessert,cake,strawberry") + .tags(List.of("dessert", "cake", "strawberry")) .build(), Food.builder() .foodName("Grilled Chicken Breast") .price(BigDecimal.valueOf(1350)) - .rate(4.5) + .rate(BigDecimal.valueOf(4.5)) .store(store2) .currency(currency) .totalQuantity(55) - .tags("protein,chicken,healthy") + .tags(List.of("protein", "chicken", "healthy")) .build(), Food.builder() .foodName("Mushroom Soup") .price(BigDecimal.valueOf(900)) - .rate(4.3) + .rate(BigDecimal.valueOf(4.3)) .store(store3) .currency(currency) .totalQuantity(95) - .tags("soup,warm,healthy") + .tags(List.of("soup", "warm", "healthy")) .build(), Food.builder() .foodName("Butter Chicken") .price(BigDecimal.valueOf(1250)) - .rate(4.6) + .rate(BigDecimal.valueOf(4.6)) .store(store1) .currency(currency) .totalQuantity(85) - .tags("Indian,spicy,curry") + .tags(List.of("Indian", "spicy", "curry")) .build(), Food.builder() .foodName("Garlic Bread") .price(BigDecimal.valueOf(700)) - .rate(4.2) - .store(store2) - .currency(currency) - .totalQuantity(120) - .tags("bread,garlic,side-dish") - .build(), - - Food.builder() - .foodName("Asian Bread") - .price(BigDecimal.valueOf(700)) - .rate(4.2) + .rate(BigDecimal.valueOf(4.2)) .store(store2) .currency(currency) .totalQuantity(120) - .tags("bread,garlic,side-dish") - .build(), - - Food.builder() - .foodName("Garlic Bread") - .price(BigDecimal.valueOf(700)) - .rate(4.2) - .store(store4) - .currency(currency) - .totalQuantity(120) - .tags("bread,garlic,side-dish") - .build(), - - Food.builder() - .foodName("Garlic Bread") - .price(BigDecimal.valueOf(700)) - .rate(4.2) - .store(store5) - .currency(currency) - .totalQuantity(120) - .tags("bread,garlic,side-dish") + .tags(List.of("bread", "garlic", "side-dish")) .build() ); } diff --git a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java index 020b864..4a28aca 100644 --- a/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java +++ b/foods/src/test/java/com/f_lab/joyeuse_planete/foods/service/FoodServiceTest.java @@ -8,7 +8,6 @@ import com.f_lab.joyeuse_planete.foods.domain.FoodSearchCondition; import com.f_lab.joyeuse_planete.foods.dto.request.UpdateFoodRequestDTO; import com.f_lab.joyeuse_planete.foods.dto.response.FoodDTO; -import com.f_lab.joyeuse_planete.foods.repository.CurrencyRepository; import com.f_lab.joyeuse_planete.foods.repository.FoodRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -36,8 +35,6 @@ class FoodServiceTest { FoodService foodService; @Mock FoodRepository foodRepository; - @Mock - CurrencyRepository currencyRepository; @Test @DisplayName("getFood() 호출시 성공") @@ -133,7 +130,6 @@ void testUpdateFoodSuccess() { // when when(foodRepository.findById(anyLong())).thenReturn(Optional.of(food)); - when(currencyRepository.findByCurrencyCode(anyString())).thenReturn(Optional.of(currency)); foodService.updateFood(foodId, request); // then @@ -141,23 +137,6 @@ void testUpdateFoodSuccess() { assertThat(food.getCurrency().getCurrencyCode()).isEqualTo(expectedCurrencyCode); } - @Test - @DisplayName("updateFood() 호출시 실패") - void testUpdateFoodOnNotExistingCurrencyFail() { - // given - Long foodId = 1L; - Food food = createFood(foodId); - UpdateFoodRequestDTO request = createExpectedUpdateFoodRequestDTO(food); - - // when - when(foodRepository.findById(anyLong())).thenReturn(Optional.of(food)); - when(currencyRepository.findByCurrencyCode(anyString())).thenReturn(Optional.empty()); - - // then - assertThatThrownBy(() -> foodService.updateFood(foodId, request)) - .isInstanceOf(JoyeusePlaneteApplicationException.class) - .hasMessage(ErrorCode.CURRENCY_NOT_EXIST_EXCEPTION.getDescription()); - } @Test @DisplayName("foodService 가 올바로 foodRepository 호출하고 Page를 return 하는 것을 확인") @@ -186,7 +165,7 @@ private Food createFood(Long foodId) { .store(createStore()) .currency(createCurrency()) .foodName(foodName) - .rate(4.5) + .rate(BigDecimal.valueOf(4.5)) .price(price) .totalQuantity(totalQuantity) .build();