Skip to content
6 changes: 6 additions & 0 deletions back/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// 테스트 의존성
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
testImplementation 'org.assertj:assertj-core'

// swagger
implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.7.0'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public class CartServiceImpl implements CartService {

/**
* 사용자의 장바구니에 상품을 추가
* 이미 장바구니에 존재하는 상품이면 수량을 증가시키고,
* 이미 장바구니에 존재하는 상품이면 수량을 증가
* 그렇지 않으면 새로 장바구니 항목을 생성
*
* @param requestDto 장바구니에 추가할 상품 ID 및 수량 정보
Expand Down Expand Up @@ -75,6 +75,7 @@ public CartResponseDto addToCart(AddToCartRequestDto requestDto, CustomPrincipal
cart = optionalCart.get();
cart.increaseQuantity(requestDto.getQuantity());
cartRepository.save(cart);

} else {
cart = Cart.builder()
.user(user)
Expand Down Expand Up @@ -210,7 +211,7 @@ public CartListResponseDto getCartList(CustomPrincipal principal) {

/**
* 결제를 위해 선택된 장바구니 항목 목록을 조회00
* 본인 소유의 장바구니 항목만 필터링하며,
* 본인 소유의 장바구니 항목만 필터링
* 배송비 정책에 따라 배송비를 계산하여 함께 반환
*
* @param cartIds 결제할 장바구니 항목 ID 목록
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class PaymentController {

@Operation(summary = "결제 성공 처리", description = "Toss 결제 승인 시 호출되는 콜백 URL")
@GetMapping("/success")
public ResponseEntity<String> PaymentSuccess(
public ResponseEntity<String> paymentSuccess(
@RequestParam String paymentKey,
@RequestParam String orderId,
@RequestParam int amount) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public class PaymentCancelRequestDto {
private String cancelReason;

@Builder
public PaymentCancelRequestDto (Long orderId, List<CanceledItemDto> canceledItmes, String cancelReason) {
public PaymentCancelRequestDto (Long orderId, List<CanceledItemDto> canceledItems, String cancelReason) {
this.orderId = orderId;
this.canceledItems = canceledItmes;
this.canceledItems = canceledItems;
this.cancelReason = cancelReason;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.bubble.giju.domain.payment.dto.response;

import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.Getter;


import java.time.LocalDateTime;
import java.time.OffsetDateTime;

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import lombok.Getter;

import java.time.OffsetDateTime;
import java.time.ZonedDateTime;

/*
* 응답 JSON에 정의하지 않은 필드가 있어도 무시하고 넘어감
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;

@Entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;

@Entity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.bubble.giju.domain.payment.service.paymentImpl;
package com.bubble.giju.domain.payment.service.impl;

import com.bubble.giju.domain.cart.entity.Cart;
import com.bubble.giju.domain.cart.repository.CartRepository;
Expand All @@ -10,7 +10,6 @@

import com.bubble.giju.domain.order.repository.OrderCartMappingRepository;
import com.bubble.giju.domain.order.repository.OrderRepository;
import com.bubble.giju.domain.payment.dto.request.CanceledItemDto;
import com.bubble.giju.domain.payment.dto.request.PaymentCancelRequestDto;
import com.bubble.giju.domain.payment.dto.response.*;
import com.bubble.giju.domain.payment.entity.Payment;
Expand All @@ -20,7 +19,7 @@
import com.bubble.giju.domain.payment.repository.PaymentFailInfoRepository;
import com.bubble.giju.domain.payment.repository.PaymentRepository;
import com.bubble.giju.domain.payment.service.PaymentService;
import com.bubble.giju.domain.payment.tossClient.TossClientImpl.TossClientImpl;
import com.bubble.giju.domain.payment.tossclient.impl.TossClientImpl;
import com.bubble.giju.domain.user.dto.CustomPrincipal;

import com.bubble.giju.global.config.CustomException;
Expand Down Expand Up @@ -50,6 +49,28 @@ public class PaymentServiceImpl implements PaymentService {
private static final String CANCEL_REASON = "결제 정보 불일치로 인한 자동 취소";
private final CartRepository cartRepository;


/**
* 결제 승인 성공 시 처리 로직
*
* - 결제 버튼 클릭 후 결제 승인이 완료되면 Toss 결제 승인 API를 호출해
* 결제 정보를 검증 및 저장하고,
* - Order 상태를 'SUCCEEDED'로 변경하며
* - 장바구니를 비우는 등 후속 처리
*
* 주요 단계
* 1. Order 조회 및 금액 검증
* 2. soft-delete 여부 확인
* 3. Toss 결제 승인 API 호출
* 4. 결제정보(Payment) 저장
* 5. 추가 검증 실패 시 결제 취소 처리
* 6. 주문 상태 업데이트
* 7. 장바구니 항목 정리
*
* @param paymentKey TossPayments에서 발급된 결제 키
* @param orderId 클라이언트가 전달한 주문 ID
* @param amount 결제 승인된 금액
*/
@Transactional
@Override
public void paymentSuccess(String paymentKey, String orderId, int amount) {
Expand Down Expand Up @@ -90,7 +111,7 @@ public void paymentSuccess(String paymentKey, String orderId, int amount) {

//추가 검증
if (!orderId.equals(tossResponse.getOrderId()) ||
order.getTotalAmount() != tossResponse.getTotalAmount()) {
(order.getTotalAmount()+order.getDeliveryCharge()) != tossResponse.getTotalAmount()) {

//결제 취소
TossCancelResponseDto cancelResponse = tossClientImpl.cancelPayment(
Expand Down Expand Up @@ -128,7 +149,23 @@ public void paymentSuccess(String paymentKey, String orderId, int amount) {
deleteCartsAndMappingsByOrder(order);
}


/**
* 결제 실패 처리 로직
*
* - TossPayments 결제 실패 콜백을 처리하는 메서드
* - 결제가 실패했을 경우 Payment 엔티티와 실패 정보를 저장하고,
* 해당 주문의 상태를 'FAILED'로 업데이트
*
* 주요 단계
* 1. 주문 조회
* 2. 실패 상태의 Payment 생성 및 저장
* 3. 실패 상세 정보-PaymentFailInfo 저장
* 4. 주문 상태를 FAILED로 변경
*
* @param code TossPayments가 반환한 실패 코드
* @param message TossPayments가 반환한 실패 메시지
* @param orderId 결제가 실패한 주문 ID
*/
@Transactional
@Override
public void paymentFail(String code, String message, String orderId) {
Expand Down Expand Up @@ -160,6 +197,39 @@ public void paymentFail(String code, String message, String orderId) {
orderRepository.save(order);
}

/**
* 결제 취소 처리 로직
*
* - 사용자가 결제 완료(SUCCEEDED) 상태인 주문에 대해 전액 혹은 부분 취소를 요청하면
* TossPayments 취소 API를 호출하고,
* 취소 정보를 기록하며,
* 주문 상태를 업데이트
*
* 주요 단계
* 1. 사용자 인증 및 주문 조회
* 2. 주문 상태 검증 (SUCCEEDED 상태만 취소 가능)
* 3. 결제 정보 조회
* 4. 취소할 금액 계산
* 5. TossPayments 결제 취소 API 호출
* 6. PaymentCancelInfo 생성 및 저장
* 7. 주문 상태(전체 취소/부분 취소) 업데이트
* 8. 응답 DTO 반환
*
* 반환값
* - PaymentCancelResponseDto 객체를 반환
* - 결제 취소 처리 결과 정보를 클라이언트에게 전달
* - orderId: 취소 처리된 주문의 ID
* - cancelReason: 취소 사유
* - cancelAmount: 취소된 금액
* - isFullCancel: 전액 취소 여부 (true: 전체 취소, false: 부분 취소)
* - canceledAt: 취소 완료 시간
* - receiptUrl: 카드 영수증 URL (있으면)
* - cashReceiptUrl: 현금 영수증 URL (있으면)
*
* @param principal 인증된 사용자 정보
* @param dto 취소 요청 정보 (주문 ID, 취소 사유, 취소할 항목들)
* @return PaymentCancelResponseDto 결제 취소 처리 결과
*/
@Transactional
@Override
public PaymentCancelResponseDto paymentCancel(CustomPrincipal principal, PaymentCancelRequestDto dto) {
Expand Down Expand Up @@ -279,25 +349,4 @@ private void updateCanceledPayment(Payment payment, TossCancelResponseDto respon
paymentRepository.save(payment);
}


/* public Order findOrderByStringId(String orderId) {
// 숫자 아닌 문자 모두 제거
String orderIdStr = orderId.replaceAll("[^0-9]", "");

if (orderIdStr.isEmpty()) {
throw new CustomException(ErrorCode.INVALID_ORDER_ID);
}

// Long 타입으로 변환
Long orderIdLong;
try {
orderIdLong = Long.parseLong(orderIdStr);
} catch (NumberFormatException e) {
throw new CustomException(ErrorCode.INVALID_ORDER_ID_FORMAT);
}

// DB에서 주문 조회
return orderRepository.findById(orderIdLong)
.orElseThrow(() -> new CustomException(ErrorCode.NON_EXISTENT_ORDER));
}*/
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.bubble.giju.domain.payment.tossClient;
package com.bubble.giju.domain.payment.tossclient;

import com.bubble.giju.domain.payment.dto.response.TossCancelResponseDto;
import com.bubble.giju.domain.payment.dto.response.TossPaymentResponseDto;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
package com.bubble.giju.domain.payment.tossClient.TossClientImpl;
package com.bubble.giju.domain.payment.tossclient.impl;

import com.bubble.giju.domain.payment.dto.response.TossCancelResponseDto;
import com.bubble.giju.domain.payment.dto.response.TossPaymentResponseDto;
import com.bubble.giju.domain.payment.tossClient.TossClient;
import com.bubble.giju.domain.payment.tossclient.TossClient;
import com.bubble.giju.global.config.CustomException;
import com.bubble.giju.global.config.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
import java.util.UUID;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.bubble.giju.domain.ranking.dto.response.RegionRankingResponseDto;
import com.bubble.giju.domain.ranking.enums.Region;
import com.bubble.giju.domain.ranking.service.RegionRankingService;
import com.bubble.giju.domain.ranking.service.impl.RegionRankingServiceImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public enum Region {
경상남도("GYEONGNAM", "경상남도"),
제주도("JEJU", "제주도");


private final String code;
private final String koreanName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public interface RegionRankingRepository extends JpaRepository<OrderDetail, Lon
JOIN od.order o
JOIN o.user u
WHERE od.region = :region
AND o.orderStatus = 'DELIVERED'
GROUP BY u.name
ORDER BY SUM(od.quantity) DESC, MAX(o.createdAt) DESC
LIMIT 10
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,7 @@
package com.bubble.giju.domain.ranking.service;

import com.bubble.giju.domain.ranking.dto.response.RegionRankingResponseDto;
import com.bubble.giju.domain.ranking.dto.response.UserRegionRankingDto;
import com.bubble.giju.domain.ranking.repository.RegionRankingRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class RegionRankingService {

private final RegionRankingRepository rankingRepository;

public RegionRankingResponseDto getTop10ByRegion(String region) {
List<UserRegionRankingDto> top10 = rankingRepository.findTop10ByRegion(region);
return new RegionRankingResponseDto(region, top10);
}
public interface RegionRankingService {
RegionRankingResponseDto getTop10ByRegion(String region);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.bubble.giju.domain.ranking.service.impl;

import com.bubble.giju.domain.ranking.dto.response.RegionRankingResponseDto;
import com.bubble.giju.domain.ranking.dto.response.UserRegionRankingDto;
import com.bubble.giju.domain.ranking.repository.RegionRankingRepository;
import com.bubble.giju.domain.ranking.service.RegionRankingService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class RegionRankingServiceImpl implements RegionRankingService {

private final RegionRankingRepository rankingRepository;

public RegionRankingResponseDto getTop10ByRegion(String region) {
List<UserRegionRankingDto> top10 = rankingRepository.findTop10ByRegion(region);
return new RegionRankingResponseDto(region, top10);
}
}
4 changes: 2 additions & 2 deletions back/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
cloud:
aws:
credentials:
access-key: ${S3STORAGE_ACCESS_KEY} # 환경 변수로부터 읽음
secret-key: ${S3STORAGE_SECRET_KEY} # 환경 변수로부터 읽음
access-key: ${S3STORAGE_ACCESS_KEY:test} # 환경 변수로부터 읽음
secret-key: ${S3STORAGE_SECRET_KEY:test} # 환경 변수로부터 읽음
region:
static: ap-northeast-2 # 환경 변수 또는 기본값 :contentReference[oaicite:0]{index=0}

Expand Down
Loading
Loading