Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d197bc9
feat: 직접 계좌이체 주문을 위한 결제 모델 및 계좌 정보 확장 (#2516)
Soundbar91 Oct 27, 2026
854acad
feat: PG 승인 없이 계좌이체 배달 주문 생성 API 구현 (#2516)
Soundbar91 Oct 27, 2026
0cad902
fix: 직접 계좌이체 주문 취소 시 PG 호출 제외 (#2516)
Soundbar91 Oct 27, 2026
c6344f2
refactor: 배달 주문 생성 응답 처리 컨벤션 정리 (#2516)
Soundbar91 Oct 27, 2026
152c789
refactor: 배달 주문 장바구니 조회 시 비관적 락 제거 (#2516)
Soundbar91 Oct 27, 2026
3371536
refactor: 주문 생성 응답 설명 및 코드 컨벤션 정리 (#2516)
Soundbar91 Oct 27, 2026
5ac8a24
fix: 예금주 검증 제외 및 응답 임시 처리 (#2516)
Soundbar91 Oct 27, 2026
1533598
fix: 예금주 임시 응답값을 임금주명으로 변경 (#2516)
Soundbar91 Oct 27, 2026
5d45cfc
refactor: 배달 주문 장바구니 검증을 도메인으로 이동 (#2516)
Soundbar91 Oct 27, 2026
c0584eb
refactor: 계좌이체 정보 검증을 상점 도메인으로 이동 (#2516)
Soundbar91 Oct 27, 2026
c3b32cd
refactor: 배달 주문 금액 검증을 서비스 내부 메서드로 변경 (#2516)
Soundbar91 Oct 27, 2026
b6d0b5f
refactor: 배달 주문 금액 검증 코드 컨벤션 정리 (#2516)
Soundbar91 Oct 27, 2026
21c8886
refactor: 배달 주문 생성을 도메인 정적 팩토리로 이동 (#2516)
Soundbar91 Oct 27, 2026
71eebfc
refactor: 주문 금액 검증을 유틸 클래스로 분리 (#2516)
Soundbar91 Oct 27, 2026
a1540f6
refactor: 계좌이체 Flyway DDL 분리 및 예금주 컬럼 제외 (#2516)
Soundbar91 Oct 27, 2026
e7ae0d1
refactor: 주문 생성 및 취소 시각을 Clock 기반으로 전달 (#2516)
Soundbar91 Oct 27, 2026
66e0558
docs: 배달 주문 API 설명 정리 및 Swagger 응답 코드 추가 (#2516)
Soundbar91 Oct 27, 2026
ed6751b
feat: 포장 주문 도메인 생성 및 금액 검증 추가 (#2516)
Soundbar91 Oct 27, 2026
260a70c
feat: 계좌이체 포장 주문 생성 API 구현 (#2516)
Soundbar91 Oct 27, 2026
fdda007
refactor: 배달 및 포장 주문 생성을 OrderService로 통합 (#2516)
Soundbar91 Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/java/in/koreatech/koin/domain/order/cart/model/Cart.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,25 @@ public void validateOrderType(OrderType orderType) {
}
}

public void validateDeliveryOrder() {
if (cartMenuItems.isEmpty()) {
throw CustomException.of(ApiResponseCode.NOT_FOUND_CART_ITEM);
}
validateOrderType(OrderType.DELIVERY);
orderableShop.requireShopOpen();
orderableShop.requireMinimumOrderAmount(calculateItemsAmount());
cartMenuItems.forEach(item -> item.getOrderableShopMenu().validateSoldOut());
}

public void validateTakeoutOrder() {
if (cartMenuItems.isEmpty()) {
throw CustomException.of(ApiResponseCode.NOT_FOUND_CART_ITEM);
}
validateOrderType(OrderType.TAKE_OUT);
orderableShop.requireShopOpen();
cartMenuItems.forEach(item -> item.getOrderableShopMenu().validateSoldOut());
}

public Integer getItemTypeCount() {
return this.cartMenuItems.size();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,99 @@
package in.koreatech.koin.domain.order.order.controller;

import static in.koreatech.koin.domain.user.model.UserType.STUDENT;
import static in.koreatech.koin.global.code.ApiResponseCode.*;

import java.util.List;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;

import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest;
import in.koreatech.koin.domain.order.order.dto.request.OrderSearchPeriodCriteria;
import in.koreatech.koin.domain.order.order.dto.request.OrderStatusCriteria;
import in.koreatech.koin.domain.order.order.dto.request.OrderTypeCriteria;
import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest;
import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse;
import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse;
import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.code.ApiResponseCodes;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;

@Tag(name = "(Normal) Order: 주문", description = "주문을 관리한다.")
@RequestMapping("/order")
public interface OrderApi {

@ApiResponseCodes({
OK,
INVALID_REQUEST_BODY,
NOT_READABLE_HTTP_MESSAGE,
INVALID_ADDRESS_FORMAT,
SHOP_NOT_DELIVERABLE,
SHOP_CLOSED,
ORDER_AMOUNT_BELOW_MINIMUM,
MENU_SOLD_OUT,
SHOP_BANK_ACCOUNT_UNAVAILABLE,
ORDER_PRICE_MISMATCH,
UNAUTHORIZED_USER,
WITHDRAWN_USER,
FORBIDDEN_USER_TYPE,
FORBIDDEN_STUDENT,
NOT_FOUND_USER,
NOT_FOUND_CART,
NOT_FOUND_CART_ITEM
})
@Operation(
summary = "계좌이체 배달 주문을 생성한다.",
description = """
로그인한 학생의 장바구니로 계좌이체 배달 주문을 생성한다.
메뉴 금액과 배달비를 검증하고 주문 생성 후 장바구니를 비운다.
주문 정보, 입금할 금액과 상점 계좌 정보를 반환한다.
"""
)
@PostMapping("/delivery")
ResponseEntity<OrderCreateResponse> createDeliveryOrder(
@RequestBody @Valid DeliveryOrderRequest request,
@Auth(permit = {STUDENT}) Integer userId
);

@ApiResponseCodes({
OK,
INVALID_REQUEST_BODY,
NOT_READABLE_HTTP_MESSAGE,
SHOP_NOT_TAKEOUT_AVAILABLE,
SHOP_CLOSED,
MENU_SOLD_OUT,
SHOP_BANK_ACCOUNT_UNAVAILABLE,
ORDER_PRICE_MISMATCH,
UNAUTHORIZED_USER,
WITHDRAWN_USER,
FORBIDDEN_USER_TYPE,
FORBIDDEN_STUDENT,
NOT_FOUND_USER,
NOT_FOUND_CART,
NOT_FOUND_CART_ITEM
})
@Operation(
summary = "계좌이체 포장 주문을 생성한다.",
description = """
로그인한 학생의 장바구니로 계좌이체 포장 주문을 생성한다.
메뉴 금액과 총 결제 금액을 검증하고 주문 생성 후 장바구니를 비운다.
배달비는 부과하지 않으며 주문 정보, 입금할 금액과 상점 계좌 정보를 반환한다.
"""
)
@PostMapping("/takeout")
ResponseEntity<OrderCreateResponse> createTakeoutOrder(
@RequestBody @Valid TakeoutOrderRequest request,
@Auth(permit = {STUDENT}) Integer userId
);

@Operation(
summary = "주문 이력을 조회한다.",
description = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import in.koreatech.koin.domain.order.order.dto.request.DeliveryOrderRequest;
import in.koreatech.koin.domain.order.order.dto.request.OrderSearchCondition;
import in.koreatech.koin.domain.order.order.dto.request.OrderSearchPeriodCriteria;
import in.koreatech.koin.domain.order.order.dto.request.OrderStatusCriteria;
import in.koreatech.koin.domain.order.order.dto.request.OrderTypeCriteria;
import in.koreatech.koin.domain.order.order.dto.request.TakeoutOrderRequest;
import in.koreatech.koin.domain.order.order.dto.response.InprogressOrderResponse;
import in.koreatech.koin.domain.order.order.dto.response.OrderCreateResponse;
import in.koreatech.koin.domain.order.order.dto.response.OrdersResponse;
import in.koreatech.koin.domain.order.order.service.OrderService;
import in.koreatech.koin.global.auth.Auth;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -27,6 +33,24 @@ public class OrderController implements OrderApi {

private final OrderService orderService;

@PostMapping("/delivery")
public ResponseEntity<OrderCreateResponse> createDeliveryOrder(
@RequestBody @Valid DeliveryOrderRequest request,
@Auth(permit = {STUDENT}) Integer userId
) {
OrderCreateResponse response = orderService.createDeliveryOrder(userId, request);
return ResponseEntity.ok(response);
}

@PostMapping("/takeout")
public ResponseEntity<OrderCreateResponse> createTakeoutOrder(
@RequestBody @Valid TakeoutOrderRequest request,
@Auth(permit = {STUDENT}) Integer userId
) {
OrderCreateResponse response = orderService.createTakeoutOrder(userId, request);
return ResponseEntity.ok(response);
}

@GetMapping
public ResponseEntity<OrdersResponse> getOrders(
@RequestParam(name = "page", required = false, defaultValue = "1") Integer page,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package in.koreatech.koin.domain.order.order.dto.request;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import java.math.BigDecimal;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

import in.koreatech.koin.domain.order.delivery.model.AddressType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;

@JsonNaming(value = SnakeCaseStrategy.class)
public record DeliveryOrderRequest(
@Schema(description = "배달 주소", example = "충청남도 천안시 동남구 병천면 충절로 1600", requiredMode = REQUIRED)
@NotBlank(message = "배달 주소는 필수 입력사항입니다.")
@Size(max = 100, message = "배달 주소는 100자 이하여야 합니다.")
String address,

@Schema(description = "배달 상세 주소", example = "은솔관 422호", requiredMode = NOT_REQUIRED)
String addressDetail,

@Schema(description = "배달 주소 경도", example = "127.28372942", requiredMode = REQUIRED)
@NotNull(message = "배달 주소 경도는 필수 입력사항입니다.")
BigDecimal longitude,

@Schema(description = "배달 주소 위도", example = "36.76125794", requiredMode = REQUIRED)
@NotNull(message = "배달 주소 위도는 필수 입력사항입니다.")
BigDecimal latitude,

@Schema(description = "연락처", example = "01012345678", requiredMode = REQUIRED)
@NotBlank(message = "연락처는 필수 입력사항입니다.")
@Pattern(regexp = "^\\d{11}$", message = "전화번호 형식이 올바르지 않습니다. 11자리 숫자로 입력해 주세요.")
String phoneNumber,

@Schema(description = "사장님에게", example = "리뷰 이벤트 감사합니다.", requiredMode = NOT_REQUIRED)
@Size(max = 50, message = "사장님 요청 사항은 50자 이하여야 합니다.")
String toOwner,

@Schema(description = "라이더에게", example = "문 앞에 놔주세요.", requiredMode = NOT_REQUIRED)
@Size(max = 50, message = "라이더 요청 사항은 50자 이하여야 합니다.")
String toRider,

@Schema(description = "메뉴 총 금액", example = "1234", requiredMode = REQUIRED)
@NotNull(message = "메뉴 총 금액은 필수 입력사항입니다.")
@PositiveOrZero(message = "금액은 0 이상이어야 합니다.")
Integer totalMenuPrice,

@Schema(description = "배달 타입", example = "OFF_CAMPUS", requiredMode = REQUIRED)
@NotNull(message = "배달 타입은 필수 입력사항입니다.")
AddressType deliveryType,

@Schema(description = "배달 팁", example = "1234", requiredMode = REQUIRED)
@NotNull(message = "배달 팁은 필수 입력사항입니다.")
@PositiveOrZero(message = "금액은 0 이상이어야 합니다.")
Integer deliveryTip,

@Schema(description = "수저, 포크 수령 여부", example = "true", requiredMode = REQUIRED)
@NotNull(message = "수저, 포크 수령 여부는 필수 입력사항입니다.")
Boolean provideCutlery,

@Schema(description = "결제 금액", example = "10000", requiredMode = REQUIRED)
@NotNull(message = "결제 금액은 필수 입력사항입니다.")
@PositiveOrZero(message = "금액은 0 이상이어야 합니다.")
Integer totalAmount
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package in.koreatech.koin.domain.order.order.dto.request;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;

@JsonNaming(value = SnakeCaseStrategy.class)
public record TakeoutOrderRequest(
@Schema(description = "연락처", example = "01012345678", requiredMode = REQUIRED)
@NotBlank(message = "연락처는 필수 입력사항입니다.")
@Pattern(regexp = "^\\d{11}$", message = "전화번호 형식이 올바르지 않습니다. 11자리 숫자로 입력해 주세요.")
String phoneNumber,

@Schema(description = "사장님에게", example = "리뷰 이벤트 감사합니다.", requiredMode = NOT_REQUIRED)
@Size(max = 50, message = "사장님 요청 사항은 50자 이하여야 합니다.")
String toOwner,

@Schema(description = "수저, 포크 수령 여부", example = "true", requiredMode = REQUIRED)
@NotNull(message = "수저, 포크 수령 여부는 필수 입력사항입니다.")
Boolean provideCutlery,

@Schema(description = "메뉴 총 금액", example = "1234", requiredMode = REQUIRED)
@NotNull(message = "메뉴 총 금액은 필수 입력사항입니다.")
@PositiveOrZero(message = "금액은 0 이상이어야 합니다.")
Integer totalMenuPrice,

@Schema(description = "결제 금액", example = "10000", requiredMode = REQUIRED)
@NotNull(message = "결제 금액은 필수 입력사항입니다.")
@PositiveOrZero(message = "금액은 0 이상이어야 합니다.")
Integer totalAmount
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package in.koreatech.koin.domain.order.order.dto.response;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

import in.koreatech.koin.domain.order.order.model.Order;
import in.koreatech.koin.domain.payment.dto.response.BankAccountResponse;
import in.koreatech.koin.domain.payment.model.entity.Payment;
import io.swagger.v3.oas.annotations.media.Schema;

@JsonNaming(value = SnakeCaseStrategy.class)
public record OrderCreateResponse(
@Schema(description = "주문 ID", example = "123", requiredMode = REQUIRED)
Integer orderId,

@Schema(description = "주문 번호", example = "A1B2C3D4E5", requiredMode = REQUIRED)
String orderNumber,

@Schema(description = "결제 ID", example = "456", requiredMode = REQUIRED)
Integer paymentId,

@Schema(description = "주문 유형", example = "DELIVERY", requiredMode = REQUIRED)
String orderType,

@Schema(description = "주문 상태", example = "CONFIRMING", requiredMode = REQUIRED)
String orderStatus,

@Schema(description = "결제 방법", example = "계좌이체", requiredMode = REQUIRED)
String paymentMethod,

@Schema(description = "총 결제 금액", example = "18000", requiredMode = REQUIRED)
Integer amount,

@Schema(description = "상점 계좌 정보", requiredMode = REQUIRED)
BankAccountResponse bankAccount
) {
public static OrderCreateResponse of(Order order, Payment payment) {
return new OrderCreateResponse(
order.getId(),
order.getOrderNumber(),
payment.getId(),
order.getOrderType().name(),
order.getStatus().name(),
payment.getPaymentMethod().getDisplayName(),
payment.getAmount(),
BankAccountResponse.from(payment)
);
}
}
Loading
Loading