diff --git a/src/main/java/com/delfood/config/RedisConfig.java b/src/main/java/com/delfood/config/RedisConfig.java index ea9a8fe..64d3d4c 100644 --- a/src/main/java/com/delfood/config/RedisConfig.java +++ b/src/main/java/com/delfood/config/RedisConfig.java @@ -4,7 +4,9 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.sql.SQLException; import java.time.Duration; +import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -17,6 +19,9 @@ import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration public class RedisConfig { @@ -29,7 +34,7 @@ public class RedisConfig { @Value("${spring.redis.password}") private String redisPwd; - @Value("${spring.redis.defaultExpireSecond}") + @Value("${default.expire.second}") private long defaultExpireSecond; @@ -90,7 +95,8 @@ public RedisTemplate redisTemplate(ObjectMapper objectMapper) { redisTemplate.setValueSerializer(serializer); redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(serializer); - + redisTemplate.setEnableTransactionSupport(true); // transaction 허용 + return redisTemplate; } @@ -125,5 +131,6 @@ public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectio return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory) .cacheDefaults(configuration).build(); } - + + } diff --git a/src/main/java/com/delfood/controller/CartControllelr.java b/src/main/java/com/delfood/controller/CartControllelr.java new file mode 100644 index 0000000..c2010f2 --- /dev/null +++ b/src/main/java/com/delfood/controller/CartControllelr.java @@ -0,0 +1,66 @@ +package com.delfood.controller; + +import com.delfood.aop.MemberLoginCheck; +import com.delfood.dto.ItemDTO; +import com.delfood.service.CartService; +import com.delfood.utils.SessionUtil; +import java.util.List; +import javax.servlet.http.HttpSession; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +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.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CartControllelr { + + @Autowired + private CartService cartService; + + @PostMapping("/members/cart/menus") + @MemberLoginCheck + public void addMenu(@RequestBody ItemDTO item, HttpSession session) { + cartService.addOrdersItem(item, SessionUtil.getLoginMemberId(session)); + } + + @GetMapping("/members/cart/menus") + @MemberLoginCheck + public List getCart(HttpSession session) { + return cartService.getItems(SessionUtil.getLoginMemberId(session)); + } + + @DeleteMapping("/members/cart/menus") + @MemberLoginCheck + public void clearCart(HttpSession session) { + cartService.claer(SessionUtil.getLoginMemberId(session)); + } + + @DeleteMapping("/members/cart/menus/{index}") + @MemberLoginCheck + public void deleteCartMenu(HttpSession session, @PathVariable long index) { + cartService.deleteCartMenu(SessionUtil.getLoginMemberId(session), index); + } + + @GetMapping("/members/cart/price") + @MemberLoginCheck + public CartPriceResponse cartPrice(HttpSession session) { + String memberId = SessionUtil.getLoginMemberId(session); + return new CartPriceResponse(cartService.getItems(memberId), cartService.allPrice(memberId)); + } + + + + // Response + @Getter + @AllArgsConstructor + private static class CartPriceResponse { + private List items; + private long totalPrice; + } + +} diff --git a/src/main/java/com/delfood/controller/OrderController.java b/src/main/java/com/delfood/controller/OrderController.java new file mode 100644 index 0000000..af5559b --- /dev/null +++ b/src/main/java/com/delfood/controller/OrderController.java @@ -0,0 +1,142 @@ +package com.delfood.controller; + +import com.delfood.aop.MemberLoginCheck; +import com.delfood.controller.response.OrderResponse; +import com.delfood.dto.ItemsBillDTO; +import com.delfood.dto.OrderDTO; +import com.delfood.dto.OrderItemDTO; +import com.delfood.error.exception.order.TotalPriceMismatchException; +import com.delfood.dto.OrderBillDTO; +import com.delfood.service.OrderService; +import com.delfood.utils.SessionUtil; +import java.util.List; +import javax.servlet.http.HttpSession; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; +import org.codehaus.commons.nullanalysis.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/orders/") +@Log4j2 +public class OrderController { + + @Autowired + OrderService orderService; + + /** + * 아이템들의 가격과 정보를 조회한다. + * @author jun + * @param items 가격을 계산할 아이템들 + * @return + */ + @GetMapping("price") + @MemberLoginCheck + public long getItemsBill(HttpSession session, @RequestBody List items) { + return orderService.totalPrice(SessionUtil.getLoginMemberId(session), items); + } + + /** + * 주문 정보를 조회한다. 주문한 메뉴, 옵션, 가격 등의 정보를 조회할 수 있다. + * @author jun + * @param orderId 주문번호 + * @return + */ + @GetMapping("{orderId}/bill") + @MemberLoginCheck + public OrderBillDTO orderInfo(@PathVariable("orderId") Long orderId) { + return orderService.getPreOrderBill(orderId); + } + + /** + * 주문을 진행한다. + * 클라이언트에서 계산한 총 가격과 서버에서 계산한 총 가격이 다를 시 에러를 발생시킨다. + * @param session 사용자의 세션 + * @param request 주문 정보 + * @return + */ + @PostMapping + @MemberLoginCheck + public OrderResponse order(HttpSession session, @RequestBody OrderRequest request) { + if (request.getItems().isEmpty()) { + // items가 null일때도 NullpointerException이 발생한다 + throw new NullPointerException("아이템이 없습니다."); + } + + // 해당 아이템들이 해당 매장의 것인지 검증 + if (orderService.isShopItems(request.getItems(), request.getShopId()) == false) { + log.error("주문하신 매장의 메뉴 또는 옵션이 아닙니다."); + throw new IllegalArgumentException("주문하신 매장의 메뉴 또는 옵션이 아닙니다."); + } + // 클라이언트가 계산한 금액과 서버에서 계산한 금액이 같은지 비교 + long totalPriceFromServer = + orderService.totalPrice(SessionUtil.getLoginMemberId(session), request.getItems()); + if (totalPriceFromServer != request.getTotalPrice()) { + log.error("Total Price Mismatch! client price : {}, server price : {}", + request.getTotalPrice(), + totalPriceFromServer); + throw new TotalPriceMismatchException("Total Price Mismatch!"); + } + + return orderService.order(SessionUtil.getLoginMemberId(session), request.getItems(), + request.getShopId()); + } + + /** + * 아이템 리스트들을 상세하게 계산서로 발행한다. + * @param session 사용자의 세션 + * @param items 주문하기 전 아이템들 + * @return + */ + @GetMapping("bill") + @MemberLoginCheck + public ItemsBillDTO getBill(HttpSession session, @RequestBody List items) { + return orderService.getBill(SessionUtil.getLoginMemberId(session), items); + } + + /** + * 회원 주문내역을 모두 조회한다. + * 추후 페이징 추가 해야한다. + * @author jun + * @param session 사용자의 세션 + * @return + */ + @GetMapping + @MemberLoginCheck + public List myOrders(HttpSession session, @Nullable Long lastViewedOrderId) { + return orderService.getMemberOrder(SessionUtil.getLoginMemberId(session), lastViewedOrderId); + } + + /** + * 주문 번호를 기반으로 주문 상세내역을 조회한다. + * 추후 해당 회원의 주문인지 확인하는 로직을 작성해야한다. + * @param session 사용자의 세션 + * @param orderId 주문 아이디 + * @return + */ + @GetMapping("{orderId}") + @MemberLoginCheck + public OrderDTO getOrder(HttpSession session, @PathVariable Long orderId) { + OrderDTO orderInfo = orderService.getOrder(orderId); + String memberId = SessionUtil.getLoginMemberId(session); + if (memberId.equals(orderInfo.getMemberId()) == false) { + throw new IllegalArgumentException("해당 회원의 주문이 아닙니다!"); + } + return orderInfo; + } + + // request + @Getter + private static class OrderRequest { + private Long shopId; + private List items; + private long totalPrice; + } +} diff --git a/src/main/java/com/delfood/controller/response/OrderResponse.java b/src/main/java/com/delfood/controller/response/OrderResponse.java new file mode 100644 index 0000000..7acfd11 --- /dev/null +++ b/src/main/java/com/delfood/controller/response/OrderResponse.java @@ -0,0 +1,12 @@ +package com.delfood.controller.response; + +import com.delfood.dto.ItemsBillDTO; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class OrderResponse { + ItemsBillDTO bill; + Long orderId; +} diff --git a/src/main/java/com/delfood/dao/CartDao.java b/src/main/java/com/delfood/dao/CartDao.java new file mode 100644 index 0000000..ecb15be --- /dev/null +++ b/src/main/java/com/delfood/dao/CartDao.java @@ -0,0 +1,145 @@ +package com.delfood.dao; + +import com.delfood.dto.ItemDTO; +import com.delfood.utils.RedisKeyFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class CartDao { + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private ObjectMapper objectMapper; + + @Value("${cart.expire.second}") + private long cartExpireSecond; + + /** + * redis list에 해당 메뉴를 추가한다. + * RedisKeyFactory로 고객의 아이디, 내부 키를 이용해 키를 생산한 후 메뉴를 저장시킨다. + * @author jun + * @param item 장바구니에 추가할 메뉴 + * @param memberId 고객 아이디 + * @return + */ + public void addItem(ItemDTO item, String memberId) { + final String key = RedisKeyFactory.generateCartKey(memberId); + + redisTemplate.watch(key); // 해당 키를 감시한다. 변경되면 로직 취소. + + try { + if (redisTemplate.opsForList().size(key) >= 10) { + throw new IndexOutOfBoundsException("장바구니에는 10종류 이상 담을 수 없습니다."); + } + + redisTemplate.multi(); + redisTemplate.opsForList().rightPush(key, item); + redisTemplate.expire(key, cartExpireSecond, TimeUnit.SECONDS); + + redisTemplate.exec(); + } catch (Exception e) { + redisTemplate.discard(); // 트랜잭션 종료시 unwatch()가 호출된다 + throw e; + } + } + + /** + * 해당 고객 장바구니에 있는 모든 메뉴를 조회한다. + * @author jun + * @param memberId 고객 아이디 + * @return + */ + public List findAllByMemberId(String memberId) { + List items = redisTemplate.opsForList() + .range(RedisKeyFactory.generateCartKey(memberId), 0, -1) + .stream() + .map(item -> objectMapper.convertValue(item, ItemDTO.class)) + .collect(Collectors.toList()); + return items; + } + + /** + * 고객 장바구니를 비운다. redis에서 해당 키에있는 내용을 모두 삭제한다. + * @author jun + * @param memberId 고객 아이디 + * @return + */ + public boolean deleteByMemberId(String memberId) { + return redisTemplate.delete(RedisKeyFactory.generateCartKey(memberId)); + } + + /** + * 고객 장바구니에 있는 메뉴의 개수를 구한다. + * 주문 메뉴의 총 개수가 아닌, 장바구니 list의 size를 반환한다. + * @author jun + * @param memberId 고객 아이디 + * @return + */ + public long getMenuCount(String memberId) { + return redisTemplate.opsForList() + .range(RedisKeyFactory.generateCartKey(memberId), 0, -1) + .size(); + } + + /** + * 장바구니에 있는모든 메뉴의 개수를 더하여 반환한다. + * @author jun + * @param memberId 고객 아이디 + * @return + */ + public long getMenuCountSum(String memberId) { + return redisTemplate.opsForList() + .range(RedisKeyFactory.generateCartKey(memberId), 0, -1) + .stream() + .mapToLong(item -> objectMapper.convertValue(item, ItemDTO.class).getCount()) + .sum(); + } + + /** + * 고객 장바구니에서 해당 인덱스에 해당하는 메뉴를 삭제한다. + * @author jun + * @param memberId 고객 아이디 + * @param index 삭제할 메뉴 인덱스 + * @return 삭제에 성공할 시 true + */ + public boolean deleteByMemberIdAndIndex(String memberId, long index) { + /* + * opsForList().remove(key, count, value) + * key : list를 조회할 수 있는 key + * count > 0이면 head부터 순차적으로 조회하며 count의 절대값에 해당하는 개수만큼 제거 + * count < 0이면 tail부터 순차적으로 조회하며 count의 절대값에 해당하는 개수만큼 제거 + * count = 0이면 모두 조회한 후 value에 해당하는 값 모두 제거 + * value : 주어진 값과 같은 value를 가지는 대상이 삭제 대상이 된다 + * return값으로는 삭제한 인자의 개수를 리턴한다. + * + * 해당 리스트에서 인덱스에 해당하는 값을 조회한 후, remove의 value값 인자로 넘겨준다. + * 그 후 count에 1 값을 주면 head부터 순차적으로 조회하며 index에 해당하는 값을 제거할것이다. + * return값이 1이면 1개를 삭제한 것이니 성공, 1이 아니라면 잘 삭제된것이 아니니 실패이다. + */ + Long remove = redisTemplate.opsForList().remove(RedisKeyFactory.generateCartKey(memberId), 1, + redisTemplate.opsForList().index(RedisKeyFactory.generateCartKey(memberId), index)); + return remove == 1; + } + + /** + * 해당 고객 장바구니의 가장 첫 번째 메뉴데이터를 조회한다. + * @author jun + * @param memberId 고객 아이디 + * @return 리스트 첫 번째 메뉴데이터. 데이터가 없을 시 null + */ + public ItemDTO findPeekByMemberId(String memberId) { + String key = RedisKeyFactory.generateCartKey(memberId); + return redisTemplate.opsForList().size(key) == 0 ? null + : objectMapper.convertValue(redisTemplate.opsForList().index(key, 0), ItemDTO.class); + } + +} diff --git a/src/main/java/com/delfood/dto/ItemDTO.java b/src/main/java/com/delfood/dto/ItemDTO.java new file mode 100644 index 0000000..416c743 --- /dev/null +++ b/src/main/java/com/delfood/dto/ItemDTO.java @@ -0,0 +1,53 @@ +package com.delfood.dto; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@EqualsAndHashCode(of = {"menuInfo", "options", "shopInfo"}) +public class ItemDTO { + private CacheMenuDTO menuInfo; // id, name, price + private List options; // id, name, price + private long count; + private long price; + private CacheShopDTO shopInfo; // id, name + + /** + * 필요한 값을 모두 가지고 있는지 검사한다. + * @author jun + * @return + */ + public boolean hasNullDataBeforeInsertCart() { + return menuInfo.getId() == null + || menuInfo.getName() == null + || menuInfo.getPrice() == null + || options.stream().anyMatch(option -> option.getId() == null + || option.getName() == null || option.getPrice() == null); + } + + @Getter + @EqualsAndHashCode + public static class CacheMenuDTO { + private Long id; + private String name; + private Long price; + } + + @Getter + @EqualsAndHashCode + public static class CacheShopDTO { + private Long id; + private String name; + } + + @Getter + @EqualsAndHashCode + public static class CacheOptionDTO { + private Long id; + private String name; + private Long price; + } +} diff --git a/src/main/java/com/delfood/dto/ItemsBillDTO.java b/src/main/java/com/delfood/dto/ItemsBillDTO.java new file mode 100644 index 0000000..53af7ce --- /dev/null +++ b/src/main/java/com/delfood/dto/ItemsBillDTO.java @@ -0,0 +1,125 @@ +package com.delfood.dto; + +import java.util.ArrayList; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; + +// 사용자에게 전달하는 최종 주문서 DTO +@Getter +@NoArgsConstructor +public class ItemsBillDTO { + @NonNull + private List menus; + + private long totalPrice; + @NonNull + private String memberId; + @NonNull + private AddressDTO addressInfo; + @NonNull + private ShopInfo shopInfo; + + private DeliveryInfo deliveryInfo; + + /** + * 해당 인자를 세팅하여 새로운 객체를 반환한다. + * 리스트인 'menus'에는 ArrayList를 할당한다. + * @param memberId 고객 아이디 + * @param addressInfo 고객 주소정보 + */ + @Builder + public ItemsBillDTO(@NonNull String memberId, + @NonNull AddressDTO addressInfo, + @NonNull ShopInfo shopInfo, + double distanceMeter, + long deliveryPrice, + long itemsPrice, + List menus) { + this.memberId = memberId; + this.addressInfo = addressInfo; + this.shopInfo = shopInfo; + this.deliveryInfo = DeliveryInfo.builder() + .deliveryPrice(deliveryPrice) + .build(); + this.menus = menus; + this.totalPrice = totalPrice(); + } + + @Getter + @NoArgsConstructor + public static class MenuInfo { + private long id; + private String name; + private long price; + private List options; + + /** + * 메뉴 정보의 간략한 정보를 저장하는 DTO를 생성한다. + * @param id 메뉴 아이디 + * @param name 메뉴의 이름 + * @param price 메뉴 가격 + */ + @Builder + public MenuInfo(long id, @NonNull String name, long price) { + this.id = id; + this.name = name; + this.price = price; + options = new ArrayList(); + } + + @Getter + @NoArgsConstructor + public static class OptionInfo { + private long id; + @NonNull + private String name; + private long price; + + /** + * 직접 생성할 경우 Builder를 통해서 생성하게 한다. + * @param id 옵션 아이디 + * @param name 옵션 이름 + * @param price 옵션 가격 + */ + @Builder + public OptionInfo(long id, String name, long price) { + this.id = id; + this.name = name; + this.price = price; + } + } + } + + @Getter + @Builder + public static class ShopInfo { + + private long id; + + @NonNull + private String name; + + private String addressCode; + } + + @Builder + @Getter + public static class DeliveryInfo { + private long deliveryPrice; + } + + /** + * 메뉴 가격, 옵션 가격, 배달 가격을 합친 총 가격을 계산한다. + * @author jun + * @return + */ + public long totalPrice() { + return menus.stream().mapToLong(menu -> menu.getPrice() + + menu.getOptions().stream().mapToLong(option -> option.getPrice()).sum()).sum() + + deliveryInfo.getDeliveryPrice(); + } + +} diff --git a/src/main/java/com/delfood/dto/MenuDTO.java b/src/main/java/com/delfood/dto/MenuDTO.java index 671fc84..acc19ea 100644 --- a/src/main/java/com/delfood/dto/MenuDTO.java +++ b/src/main/java/com/delfood/dto/MenuDTO.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.List; import org.apache.ibatis.type.Alias; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -12,6 +13,7 @@ @Setter @ToString @Alias("menu") +@EqualsAndHashCode(of = {"id"}) public class MenuDTO { // 기본, 삭제, 숨김, 품절 @@ -28,13 +30,10 @@ public enum Status { private String photo; // 사진 (경로 저장) - @NonNull private LocalDateTime createdAt; // 등록일 - @NonNull private LocalDateTime updatedAt; // 최종 수정일 - @NonNull private Status status; // 상태 private Long priority; // 우선순위 diff --git a/src/main/java/com/delfood/dto/OptionDTO.java b/src/main/java/com/delfood/dto/OptionDTO.java index 401b60b..c3f5775 100644 --- a/src/main/java/com/delfood/dto/OptionDTO.java +++ b/src/main/java/com/delfood/dto/OptionDTO.java @@ -3,7 +3,7 @@ import com.delfood.dto.MenuDTO.Status; import java.time.LocalDateTime; - +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -14,6 +14,7 @@ @Getter @Setter @ToString +@EqualsAndHashCode(of = {"id"}) @Alias("option") public class OptionDTO { @@ -30,7 +31,6 @@ public enum Status { @NonNull private Long price; // 가격 - @NonNull private Status status; // 상태 private Long menuId; // 메뉴 아이디 diff --git a/src/main/java/com/delfood/dto/OrderBillDTO.java b/src/main/java/com/delfood/dto/OrderBillDTO.java new file mode 100644 index 0000000..0783561 --- /dev/null +++ b/src/main/java/com/delfood/dto/OrderBillDTO.java @@ -0,0 +1,30 @@ +package com.delfood.dto; + +import com.delfood.dto.ItemsBillDTO.MenuInfo; +import com.delfood.dto.OrderDTO.OrderStatus; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Getter; + +@Getter +public class OrderBillDTO { + private Long orderId; + private String memberId; + private OrderStatus orderStatus; + private LocalDateTime startTime; + private Long deliveryCost; + private SimpleAddressInfo addressInfo; + private List menus; + + @Getter + public static class SimpleAddressInfo { + private String buildingManagementNumber; + private String cityName; + private String cityCountryName; + private String townName; + private String roadName; + private Integer buildingNumber; + private Integer buildingSideNumber; + private String addressDetail; + } +} diff --git a/src/main/java/com/delfood/dto/OrderDTO.java b/src/main/java/com/delfood/dto/OrderDTO.java new file mode 100644 index 0000000..b3bda8a --- /dev/null +++ b/src/main/java/com/delfood/dto/OrderDTO.java @@ -0,0 +1,70 @@ +package com.delfood.dto; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; + +import org.apache.ibatis.type.Alias; +import org.codehaus.commons.nullanalysis.Nullable; +import com.fasterxml.jackson.annotation.JsonFormat; + +@Getter +@Alias("order") +@NoArgsConstructor // Mybatis에서 기본 생성자가 없으면 예외처리를 한다 +public class OrderDTO { + + public enum OrderStatus { + BEFORE_PAYMENT, ORDER_REQUEST, ORDER_APPROVAL, IN_DELIVERY, DELIVERY_COMPLETE + } + + private Long id; + + // 응답 데이터의 형식을 지정해준다. + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + @NonNull + private OrderStatus orderStatus; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime expectedArrivalTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime arrivalTime; + + private String riderId; + + @NonNull + private String memberId; + + @NonNull + private String addressCode; + + private String addressDetail; + + private Long deliveryCost; + + private Long shopId; + + // 조회할 때만 사용하는 컬럼. + @Nullable + private String shopName; + + List items; + + @Builder + public OrderDTO(String memberId, String addressCode, String addressDetail, Long shopId, + long deliveryCost) { + this.orderStatus = OrderStatus.BEFORE_PAYMENT; + this.memberId = memberId; + this.addressCode = addressCode; + this.addressDetail = addressDetail; + this.shopId = shopId; + this.deliveryCost = deliveryCost; + } +} diff --git a/src/main/java/com/delfood/dto/OrderItemDTO.java b/src/main/java/com/delfood/dto/OrderItemDTO.java new file mode 100644 index 0000000..46e5d3a --- /dev/null +++ b/src/main/java/com/delfood/dto/OrderItemDTO.java @@ -0,0 +1,20 @@ +package com.delfood.dto; + +import java.time.LocalDateTime; +import java.util.List; +import com.delfood.dto.OrderDTO.OrderStatus; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +@Getter +@Setter +public class OrderItemDTO { + private String id; + private Long menuId; + private String menuName; + private Long menuPrice; + private Long orderId; + private Long count; + private List options; +} diff --git a/src/main/java/com/delfood/dto/OrderItemOptionDTO.java b/src/main/java/com/delfood/dto/OrderItemOptionDTO.java new file mode 100644 index 0000000..85043cb --- /dev/null +++ b/src/main/java/com/delfood/dto/OrderItemOptionDTO.java @@ -0,0 +1,18 @@ +package com.delfood.dto; + +import java.time.LocalDateTime; +import java.util.List; +import com.delfood.dto.OrderDTO.OrderStatus; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +@Getter +@Setter +public class OrderItemOptionDTO { + private String id; + private Long optionId; + private String optionName; + private Long optionPrice; + private String orderItemId; +} diff --git a/src/main/java/com/delfood/dto/OrdersItemDTO.java b/src/main/java/com/delfood/dto/OrdersItemDTO.java new file mode 100644 index 0000000..6f68775 --- /dev/null +++ b/src/main/java/com/delfood/dto/OrdersItemDTO.java @@ -0,0 +1,27 @@ +package com.delfood.dto; + +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.codehaus.commons.nullanalysis.NotNull; + +@Getter +@Setter +@EqualsAndHashCode(of = {"menuId", "count", "ordersItemOptions"}) +public class OrdersItemDTO { + private Long id; + @NotNull + private Long menuId; + private Long orderId; + @NotNull + private Long count; + + private List ordersItemOptions; + + public boolean hasNullDataBeforeInsertCart() { + return menuId == null + || count == null + || count <= 0; + } +} diff --git a/src/main/java/com/delfood/dto/OrdersItemOptionDTO.java b/src/main/java/com/delfood/dto/OrdersItemOptionDTO.java new file mode 100644 index 0000000..c72b5dd --- /dev/null +++ b/src/main/java/com/delfood/dto/OrdersItemOptionDTO.java @@ -0,0 +1,14 @@ +package com.delfood.dto; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@EqualsAndHashCode(of = {"optionId", "ordersItemId"}) +public class OrdersItemOptionDTO { + private Long id; + private Long optionId; + private Long ordersItemId; +} diff --git a/src/main/java/com/delfood/dto/PaymentDTO.java b/src/main/java/com/delfood/dto/PaymentDTO.java new file mode 100644 index 0000000..3eb20e4 --- /dev/null +++ b/src/main/java/com/delfood/dto/PaymentDTO.java @@ -0,0 +1,91 @@ +package com.delfood.dto; + +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import org.codehaus.commons.nullanalysis.NotNull; +import org.codehaus.commons.nullanalysis.Nullable; + +@Getter +@ToString(of = {"id", "type", "amountPayment", "payTime", "orderId", "status", "amountDiscount", + "doSuccess"}) +public class PaymentDTO { + private Long id; + @NotNull + private Type type; + @NotNull + private Long amountPayment; + private LocalDateTime payTime; + @NotNull + private Long orderId; + @NotNull + private Status status; + @NotNull + private Long amountDiscount; + + // 성공시킬지 실패시킬지 결정하는 필드 + private boolean doSuccess; + + /** + * payTime, status는 자동으로 입력된다. + * @param type 결제 타입 + * @param amountPayment 실제 결제 가격 + * @param orderId 주문 아이디 + * @param amountDiscount 할인된 가격 + */ + @Builder + public PaymentDTO(@NonNull Type type, + @NonNull Long amountPayment, + @NonNull Long orderId, + @NonNull Long amountDiscount, + @Nullable Status status) { + this.type = type; + this.amountPayment = amountPayment; + this.orderId = orderId; + this.amountDiscount = amountDiscount; + + this.doSuccess = true; + this.payTime = LocalDateTime.now(); + this.status = status == null ? Status.READY : status; + } + + private void success(Status status) { + this.status = status; + } + + public void doFail() { + doSuccess = false; + } + + public void doSuccess() { + doSuccess = true; + } + + public enum Type { + CARD, CASH + } + + public enum Status { + READY, SUCCESS, FAIL + } + + /** + * 성공적으로 결제가 이루어 질 때 이 메소드를 호출한 후 리턴값을 사용한다. + * 해당 메소드가 반환하는 값은 인스턴스의 복제 인스턴스이다. + * 복제 인스턴스의 'status' 컬럼만 SUCCESS로 변한 채로 반환된다. + * @author jun + * @return + */ + public PaymentDTO pay() { + PaymentDTO pay = PaymentDTO.builder() + .type(getType()) + .amountPayment(getAmountPayment()) + .orderId(getOrderId()) + .amountDiscount(getAmountDiscount()) + .status(Status.SUCCESS) + .build(); + return pay; + } +} diff --git a/src/main/java/com/delfood/dto/ShopDTO.java b/src/main/java/com/delfood/dto/ShopDTO.java index 746035b..9ad94e3 100644 --- a/src/main/java/com/delfood/dto/ShopDTO.java +++ b/src/main/java/com/delfood/dto/ShopDTO.java @@ -1,120 +1,115 @@ -package com.delfood.dto; - -import java.time.LocalDateTime; -import java.util.List; -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; -import lombok.Setter; -import lombok.ToString; - -@Builder -@Getter -@Setter -@ToString -public class ShopDTO { - - // 배달 타입. 자체배달, 라이더 매칭 배달 - public enum DeliveryType { - SELF_DELIVERY, COMPANY_DELIVERY - } - - - - public enum Status { - DEFAULT, DELETED - } - - // 즉시 결제, 만나서 결제 - public enum OrderType { - THIS_PAYMENT, MEET_PAYMENT - } - - public enum WorkCondition { - OPEN, CLOSE - } - - - // 아아디 - @NonNull - private Long id; - - // 가게 이름 - @NonNull - private String name; - - // 배달형태 - @NonNull - private DeliveryType deliveryType; - - // 주력메뉴 치킨, 피자, 분식 등 - @NonNull - private Long signatureMenuId; - - // 가게 전화번호 - private String tel; - - // 주소 코드 - private String addressCode; - - // 상세 주소 - private String addressDetail; - - // 사업자번호 - @NonNull - private String bizNumber; - - // 가게 소개 - @NonNull - private String info; - - // 최소 주문금액 - @NonNull - private Long minOrderPrice; - - // 안내 및 혜택 - private String notice; - - // 운영 시간 - private String operatingTime; - - // 사장 아이디 - private String ownerId; - - // 가게 등록일 - private LocalDateTime createdAt; - - // 최종 수정일 - private LocalDateTime updatedAt; - - // 상태 삭제되었을시 DELETE 평소에는 DEFAULT - private Status status; - - // 주문 타입 바로결제, 전화결제 등 결정 - @NonNull - private OrderType orderType; - - // 원산지 정보 원산지 표기정보를 작성 - private String originInfo; - - // 영업 상태 OPEN, CLOSED 등 - private WorkCondition workCondition; - - /** - * 매장 입점 전 필수 입력 데이터가 누락된 것이 없는지 확인. - * - * @author jun - * @param shopInfo 매장 데이터 - * @return 누락된 데이터가 있다면 true - */ - public static boolean hasNullDataBeforeCreate(ShopDTO shopInfo) { - if (shopInfo.getName() == null || shopInfo.getDeliveryType() == null - || shopInfo.getSignatureMenuId() == null || shopInfo.getBizNumber() == null - || shopInfo.getInfo() == null || shopInfo.getMinOrderPrice() == null - || shopInfo.getOrderType() == null || shopInfo.getTel() == null - || shopInfo.getAddressCode() == null) { - return true; - } - return false; - } -} +package com.delfood.dto; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; + +@Builder +@Getter +@Setter +@EqualsAndHashCode(of = {"id"}) +@ToString +public class ShopDTO { + + // 배달 타입. 자체배달, 라이더 매칭 배달 + public enum DeliveryType { + SELF_DELIVERY, COMPANY_DELIVERY + } + + + public enum Status { + DEFAULT, DELETED + } + + // 즉시 결제, 만나서 결제 + public enum OrderType { + THIS_PAYMENT, MEET_PAYMENT + } + + public enum WorkCondition { + OPEN, CLOSE + } + + + // 아아디 + @NonNull + private Long id; + + // 가게 이름 + @NonNull + private String name; + + // 배달형태 + private DeliveryType deliveryType; + + // 주력메뉴 치킨, 피자, 분식 등 + private Long signatureMenuId; + + // 가게 전화번호 + private String tel; + + // 주소 코드 + private String addressCode; + + // 상세 주소 + private String addressDetail; + + // 사업자번호 + private String bizNumber; + + // 가게 소개 + private String info; + + // 최소 주문금액 + private Long minOrderPrice; + + // 안내 및 혜택 + private String notice; + + // 운영 시간 + private String operatingTime; + + // 사장 아이디 + private String ownerId; + + // 가게 등록일 + private LocalDateTime createdAt; + + // 최종 수정일 + private LocalDateTime updatedAt; + + // 상태 삭제되었을시 DELETE 평소에는 DEFAULT + private Status status; + + // 주문 타입 바로결제, 전화결제 등 결정 + private OrderType orderType; + + // 원산지 정보 원산지 표기정보를 작성 + private String originInfo; + + // 영업 상태 OPEN, CLOSED 등 + private WorkCondition workCondition; + + /** + * 매장 입점 전 필수 입력 데이터가 누락된 것이 없는지 확인. + * + * @author jun + * @param shopInfo 매장 데이터 + * @return 누락된 데이터가 있다면 true + */ + public static boolean hasNullDataBeforeCreate(ShopDTO shopInfo) { + if (shopInfo.getName() == null || shopInfo.getDeliveryType() == null + || shopInfo.getSignatureMenuId() == null || shopInfo.getBizNumber() == null + || shopInfo.getInfo() == null || shopInfo.getMinOrderPrice() == null + || shopInfo.getOrderType() == null || shopInfo.getTel() == null + || shopInfo.getAddressCode() == null) { + return true; + } + return false; + } +} diff --git a/src/main/java/com/delfood/dto/address/Position.java b/src/main/java/com/delfood/dto/address/Position.java new file mode 100644 index 0000000..b3fd59e --- /dev/null +++ b/src/main/java/com/delfood/dto/address/Position.java @@ -0,0 +1,29 @@ +package com.delfood.dto.address; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class Position { + private double xPos; + private double yPos; + + @Builder + public Position(double xPos, double yPos) { + this.xPos = xPos; + this.yPos = yPos; + } + + /** + * 대상 위치와의 거리를 계산한다. + * @author jun + * @param position 거리를 계산할 위치 + * @return + */ + public double distanceMeter(Position position) { + return Math.sqrt( + Math.pow(this.xPos - position.getXPos(), 2) + Math.pow(this.yPos - position.getYPos(), 2)); + } +} diff --git a/src/main/java/com/delfood/error/ErrorController.java b/src/main/java/com/delfood/error/ErrorController.java index 8e99cb0..050c31c 100644 --- a/src/main/java/com/delfood/error/ErrorController.java +++ b/src/main/java/com/delfood/error/ErrorController.java @@ -1,80 +1,89 @@ -package com.delfood.error; - -import com.delfood.error.exception.DuplicateException; -import com.delfood.error.exception.DuplicateIdException; -import com.delfood.error.exception.menuGroup.InvalidMenuGroupCountException; -import com.delfood.error.exception.menuGroup.InvalidMenuGroupIdException; -import com.delfood.error.exception.shop.CanNotCloseShopException; -import com.delfood.error.exception.shop.CanNotOpenShopException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.client.HttpStatusCodeException; - -/* - * 스프링에서 지원하는 예외처리 어노테이션 - * - * @ExceptionHandler({Exception1.class, Exception2.class}) - * - @Controller, @RestController에서 사용 가능 - * - 컨트롤러 클래스 내에서 발생하는 예외를 해당 어노테이션이 적용된 메서드를 통해 처리한다. - * - * @ControllerAdvice ( @RestControllerAdvice ) - * - 모든 @Controller 또는 @RestController에 적용되는 공통클래스를 만들 때 사용한다. - */ -@RestControllerAdvice -public class ErrorController { - - /** - * 패키지 명을 제외한 클래스 이름을 반환한다. - * - * @param e 에러 - * @return - */ - private static String getSimpleName(Exception e) { - return e.getClass().getSimpleName(); - } - - @ResponseStatus(HttpStatus.CONFLICT) // 반환할 상태코드 설정한다. - @ExceptionHandler(DuplicateIdException.class) // 처리할 에러를 설정한다. - public ErrorMsg handleDuplicateIdException(DuplicateIdException e) { - // Exception 객체의 현지화 메시지와 클래스 이름을 반환한다. - return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(InvalidMenuGroupCountException.class) - public ErrorMsg handleInvalidMenuGroupCountException(InvalidMenuGroupCountException e) { - return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(InvalidMenuGroupIdException.class) - public ErrorMsg handleInvalidMenuGroupIdException(InvalidMenuGroupIdException e) { - return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); - } - - @ExceptionHandler(HttpStatusCodeException.class) - public ResponseEntity handleHttpStatusCodeException(HttpStatusCodeException e) { - return ResponseEntity.status(e.getStatusCode()).build(); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(IllegalArgumentException.class) - public ErrorMsg handleIllegalArgumentException(IllegalArgumentException e) { - return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(value = {CanNotOpenShopException.class, CanNotCloseShopException.class}) - public ErrorMsg handleCannotShopException(RuntimeException e) { - return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); - } - - @ResponseStatus(HttpStatus.CONFLICT) - @ExceptionHandler(DuplicateException.class) - public ErrorMsg handleDuplicatedMenuGroupNameException(DuplicateException e) { - return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); - } -} +package com.delfood.error; + +import com.delfood.error.exception.DuplicateException; +import com.delfood.error.exception.DuplicateIdException; +import com.delfood.error.exception.cart.DuplicateItemException; +import com.delfood.error.exception.menuGroup.InvalidMenuGroupCountException; +import com.delfood.error.exception.menuGroup.InvalidMenuGroupIdException; +import com.delfood.error.exception.mockPay.MockPayException; +import com.delfood.error.exception.order.TotalPriceMismatchException; +import com.delfood.error.exception.shop.CanNotCloseShopException; +import com.delfood.error.exception.shop.CanNotOpenShopException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.client.HttpStatusCodeException; + +/* + * 스프링에서 지원하는 예외처리 어노테이션 + * + * @ExceptionHandler({Exception1.class, Exception2.class}) + * - @Controller, @RestController에서 사용 가능 + * - 컨트롤러 클래스 내에서 발생하는 예외를 해당 어노테이션이 적용된 메서드를 통해 처리한다. + * + * @ControllerAdvice ( @RestControllerAdvice ) + * - 모든 @Controller 또는 @RestController에 적용되는 공통클래스를 만들 때 사용한다. + */ +@RestControllerAdvice +public class ErrorController { + + /** + * 패키지 명을 제외한 클래스 이름을 반환한다. + * + * @param e 에러 + * @return + */ + private static String getSimpleName(Exception e) { + return e.getClass().getSimpleName(); + } + + @ResponseStatus(HttpStatus.CONFLICT) // 반환할 상태코드 설정한다. + @ExceptionHandler(DuplicateIdException.class) // 처리할 에러를 설정한다. + public ErrorMsg handleDuplicateIdException(DuplicateIdException e) { + // Exception 객체의 현지화 메시지와 클래스 이름을 반환한다. + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(InvalidMenuGroupCountException.class) + public ErrorMsg handleInvalidMenuGroupCountException(InvalidMenuGroupCountException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(InvalidMenuGroupIdException.class) + public ErrorMsg handleInvalidMenuGroupIdException(InvalidMenuGroupIdException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + + @ExceptionHandler(HttpStatusCodeException.class) + public ResponseEntity handleHttpStatusCodeException(HttpStatusCodeException e) { + return ResponseEntity.status(e.getStatusCode()).build(); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalArgumentException.class) + public ErrorMsg handleIllegalArgumentException(IllegalArgumentException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(value = {CanNotOpenShopException.class, CanNotCloseShopException.class}) + public ErrorMsg handleCannotShopException(RuntimeException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + + + @ResponseStatus(HttpStatus.CONFLICT) + @ExceptionHandler(DuplicateItemException.class) + public ErrorMsg handleDuplicatedItemException(DuplicateItemException e) { + return new ErrorMsg(e.getLocalizedMessage(), getSimpleName(e)); + } + + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ExceptionHandler(MockPayException.class) + public ErrorMsg handleMockPayException(MockPayException e) { + +} diff --git a/src/main/java/com/delfood/error/exception/cart/DuplicateItemException.java b/src/main/java/com/delfood/error/exception/cart/DuplicateItemException.java new file mode 100644 index 0000000..da725c4 --- /dev/null +++ b/src/main/java/com/delfood/error/exception/cart/DuplicateItemException.java @@ -0,0 +1,7 @@ +package com.delfood.error.exception.cart; + +public class DuplicateItemException extends RuntimeException{ + public DuplicateItemException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/delfood/error/exception/mockPay/MockPayException.java b/src/main/java/com/delfood/error/exception/mockPay/MockPayException.java new file mode 100644 index 0000000..53c9f0e --- /dev/null +++ b/src/main/java/com/delfood/error/exception/mockPay/MockPayException.java @@ -0,0 +1,7 @@ +package com.delfood.error.exception.mockPay; + +public class MockPayException extends RuntimeException { + public MockPayException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/delfood/error/exception/order/TotalPriceMismatchException.java b/src/main/java/com/delfood/error/exception/order/TotalPriceMismatchException.java new file mode 100644 index 0000000..3f3df42 --- /dev/null +++ b/src/main/java/com/delfood/error/exception/order/TotalPriceMismatchException.java @@ -0,0 +1,7 @@ +package com.delfood.error.exception.order; + +public class TotalPriceMismatchException extends IllegalArgumentException { + public TotalPriceMismatchException(String msg) { + super(msg); + } +} diff --git a/src/main/java/com/delfood/mapper/AddressMapper.java b/src/main/java/com/delfood/mapper/AddressMapper.java index e95e718..6acc9f8 100644 --- a/src/main/java/com/delfood/mapper/AddressMapper.java +++ b/src/main/java/com/delfood/mapper/AddressMapper.java @@ -3,6 +3,7 @@ import com.delfood.controller.reqeust.GetAddressByZipRequest; import com.delfood.controller.reqeust.GetAddressesByRoadRequest; import com.delfood.dto.AddressDTO; +import com.delfood.dto.address.Position; import java.util.List; import org.springframework.stereotype.Repository; @@ -13,4 +14,10 @@ public interface AddressMapper { public List findByZipName(GetAddressByZipRequest searchInfo); public List findByRoadName(GetAddressesByRoadRequest searchInfo); + + public Position findPositionByMemberId(String memberId); + + public Position findPositionByShopId(Long shopId); + + public Position findPositionByAddressCode(String addressCode); } diff --git a/src/main/java/com/delfood/mapper/MenuMapper.java b/src/main/java/com/delfood/mapper/MenuMapper.java index e650b66..c8158f9 100644 --- a/src/main/java/com/delfood/mapper/MenuMapper.java +++ b/src/main/java/com/delfood/mapper/MenuMapper.java @@ -70,5 +70,13 @@ public interface MenuMapper { * @return */ public MenuDTO findById(Long id); + + /** + * 메뉴를 옵션과 함께 조회한다. + * @author jun + * @param id 메뉴 아이디 + * @return + */ + public MenuDTO findMenuWithOptionsById(Long id); } diff --git a/src/main/java/com/delfood/mapper/OptionMapper.java b/src/main/java/com/delfood/mapper/OptionMapper.java index f111a81..1d59d97 100644 --- a/src/main/java/com/delfood/mapper/OptionMapper.java +++ b/src/main/java/com/delfood/mapper/OptionMapper.java @@ -1,7 +1,8 @@ package com.delfood.mapper; import com.delfood.dto.OptionDTO; - +import com.delfood.dto.OrderItemDTO; +import com.delfood.dto.OrderItemOptionDTO; import java.util.List; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; @@ -36,4 +37,20 @@ public interface OptionMapper { * @return */ public int deleteOption(Long id); + + /** + * 옵션 가격을 계산한다. + * @param options 계산할 옵아이템들 + * @return + */ + public long totalPrice(List options); + + /** + * 옵션 조회. + * + * @author jun + * @param id 옵션 아이디 + * @return + */ + public OptionDTO findById(Long id); } diff --git a/src/main/java/com/delfood/mapper/OrderMapper.java b/src/main/java/com/delfood/mapper/OrderMapper.java new file mode 100644 index 0000000..5efe3f4 --- /dev/null +++ b/src/main/java/com/delfood/mapper/OrderMapper.java @@ -0,0 +1,32 @@ +package com.delfood.mapper; + +import com.delfood.dto.OrderDTO; +import com.delfood.dto.OrderItemDTO; +import com.delfood.dto.OrderItemOptionDTO; +import com.delfood.dto.ItemsBillDTO.MenuInfo; +import com.delfood.dto.OrderBillDTO; +import java.util.List; +import lombok.NonNull; + +public interface OrderMapper { + // 파라미터로 넘어온 인스턴스값을 세팅한다 + Long addOrder(OrderDTO orderInfo); + + Long addOrderItem(OrderItemDTO item); + + OrderDTO findById(Long id); + + Long addOrderItems(List items); + + Long addOrderItemOptions(List options); + + OrderBillDTO findOrderBill(Long orderId); + + long findItemsPrice(List items); + + List findItemsBill(List items); + + List findByMemberId(String memberId, Long lastViewedOrderId); + + boolean isShopItem(List items, Long shopId); +} diff --git a/src/main/java/com/delfood/mapper/PaymentMapper.java b/src/main/java/com/delfood/mapper/PaymentMapper.java new file mode 100644 index 0000000..91a5345 --- /dev/null +++ b/src/main/java/com/delfood/mapper/PaymentMapper.java @@ -0,0 +1,9 @@ +package com.delfood.mapper; + +import org.springframework.stereotype.Repository; +import com.delfood.dto.PaymentDTO; + +@Repository +public interface PaymentMapper { + public Long insertPayment(PaymentDTO paymentInfo); +} diff --git a/src/main/java/com/delfood/mapper/ShopMapper.java b/src/main/java/com/delfood/mapper/ShopMapper.java index 7014e7f..3fce03a 100644 --- a/src/main/java/com/delfood/mapper/ShopMapper.java +++ b/src/main/java/com/delfood/mapper/ShopMapper.java @@ -1,5 +1,7 @@ package com.delfood.mapper; +import com.delfood.dto.ItemsBillDTO.ShopInfo; +import com.delfood.dto.ItemsBillDTO; import com.delfood.dto.ShopDTO; import com.delfood.dto.ShopUpdateDTO; import java.util.List; @@ -122,4 +124,6 @@ public interface ShopMapper { public List findByBeClose(String ownerId); + public ItemsBillDTO.ShopInfo findIdAndNameByMenuId(Long menuId); + } diff --git a/src/main/java/com/delfood/service/AddressService.java b/src/main/java/com/delfood/service/AddressService.java index d7843a4..364c35f 100644 --- a/src/main/java/com/delfood/service/AddressService.java +++ b/src/main/java/com/delfood/service/AddressService.java @@ -3,14 +3,21 @@ import com.delfood.controller.reqeust.GetAddressByZipRequest; import com.delfood.controller.reqeust.GetAddressesByRoadRequest; import com.delfood.dto.AddressDTO; +import com.delfood.dto.address.Position; import com.delfood.mapper.AddressMapper; +import lombok.extern.log4j.Log4j2; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; @Service +@Log4j2 public class AddressService { + public static final long DELIVERY_METER_DEFAULT = 1500L; + public static final long DELIVERY_COST_DEFAULT = 2000L; + public static final long DELIVERY_COST_PER_100M = 200L; + @Autowired AddressMapper addressMapper; @@ -28,4 +35,49 @@ public List getAddressByZipAddress(GetAddressByZipRequest searchInfo public List getAddressByRoadName(GetAddressesByRoadRequest searchInfo) { return addressMapper.findByRoadName(searchInfo); } + + /** + * 주소코드를 이용하여 거리를 계산한다. + * @param startAddressCode 시작 주소코드 + * @param endAddressCode 도착 주소코드 + * @return + */ + public double getDistanceMeter(String startAddressCode, String endAddressCode) { + Position startPosition = addressMapper.findPositionByAddressCode(startAddressCode); + Position endPosition = addressMapper.findPositionByAddressCode(endAddressCode); + + return startPosition.distanceMeter(endPosition); + } + + /** + * 거리를 기반 배달료를 계산한다. + * + * @author jun + * @param distanceMeter 거리(미터 단위) + * @return + */ + public long deliveryPrice(double distanceMeter) { + // 1.5KM까지는 기본료 2000원 + // 이후 100m마다 200원이 추가된다. + // ex) 1500m = 2000원, 1600m = 2200원, 1650m = 2200원 + long extraCharge = distanceMeter <= 1500 ? 0 : + (long) (distanceMeter - DELIVERY_METER_DEFAULT) / 100 * DELIVERY_COST_PER_100M; + + long deliveryCost = DELIVERY_COST_DEFAULT; + + return deliveryCost + extraCharge; + } + + /** + * 회원과 매장의 아이디를 기준으로 배달료를 계산한다. + * @param memberId 회원 id + * @param shopId 매장 id + * @return + */ + public long deliveryPrice(String memberId, Long shopId) { + Position memberPosition = addressMapper.findPositionByMemberId(memberId); + Position shopPosition = addressMapper.findPositionByShopId(shopId); + double distanceMeter = memberPosition.distanceMeter(shopPosition); + return deliveryPrice(distanceMeter); + } } diff --git a/src/main/java/com/delfood/service/CartService.java b/src/main/java/com/delfood/service/CartService.java new file mode 100644 index 0000000..e822611 --- /dev/null +++ b/src/main/java/com/delfood/service/CartService.java @@ -0,0 +1,162 @@ +package com.delfood.service; + +import com.delfood.dao.CartDao; +import com.delfood.dto.ItemDTO; +import com.delfood.dto.OptionDTO; +import com.delfood.dto.ItemDTO.CacheOptionDTO; +import com.delfood.error.exception.cart.DuplicateItemException; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class CartService { + @Autowired + private CartDao cartDao; + + private static final long MAX_CART_ITEM_COUNT = 10; + + /** + * 장바구니에 메뉴를 저장한다. + * 장바구니는 하나의 매장에 대한 메뉴만 저장이 가능하다. + * 다른 매장 메뉴를 저장하려고 시도할 시 에러를 발생시킨다. + * 장바구니에는 최대 10종류의 아이템을 담을 수 있다. + * + * @author jun + * @param item 저장할 아이템 + * @param memberId 고객 아이디 + */ + public void addOrdersItem(ItemDTO item, String memberId) { + if (item.hasNullDataBeforeInsertCart()) { + throw new NullPointerException("필수 입력 데이터가 누락되었습니다."); + } + + /* + * 같은 매장의 메뉴인지 확인한다. + * 장바구니에 insert할때마다 해당 유효성 검증을 진행하기 때문에, + * 현재 장바구니에 존재하는 모든 메뉴는 같은 매장의 메뉴라는 것이 보장된다. + * 그렇기 때문에 단 하나의 메뉴만 꺼내서 입력하려는 메뉴와 비교해도 모든 장바구니의 메뉴가 같은 매장의 것이라는 것을 보장받을 수 있다. + */ + ItemDTO peekData = cartDao.findPeekByMemberId(memberId); + // 장바구니에 아이템이 존재할 시 검증 로직을 실행 + if (peekData != null) { + if (item.getShopInfo().getId().equals(peekData.getShopInfo().getId()) == false) { + throw new IllegalArgumentException("다른 매장의 메뉴를 함께 주문할 수 없습니다."); + } + } + + if (getItems(memberId).size() > MAX_CART_ITEM_COUNT) { + throw new IndexOutOfBoundsException("장바구니에는 최대 10개까지 담을 수 있습니다."); + } + + // 똑같은 메뉴, 옵션을 추가하려고 할 수 없도록 한다. + if (containsEqualItem(memberId, item)) { + throw new DuplicateItemException("똑같은 메뉴를 장바구니에 담을 수 없습니다."); + } + + cartDao.addItem(item, memberId); + } + + /** + * 고객의 장바구니에 들어있는 메뉴, 옵션들을 모두 조회한다. + * @param memberId 고객 아이디 + * @return + */ + public List getItems(String memberId) { + return cartDao.findAllByMemberId(memberId); + } + + /** + * 고객 장바구니를 비운다. + * @param memberId 고객 아이디 + */ + public void claer(String memberId) { + cartDao.deleteByMemberId(memberId); + } + + /** + * 장바구니에서 특정 메뉴를 제거한다. + * 특정 메뉴는 index번호로 조회할 수 있으며 index가 범위를 초과하였을 때 오류를 발생시킨다. + * @param memberId 고객 아이디 + * @param index 제거할 메뉴의 인덱스 + */ + public void deleteCartMenu(String memberId, long index) { + long menuCount = cartDao.getMenuCount(memberId); + if (index >= menuCount + 1 || index < 0) { + throw new IndexOutOfBoundsException("index의 범위가 초과되었습니다."); + } + if (cartDao.deleteByMemberIdAndIndex(memberId, index) == false) { + throw new RuntimeException("삭제 실패!"); + } + } + + /** + * 동일한 아이템(동일메뉴, 동일 옵션)을 장바구니에 가지고 있는지 검사한다. + * @param memberId 고객 아이디 + * @param item 검사할 메뉴 + * @return + */ + public boolean containsEqualItem(String memberId, ItemDTO item) { + List items = cartDao.findAllByMemberId(memberId); + return items.contains(item); + } + + /** + * 미완성 로직
+ * 배달료, 쿠폰 계산을 추가할 예정!

+ * 사용자 장바구니에 있는 총 가격을 계산한다. + * menuService.getMenuInfo()에 캐싱 처리를 하여 DB호출을 최대한 줄일 예정. + * + * @author jun + * @param memberId 고객 아이디 + * @return + */ + public long allPrice(String memberId) { + // To-Do : 배달료 계산 로직을 추가해야 함 + // To-Do : 쿠폰 계산 로직을 추가해야 함 + + List ordersItems = getItems(memberId); + + return ordersItems.stream().mapToLong(this::price).sum(); + } + + /** + * 하나의 아이템에 대한 가격을 계산한다. + * + * @author jun + * @param item 가격을 계산할 아이템 + * @return + */ + public long price(ItemDTO item) { + long menuPrice = menuPrice(item); + long optionsPrice = optionsPrice(item); + + return menuPrice + optionsPrice; + } + + /** + * 아이템에서 옵션을 제외한 메뉴의 가격을 계산한다. + * + * @author jun + * @param item 가격을 계산할 아이템 + * @return + */ + public static long menuPrice(ItemDTO item) { + return item.getMenuInfo().getPrice() * item.getCount(); + } + + + /** + * 아이템에서 메뉴를 제외한 옵션들의 가격을 계산한다. + * + * @author jun + * @param item 가격을 계산할 아이템 + * @return + */ + public static long optionsPrice(ItemDTO item) { + return item.getOptions().stream() + .mapToLong(CacheOptionDTO::getPrice) + .sum(); + } + +} diff --git a/src/main/java/com/delfood/service/MenuService.java b/src/main/java/com/delfood/service/MenuService.java index e142172..78cee57 100644 --- a/src/main/java/com/delfood/service/MenuService.java +++ b/src/main/java/com/delfood/service/MenuService.java @@ -110,5 +110,15 @@ public void updateMenu(MenuDTO menuInfo) { throw new RuntimeException("update menu error!"); } } + + /** + * 메뉴를 옵션정보와 함께 조회한다. + * @author jun + * @param id + * @return + */ + public MenuDTO getMenuInfoWithOptions(Long id) { + return menuMapper.findMenuWithOptionsById(id); + } } diff --git a/src/main/java/com/delfood/service/MockPayService.java b/src/main/java/com/delfood/service/MockPayService.java new file mode 100644 index 0000000..f13e7ed --- /dev/null +++ b/src/main/java/com/delfood/service/MockPayService.java @@ -0,0 +1,28 @@ +package com.delfood.service; + +import com.delfood.aop.MemberLoginCheck; +import com.delfood.dto.PaymentDTO; +import com.delfood.error.exception.mockPay.MockPayException; +import lombok.NonNull; +import org.springframework.stereotype.Service; + +@Service +@MemberLoginCheck +public class MockPayService { + + /** + * 결제 서버에 결제를 요청한다. + * 결제 성공시 상태값이 SUCCESS로 세팅된 복제 인스턴스가 반환된다. + * 결제 실패시 예외를 발생시킨다. + * + * @author jun + * @param payment 결제 정보 + */ + public PaymentDTO pay(@NonNull PaymentDTO payment) { + if (payment.isDoSuccess() == false) { + throw new MockPayException("결제 진행을 실패하였습니다."); + } + return payment.pay(); + + } +} diff --git a/src/main/java/com/delfood/service/OptionService.java b/src/main/java/com/delfood/service/OptionService.java index 9af4c00..4744082 100644 --- a/src/main/java/com/delfood/service/OptionService.java +++ b/src/main/java/com/delfood/service/OptionService.java @@ -1,6 +1,8 @@ package com.delfood.service; import com.delfood.dto.OptionDTO; +import com.delfood.dto.OrderItemDTO; +import com.delfood.dto.OrderItemOptionDTO; import com.delfood.mapper.OptionMapper; import java.util.List; import lombok.extern.log4j.Log4j2; @@ -63,6 +65,18 @@ public void deleteOption(Long id) { throw new RuntimeException("delete option error!"); } } + + public OptionDTO getOption(Long id) { + return optionMapper.findById(id); + } + + public long totalPrice(List options) { + return optionMapper.totalPrice(options); + } + + public OptionDTO getOptionInfo(Long id) { + return optionMapper.findById(id); + } diff --git a/src/main/java/com/delfood/service/OrderService.java b/src/main/java/com/delfood/service/OrderService.java new file mode 100644 index 0000000..932a8f3 --- /dev/null +++ b/src/main/java/com/delfood/service/OrderService.java @@ -0,0 +1,231 @@ +package com.delfood.service; + +import com.delfood.controller.response.OrderResponse; +import com.delfood.dto.AddressDTO; +import com.delfood.dto.ItemsBillDTO; +import com.delfood.dto.ItemsBillDTO.MenuInfo; +import com.delfood.dto.ItemsBillDTO.ShopInfo; +import com.delfood.dto.ItemsBillDTO.MenuInfo.OptionInfo; +import com.delfood.dto.MemberDTO.Status; +import com.delfood.error.exception.order.TotalPriceMismatchException; +import com.delfood.dto.MemberDTO; +import com.delfood.dto.MenuDTO; +import com.delfood.dto.OptionDTO; +import com.delfood.dto.OrderDTO; +import com.delfood.dto.OrderItemDTO; +import com.delfood.dto.OrderItemOptionDTO; +import com.delfood.dto.PaymentDTO; +import com.delfood.dto.PaymentDTO.Type; +import com.delfood.dto.OrderBillDTO; +import com.delfood.mapper.OptionMapper; +import com.delfood.mapper.OrderMapper; +import com.delfood.utils.OrderUtil; +import lombok.NonNull; +import lombok.extern.log4j.Log4j2; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Log4j2 +public class OrderService { + @Autowired + private OrderMapper orderMapper; + + @Autowired + private MemberService memberService; + + @Autowired + private ShopService shopService; + + @Autowired + private AddressService addressService; + + @Autowired + private MockPayService mockPayService; + + @Autowired + private PaymentService paymentService; + + /** + * 미완성 로직
+ * 주문 요청을 진행한다. + * 사용자가 주문 요청시 전달받은 가격과, 서버에서 직접 비교한 가격을 비교하여 다르면 예외처리 할 예정. + * @param memberId 고객 아이디 + * @param items 주문한 아이템들 + * @return + */ + @Transactional + public OrderResponse order(String memberId, List items, long shopId) { + + // 주문 준비 작업. 결제 전. + Long orderId = preOrder(memberId, items, shopId); + + // 계산서 발행 + ItemsBillDTO bill = getBill(memberId, items); + + // 가상 결제 진행 + PaymentDTO paymentInfo = PaymentDTO.builder() + .type(Type.CARD) + .amountPayment(bill.getTotalPrice()) // 추후 할인 금액을 빼줘야함 + .orderId(orderId) + .amountDiscount(0L) // 쿠폰 로직 제작 후 작성 예정 + .build(); + + PaymentDTO payResult = mockPayService.pay(paymentInfo); + paymentService.insertPayment(payResult); + + // 사장님에게 알림(푸시) + + + return new OrderResponse(bill, orderId); + } + + /** + * 주문 테이블에 insert를 진행한다. + * 주문 메뉴, 주문 옵션이 추가된다. + * + * @param memberId 고객 아이디 + * @param items 주문할 아이템들 + * @return + */ + @Transactional + private Long preOrder(String memberId, List items, Long shopId) { + MemberDTO memberInfo = memberService.getMemberInfo(memberId); + OrderDTO order = OrderDTO + .builder() + .memberId(memberId) + .addressCode(memberInfo.getAddressCode()) + .addressDetail(memberInfo.getAddressDetail()) + .shopId(shopId) + .deliveryCost(addressService.deliveryPrice(memberId, shopId)) + .build(); + + orderMapper.addOrder(order); + Long orderId = order.getId(); + + log.debug("addOrder Finished"); + log.debug("order id : {}", orderId); + + List options = new ArrayList(); + + for (int i = 0; i < items.size(); i++) { + OrderItemDTO item = items.get(i); + item.setId(OrderUtil.generateOrderItemKey(memberId, i)); + item.setOrderId(orderId); + for (int j = 0; j < item.getOptions().size(); j++) { + OrderItemOptionDTO option = item.getOptions().get(j); + option.setId(OrderUtil.generateOrderItemOptionKey(memberId, i, j)); + option.setOrderItemId(item.getId()); + options.add(option); + } + } + + orderMapper.addOrderItems(items); + orderMapper.addOrderItemOptions(options); + + return order.getId(); + } + + + /** + * 고객이 선정한 메뉴의 총 계산서를 출력한다. + * @author jun + * @param memberId 고객 아이디 + * @param items 주문 상품들. 메뉴, 옵션 리스트가 존재한다. + * @return + */ + @Transactional(readOnly = true) + public ItemsBillDTO getBill(String memberId, List items) { + // 고객 주소 정보 추출 + AddressDTO addressInfo = memberService.getMemberInfo(memberId).getAddressInfo(); + // 매장 정보 추출 + ShopInfo shopInfo = shopService.getShopByMenuId(items.get(0).getMenuId()); + // 배달료 계산 + long deliveryPrice = addressService.deliveryPrice(memberId, shopInfo.getId()); + + // 계산서 생성 + ItemsBillDTO bill = ItemsBillDTO.builder() + .memberId(memberId) + .addressInfo(addressInfo) + .shopInfo(shopInfo) + .deliveryPrice(deliveryPrice) + .menus(orderMapper.findItemsBill(items)) + .build(); + return bill; + } + + + /** + * 총 가격을 계산한다. + * @author jun + * @param items 계산할 아이템들 + * @return 총 가격 + */ + @Transactional(readOnly = true) + public long totalPrice(String memberId, List items) { + long totalPrice = orderMapper.findItemsPrice(items); + long deliveryPrice = addressService.deliveryPrice(memberId, + shopService.getShopByMenuId(items.get(0).getMenuId()).getId()); + + return totalPrice + deliveryPrice; + } + + + /** + * 두 주소 사이 거리(Meter 단위)를 조회한다. + * @author jun + * @param startAddressCode 시작 주소 + * @param endAddressCode 도착 주소 + * @return + */ + public double addressDistance(String startAddressCode, String endAddressCode) { + return addressService.getDistanceMeter(startAddressCode, endAddressCode); + } + + /** + * 주문 정보를 조회한다. + * @param orderId 주문 아이디 + * @return + */ + public OrderBillDTO getPreOrderBill(Long orderId) { + return orderMapper.findOrderBill(orderId); + } + + /** + * 고객의 주문 내역을 확인한다. + * @author jun + * @param memberId 고객아이디 + * @return + */ + public List getMemberOrder(String memberId, Long lastViewedOrderId) { + return orderMapper.findByMemberId(memberId, lastViewedOrderId); + } + + /** + * 주문 번호를 기반으로 주문 상세를 조회한다. + * @author jun + * @param orderId 주문 아이디 + * @param memberId 고객 아이디 + * @return + */ + public OrderDTO getOrder(Long orderId) { + OrderDTO orderInfo = orderMapper.findById(orderId); + return orderInfo; + } + + /** + * 해당 매장의 아이템인지 확인한다. + * @author jun + * @param items 해당 매장의 아이템인지 확인할 아이템들 + * @param shopId 매장 아이디 + * @return + */ + public boolean isShopItems(List items, Long shopId) { + return orderMapper.isShopItem(items, shopId); + } + +} diff --git a/src/main/java/com/delfood/service/PaymentService.java b/src/main/java/com/delfood/service/PaymentService.java new file mode 100644 index 0000000..eb770d7 --- /dev/null +++ b/src/main/java/com/delfood/service/PaymentService.java @@ -0,0 +1,24 @@ +package com.delfood.service; + +import com.delfood.dto.PaymentDTO; +import com.delfood.mapper.PaymentMapper; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Log4j2 +public class PaymentService { + @Autowired + private PaymentMapper paymentMapper; + + @Transactional + public void insertPayment(PaymentDTO paymentInfo) { + long result = paymentMapper.insertPayment(paymentInfo); + if (result != 1) { + log.error("결제 입력 오류. 결제 정보 : {}", paymentInfo); + throw new RuntimeException("결제 입력 오류!"); + } + } +} diff --git a/src/main/java/com/delfood/service/ShopService.java b/src/main/java/com/delfood/service/ShopService.java index d6b1448..df8cf44 100644 --- a/src/main/java/com/delfood/service/ShopService.java +++ b/src/main/java/com/delfood/service/ShopService.java @@ -1,289 +1,295 @@ -package com.delfood.service; - -import com.delfood.dto.DeliveryLocationDTO; -import com.delfood.dto.ShopDTO; -import com.delfood.dto.ShopUpdateDTO; -import com.delfood.error.exception.shop.CanNotCloseShopException; -import com.delfood.error.exception.shop.CanNotOpenShopException; -import com.delfood.mapper.DeliveryLocationMapper; -import com.delfood.mapper.ShopMapper; -import java.util.List; -import java.util.Set; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.HttpStatusCodeException; - -@Service -@Log4j2 -public class ShopService { - @Autowired - private ShopMapper shopMapper; - - @Autowired - private WorkService workService; - - @Autowired - private AddressService addressService; - - @Autowired - DeliveryLocationMapper deliveryLocateionMapper; - - /** - * 매장 정보 삽입 메서드. - * - * @author jun - * @param shopInfo 삽입할 매장의 데이터 - * @return - */ - @Transactional(rollbackFor = RuntimeException.class) - public void addShop(ShopDTO shopInfo) { - int insertShop = shopMapper.insertShop(shopInfo); - if (insertShop != 1) { - log.error("insert ERROR - {}", shopInfo); - throw new RuntimeException("Shop insert ERROR"); - } - } - - /** - * 사장님 가게들의 정보를 불러오는 메서드. - * - * @param ownerId 사장님 id - * @param lastId 마지막으로 조회한 매장 id - * @return - */ - public List getMyShops(String ownerId, Long lastId) { - return shopMapper.findByOwnerId(ownerId, lastId); - } - - /** - * 사장님이 가진 총 가게 개수를 불러오는 메서드. - * - * @param ownerId 사장님 아이디 - * @return - */ - public long getMyShopCount(String ownerId) { - return shopMapper.countByOwnerId(ownerId); - } - - /** - * 한 가게의 정보를 불러오는 메소드. - * - * @author jinyoung - * - * @param id 가게 아이디 - * @return - */ - public ShopDTO getMyShopInfo(Long id) { - return shopMapper.findById(id); - } - - /** - * 메뉴 그룹 추가 시 검증 메서드. - * 사장님 아이디와 매장 아이디가 일치하는 매장이 존재하는 지 조회한다. - * - * @author jinyoung - * - * @param ownerId 사장 아이디 - * @param shopId 매장 아이디 - * @return - */ - public boolean checkShopId(String ownerId, Long shopId) { - return shopMapper.countByOwnerIdAndShopId(ownerId, shopId) != 1; - } - - - /** - * 매장 주인이 사장님인지 조회한다. - * - * @param shopId 매장 id - * @param ownerId 사장님 id - * @return 주인이 맞으면 true - */ - public boolean isShopOwner(Long shopId, String ownerId) { - if (shopId == null || ownerId == null) { - log.error("isShopOwner ERROR! id is null. shopId : {}, ownerId : {}", shopId, ownerId); - throw new NullPointerException("isShopOwner ERROR!"); - } - return shopMapper.countByShopIdAndOwnerId(shopId, ownerId) == 1L; - } - - /** - * 매장 정보 변경 메서드. 변경 오류시 롤백한다. - * - * @author jun - * @param updateInfo 변경할 정보 - */ - @Transactional - public void updateShop(ShopUpdateDTO updateInfo) { - int updateResult = shopMapper.updateShop(updateInfo); - if (updateResult != 1) { - log.error("update shop ERROR! updateInfo : {}", updateInfo); - throw new RuntimeException("shop update ERROR!"); - } - } - - /** - * 매장 영업 시작 메서드. 영업 시작을 기록하고 매장의 상태를 OPEN으로 변경한다. - * - * @author jun - * @param shopId 영업을 시작할 매장 id - */ - @Transactional - public void openShop(Long shopId) { - // 매장이 오픈중일 때 - if (!isClose(shopId)) { - throw new CanNotOpenShopException("영업이 이미 진행중입니다."); - } - - workService.addWork(shopId); - int openResult = shopMapper.updateShopOpenById(shopId); - if (openResult != 1) { - log.error("open shop ERROR! shopId : {}, openResult : {}", shopId, openResult); - throw new RuntimeException("open shop ERROR!"); - } - } - - /** - * 매장 영업 종료 메서드. 영업 종료를 기록하고 매장의 상태를 CLOSE로 변경한다. - * - * @author jun - * @param shopId 영업을 종료할 매장 id - */ - @Transactional - public void closeShop(Long shopId) { - // 해당 매장이 영업중이 아닐시 - if (isClose(shopId)) { - throw new CanNotCloseShopException("이미 영업을 종료한 매장입니다. 영업 종료를 시도할 수 없습니다."); - } - workService.closeWork(shopId); - int closeResult = shopMapper.updateShopCloseById(shopId); - if (closeResult != 1) { - log.error("close Shop ERROR! shopId : {}, closeResult : {}", shopId, closeResult); - } - - } - - /** - * 매장이 OPEN상태가 아닌지 체크하는 메서드. 매장이 OPEN이 아니라면 닫을 수 없기 때문에 체크한다. - * - * @author jun - * @param shopId 체크할 매장의 id - * @return 매장이 오픈상태가 아니라면 true - */ - public boolean isClose(Long shopId) { - long isNotOpenResult = shopMapper.countByIdIsNotOpen(shopId); - return isNotOpenResult == 1; - } - - /** - * 배달가능지역 추가 메서드. - * - * @author jun - * @param shopId 배달 지역을 추가할 매장의 id - * @param requestTownCodes 읍면동코드 리스트. ADDRESS PK의 첫 10자리와 같다 - */ - @SuppressWarnings("serial") - @Transactional - public void addDeliveryLocation(Long shopId, Set requestTownCodes) { - int result = -1; - try { - result = deliveryLocateionMapper.insertDeliveryLocation(shopId, requestTownCodes); - } catch (DataIntegrityViolationException e) { - log.error("중복된 배달 지역 추가 시도! 에러 메세지 : " + e.getMessage()); - throw new HttpStatusCodeException(HttpStatus.BAD_REQUEST, "중복된 배달 지역을 추가할 수 없습니다.") {}; - } - - if (result != requestTownCodes.size()) { - log.error( - "addDelivertLocation ERROR! shopId : {}, requestdeliveryLocations : {}, result : {}", - shopId, requestTownCodes, result); - throw new RuntimeException("addDelivertLocation ERROR!"); - } - - } - - /** - * 해당 배달 지역을 삭제할 권한이 사용자에게 있는지 검사하는 메서드. - * - * @author jun - * @param deliveryLocationId 배달 지역 id - * @param ownerId 사장님 id - * @return 권한이 있다면 true - */ - public boolean isShopOwnerByDeliveryLocationId(Long deliveryLocationId, String ownerId) { - long result = - deliveryLocateionMapper.countByOwnerIdAndDeliveryLocationId(deliveryLocationId, ownerId); - return result == 1; - } - - /** - * 배달 지역을 삭제한다. - * - * @author jun - * @param deliveryLocationId 삭제할 배달 지역 id - */ - @Transactional - public void deleteDeliveryLocation(Long deliveryLocationId) { - int result = deliveryLocateionMapper.deleteDeliveryLocation(deliveryLocationId); - if (result != 1) { - throw new RuntimeException("delete Deliveery Location ERROR"); - } - } - - /** - * 매장 정보를 조회한다. - * - * @author jun - * @param shopId 조회할 매장의 id - * @return - */ - public ShopDTO getShop(Long shopId) { - return shopMapper.findById(shopId); - } - - - - /** - * 배달 가능 지역을 조회한다. 배달 가능 지역 정보를 내부에 포함하여 리턴한다. - * - * @author jun - * @param shopId 조회할 매장의 아이디 - * @return - */ - @Cacheable(value = "DELIVERY_LOCATION", key = "#shopId") - public List getDeliveryLocations(Long shopId) { - return deliveryLocateionMapper.findByShopId(shopId); - } - - - /** - * 자신이 가지고 있는 모든 매장을 마감한다. - * - * @param ownerId 사장님 아이디 - * @return - */ - @Transactional - public List closeAllShops(String ownerId) { - List closeShops = shopMapper.findByBeClose(ownerId); - closeShops.stream().forEach(e -> closeShop(e.getId())); - return closeShops; - } - - /** - * 자신이 가지고 있는 모든 매장을 오픈한다. - * - * @param ownerId 사장님 아이디 - * @return - */ - @Transactional - public List openAllShops(String ownerId) { - List openShops = shopMapper.findByBeOpen(ownerId); - openShops.stream().forEach(e -> openShop(e.getId())); - return openShops; - } -} +package com.delfood.service; + +import com.delfood.dto.AddressDTO; +import com.delfood.dto.DeliveryLocationDTO; +import com.delfood.dto.ItemsBillDTO; +import com.delfood.dto.ShopDTO; +import com.delfood.dto.ShopUpdateDTO; +import com.delfood.error.exception.shop.CanNotCloseShopException; +import com.delfood.error.exception.shop.CanNotOpenShopException; +import com.delfood.mapper.DeliveryLocationMapper; +import com.delfood.mapper.ShopMapper; +import java.util.List; +import java.util.Set; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpStatusCodeException; + +@Service +@Log4j2 +public class ShopService { + @Autowired + private ShopMapper shopMapper; + + @Autowired + private WorkService workService; + + @Autowired + private AddressService addressService; + + @Autowired + DeliveryLocationMapper deliveryLocateionMapper; + + /** + * 매장 정보 삽입 메서드. + * + * @author jun + * @param shopInfo 삽입할 매장의 데이터 + * @return + */ + @Transactional(rollbackFor = RuntimeException.class) + public void addShop(ShopDTO shopInfo) { + int insertShop = shopMapper.insertShop(shopInfo); + if (insertShop != 1) { + log.error("insert ERROR - {}", shopInfo); + throw new RuntimeException("Shop insert ERROR"); + } + } + + /** + * 사장님 가게들의 정보를 불러오는 메서드. + * + * @param ownerId 사장님 id + * @param lastId 마지막으로 조회한 매장 id + * @return + */ + public List getMyShops(String ownerId, Long lastId) { + return shopMapper.findByOwnerId(ownerId, lastId); + } + + /** + * 사장님이 가진 총 가게 개수를 불러오는 메서드. + * + * @param ownerId 사장님 아이디 + * @return + */ + public long getMyShopCount(String ownerId) { + return shopMapper.countByOwnerId(ownerId); + } + + /** + * 한 가게의 정보를 불러오는 메소드. + * + * @author jinyoung + * + * @param id 가게 아이디 + * @return + */ + public ShopDTO getMyShopInfo(Long id) { + return shopMapper.findById(id); + } + + /** + * 메뉴 그룹 추가 시 검증 메서드. + * 사장님 아이디와 매장 아이디가 일치하는 매장이 존재하는 지 조회한다. + * + * @author jinyoung + * + * @param ownerId 사장 아이디 + * @param shopId 매장 아이디 + * @return + */ + public boolean checkShopId(String ownerId, Long shopId) { + return shopMapper.countByOwnerIdAndShopId(ownerId, shopId) != 1; + } + + + /** + * 매장 주인이 사장님인지 조회한다. + * + * @param shopId 매장 id + * @param ownerId 사장님 id + * @return 주인이 맞으면 true + */ + public boolean isShopOwner(Long shopId, String ownerId) { + if (shopId == null || ownerId == null) { + log.error("isShopOwner ERROR! id is null. shopId : {}, ownerId : {}", shopId, ownerId); + throw new NullPointerException("isShopOwner ERROR!"); + } + return shopMapper.countByShopIdAndOwnerId(shopId, ownerId) == 1L; + } + + /** + * 매장 정보 변경 메서드. 변경 오류시 롤백한다. + * + * @author jun + * @param updateInfo 변경할 정보 + */ + @Transactional + public void updateShop(ShopUpdateDTO updateInfo) { + int updateResult = shopMapper.updateShop(updateInfo); + if (updateResult != 1) { + log.error("update shop ERROR! updateInfo : {}", updateInfo); + throw new RuntimeException("shop update ERROR!"); + } + } + + /** + * 매장 영업 시작 메서드. 영업 시작을 기록하고 매장의 상태를 OPEN으로 변경한다. + * + * @author jun + * @param shopId 영업을 시작할 매장 id + */ + @Transactional + public void openShop(Long shopId) { + // 매장이 오픈중일 때 + if (!isClose(shopId)) { + throw new CanNotOpenShopException("영업이 이미 진행중입니다."); + } + + workService.addWork(shopId); + int openResult = shopMapper.updateShopOpenById(shopId); + if (openResult != 1) { + log.error("open shop ERROR! shopId : {}, openResult : {}", shopId, openResult); + throw new RuntimeException("open shop ERROR!"); + } + } + + /** + * 매장 영업 종료 메서드. 영업 종료를 기록하고 매장의 상태를 CLOSE로 변경한다. + * + * @author jun + * @param shopId 영업을 종료할 매장 id + */ + @Transactional + public void closeShop(Long shopId) { + // 해당 매장이 영업중이 아닐시 + if (isClose(shopId)) { + throw new CanNotCloseShopException("이미 영업을 종료한 매장입니다. 영업 종료를 시도할 수 없습니다."); + } + workService.closeWork(shopId); + int closeResult = shopMapper.updateShopCloseById(shopId); + if (closeResult != 1) { + log.error("close Shop ERROR! shopId : {}, closeResult : {}", shopId, closeResult); + } + + } + + /** + * 매장이 OPEN상태가 아닌지 체크하는 메서드. 매장이 OPEN이 아니라면 닫을 수 없기 때문에 체크한다. + * + * @author jun + * @param shopId 체크할 매장의 id + * @return 매장이 오픈상태가 아니라면 true + */ + public boolean isClose(Long shopId) { + long isNotOpenResult = shopMapper.countByIdIsNotOpen(shopId); + return isNotOpenResult == 1; + } + + /** + * 배달가능지역 추가 메서드. + * + * @author jun + * @param shopId 배달 지역을 추가할 매장의 id + * @param requestTownCodes 읍면동코드 리스트. ADDRESS PK의 첫 10자리와 같다 + */ + @SuppressWarnings("serial") + @Transactional + public void addDeliveryLocation(Long shopId, Set requestTownCodes) { + int result = -1; + try { + result = deliveryLocateionMapper.insertDeliveryLocation(shopId, requestTownCodes); + } catch (DataIntegrityViolationException e) { + log.error("중복된 배달 지역 추가 시도! 에러 메세지 : " + e.getMessage()); + throw new HttpStatusCodeException(HttpStatus.BAD_REQUEST, "중복된 배달 지역을 추가할 수 없습니다.") {}; + } + + if (result != requestTownCodes.size()) { + log.error( + "addDelivertLocation ERROR! shopId : {}, requestdeliveryLocations : {}, result : {}", + shopId, requestTownCodes, result); + throw new RuntimeException("addDelivertLocation ERROR!"); + } + + } + + /** + * 해당 배달 지역을 삭제할 권한이 사용자에게 있는지 검사하는 메서드. + * + * @author jun + * @param deliveryLocationId 배달 지역 id + * @param ownerId 사장님 id + * @return 권한이 있다면 true + */ + public boolean isShopOwnerByDeliveryLocationId(Long deliveryLocationId, String ownerId) { + long result = + deliveryLocateionMapper.countByOwnerIdAndDeliveryLocationId(deliveryLocationId, ownerId); + return result == 1; + } + + /** + * 배달 지역을 삭제한다. + * + * @author jun + * @param deliveryLocationId 삭제할 배달 지역 id + */ + @Transactional + public void deleteDeliveryLocation(Long deliveryLocationId) { + int result = deliveryLocateionMapper.deleteDeliveryLocation(deliveryLocationId); + if (result != 1) { + throw new RuntimeException("delete Deliveery Location ERROR"); + } + } + + /** + * 매장 정보를 조회한다. + * + * @author jun + * @param shopId 조회할 매장의 id + * @return + */ + public ShopDTO getShop(Long shopId) { + return shopMapper.findById(shopId); + } + + + + /** + * 배달 가능 지역을 조회한다. 배달 가능 지역 정보를 내부에 포함하여 리턴한다. + * + * @author jun + * @param shopId 조회할 매장의 아이디 + * @return + */ + @Cacheable(value = "DELIVERY_LOCATION", key = "#shopId") + public List getDeliveryLocations(Long shopId) { + return deliveryLocateionMapper.findByShopId(shopId); + } + + + /** + * 자신이 가지고 있는 모든 매장을 마감한다. + * + * @param ownerId 사장님 아이디 + * @return + */ + @Transactional + public List closeAllShops(String ownerId) { + List closeShops = shopMapper.findByBeClose(ownerId); + closeShops.stream().forEach(e -> closeShop(e.getId())); + return closeShops; + } + + /** + * 자신이 가지고 있는 모든 매장을 오픈한다. + * + * @param ownerId 사장님 아이디 + * @return + */ + @Transactional + public List openAllShops(String ownerId) { + List openShops = shopMapper.findByBeOpen(ownerId); + openShops.stream().forEach(e -> openShop(e.getId())); + return openShops; + } + + public ItemsBillDTO.ShopInfo getShopByMenuId(Long menuId) { + return shopMapper.findIdAndNameByMenuId(menuId); + } +} diff --git a/src/main/java/com/delfood/utils/OrderUtil.java b/src/main/java/com/delfood/utils/OrderUtil.java new file mode 100644 index 0000000..4c57206 --- /dev/null +++ b/src/main/java/com/delfood/utils/OrderUtil.java @@ -0,0 +1,14 @@ +package com.delfood.utils; + +public class OrderUtil { + + private OrderUtil() {} + + public static String generateOrderItemKey(String memberId, long idx) { + return memberId + ":" + idx + ":" + System.currentTimeMillis(); + } + + public static String generateOrderItemOptionKey(String memberId, long itemIdx, long optionIdx) { + return memberId + ":" + itemIdx + ":" + optionIdx + ":" + System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/delfood/utils/RedisKeyFactory.java b/src/main/java/com/delfood/utils/RedisKeyFactory.java new file mode 100644 index 0000000..882cc82 --- /dev/null +++ b/src/main/java/com/delfood/utils/RedisKeyFactory.java @@ -0,0 +1,18 @@ +package com.delfood.utils; + +public class RedisKeyFactory { + public enum Key { + CART + } + + // 인스턴스화 방지 + private RedisKeyFactory() {} + + private static String generateKey(String id, Key key) { + return id + ":" + key; + } + + public static String generateCartKey(String memberId) { + return generateKey(memberId, Key.CART); + } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 26bb3fd..7b7798e 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -5,7 +5,10 @@ spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy spring.redis.host=localhost spring.redis.port=6379 spring.redis.password= -spring.redis.defaultExpireSecond=36288000 + +# expire +default.expire.second=60 +cart.expire.second=3600 # Log diff --git a/src/main/resources/mybatis/mapper/address.xml b/src/main/resources/mybatis/mapper/address.xml index c9ab3bf..bdf9382 100644 --- a/src/main/resources/mybatis/mapper/address.xml +++ b/src/main/resources/mybatis/mapper/address.xml @@ -74,5 +74,26 @@ ORDER BY building_management_number LIMIT 10 - + + + + + + diff --git a/src/main/resources/mybatis/mapper/menu.xml b/src/main/resources/mybatis/mapper/menu.xml index 7b64b8a..0912464 100644 --- a/src/main/resources/mybatis/mapper/menu.xml +++ b/src/main/resources/mybatis/mapper/menu.xml @@ -1,85 +1,110 @@ - - - - - - - - - - - - - - - - - - - - SELECT id, name, price, photo, created_at createdAt, - updated_at updatedAt, status, priority, content, menuGroupId - FROM MENU - - - - INSERT INTO MENU(name, price, photo, priority, content, menu_group_id) - VALUES(#{name}, #{price}, #{photo}, - (SELECT IFNULL(MAX(priority)+1, 0) FROM MENU AS MENUS WHERE menu_group_id = #{menuGroupId}), - #{content}, #{menuGroupId}) - - SELECT LAST_INSERT_ID() - - - - - UPDATE MENU - SET status = "DELETED" - WHERE id = #{id} - - - - - - UPDATE MENU - SET priority = CASE - - WHEN id = #{id} THEN #{index} - - END - WHERE id IN ( - SELECT * FROM - (SELECT id FROM MENU - WHERE menu_group_id = #{menuGroupId} - AND status != "DELETED") - TMP) - - - - UPDATE MENU - SET name = #{name}, - price = #{price}, - photo = #{photo}, - content = #{content} - WHERE id = #{id} - - - - - - + + + + + + + + + + + + + + + + + + + + SELECT id, name, price, photo, created_at createdAt, + updated_at updatedAt, status, priority, content, menuGroupId + FROM MENU + + + + INSERT INTO MENU(name, price, photo, priority, content, menu_group_id) + VALUES(#{name}, #{price}, #{photo}, + (SELECT IFNULL(MAX(priority)+1, 0) FROM MENU AS MENUS WHERE menu_group_id = #{menuGroupId}), + #{content}, #{menuGroupId}) + + SELECT LAST_INSERT_ID() + + + + + UPDATE MENU + SET status = "DELETED" + WHERE id = #{id} + + + + + + UPDATE MENU + SET priority = CASE + + WHEN id = #{id} THEN #{index} + + END + WHERE id IN ( + SELECT * FROM + (SELECT id FROM MENU + WHERE menu_group_id = #{menuGroupId} + AND status != "DELETED") + TMP) + + + + UPDATE MENU + SET name = #{name}, + price = #{price}, + photo = #{photo}, + content = #{content} + WHERE id = #{id} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mybatis/mapper/option.xml b/src/main/resources/mybatis/mapper/option.xml index e4827c3..9f41704 100644 --- a/src/main/resources/mybatis/mapper/option.xml +++ b/src/main/resources/mybatis/mapper/option.xml @@ -26,5 +26,17 @@ SET status = "DELETE" WHERE id = #{id} + + + + diff --git a/src/main/resources/mybatis/mapper/orders.xml b/src/main/resources/mybatis/mapper/orders.xml new file mode 100644 index 0000000..927c99f --- /dev/null +++ b/src/main/resources/mybatis/mapper/orders.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + INSERT INTO ORDERS + (member_id, address_code, address_detail, shop_id) + VALUES + (#{memberId}, #{addressCode}, #{addressDetail}, #{shopId}) + + + + INSERT INTO ORDERS_ITEM + (menu_id, order_id, count) + VALUES + (#{menuId}, #{orderId}, #{count}) + + + + INSERT INTO ORDERS_ITEM + (id, menu_id, order_id, count) + VALUES + + (#{item.id}, #{item.menuId}, #{item.orderId}, #{item.count}) + + + + + INSERT INTO ORDERS_ITEM_OPTION + (id, option_id, order_item_id) + VALUES + + (#{option.id}, #{option.optionId}, #{option.orderItemId}) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mybatis/mapper/payment.xml b/src/main/resources/mybatis/mapper/payment.xml new file mode 100644 index 0000000..0db374a --- /dev/null +++ b/src/main/resources/mybatis/mapper/payment.xml @@ -0,0 +1,10 @@ + + + + + INSERT INTO PAYMENT + (type, amount_payment, pay_time, order_id, status, amount_discount) + VALUES + (#{type}, #{amountPayment}, #{payTime}, #{orderId}, #{status}, #{amountDiscount}) + + diff --git a/src/main/resources/mybatis/mapper/shop.xml b/src/main/resources/mybatis/mapper/shop.xml index 51a3c97..fb51671 100644 --- a/src/main/resources/mybatis/mapper/shop.xml +++ b/src/main/resources/mybatis/mapper/shop.xml @@ -172,4 +172,11 @@ AND owner_id = #{ownerId} AND work_condition = 'OPEN' + + diff --git a/src/test/java/com/delfood/service/CartServiceTest.java b/src/test/java/com/delfood/service/CartServiceTest.java new file mode 100644 index 0000000..b58df32 --- /dev/null +++ b/src/test/java/com/delfood/service/CartServiceTest.java @@ -0,0 +1,26 @@ +package com.delfood.service; + +import static org.mockito.BDDMockito.given; + +import com.delfood.dao.CartDao; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class CartServiceTest { + @InjectMocks + CartService service; + + @Mock + CartDao dao; + + + // 로직이 확정되면 테스트코드를 다시 작성할 예정입니다 + @Test + public void mockTest() { + + } +}