diff --git a/src/main/java/com/kt/common/exception/ErrorCode.java b/src/main/java/com/kt/common/exception/ErrorCode.java index ed6187f2..28191acf 100644 --- a/src/main/java/com/kt/common/exception/ErrorCode.java +++ b/src/main/java/com/kt/common/exception/ErrorCode.java @@ -94,6 +94,10 @@ public enum ErrorCode { //payment NOT_FOUND_PAYMENT(HttpStatus.BAD_REQUEST, "해당 결제 정보를 찾을 수 없습니다"), NOT_FOUND_PAYMENT_TYPE(HttpStatus.BAD_REQUEST, "결제수단을 찾을 수 없습니다"), + FAIL_PAYMENT(HttpStatus.BAD_REQUEST, "결제에 실패했습니다."), + ALREADY_PAID_ORDER(HttpStatus.BAD_REQUEST, "이미 결제가 완료된 주문입니다."), + CANNOT_CANCEL_PAYMENT_AMOUNT_MISMATCH(HttpStatus.BAD_REQUEST, "결제 취소 금액이 결제 금액과 일치하지 않습니다."), + PAYMENT_CANCEL_FAILED(HttpStatus.BAD_REQUEST, "결제 취소에 실패했습니다."), //rate limit GLOBAL_EXCEED_REQUEST_LIMIT(HttpStatus.TOO_MANY_REQUESTS, "요청 횟수 제한을 초과했습니다."), diff --git a/src/main/java/com/kt/config/RestTemplateConfiguration.java b/src/main/java/com/kt/config/RestTemplateConfiguration.java new file mode 100644 index 00000000..a90a3b2c --- /dev/null +++ b/src/main/java/com/kt/config/RestTemplateConfiguration.java @@ -0,0 +1,15 @@ +package com.kt.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfiguration { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/src/main/java/com/kt/config/SecurityConfiguration.java b/src/main/java/com/kt/config/SecurityConfiguration.java index 6d3337ee..8cb3b6f0 100644 --- a/src/main/java/com/kt/config/SecurityConfiguration.java +++ b/src/main/java/com/kt/config/SecurityConfiguration.java @@ -28,8 +28,8 @@ public class SecurityConfiguration { - private static final String[] GET_PERMIT_ALL = {"/api/health/**", "/swagger-ui.html","/swagger-ui/**", "/v3/api-docs/**", "/actuator/**"}; - private static final String[] POST_PERMIT_ALL = {"/api/users/auth/signup", "/api/users/auth/login","/api/admin/users/auth/signup", "/api/admin/users/auth/login","/api/users/reissue"}; + private static final String[] GET_PERMIT_ALL = {"/api/health/**", "/swagger-ui.html","/swagger-ui/**", "/v3/api-docs/**", "/actuator/**", "/payment-*.html", "/api/payments/client-key", "/*.css"}; + private static final String[] POST_PERMIT_ALL = {"/api/users/auth/signup", "/api/users/auth/login","/api/admin/users/auth/signup", "/api/admin/users/auth/login","/api/users/reissue", "/api/payments/confirm"}; private static final String[] PUT_PERMIT_ALL = {"/api/v1/public/**"}; private static final String[] PATCH_PERMIT_ALL = {"/api/v1/public/**"}; private static final String[] DELETE_PERMIT_ALL = {"/api/v1/public/**"}; diff --git a/src/main/java/com/kt/controller/payment/PaymentController.java b/src/main/java/com/kt/controller/payment/PaymentController.java index 9ca7a7d7..f56612f3 100644 --- a/src/main/java/com/kt/controller/payment/PaymentController.java +++ b/src/main/java/com/kt/controller/payment/PaymentController.java @@ -1,7 +1,15 @@ package com.kt.controller.payment; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + import org.springframework.data.domain.Page; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -10,14 +18,26 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import com.kt.common.exception.CustomException; +import com.kt.common.exception.ErrorCode; import com.kt.common.request.Paging; import com.kt.common.response.ApiResult; import com.kt.common.support.SwaggerAssistance; +import com.kt.domain.order.Order; +import com.kt.domain.order.OrderStatus; import com.kt.dto.payment.PaymentCreateRequest; import com.kt.dto.payment.PaymentDetailResponse; import com.kt.dto.payment.PaymentListResponse; +import com.kt.dto.payment.PaymentOrderInfoResponse; +import com.kt.dto.payment.PaymentTossCancelRequest; +import com.kt.dto.payment.PaymentTossCancelResponse; +import com.kt.dto.payment.PaymentTossConfirmRequest; +import com.kt.properties.TossPaymentsProperties; import com.kt.security.CustomUserDetails; +import com.kt.service.order.OrderService; import com.kt.service.payment.PaymentService; import io.swagger.v3.oas.annotations.Operation; @@ -32,6 +52,9 @@ public class PaymentController extends SwaggerAssistance { private final PaymentService paymentService; + private final OrderService orderService; + private final TossPaymentsProperties tossPaymentsProperties; + private final RestTemplate restTemplate = new RestTemplate(); @GetMapping @ResponseStatus(HttpStatus.OK) @@ -53,11 +76,147 @@ public ApiResult getPayment( return ApiResult.ok(paymentService.getPayment(currentUser.getId(), paymentId)); } - @PostMapping("") + /*결제기록 등록 -> toss 결제 이후 성공 시 등록 변경*/ + /*@PostMapping("") @Operation(summary = "결제 기록 등록") public ApiResult create(@Valid @RequestBody PaymentCreateRequest request, @AuthenticationPrincipal CustomUserDetails currentUser) { paymentService.create(request, currentUser.getId()); return ApiResult.ok(); + }*/ + + @PostMapping("/confirm") + @Operation(summary = "Toss 결제 승인 요청") + public ResponseEntity confirmPayment(@RequestBody PaymentTossConfirmRequest request, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + try { + String url = tossPaymentsProperties.getApiUrl() + "/confirm"; + String auth = tossPaymentsProperties.getSecretKey() + ":"; + String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + + String orderId = request.orderId().split("-")[0]; + Long orderIdLong = Long.parseLong(orderId); + var order = orderService.getOrderDetail(orderIdLong, currentUser.getId()); + if (order.orderStatus() != OrderStatus.ORDERED) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(Map.of( + "code", ErrorCode.ALREADY_PAID_ORDER.getStatus(), + "message", ErrorCode.ALREADY_PAID_ORDER.getMessage() + )); + } + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Basic " + encodedAuth); + headers.setContentType(MediaType.APPLICATION_JSON); + System.out.println(request.toString()); + Map body = Map.of( + "paymentKey", request.paymentKey(), + "orderId", request.orderId(), + "amount", request.amount().toString() + ); + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(url, entity, Map.class); + + if (response.getStatusCode() == HttpStatus.OK) { + Map tossPayment = response.getBody(); + Long paymentId = paymentService.create(tossPayment, currentUser.getId()); + + Map responseBody = new HashMap<>(tossPayment); + responseBody.put("paymentId", paymentId); + + return ResponseEntity.ok(responseBody); + } + + return ResponseEntity.ok(response.getBody()); + + } catch (HttpClientErrorException e) { + // Toss API에서 반환한 에러 응답을 그대로 전달 + return ResponseEntity + .status(e.getStatusCode()) + .body(e.getResponseBodyAsString()); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of( + "code", "INTERNAL_SERVER_ERROR", + "message", "결제 승인 처리 중 오류가 발생했습니다: " + e.getMessage() + )); + } + } + + @GetMapping("/order/{orderId}") + @Operation(summary = "주문 ID로 결제 정보 조회") + public ApiResult getPaymentByOrderId( + @PathVariable Long orderId, + @AuthenticationPrincipal CustomUserDetails currentUser + ) { + return ApiResult.ok(paymentService.getPaymentByOrderId(currentUser.getId(), orderId)); + } + + @PostMapping("/{paymentId}/cancel") + @Operation(summary = "결제 취소") + public ResponseEntity cancelPayment( + @PathVariable Long paymentId, + @RequestBody PaymentTossCancelRequest request, + @AuthenticationPrincipal CustomUserDetails currentUser) { + try { + String paymentKey = paymentService.getPaymentKey(paymentId, currentUser.getId()); + + + String url = tossPaymentsProperties.getApiUrl() + "/" + paymentKey + "/cancel"; + String auth = tossPaymentsProperties.getSecretKey() + ":"; + String encodedAuth = java.util.Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Basic " + encodedAuth); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map body = new HashMap<>(); + body.put("cancelReason", request.cancelReason()); + if (request.cancelAmount() != null) { + body.put("cancelAmount", request.cancelAmount()); + } + + HttpEntity> entity = new HttpEntity<>(body, headers); + ResponseEntity tossResponse = restTemplate.postForEntity(url, entity, Map.class); + + if (tossResponse.getStatusCode() == HttpStatus.OK) { + PaymentTossCancelResponse response = paymentService.cancelPayment(request, currentUser.getId(), paymentId); + return ResponseEntity.ok(ApiResult.ok(response)); + } + + throw new CustomException(ErrorCode.PAYMENT_CANCEL_FAILED); + + } catch (HttpClientErrorException e) { + // Toss API에서 반환한 에러 응답을 그대로 전달 + return ResponseEntity + .status(e.getStatusCode()) + .body(e.getResponseBodyAsString()); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of( + "code", "INTERNAL_SERVER_ERROR", + "message", "결제 승인 처리 중 오류가 발생했습니다: " + e.getMessage() + )); + } + } + + @GetMapping("/order/{orderId}/info") + @Operation(summary = "주문 번호로 결제 정보 조회") + public ApiResult getPaymentDetail(@PathVariable Long orderId, + @AuthenticationPrincipal CustomUserDetails currentUser) { + return ApiResult.ok(paymentService.getPaymentDetail(currentUser.getId(), orderId)); + } + + @GetMapping("/client-key") + @Operation(summary = "Toss 클라이언트 키 조회") + public ApiResult> getClientKey() { + return ApiResult.ok(Map.of("clientKey", tossPaymentsProperties.getClientKey())); } diff --git a/src/main/java/com/kt/domain/payment/Payment.java b/src/main/java/com/kt/domain/payment/Payment.java index 67c49fa5..d4e174d3 100644 --- a/src/main/java/com/kt/domain/payment/Payment.java +++ b/src/main/java/com/kt/domain/payment/Payment.java @@ -28,6 +28,9 @@ public class Payment extends BaseEntity { @Column(nullable = false) private Long finalPrice; // 총 결제 금액 (상품금액 + 배송비) + @Column(nullable = false) + private String paymentKey; + @Column(nullable = false) private boolean isDeleted = false; @@ -44,13 +47,15 @@ public Payment( PaymentType paymentType, Long totalPrice, Long deliveryFee, - Long finalPrice + Long finalPrice, + String paymentKey ) { this.order = order; this.paymentType = paymentType; this.totalPrice = totalPrice; this.deliveryFee = deliveryFee; this.finalPrice = finalPrice; + this.paymentKey = paymentKey; this.isDeleted = false; } diff --git a/src/main/java/com/kt/dto/discount/response/DiscountInfo.java b/src/main/java/com/kt/dto/discount/response/DiscountInfo.java new file mode 100644 index 00000000..cad2e23d --- /dev/null +++ b/src/main/java/com/kt/dto/discount/response/DiscountInfo.java @@ -0,0 +1,8 @@ +package com.kt.dto.discount.response; + +public record DiscountInfo( + String discountName, + String discountType, + Long discountPrice +) { +} diff --git a/src/main/java/com/kt/dto/payment/PaymentDetailResponse.java b/src/main/java/com/kt/dto/payment/PaymentDetailResponse.java index a3adbce9..7f58d997 100644 --- a/src/main/java/com/kt/dto/payment/PaymentDetailResponse.java +++ b/src/main/java/com/kt/dto/payment/PaymentDetailResponse.java @@ -7,6 +7,7 @@ public record PaymentDetailResponse( Long id, // 결제 ID Long orderId, // 주문 ID + String paymentKey, // 결제 고유 키 String userLoginId, // 결제자 아이디(직접 결제한 유저는 로그인한 유저) String userName, // 결제자 이름 OrderStatus orderStatus, // 주문 상태 diff --git a/src/main/java/com/kt/dto/payment/PaymentOrderInfoResponse.java b/src/main/java/com/kt/dto/payment/PaymentOrderInfoResponse.java new file mode 100644 index 00000000..4de04219 --- /dev/null +++ b/src/main/java/com/kt/dto/payment/PaymentOrderInfoResponse.java @@ -0,0 +1,57 @@ +package com.kt.dto.payment; + +import java.time.LocalDateTime; +import java.util.List; + +import com.kt.domain.order.OrderStatus; +import com.kt.dto.order.response.OrderProductResponse; + +public record PaymentOrderInfoResponse( + Long orderId, + OrderStatus orderStatus, + String orderStatusDescription, + LocalDateTime createdAt, + String receiverName, + String receiverPhone, + String receiverAddress, + UserPaymentInfo user, + List orderProducts, + PriceInfo priceInfo +) { + public record UserPaymentInfo( + String name, + String email, + String mobile, + MembershipInfo membership, + Long money + ) {} + + public record MembershipInfo( + String level + ) {} + + public record AppliedDiscountInfo( + String discountName, + String discountType, + Long discountAmount + ) {} + + public record PriceInfo( + Long totalProductPrice, + Long totalDiscountPrice, + Long deliveryFee, + Long finalPrice + ) {} + + public record OrderProductInfo( + Long productId, + String productName, + Long productVariantId, + Long productCount, + Long price, + Long totalPrice, + Long discountPrice, + Long discountedPrice, + List appliedDiscounts // 할인 목록 포함 + ) {} +} diff --git a/src/main/java/com/kt/dto/payment/PaymentTossCancelRequest.java b/src/main/java/com/kt/dto/payment/PaymentTossCancelRequest.java new file mode 100644 index 00000000..292320a3 --- /dev/null +++ b/src/main/java/com/kt/dto/payment/PaymentTossCancelRequest.java @@ -0,0 +1,16 @@ +package com.kt.dto.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record PaymentTossCancelRequest( + @Schema(description = "취소 사유", example = "고객 변심") + @NotBlank(message = "취소 사유는 필수입니다") + String cancelReason, + + @Schema(description = "취소 금액", example = "50000") + @NotNull(message = "취소 금액은 필수입니다") + Long cancelAmount +) { +} diff --git a/src/main/java/com/kt/dto/payment/PaymentTossCancelResponse.java b/src/main/java/com/kt/dto/payment/PaymentTossCancelResponse.java new file mode 100644 index 00000000..c0cd72ec --- /dev/null +++ b/src/main/java/com/kt/dto/payment/PaymentTossCancelResponse.java @@ -0,0 +1,8 @@ +package com.kt.dto.payment; + +public record PaymentTossCancelResponse( + Long cancelAmount, + String cancelReason, + String canceledAt +) { +} diff --git a/src/main/java/com/kt/dto/payment/PaymentTossConfirmRequest.java b/src/main/java/com/kt/dto/payment/PaymentTossConfirmRequest.java new file mode 100644 index 00000000..80f07304 --- /dev/null +++ b/src/main/java/com/kt/dto/payment/PaymentTossConfirmRequest.java @@ -0,0 +1,26 @@ +package com.kt.dto.payment; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record PaymentTossConfirmRequest( + + @Schema(description = "주문 ID", example = "1") + @NotNull(message = "주문 ID는 필수입니다") + String orderId, + + @Schema(description = "결제 KEY", example = "tgen_20260104212559xFAo0") + @NotBlank(message = "결제 KEY는 필수입니다") + String paymentKey, + + @Schema(description = "결제 금액", example = "50000") + @NotNull(message = "결제 금액은 필수입니다") + Long amount, + + @Schema(description = "결제타입", example = "간편결제") + @NotBlank(message = "결제타입은 필수입니다") + String method + +) { +} diff --git a/src/main/java/com/kt/properties/TossPaymentsProperties.java b/src/main/java/com/kt/properties/TossPaymentsProperties.java new file mode 100644 index 00000000..ac2b9067 --- /dev/null +++ b/src/main/java/com/kt/properties/TossPaymentsProperties.java @@ -0,0 +1,18 @@ +package com.kt.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Component +@ConfigurationProperties(prefix = "toss.payments") +public class TossPaymentsProperties { + private String clientKey; + private String secretKey; + private String apiUrl; +} diff --git a/src/main/java/com/kt/repository/order/OrderRepositoryCustom.java b/src/main/java/com/kt/repository/order/OrderRepositoryCustom.java index 8707cf4c..981300cc 100644 --- a/src/main/java/com/kt/repository/order/OrderRepositoryCustom.java +++ b/src/main/java/com/kt/repository/order/OrderRepositoryCustom.java @@ -1,5 +1,7 @@ package com.kt.repository.order; +import java.util.Optional; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -8,4 +10,6 @@ public interface OrderRepositoryCustom { Page getOrders(Long userId, Pageable pageable); + + Order getOrderDetailById(Long userId, Long orderId); } diff --git a/src/main/java/com/kt/repository/order/orderRepositoryCustomImpl.java b/src/main/java/com/kt/repository/order/orderRepositoryCustomImpl.java index d96bcf9a..806df38f 100644 --- a/src/main/java/com/kt/repository/order/orderRepositoryCustomImpl.java +++ b/src/main/java/com/kt/repository/order/orderRepositoryCustomImpl.java @@ -1,6 +1,7 @@ package com.kt.repository.order; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -61,4 +62,20 @@ public Page getOrders(Long userId, Pageable pageable) { return new PageImpl<>(content, pageable, total); } + + @Override + public Order getOrderDetailById(Long userId, Long orderId) { + + return queryFactory + .select(order) + .from(order) + .join(order.user, user).fetchJoin() + .join(user.membership, membership).fetchJoin() + .where( + order.id.eq(orderId), + order.user.id.eq(userId), + order.isDeleted.eq(false) + ) + .fetchOne(); + } } diff --git a/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustom.java b/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustom.java index 2542bd9e..c2894a77 100644 --- a/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustom.java +++ b/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustom.java @@ -1,6 +1,11 @@ package com.kt.repository.orderproduct; +import java.util.List; + +import com.kt.domain.orderproduct.OrderProduct; + public interface OrderProductRepositoryCustom { boolean hasInvalidStatusWithVariantId(Long variantId); boolean hasInvalidStatusWithProductId(Long productId); + List getOrderProductByOrderId(Long orderId); } diff --git a/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustomImpl.java b/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustomImpl.java index cc42b65a..0056136b 100644 --- a/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustomImpl.java +++ b/src/main/java/com/kt/repository/orderproduct/OrderProductRepositoryCustomImpl.java @@ -5,7 +5,9 @@ import org.springframework.stereotype.Repository; import com.kt.domain.order.OrderStatus; +import com.kt.domain.orderproduct.OrderProduct; import com.kt.domain.orderproduct.QOrderProduct; +import com.kt.domain.product.QProduct; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; @@ -15,6 +17,7 @@ public class OrderProductRepositoryCustomImpl implements OrderProductRepositoryCustom { private final JPAQueryFactory queryFactory; private final QOrderProduct orderProduct = QOrderProduct.orderProduct; + private final QProduct product = QProduct.product; @Override public boolean hasInvalidStatusWithVariantId(Long variantId) { @@ -56,5 +59,13 @@ public boolean hasInvalidStatusWithProductId(Long productId) { .fetchFirst() != null; //존재하는 상품이 있으면 true } + @Override + public List getOrderProductByOrderId(Long orderId) { + return queryFactory + .selectFrom(orderProduct) + .join(orderProduct.product, product).fetchJoin() + .where(orderProduct.order.id.eq(orderId)) + .fetch(); + } } diff --git a/src/main/java/com/kt/repository/payment/PaymentRepository.java b/src/main/java/com/kt/repository/payment/PaymentRepository.java index bcb8761a..36b3e1a4 100644 --- a/src/main/java/com/kt/repository/payment/PaymentRepository.java +++ b/src/main/java/com/kt/repository/payment/PaymentRepository.java @@ -15,7 +15,14 @@ default Payment findByIdAndOrderUserIdAndIsDeletedFalseOrThorw(Long paymentId, L return findByIdAndOrderUserIdAndIsDeletedFalse(paymentId, userId).orElseThrow(() -> new CustomException(errorCode)); } + default Payment findByOrderIdAndIsDeletedFalseOrThorw(Long orderId, ErrorCode errorCode) { + return findByOrderIdAndIsDeletedFalse(orderId).orElseThrow(() -> new CustomException(errorCode)); + } + @EntityGraph(attributePaths = {"order", "paymentType"}) Optional findByIdAndOrderUserIdAndIsDeletedFalse(Long paymentId, Long userId); + + @EntityGraph(attributePaths = {"order", "paymentType"}) + Optional findByOrderIdAndIsDeletedFalse(Long orderId); } diff --git a/src/main/java/com/kt/repository/payment/PaymentRepositoryCustom.java b/src/main/java/com/kt/repository/payment/PaymentRepositoryCustom.java index dfd408d7..b4f3441e 100644 --- a/src/main/java/com/kt/repository/payment/PaymentRepositoryCustom.java +++ b/src/main/java/com/kt/repository/payment/PaymentRepositoryCustom.java @@ -1,11 +1,13 @@ package com.kt.repository.payment; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import com.kt.dto.payment.PaymentListResponse; +import com.kt.dto.payment.PaymentOrderInfoResponse; public interface PaymentRepositoryCustom { Page getMyAllPayment(Long userId, Pageable pageable); + + PaymentOrderInfoResponse getPaymentDetailByOrderId(Long orderId); } diff --git a/src/main/java/com/kt/repository/payment/PaymentRepositoryCustomImpl.java b/src/main/java/com/kt/repository/payment/PaymentRepositoryCustomImpl.java index a22b3a1e..94d6f41a 100644 --- a/src/main/java/com/kt/repository/payment/PaymentRepositoryCustomImpl.java +++ b/src/main/java/com/kt/repository/payment/PaymentRepositoryCustomImpl.java @@ -5,11 +5,15 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; +import com.kt.domain.discount.QDiscount; +import com.kt.domain.membership.QMembership; import com.kt.domain.order.QOrder; +import com.kt.domain.orderproduct.QOrderProduct; import com.kt.domain.payment.QPayment; import com.kt.domain.paymenttype.QPaymentType; import com.kt.domain.user.QUser; import com.kt.dto.payment.PaymentListResponse; +import com.kt.dto.payment.PaymentOrderInfoResponse; import com.kt.dto.payment.QPaymentListResponse; import com.querydsl.jpa.impl.JPAQueryFactory; @@ -23,6 +27,9 @@ public class PaymentRepositoryCustomImpl implements PaymentRepositoryCustom { private final QOrder order = QOrder.order; private final QPaymentType paymentType = QPaymentType.paymentType; private final QUser user = QUser.user; + private final QOrderProduct orderProduct = QOrderProduct.orderProduct; + private final QMembership membership = QMembership.membership; + private final QDiscount discount = QDiscount.discount; @Override public Page getMyAllPayment(Long userId, Pageable pageable) { @@ -68,4 +75,9 @@ public Page getMyAllPayment(Long userId, Pageable pageable) return new PageImpl<>(content, pageable, total); } + @Override + public PaymentOrderInfoResponse getPaymentDetailByOrderId(Long orderId) { + return null; + } + } diff --git a/src/main/java/com/kt/repository/paymenttype/PaymentTypeRepository.java b/src/main/java/com/kt/repository/paymenttype/PaymentTypeRepository.java index f2af769a..a736d489 100644 --- a/src/main/java/com/kt/repository/paymenttype/PaymentTypeRepository.java +++ b/src/main/java/com/kt/repository/paymenttype/PaymentTypeRepository.java @@ -1,9 +1,12 @@ package com.kt.repository.paymenttype; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.kt.domain.paymenttype.PaymentType; public interface PaymentTypeRepository extends JpaRepository { + Optional findByName(String name); } diff --git a/src/main/java/com/kt/service/discount/DiscountCalcService.java b/src/main/java/com/kt/service/discount/DiscountCalcService.java index 322dd130..0cf26195 100644 --- a/src/main/java/com/kt/service/discount/DiscountCalcService.java +++ b/src/main/java/com/kt/service/discount/DiscountCalcService.java @@ -13,6 +13,7 @@ import com.kt.domain.discount.policy.DiscountPolicyFactory; import com.kt.domain.discountMembership.DiscountMembership; import com.kt.domain.discountProduct.DiscountProduct; +import com.kt.dto.discount.response.DiscountInfo; import com.kt.dto.discount.response.DiscountResult; import com.kt.repository.discountmembership.DiscountMembershipRepository; import com.kt.repository.discountproduct.DiscountProductRepository; @@ -99,4 +100,82 @@ public Map> getProductsDiscount(List productIds) { return result; } + public List getDiscountsInfoList(Long originalPrice, + Discount membershipDiscount, + List productDiscounts) { + List result = new ArrayList<>(); + + long maxDiscountPrice = 0L; + String selectedCase = ""; + Discount selectedProductDiscount = null; + + if (membershipDiscount != null) { + long membershipOnly = calcDiscount(originalPrice, membershipDiscount); + if (membershipOnly > maxDiscountPrice) { + maxDiscountPrice = membershipOnly; + selectedCase = "MEMBERSHIP_ONLY"; + } + } + + if (productDiscounts != null && !productDiscounts.isEmpty()) { + for (Discount productDiscount : productDiscounts) { + long productOnly = calcDiscount(originalPrice, productDiscount); + if (productOnly > maxDiscountPrice) { + maxDiscountPrice = productOnly; + selectedCase = "PRODUCT_ONLY"; + selectedProductDiscount = productDiscount; + } + } + } + + if (membershipDiscount != null && productDiscounts != null) { + for (Discount productDiscount : productDiscounts) { + if (membershipDiscount.isCombinable() && productDiscount.isCombinable()) { + long combined = calcCombined(originalPrice, membershipDiscount, productDiscount); + if (combined > maxDiscountPrice) { + maxDiscountPrice = combined; + selectedCase = "COMBINED"; + selectedProductDiscount = productDiscount; + } + } + } + } + + switch (selectedCase) { + case "MEMBERSHIP_ONLY": + result.add(new DiscountInfo( + membershipDiscount.getName(), + "MEMBERSHIP", + calcDiscount(originalPrice, membershipDiscount) + )); + break; + case "PRODUCT_ONLY": + if (selectedProductDiscount != null) { + result.add(new DiscountInfo( + selectedProductDiscount.getName(), + "PRODUCT", + calcDiscount(originalPrice, selectedProductDiscount) + )); + } + break; + case "COMBINED": + result.add(new DiscountInfo( + membershipDiscount.getName(), + "MEMBERSHIP", + calcDiscount(originalPrice, membershipDiscount) + )); + + result.add(new DiscountInfo( + selectedProductDiscount.getName(), + "PRODUCT", + calcDiscount(originalPrice, selectedProductDiscount) + )); + break; + default: + break; + } + + return result; + } + } diff --git a/src/main/java/com/kt/service/payment/PaymentService.java b/src/main/java/com/kt/service/payment/PaymentService.java index 50c60f5d..a5e6fbe9 100644 --- a/src/main/java/com/kt/service/payment/PaymentService.java +++ b/src/main/java/com/kt/service/payment/PaymentService.java @@ -1,22 +1,40 @@ package com.kt.service.payment; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.kt.common.exception.CustomException; import com.kt.common.exception.ErrorCode; +import com.kt.common.support.Preconditions; import com.kt.domain.discount.Discount; +import com.kt.domain.discount.policy.DiscountPolicy; +import com.kt.domain.discount.policy.DiscountPolicyFactory; +import com.kt.domain.order.Order; import com.kt.domain.order.OrderStatus; +import com.kt.domain.orderproduct.OrderProduct; import com.kt.domain.payment.Payment; +import com.kt.dto.discount.response.DiscountInfo; import com.kt.dto.discount.response.DiscountResult; +import com.kt.dto.order.response.OrderProductResponse; import com.kt.dto.payment.PaymentCreateRequest; import com.kt.dto.payment.PaymentDetailResponse; import com.kt.dto.payment.PaymentListResponse; +import com.kt.dto.payment.PaymentOrderInfoResponse; +import com.kt.dto.payment.PaymentTossCancelRequest; +import com.kt.dto.payment.PaymentTossCancelResponse; +import com.kt.dto.payment.PaymentTossConfirmRequest; import com.kt.repository.order.OrderRepository; +import com.kt.repository.order.OrderRepositoryCustom; +import com.kt.repository.orderproduct.OrderProductRepositoryCustom; import com.kt.repository.payment.PaymentRepository; import com.kt.repository.payment.PaymentRepositoryCustom; import com.kt.repository.paymenttype.PaymentTypeRepository; @@ -37,47 +55,32 @@ public class PaymentService { private final UserRepository userRepository; private final PaymentRepositoryCustom paymentRepositoryCustom; private final DiscountCalcService discountCalcService; + private final OrderRepositoryCustom orderRepositoryCustom; + private final OrderProductRepositoryCustom orderProductRepositoryCustom; - public void create(PaymentCreateRequest request, Long userId) { - var order = orderRepository.findByIdAndUserIdOrThrow(request.orderId(), userId, ErrorCode.NOT_FOUND_ORDER); + public Long create(Map tossResponse, Long userId) { + String orderId = tossResponse.get("orderId").toString().split("-")[0]; - var paymentType = paymentTypeRepository.findById(request.paymentTypeId()).orElseThrow(); + var order = orderRepository.findByIdAndUserIdOrThrow(Long.parseLong(orderId), userId, ErrorCode.NOT_FOUND_ORDER); - // 멤버십 할인 조회 - Discount membershipDiscount = discountCalcService.getMembershipDiscount(userId); - - // 상품별 할인 조회 - Map> productDiscount = discountCalcService.getProductsDiscount( - order.getOrderProducts().stream() - .map(op -> op.getProduct().getId()) - .distinct() - .toList()); + var paymentType = paymentTypeRepository.findByName(tossResponse.get("method").toString()).orElseThrow(); - Long totalPrice = 0L; + long amount = Long.parseLong(tossResponse.get("totalAmount").toString()); - for (var orderProduct : order.getOrderProducts()) { - DiscountResult result = discountCalcService.calculate( - orderProduct.getProduct().getPrice() * orderProduct.getCount(), - membershipDiscount, - productDiscount.getOrDefault(orderProduct.getProduct().getId(), List.of()) - ); - - totalPrice += result.discountedPrice(); - System.out.println("총 가격: " + totalPrice); - } - - Long finalPrice = totalPrice + DELIVERY_FEE; var payment = new Payment( order, paymentType, - totalPrice, + amount, DELIVERY_FEE, - finalPrice + amount - DELIVERY_FEE, + tossResponse.get("paymentKey").toString() ); - paymentRepository.save(payment); + Payment savedPayment = paymentRepository.save(payment); order.updateStatus(OrderStatus.PAID); + + return savedPayment.getId(); } public Page getMyAllPayment(Long userId, Pageable pageable) { @@ -94,6 +97,7 @@ public PaymentDetailResponse getPayment(Long userId, Long paymentId) { return new PaymentDetailResponse( payment.getId(), payment.getOrder().getId(), + payment.getPaymentKey(), payment.getOrder().getUser().getLoginId(), payment.getOrder().getUser().getName(), payment.getOrder().getOrderStatus(), @@ -105,4 +109,148 @@ public PaymentDetailResponse getPayment(Long userId, Long paymentId) { ); } + public PaymentOrderInfoResponse getPaymentDetail(Long userId, Long orderId) { + + //1. 주문자 정보 등 조회 + Order order = orderRepositoryCustom.getOrderDetailById(userId, orderId); + + //2. 주문 상품 정보 조회 + List orderProducts = orderProductRepositoryCustom.getOrderProductByOrderId(orderId); + + //3. 멤버십 할인 및 계산 + Discount membershipDiscount = discountCalcService.getMembershipDiscount(userId); + + //4. 상품별 할인 및 계산 + Map> productDiscounts = discountCalcService.getProductsDiscount( + orderProducts.stream() + .map(op -> op.getProduct().getId()) + .distinct() + .toList()); + + long totalProductPrice = 0L; + long totalDiscountPrice = 0L; + List orderProductInfos = new ArrayList<>(); + + + for (OrderProduct op : orderProducts) { + long originalPrice = op.getProduct().getPrice() * op.getCount(); + + DiscountResult result = discountCalcService.calculate( + originalPrice, + membershipDiscount, + productDiscounts.getOrDefault(op.getProduct().getId(), List.of()) + ); + + List discountInfos = discountCalcService.getDiscountsInfoList( + originalPrice, + membershipDiscount, + productDiscounts.getOrDefault(op.getProduct().getId(), List.of()) + ); + + List appliedDiscounts = discountInfos.stream() + .map(di -> new PaymentOrderInfoResponse.AppliedDiscountInfo( + di.discountName(), + di.discountType(), + di.discountPrice() + )) + .toList(); + + orderProductInfos.add(new PaymentOrderInfoResponse.OrderProductInfo( + op.getProduct().getId(), + op.getProduct().getName(), + op.getVariantId(), + op.getCount(), + op.getProduct().getPrice(), + originalPrice, + result.discountPrice(), + result.discountedPrice(), + appliedDiscounts + )); + + totalProductPrice += originalPrice; + totalDiscountPrice += result.discountPrice(); + } + + Long finalPrice = totalProductPrice - totalDiscountPrice + DELIVERY_FEE; + + return new PaymentOrderInfoResponse( + order.getId(), + order.getOrderStatus(), + order.getOrderStatus().name(), + order.getCreatedAt(), + order.getReceiverName(), + order.getReceiverPhone(), + order.getReceiverAddress(), + new PaymentOrderInfoResponse.UserPaymentInfo( + order.getUser().getName(), + order.getUser().getEmail(), + order.getUser().getMobile(), + new PaymentOrderInfoResponse.MembershipInfo( + order.getUser().getMembership().getLevel() + ), + order.getUser().getMoney() + ), + orderProductInfos, + new PaymentOrderInfoResponse.PriceInfo( + totalProductPrice, + totalDiscountPrice, + DELIVERY_FEE, + finalPrice + ) + ); + } + + public PaymentDetailResponse getPaymentByOrderId(Long userId, Long orderId) { + var order = orderRepository.findByIdAndUserIdOrThrow(orderId, userId, ErrorCode.NOT_FOUND_ORDER); + + var payment = paymentRepository.findByOrderIdAndIsDeletedFalseOrThorw(order.getId(), ErrorCode.NOT_FOUND_PAYMENT); + + return new PaymentDetailResponse( + payment.getId(), + payment.getOrder().getId(), + payment.getPaymentKey(), + payment.getOrder().getUser().getLoginId(), + payment.getOrder().getUser().getName(), + payment.getOrder().getOrderStatus(), + payment.getPaymentType().getName(), + payment.getTotalPrice(), + payment.getDeliveryFee(), + payment.getFinalPrice(), + payment.getCreatedAt() + ); + } + + public PaymentTossCancelResponse cancelPayment(PaymentTossCancelRequest request, Long userId, Long paymentId) { + var user = userRepository.findByIdOrThrow(userId, ErrorCode.NOT_FOUND_USER); + + var payment = paymentRepository.findByIdAndOrderUserIdAndIsDeletedFalseOrThorw(paymentId, user.getId(), ErrorCode.NOT_FOUND_PAYMENT); + Preconditions.validate(payment.getTotalPrice().equals(request.cancelAmount()), ErrorCode.CANNOT_CANCEL_PAYMENT_AMOUNT_MISMATCH); + + OrderStatus status = payment.getOrder().getOrderStatus(); + boolean canCancel = status == OrderStatus.PAID || + status == OrderStatus.PROCESSING || + status == OrderStatus.SHIPPED || + status == OrderStatus.DELIVERED; + Preconditions.validate(canCancel, ErrorCode.CANNOT_CANCEL_ORDER); + + payment.delete(); + + payment.getOrder().updateStatus(OrderStatus.CANCELLED); + + return new PaymentTossCancelResponse( + payment.getFinalPrice(), + request.cancelReason(), + LocalDateTime.now().toString() + ); + + } + + public String getPaymentKey(Long paymentId, Long userId) { + var user = userRepository.findByIdOrThrow(userId, ErrorCode.NOT_FOUND_USER); + + var payment = paymentRepository.findByIdAndOrderUserIdAndIsDeletedFalseOrThorw(paymentId, user.getId(), ErrorCode.NOT_FOUND_PAYMENT); + + return payment.getPaymentKey(); + } + } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 65eb6296..fba8a185 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -32,4 +32,10 @@ server: logstash: host: ${logstashhost:localhost} - port: ${logstashport:9601} \ No newline at end of file + port: ${logstashport:9601} + +toss: + payments: + client-key: ${payment.client.key:test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm} + secret-key: ${payments.secret-key:test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6} + api-url: ${payments.api-url:https://api.tosspayments.com/v1/payments} diff --git a/src/main/resources/application-integration.yml b/src/main/resources/application-integration.yml index c522ded6..cdffce63 100644 --- a/src/main/resources/application-integration.yml +++ b/src/main/resources/application-integration.yml @@ -37,4 +37,10 @@ jwt: refresh-token-expiration: ${kt.jwt.refresh-token-expiration:43200000} server: - port: ${shopping.server.port:8080} \ No newline at end of file + port: ${shopping.server.port:8080} + +toss: + payments: + client-key: ${payment.client.key:test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm} + secret-key: ${payments.secret-key:test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6} + api-url: ${payments.api-url:https://api.tosspayments.com/v1/payments} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 5f8a7f83..4e60ac8f 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -26,5 +26,11 @@ slack: bot-token: ${slack.token} log-channel: ${slack.channel} +toss: + payments: + client-key: ${payment.client.key:test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm} + secret-key: ${payments.secret-key:test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6} + api-url: ${payments.api-url:https://api.tosspayments.com/v1/payments} + server: port: 8080 \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index f9943a84..ac0cfbb8 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -28,4 +28,10 @@ slack: log-channel: ${slack.channel} server: - port: ${shopping.server.port} \ No newline at end of file + port: ${shopping.server.port} + +toss: + payments: + client-key: ${payment.client.key} + secret-key: ${payments.secret-key} + api-url: ${payments.api-url} \ No newline at end of file diff --git a/src/main/resources/static/order.css b/src/main/resources/static/order.css new file mode 100644 index 00000000..5dc3376f --- /dev/null +++ b/src/main/resources/static/order.css @@ -0,0 +1,221 @@ +/* 추가 스타일 */ +.input-group { + margin-bottom: 20px; +} + +.input-group label { + display: block; + margin-bottom: 8px; + color: #333D4B; + font-weight: 600; + font-size: 15px; +} + +.input-group input { + width: 100%; + padding: 12px 16px; + border: 1px solid #d1d6db; + border-radius: 8px; + font-size: 14px; + box-sizing: border-box; + transition: border-color 0.2s; +} + +.input-group input:focus { + outline: none; + border-color: #3182f6; + box-shadow: 0 0 0 3px rgba(49, 130, 246, 0.1); +} + +.order-summary, .delivery-info, .customer-info, .product-list, .payment-summary { + background-color: #f8f9fa; + padding: 20px; + border-radius: 10px; + margin-top: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.order-summary h3, .delivery-info h3, .customer-info h3, .product-list h3, .payment-summary h3 { + margin-top: 0; + margin-bottom: 15px; + color: #333D4B; + font-size: 18px; + border-bottom: 2px solid #3182f6; + padding-bottom: 10px; +} + +.order-summary p, .delivery-info p, .customer-info p { + margin: 10px 0; + color: #4e5968; + line-height: 1.8; +} + +.product-list table { + width: 100%; + border-collapse: collapse; + margin-top: 15px; + background-color: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.product-list table th { + background-color: #3182f6; + color: white; + padding: 14px 12px; + text-align: left; + font-weight: 600; + font-size: 14px; +} + +.product-list table td { + padding: 14px 12px; + border-bottom: 1px solid #e5e8eb; + color: #4e5968; + font-size: 14px; +} + +.product-list table tr:last-child td { + border-bottom: none; +} + +.product-list table tbody tr:hover { + background-color: #f8f9fa; + transition: background-color 0.2s; +} + +.payment-summary { + background: linear-gradient(135deg, #e8f3ff 0%, #f0f7ff 100%); + border: 2px solid #3182f6; +} + +.price-row { + display: flex; + justify-content: space-between; + margin: 12px 0; + color: #4e5968; + font-size: 15px; + padding: 8px 0; +} + +.price-row.discount { + color: #f04452; + font-weight: 600; +} + +.price-row.final { + font-size: 20px; + color: #191f28; + margin-top: 15px; + padding-top: 15px; + border-top: 2px solid #3182f6; +} + +.payment-summary hr { + display: none; +} + +#order-details { + display: none; +} + +.status-badge { + display: inline-block; + padding: 6px 14px; + border-radius: 16px; + font-size: 13px; + font-weight: 600; + background-color: #e8f3ff; + color: #1b64da; +} + +.button-group { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.discount-text { + color: #f04452; + font-weight: 600; +} + +.loading { + text-align: center; + padding: 20px; + color: #8b95a1; +} + +@media (max-width: 768px) { + .product-list table { + font-size: 12px; + } + + .product-list table th, + .product-list table td { + padding: 10px 8px; + } + + .price-row { + font-size: 14px; + } + + .price-row.final { + font-size: 18px; + } +} + +/* 할인 정보 표시 스타일 */ +.discount-details { + background-color: #fff3cd; + padding: 8px 12px; + margin: 5px 0; + border-left: 3px solid #ffc107; + font-size: 13px; +} +.discount-item { + display: flex; + justify-content: space-between; + margin: 4px 0; + color: #856404; +} +.discount-item .discount-name { + font-weight: 600; +} +.discount-item .discount-amount { + color: #d9534f; + font-weight: 700; +} +.product-row td { + border-bottom: none !important; +} +.discount-row td { + padding: 0 !important; + border-bottom: 1px solid #e5e8eb !important; +} +.no-discount-info { + color: #6c757d; + font-style: italic; + font-size: 12px; + padding: 8px 12px; +} + +.input-group { + margin-bottom: 15px; +} + +.input-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #333; +} + +#cancel-button { + background-color: #dc3545; +} + +#cancel-button:hover { + background-color: #c82333; +} \ No newline at end of file diff --git a/src/main/resources/static/payment-fail.html b/src/main/resources/static/payment-fail.html new file mode 100644 index 00000000..3787d865 --- /dev/null +++ b/src/main/resources/static/payment-fail.html @@ -0,0 +1,40 @@ + + + + + + + + + 결제 실패 + + +
+ +

결제를 실패했어요

+ +
+
에러메시지
+
+
+
+
에러코드
+
+
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/static/payment-success.html b/src/main/resources/static/payment-success.html new file mode 100644 index 00000000..a088dddb --- /dev/null +++ b/src/main/resources/static/payment-success.html @@ -0,0 +1,124 @@ + + + + + + + + + 결제 성공 + + +
+ +

결제를 완료했어요

+ +
+ + +
+
+
결제금액
+
+
+
+
주문번호
+
+
+
+
paymentKey
+
+
+
+ +
+ Response Data : +
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/payment-test.html b/src/main/resources/static/payment-test.html new file mode 100644 index 00000000..14bffc3f --- /dev/null +++ b/src/main/resources/static/payment-test.html @@ -0,0 +1,507 @@ + + + + + + + + + + Toss 결제 테스트 + + + +
+ +
+

📦 주문 정보 조회

+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +
+
+

📋 주문 정보

+

주문번호: -

+

주문일시: -

+

주문상태: -

+
+ + +
+

🚚 배송지 정보

+

수령인: -

+

연락처: -

+

배송주소: -

+
+ + +
+

👤 주문자 정보

+

이름: -

+

이메일: -

+

휴대폰: -

+

회원등급: -

+

보유금액: -

+
+ + +
+

🛍️ 주문 상품 목록

+ + + + + + + + + + + + + + + +
상품명수량단가합계최종가
주문 정보를 불러오는 중...
+
+ + +
+

💰 결제 금액 요약

+
+ 총 상품 금액: + 0원 +
+
+ 총 할인 금액: + 0원 +
+
+ 배송비: + 0원 +
+
+ 최종 결제 금액: + 0원 +
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css new file mode 100644 index 00000000..4a5d8072 --- /dev/null +++ b/src/main/resources/static/style.css @@ -0,0 +1,251 @@ +body { + background-image: url('https://static.toss.im/ml-illust/img-back_005.jpg'); +} +.p { + padding: 0; + margin: 0; + font-family: Toss Product Sans, -apple-system, BlinkMacSystemFont, + Bazier Square, Noto Sans KR, Segoe UI, Apple SD Gothic Neo, Roboto, + Helvetica Neue, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, + Segoe UI Symbol, Noto Color Emoji; + color: #4e5968; + word-break: keep-all; + word-wrap: break-word; +} +.h4 { + font-size: 20px; + font-weight: 700; + color: #333D4B; +} +.wrapper { + max-width: 800px; + margin: 0 auto; +} +.button { + color: #f9fafb; + background-color: #3182f6; + margin: 0; + font-size: 15px; + font-weight: 400; + line-height: 18px; + white-space: nowrap; + text-align: center; + /* vertical-align: middle; */ + cursor: pointer; + border: 0 solid transparent; + user-select: none; + transition: background 0.2s ease, color 0.1s ease; + text-decoration: none; + border-radius: 7px; + padding: 11px 16px; +} +.button:hover { + color: #fff; + background-color: #1b64da; +} +.title { + margin: 0 0 4px; + font-size: 24px; + font-weight: 600; + color: #4e5968; +} +.result { + flex-direction: column; + align-items: center; + text-align: center; + text-wrap: balance; +} +.box_section { + background-color: white; + border-radius: 10px; + box-shadow: 0 10px 20px rgb(0 0 0 / 1%), 0 6px 6px rgb(0 0 0 / 6%); + padding: 40px 30px 50px 30px; + margin-top:30px; + margin-bottom:50px; + color: #333D4B +} +:root { + --checkable-size: 20px; + --checkable-input-top: 3px; + --checkable-input-left: 5px; + --checkable-input-width: 14px; + --checkable-input-height: 10px; + --checkable-input-svg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.343 4.574l4.243 4.243 7.07-7.071' fill='transparent' stroke-width='2' stroke='%23FFF' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + --checkable-label-text-padding: 8px; + --indeterminate-checkable-input-top: 7px; + --indeterminate-checkable-input-left: 5px; + --indeterminate-checkable-input-width: 14px +} + +:root .checkable--small { + --checkable-size: 20px; + --checkable-input-top: 2px; + --checkable-input-left: 4px; + --checkable-input-width: 12px; + --checkable-input-height: 9px; + --checkable-input-svg: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.286 3.645l3.536 3.536 5.892-5.893' fill='transparent' stroke-width='2' stroke='%23FFF' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + --indeterminate-checkable-input-top: 5px; + --indeterminate-checkable-input-left: 4px; + --indeterminate-checkable-input-width: 12px +} + +.checkable { + position: relative; + display: flex +} + +.checkable+.checkable { + margin-top: 12px +} + +.checkable--inline { + display: inline-block +} + +.checkable--inline+.checkable--inline { + margin-top: 0; + margin-left: 18px +} + +.checkable__label { + display: inline-block; + max-width: 100%; + min-height: 20px; + min-height: var(--checkable-size); + line-height: 1.6; + padding-left: 20px; + padding-left: var(--checkable-size); + margin-bottom: 0; + padding-top: 0; + padding-bottom: 0; + color: #4e5968; + color: var(--grey700); + cursor: pointer +} + +.checkable__input { + position: absolute; + margin: 0 0 0 -20px; + margin: 0 0 0 calc(var(--checkable-size)*-1); + top: 4px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + cursor: pointer +} + +.checkable__input:after,.checkable__input:before { + content: ""; + position: absolute +} + +.checkable__input:before { + top: -4px; + left: 0; + width: 20px; + width: var(--checkable-size); + height: 20px; + height: var(--checkable-size); + border: 2px solid #d1d6db; + border: 2px solid #d1d6db; + background-color: #fff; + background-color: white; + transition: border-color .1s ease,background-color .1s ease +} + +.checkable__input:after { + opacity: 0; + transition: opacity .1s ease; + top: 3px; + top: var(--checkable-input-top); + left: 5px; + left: var(--checkable-input-left); + width: 14px; + width: var(--checkable-input-width); + height: 10px; + height: var(--checkable-input-height); + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1.343 4.574l4.243 4.243 7.07-7.071' fill='transparent' stroke-width='2' stroke='%23FFF' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-image: var(--checkable-input-svg); + background-repeat: no-repeat +} + +.checkable__input[type=checkbox]:indeterminate:after { + top: 7px; + top: var(--indeterminate-checkable-input-top); + left: 5px; + left: var(--indeterminate-checkable-input-left); + width: 14px; + width: var(--indeterminate-checkable-input-width); + height: 0; + border: 1px solid #fff; + border: 1px solid var(--white); + border-radius: 1px; + transform: rotate(0) +} + +.checkable__input:focus { + outline: 0 +} + +.checkable__input:focus:before,.checkable__input:hover:before { + background-color: #e8f3ff; + background-color: #e8f3ff; + border-color: #3182f6; + border-color: #3182f6 +} + +.checkable__input:checked:before,.checkable__input[type=checkbox]:indeterminate:before { + border-color: #3182f6; + border-color: #3182f6; + background-color: #3182f6; + background-color: #3182f6 +} + +.checkable__input:checked:after,.checkable__input[type=checkbox]:indeterminate:after { + opacity: 1 +} + +.checkable__input:disabled:before { + background-color: #f2f4f6; + background-color: var(--grey100); + border-color: rgba(0,23,51,.02); + border-color: var(--greyOpacity50) +} + +.checkable__input:disabled:checked:before,.checkable__input:disabled[type=checkbox]:indeterminate:before { + background-color: #e5e8eb; + background-color: var(--grey200); + border-color: #e5e8eb; + border-color: var(--grey200) +} + +.checkable__input[type=checkbox]:before { + border-radius: 6px +} + +.checkable__input[type=radio]:before { + border-radius: 12px +} + +.checkable__label-text { + display: inline-block; + padding-left: 13px; + color: #4e5968; + + /* padding-left: var(--checkable-label-text-padding) */ +} + +.checkable--disabled>.checkable__input { + cursor: not-allowed +} + +.checkable--disabled>.checkable__label { + color: #b0b8c1; + color: var(--grey400); + cursor: not-allowed +} + +.checkable--read-only { + pointer-events: none +} \ No newline at end of file diff --git a/src/test/java/com/kt/service/payment/PaymentServiceTest.java b/src/test/java/com/kt/service/payment/PaymentServiceTest.java index 5afedbcc..19149bb5 100644 --- a/src/test/java/com/kt/service/payment/PaymentServiceTest.java +++ b/src/test/java/com/kt/service/payment/PaymentServiceTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.*; import java.time.LocalDate; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,8 +25,6 @@ import com.kt.domain.product.Product; import com.kt.domain.user.Gender; import com.kt.domain.user.User; -import com.kt.dto.discount.request.DiscountCreateRequest; -import com.kt.dto.payment.PaymentCreateRequest; import com.kt.repository.discount.DiscountRepository; import com.kt.repository.discountmembership.DiscountMembershipRepository; import com.kt.repository.membership.MembershipRepository; @@ -109,13 +108,13 @@ void setUp() { @Test void 주문에_대한_결제_생성_가능() { - paymentService.create(결제요청(), user.getId()); + paymentService.create(토스_결제_응답(), user.getId()); Payment payment = paymentRepository.findAll().get(0); - assertThat(payment.getTotalPrice()).isEqualTo(20000); + assertThat(payment.getTotalPrice()).isEqualTo(23000); assertThat(payment.getDeliveryFee()).isEqualTo(3000); - assertThat(payment.getFinalPrice()).isEqualTo(23000); + assertThat(payment.getFinalPrice()).isEqualTo(20000); Order updatedOrder = orderRepository.findById(order.getId()).get(); assertThat(updatedOrder.getOrderStatus()).isEqualTo(OrderStatus.PAID); @@ -137,15 +136,20 @@ void setUp() { new DiscountMembership(membershipDiscount, membership) ); - paymentService.create(결제요청(), user.getId()); + paymentService.create(토스_결제_응답(), user.getId()); Payment payment = paymentRepository.findAll().get(0); - assertThat(payment.getTotalPrice()).isEqualTo(18000); - assertThat(payment.getFinalPrice()).isEqualTo(21000); + assertThat(payment.getTotalPrice()).isEqualTo(23000); + assertThat(payment.getFinalPrice()).isEqualTo(20000); } - private PaymentCreateRequest 결제요청() { - return new PaymentCreateRequest(order.getId(), paymentType.getId()); + private Map 토스_결제_응답() { + return Map.of( + "orderId", order.getId() + "-" + System.currentTimeMillis(), + "paymentKey", "test_payment_key_" + System.currentTimeMillis(), + "method", "CARD", + "totalAmount", 23000 + ); } }