Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example.Centralthon.domain.order.entity;

import com.example.Centralthon.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "orders")
public class Order extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="order_id")
private Long id;

@Column(name = "pick_up_code", nullable = false, unique = true, length = 6)
private String pickUpCode;

@Column(nullable = false)
private int price;

@Column(name = "is_picked_up", nullable = false)
private boolean pickedUp;

public static Order toEntity(String pickUpCode, int price){
return Order.builder()
.pickUpCode(pickUpCode)
.price(price)
.pickedUp(false)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.Centralthon.domain.order.entity;

import com.example.Centralthon.domain.menu.entity.Menu;
import com.example.Centralthon.domain.store.entity.Store;
import com.example.Centralthon.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.*;

@Getter
@Setter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "order_items")
public class OrderItem extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="order_items_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "menu_id", nullable = false)
private Menu menu;

@Column(nullable = false)
private int quantity;

public static OrderItem toEntity(Order order, Menu menu, int quantity) {
return OrderItem.builder()
.order(order)
.menu(menu)
.quantity(quantity)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.Centralthon.domain.order.exception;

import com.example.Centralthon.global.exception.BaseException;

public class CodeNotCreatedException extends BaseException {
public CodeNotCreatedException() {
super(OrderErrorCode.CODE_NOT_CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.Centralthon.domain.order.exception;

import com.example.Centralthon.global.response.code.BaseResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum OrderErrorCode implements BaseResponseCode {
CODE_NOT_CREATED("ORDER_500_1",500,"서버에서 유일한 픽업 코드를 생성하지 못했습니다.");

private final String code;
private final int httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.Centralthon.domain.order.repository;

import com.example.Centralthon.domain.order.entity.OrderItem;
import org.springframework.data.repository.CrudRepository;

public interface OrderItemRepository extends CrudRepository<OrderItem, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.Centralthon.domain.order.repository;

import com.example.Centralthon.domain.order.entity.Order;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface OrderRepository extends CrudRepository<Order, Long> {
Optional<Order> findByPickUpCode(String pickUpCode);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.Centralthon.domain.order.service;

import com.example.Centralthon.domain.order.web.dto.OrderReq;
import com.example.Centralthon.domain.order.web.dto.OrderRes;

import java.util.List;

public interface OrderService {
OrderRes orderMenus(OrderReq orderReq);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.example.Centralthon.domain.order.service;

import com.example.Centralthon.domain.menu.entity.Menu;
import com.example.Centralthon.domain.menu.exception.MenuNotFoundException;
import com.example.Centralthon.domain.menu.repository.MenuRepository;
import com.example.Centralthon.domain.order.entity.Order;
import com.example.Centralthon.domain.order.entity.OrderItem;
import com.example.Centralthon.domain.order.exception.CodeNotCreatedException;
import com.example.Centralthon.domain.order.repository.OrderItemRepository;
import com.example.Centralthon.domain.order.repository.OrderRepository;
import com.example.Centralthon.domain.order.web.dto.OrderItemListReq;
import com.example.Centralthon.domain.order.web.dto.OrderReq;
import com.example.Centralthon.domain.order.web.dto.OrderRes;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;

@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
@Lazy @Autowired
private OrderServiceImpl selfProxy;

private final MenuRepository menuRepository;
private final OrderRepository orderRepository;
private final OrderItemRepository orderItemRepository;

@Override
@Transactional
public OrderRes orderMenus(OrderReq orderReq) {
Map<Long, Integer> orderList = new HashMap<>();

for (OrderItemListReq orderItemListReq : orderReq.getItems()) {
orderList.merge(orderItemListReq.getMenuId(), orderItemListReq.getCount(), Integer::sum);
}

// 메뉴 id가 없을 경우 -> MenuNotFoundException
List<Menu> menuList = menuRepository.findAllById(orderList.keySet());
if(menuList.size() != orderList.keySet().size()) {
throw new MenuNotFoundException();
}

// 최종 결제 금액 계산
int totalPrice = 0;
for (Menu menu : menuList) {
// 오버플로우 방지를 위한 addExact, multiplyExact 사용
totalPrice = Math.addExact(totalPrice, Math.multiplyExact(menu.getSalePrice(), orderList.get(menu.getId())));
}

Order order = createOrderWithUniqueCode(totalPrice);

List<OrderItem> orderItemList = new ArrayList<>();
for(Menu menu : menuList) {
OrderItem orderItem = OrderItem.toEntity(order, menu, orderList.get(menu.getId()));
orderItemList.add(orderItem);
}
orderItemRepository.saveAll(orderItemList);

return OrderRes.from(order);
}

private Order createOrderWithUniqueCode(int totalPrice) {
for(int i = 0; i < 10; i++) {
String code = generatePickUpCode();
try{
// 프록시를 적용해주지 않으면, this.saveOrder가 실행되어 트랙잭션이 실행되지 않음
return selfProxy.saveOrder(code, totalPrice);
} catch (DataIntegrityViolationException e) {
// 다음 실행 진행
}
}
throw new CodeNotCreatedException();
}

/**
* 같은 트랙잭션에서 실패하면 rollback-only 상태로 인해 정상 커밋이 불가능하므로,
* 새 트랜잭션을 열어 새로운 픽업 코드를 저장하도록 시도
* -> 새 트랜잭션을 열기 위해 @Transactional(propagation = Propagation.REQUIRES_NEW) 사용
* -> @Transactional(propagation = Propagation.REQUIRES_NEW)는 기존 트랜잭션을 중지시키고, 새 트랜잭션 시작
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public Order saveOrder(String code, int totalPrice){
Order order = Order.toEntity(code, totalPrice);
return orderRepository.save(order);
}

/**
* 픽업 코드(영문 두자리 + 숫자 네자리) 생성 메서드
*/
private String generatePickUpCode() {
ThreadLocalRandom random = ThreadLocalRandom.current();
char[] codeArray = new char[6];
String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
String number = "0123456789";

for (int i = 0; i < 2; i++) {
char alpha = alphabet.charAt(random.nextInt(alphabet.length()));
codeArray[i] = alpha;
}

for (int i = 0; i < 4; i++) {
char num = number.charAt(random.nextInt(number.length()));
codeArray[i + 2] = num;
}

return new String(codeArray);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.Centralthon.domain.order.web.controller;

import com.example.Centralthon.domain.order.service.OrderService;
import com.example.Centralthon.domain.order.web.dto.OrderReq;
import com.example.Centralthon.domain.order.web.dto.OrderRes;
import com.example.Centralthon.global.response.SuccessResponse;
import com.example.Centralthon.global.response.code.SuccessResponseCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;

@PostMapping
public ResponseEntity<SuccessResponse<OrderRes>> createOrder(@RequestBody @Valid OrderReq orderReq) {
OrderRes orderRes = orderService.orderMenus(orderReq);
return ResponseEntity.status(HttpStatus.CREATED).body(SuccessResponse.of(orderRes, SuccessResponseCode.SUCCESS_CREATED));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.Centralthon.domain.order.web.dto;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderItemListReq {
@NotNull(message = "메뉴 기본 키는 필수 값입니다.")
private Long menuId;

@NotNull(message = "수량은 필수 값입니다.")
@Min(value = 1, message = "수량은 1 이상이어야 합니다.")
private Integer count;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.Centralthon.domain.order.web.dto;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class OrderReq {
@NotEmpty(message = "주문 항목은 빈 배열일 수 없습니다.")
List<@Valid OrderItemListReq> items;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.Centralthon.domain.order.web.dto;

import com.example.Centralthon.domain.order.entity.Order;

public record OrderRes(
String code
) {
public static OrderRes from(Order order) {
return new OrderRes(order.getPickUpCode());
}
}