
주문 / 주문 상품 엔티티 분리
주문 상태(Enum) 기반 흐름 관리
재고 차감 및 복구 처리
사용자/관리자 권한 분리
JWT 인증 기반 주문 API 설계

src/main/java/com/onandhome/
├── order/
│ ├── entity/
│ │ ├── Order.java # 주문 엔티티
│ │ ├── OrderItem.java # 주문 항목
│ │ └── OrderStatus.java # 주문 상태 enum
│ ├── dto/
│ │ ├── CreateOrderRequest.java # 주문 생성 요청
│ │ ├── OrderDTO.java # 주문 응답
│ │ └── OrderItemDTO.java # 주문 항목 DTO
│ ├── OrderRepository.java # 주문 DB 접근
│ ├── OrderItemRepository.java # 주문 항목 DB
│ ├── OrderService.java # 주문 비즈니스 로직
│ └── OrderRestController.java # 주문 REST API
└── product/
└── ProductService.java # 재고 관리
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 주문자 정보
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
// 주문 번호 (고유)
@Column(unique = true, nullable = false)
private String orderNumber;
// 배송 정보
@Column(nullable = false)
private String recipientName;
@Column(nullable = false)
private String recipientPhone;
@Column(nullable = false)
private String shippingAddress;
@Column(length = 500)
private String shippingRequest;
// 결제 정보
@Column(nullable = false)
private String paymentMethod; // CARD, BANK_TRANSFER, VIRTUAL_ACCOUNT
@Column(nullable = false)
private int totalPrice;
// 주문 상태
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
// 주문 항목 (1:N)
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
// 주문 시간
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime orderedAt;
// 배송 완료 시간
@Column
private LocalDateTime deliveredAt;
// 취소 시간
@Column
private LocalDateTime canceledAt;
/**
* 주문 생성 팩토리 메서드
*/
public static Order create(User user, List<OrderItem> orderItems,
String paymentMethod) {
Order order = new Order();
order.setUser(user);
order.setOrderNumber(generateOrderNumber());
order.setPaymentMethod(paymentMethod);
order.setStatus(OrderStatus.PENDING);
// 주문 항목 추가
for (OrderItem item : orderItems) {
order.addOrderItem(item);
}
// 총 금액 계산
order.calculateTotalPrice();
return order;
}
/**
* 주문 항목 추가
*/
public void addOrderItem(OrderItem item) {
orderItems.add(item);
item.setOrder(this);
}
/**
* 총 금액 계산
*/
public void calculateTotalPrice() {
this.totalPrice = orderItems.stream()
.mapToInt(OrderItem::getTotalPrice)
.sum();
}
/**
* 주문 번호 생성
*/
private static String generateOrderNumber() {
return "ORD" + LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
) + (int)(Math.random() * 1000);
}
/**
* 주문 취소
*/
public void cancel() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("배송 준비 이전 상태만 취소 가능합니다.");
}
this.status = OrderStatus.CANCELED;
this.canceledAt = LocalDateTime.now();
}
}
@Entity
@Table(name = "order_items")
@Getter
@Setter
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id", nullable = false)
private Product product;
@Column(nullable = false)
private int quantity;
@Column(nullable = false)
private int price; // 주문 당시 가격
/**
* 주문 항목 생성
*/
public static OrderItem create(Product product, int quantity) {
OrderItem item = new OrderItem();
item.setProduct(product);
item.setQuantity(quantity);
item.setPrice(product.getPrice());
return item;
}
/**
* 총 가격 계산
*/
public int getTotalPrice() {
return price * quantity;
}
}
public enum OrderStatus {
PENDING("주문 완료"), // 주문 완료
PREPARING("배송 준비중"), // 배송 준비
SHIPPED("배송중"), // 배송중
DELIVERED("배송 완료"), // 배송 완료
CANCELED("주문 취소"); // 취소
private final String description;
OrderStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
@Getter
@Setter
public class CreateOrderRequest {
private Long userId;
// 주문 항목
private List<OrderItemRequest> orderItems;
// 배송 정보
private String recipientName;
private String recipientPhone;
private String shippingAddress;
private String shippingRequest;
// 결제 정보
private String paymentMethod; // CARD, BANK_TRANSFER, VIRTUAL_ACCOUNT
@Getter
@Setter
public static class OrderItemRequest {
private Long productId;
private int quantity;
}
}
@Getter
@Setter
@Builder
public class OrderDTO {
private Long id;
private String orderNumber;
private String userName;
private String recipientName;
private String recipientPhone;
private String shippingAddress;
private String shippingRequest;
private String paymentMethod;
private int totalPrice;
private String status;
private String statusDescription;
private List<OrderItemDTO> orderItems;
private LocalDateTime orderedAt;
private LocalDateTime deliveredAt;
private LocalDateTime canceledAt;
/**
* Entity → DTO 변환
*/
public static OrderDTO fromEntity(Order order) {
return OrderDTO.builder()
.id(order.getId())
.orderNumber(order.getOrderNumber())
.userName(order.getUser().getUsername())
.recipientName(order.getRecipientName())
.recipientPhone(order.getRecipientPhone())
.shippingAddress(order.getShippingAddress())
.shippingRequest(order.getShippingRequest())
.paymentMethod(order.getPaymentMethod())
.totalPrice(order.getTotalPrice())
.status(order.getStatus().name())
.statusDescription(order.getStatus().getDescription())
.orderItems(order.getOrderItems().stream()
.map(OrderItemDTO::fromEntity)
.collect(Collectors.toList()))
.orderedAt(order.getOrderedAt())
.deliveredAt(order.getDeliveredAt())
.canceledAt(order.getCanceledAt())
.build();
}
}
@Getter
@Setter
@Builder
public class OrderItemDTO {
private Long id;
private Long productId;
private String productName;
private int quantity;
private int price;
private int totalPrice;
public static OrderItemDTO fromEntity(OrderItem item) {
return OrderItemDTO.builder()
.id(item.getId())
.productId(item.getProduct().getId())
.productName(item.getProduct().getName())
.quantity(item.getQuantity())
.price(item.getPrice())
.totalPrice(item.getTotalPrice())
.build();
}
}
@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {
private final OrderRepository orderRepo;
private final OrderItemRepository orderItemRepo;
private final UserRepository userRepo;
private final ProductRepository productRepo;
private final CartService cartService;
/**
* 주문 생성
*/
public OrderDTO createOrder(CreateOrderRequest request) {
// 1. 사용자 조회
User user = userRepo.findById(request.getUserId())
.orElseThrow(() -> new IllegalArgumentException(
"사용자를 찾을 수 없습니다."));
// 2. 주문 항목 생성 및 재고 확인
List<OrderItem> orderItems = new ArrayList<>();
for (CreateOrderRequest.OrderItemRequest itemReq : request.getOrderItems()) {
Product product = productRepo.findById(itemReq.getProductId())
.orElseThrow(() -> new IllegalArgumentException(
"상품을 찾을 수 없습니다: " + itemReq.getProductId()));
// 재고 확인
if (product.getStock() < itemReq.getQuantity()) {
throw new IllegalStateException(
product.getName() + " 상품의 재고가 부족합니다.");
}
// 주문 항목 생성
OrderItem orderItem = OrderItem.create(product, itemReq.getQuantity());
orderItems.add(orderItem);
}
// 3. 주문 생성
Order order = Order.create(user, orderItems, request.getPaymentMethod());
// 배송 정보 설정
order.setRecipientName(request.getRecipientName());
order.setRecipientPhone(request.getRecipientPhone());
order.setShippingAddress(request.getShippingAddress());
order.setShippingRequest(request.getShippingRequest());
// 4. 재고 차감
for (OrderItem item : orderItems) {
Product product = item.getProduct();
product.setStock(product.getStock() - item.getQuantity());
productRepo.save(product);
}
// 5. 주문 저장
Order savedOrder = orderRepo.save(order);
// 6. 장바구니에서 주문한 상품 삭제
try {
for (CreateOrderRequest.OrderItemRequest itemReq : request.getOrderItems()) {
cartService.removeFromCart(user.getUserId(), itemReq.getProductId());
}
} catch (Exception e) {
// 장바구니 삭제 실패는 주문에 영향 없음
log.warn("장바구니 삭제 실패: {}", e.getMessage());
}
return OrderDTO.fromEntity(savedOrder);
}
/**
* 사용자 주문 목록 조회
*/
@Transactional(readOnly = true)
public List<OrderDTO> getUserOrders(Long userId) {
User user = userRepo.findById(userId)
.orElseThrow(() -> new IllegalArgumentException(
"사용자를 찾을 수 없습니다."));
return orderRepo.findByUserOrderByOrderedAtDesc(user)
.stream()
.map(OrderDTO::fromEntity)
.collect(Collectors.toList());
}
/**
* 주문 상세 조회
*/
@Transactional(readOnly = true)
public OrderDTO getOrderDetail(Long orderId, Long userId) {
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException(
"주문을 찾을 수 없습니다."));
// 본인 주문만 조회 가능
if (!order.getUser().getId().equals(userId)) {
throw new IllegalArgumentException("권한이 없습니다.");
}
return OrderDTO.fromEntity(order);
}
/**
* 주문 취소
*/
public void cancelOrder(Long orderId, Long userId) {
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException(
"주문을 찾을 수 없습니다."));
// 본인 주문만 취소 가능
if (!order.getUser().getId().equals(userId)) {
throw new IllegalArgumentException("권한이 없습니다.");
}
// 주문 취소
order.cancel();
// 재고 복구
for (OrderItem item : order.getOrderItems()) {
Product product = item.getProduct();
product.setStock(product.getStock() + item.getQuantity());
productRepo.save(product);
}
orderRepo.save(order);
}
/**
* 주문 상태 변경 (관리자)
*/
public void updateOrderStatus(Long orderId, OrderStatus newStatus) {
Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException(
"주문을 찾을 수 없습니다."));
order.setStatus(newStatus);
if (newStatus == OrderStatus.DELIVERED) {
order.setDeliveredAt(LocalDateTime.now());
}
orderRepo.save(order);
}
/**
* 전체 주문 목록 (관리자)
*/
@Transactional(readOnly = true)
public List<OrderDTO> getAllOrders() {
return orderRepo.findAllByOrderByOrderedAtDesc()
.stream()
.map(OrderDTO::fromEntity)
.collect(Collectors.toList());
}
}
@RestController
@RequestMapping("/api/orders")
@RequiredArgsConstructor
public class OrderRestController {
private final OrderService orderService;
private final JWTUtil jwtUtil;
/**
* 주문 생성
* POST /api/orders
*/
@PostMapping
public ResponseEntity<Map<String, Object>> createOrder(
@RequestBody CreateOrderRequest request,
@RequestHeader("Authorization") String authHeader) {
try {
String userId = getUserIdFromToken(authHeader);
request.setUserId(Long.parseLong(userId));
OrderDTO order = orderService.createOrder(request);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "주문이 완료되었습니다.");
response.put("data", order);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 내 주문 목록 조회
* GET /api/orders
*/
@GetMapping
public ResponseEntity<Map<String, Object>> getMyOrders(
@RequestHeader("Authorization") String authHeader) {
try {
Long userId = Long.parseLong(getUserIdFromToken(authHeader));
List<OrderDTO> orders = orderService.getUserOrders(userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", orders);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 주문 상세 조회
* GET /api/orders/{id}
*/
@GetMapping("/{id}")
public ResponseEntity<Map<String, Object>> getOrderDetail(
@PathVariable Long id,
@RequestHeader("Authorization") String authHeader) {
try {
Long userId = Long.parseLong(getUserIdFromToken(authHeader));
OrderDTO order = orderService.getOrderDetail(id, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", order);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 주문 취소
* PUT /api/orders/{id}/cancel
*/
@PutMapping("/{id}/cancel")
public ResponseEntity<Map<String, Object>> cancelOrder(
@PathVariable Long id,
@RequestHeader("Authorization") String authHeader) {
try {
Long userId = Long.parseLong(getUserIdFromToken(authHeader));
orderService.cancelOrder(id, userId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "주문이 취소되었습니다.");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 주문 상태 변경 (관리자)
* PUT /api/orders/{id}/status
*/
@PutMapping("/{id}/status")
public ResponseEntity<Map<String, Object>> updateOrderStatus(
@PathVariable Long id,
@RequestBody Map<String, String> request) {
try {
OrderStatus status = OrderStatus.valueOf(request.get("status"));
orderService.updateOrderStatus(id, status);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "주문 상태가 변경되었습니다.");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 전체 주문 목록 (관리자)
* GET /api/orders/admin/all
*/
@GetMapping("/admin/all")
public ResponseEntity<Map<String, Object>> getAllOrders() {
try {
List<OrderDTO> orders = orderService.getAllOrders();
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("data", orders);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* JWT에서 userId 추출
*/
private String getUserIdFromToken(String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new RuntimeException("유효하지 않은 토큰입니다.");
}
String token = authHeader.substring(7);
Map<String, Object> claims = jwtUtil.validateToken(token);
return String.valueOf(claims.get("userId"));
}
}
| Method | Endpoint | 설명 | 권한 |
|---|---|---|---|
| POST | /api/orders | 주문 생성 | 사용자 |
| GET | /api/orders | 내 주문 목록 | 사용자 |
| GET | /api/orders/{id} | 주문 상세 | 사용자 |
| PUT | /api/orders/{id}/cancel | 주문 취소 | 사용자 |
| PUT | /api/orders/{id}/status | 주문 상태 변경 | 관리자 |
| GET | /api/orders/admin/all | 전체 주문 목록 | 관리자 |



설정
Method: POST
URL: http://localhost:8080/api/orders
Headers:
Content-Type: application/json
Authorization: Bearer {your_jwt_token}
Body (raw JSON)
{
"orderItems": [
{
"productId": 10,
"quantity": 2
},
{
"productId": 15,
"quantity": 1
}
],
"recipientName": "김철수",
"recipientPhone": "010-1234-5678",
"shippingAddress": "서울특별시 강남구 테헤란로 123",
"shippingRequest": "문 앞에 놓아주세요",
"paymentMethod": "CARD"
}
응답
{
"success": true,
"message": "주문이 완료되었습니다.",
"data": {
"id": 42,
"orderNumber": "ORD20250129143500123",
"userName": "testuser",
"recipientName": "김현수",
"totalPrice": 150000,
"status": "PENDING",
"statusDescription": "주문 완료",
"orderItems": [
{
"productId": 10,
"productName": "냉장고",
"quantity": 2,
"price": 50000,
"totalPrice": 100000
},
{
"productId": 15,
"productName": "세탁기",
"quantity": 1,
"price": 50000,
"totalPrice": 50000
}
],
"orderedAt": "2025-01-29T14:35:00"
}
}
설정
Method: GET
URL: http://localhost:8080/api/orders
Headers:
Authorization: Bearer {your_jwt_token}
응답
{
"success": true,
"data": [
{
"id": 42,
"orderNumber": "ORD20250129143500123",
"totalPrice": 150000,
"status": "PENDING",
"statusDescription": "주문 완료",
"orderedAt": "2025-01-29T14:35:00"
}
]
}
설정
Method: GET
URL: http://localhost:8080/api/orders/42
Headers:
Authorization: Bearer {your_jwt_token}
응답
{
"success": true,
"data": {
"id": 42,
"orderNumber": "ORD20250129143500123",
"recipientName": "김현수",
"recipientPhone": "010-1234-5678",
"shippingAddress": "서울특별시 강남구 테헤란로 123",
"shippingRequest": "문 앞에 놓아주세요",
"paymentMethod": "CARD",
"totalPrice": 150000,
"status": "PENDING",
"orderItems": [...]
}
}
설정
Method: PUT
URL: http://localhost:8080/api/orders/42/cancel
Headers:
Authorization: Bearer {your_jwt_token}
응답
{
"success": true,
"message": "주문이 취소되었습니다."
}
재조회 확인
GET /api/orders/42
→ status: "CANCELED", canceledAt: "2025-01-29T15:00:00"
설정
Method: PUT
URL: http://localhost:8080/api/orders/42/status
Headers:
Content-Type: application/json
Authorization: Bearer {admin_jwt_token}
Body
{
"status": "SHIPPED"
}
응답
{
"success": true,
"message": "주문 상태가 변경되었습니다."
}