Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ dependencies {
// AWS S3 의존성
implementation 'software.amazon.awssdk:s3:2.17.70'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// 레디스 의존성
implementation 'org.redisson:redisson-spring-boot-starter:3.23.2'
}

tasks.named('test') {
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/brainpix/BrainpixApplication.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.brainpix;

import jakarta.annotation.PostConstruct;

import java.util.TimeZone;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;

import jakarta.annotation.PostConstruct;

@SpringBootApplication
@EnableMongoAuditing
@EnableMongoRepositories(basePackages = {"com.brainpix.message.repository", "com.brainpix.alarm.repository"})
@EnableScheduling
public class BrainpixApplication {

@PostConstruct
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public enum KakaoPayErrorCode implements ErrorCode {
IDEA_OWNER_PURCHASE_INVALID(HttpStatus.BAD_REQUEST, "KAKAOPAY400", "글 작성자는 구매할 수 없습니다."),
QUANTITY_INVALID(HttpStatus.BAD_REQUEST, "KAKAOPAY400", "구매 수량은 재고를 초과할 수 없습니다."),

// 404 Not Found - 리소스를 찾을 수 없음
PAYMENT_DATA_NOT_FOUND(HttpStatus.NOT_FOUND, "KAKAOPAY404", "결제 정보를 찾을 수 없습니다."),

// 500 Internal Server Error - 서버 내부 오류
API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "KAKAOPAY500", "카카오페이 API 호출 중 오류가 발생하였습니다.");

Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/brainpix/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.brainpix.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedisConfig {

@Value("${spring.redis.host}")
private String host;

@Value("${spring.redis.port}")
private Integer port;

@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + host + ":" + port) // TLS이면 rediss://
.setConnectionMinimumIdleSize(10)
.setConnectionPoolSize(50);
return Redisson.create(config);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.entity.KakaoPaymentData;
import com.brainpix.kakaopay.dto.KakaoPaymentDataDto;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

Expand Down Expand Up @@ -49,7 +49,7 @@ public KakaoPayReadyDto.KakaoApiResponse requestPaymentReady(

// 카카오페이 결제 승인 API
public KakaoPayApproveDto.KakaoApiResponse requestPaymentApprove(
KakaoPayApproveDto.Parameter parameter, KakaoPaymentData kakaoPaymentData) {
KakaoPayApproveDto.Parameter parameter, KakaoPaymentDataDto kakaoPaymentData) {

Map<String, String> parameters = getApproveParams(parameter, kakaoPaymentData);
HttpHeaders headers = getHeaders();
Expand Down Expand Up @@ -78,23 +78,23 @@ private Map<String, String> getReadyParams(
params.put("tax_free_amount", "0");
params.put("vat_amount", String.valueOf(parameter.getVat()));
params.put("approval_url",
"https://www.brainpix.net/purchase/approve?ideaId=" + ideaMarket.getId() + "&orderId=" + orderId);
params.put("cancel_url", "https://www.brainpix.net/purchase/cancel?ideaId=" + ideaMarket.getId());
params.put("fail_url", "https://www.brainpix.net/purchase/fail?ideaId=" + ideaMarket.getId());
"http://localhost:5173/purchase/approve?ideaId=" + ideaMarket.getId() + "&orderId=" + orderId);
params.put("cancel_url", "http://localhost:5173/purchase/cancel?ideaId=" + ideaMarket.getId());
params.put("fail_url", "http://localhost:5173/purchase/fail?ideaId=" + ideaMarket.getId());

return params;
}

// 결제 승인 파라미터 생성
private Map<String, String> getApproveParams(KakaoPayApproveDto.Parameter parameter,
KakaoPaymentData kakaoPaymentData) {
KakaoPaymentDataDto kakaoPaymentData) {

Map<String, String> params = new HashMap<>();

params.put("cid", cid);
params.put("tid", kakaoPaymentData.getTid());
params.put("partner_order_id", parameter.getOrderId());
params.put("partner_user_id", String.valueOf(kakaoPaymentData.getBuyer().getId()));
params.put("partner_user_id", String.valueOf(kakaoPaymentData.getBuyerId()));
params.put("pg_token", parameter.getPgToken());

return params;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
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 com.brainpix.api.ApiResponse;
Expand All @@ -13,6 +12,8 @@
import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.service.KakaoPayFacadeService;
import com.brainpix.security.authorization.AllUser;
import com.brainpix.security.authorization.UserId;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -28,20 +29,22 @@ public class KakaoPayController {

private final KakaoPayFacadeService kakaoPayService;

@AllUser
@Operation(summary = "결제 준비 API", description = "아이디어 식별자, 판매자 식별자, 상품 수량, 총 결제금액(vat 포함), vat를 json 본문으로 입력받아 카카오페이 쪽에 결제 정보를 생성합니다.<br>사용자를 결제 화면으로 이동시킬 수 있도록 리다이렉트 url과 주문 번호가 응답값으로 제공됩니다.<br>주문 번호는 이후 승인 단계에서 pgToken과 함께 전달해주시면 됩니다.")
@PostMapping("/ready")
public ResponseEntity<ApiResponse<KakaoPayReadyDto.Response>> kakaoPayReady(@RequestParam Long userId,
public ResponseEntity<ApiResponse<KakaoPayReadyDto.Response>> kakaoPayReady(@UserId Long userId,
@RequestBody KakaoPayReadyDto.Request request) {
log.info("카카오페이 결제 준비 API 호출");
KakaoPayReadyDto.Parameter parameter = KakaoPayReadyDtoConverter.toParameter(userId, request);
KakaoPayReadyDto.Response response = kakaoPayService.kakaoPayReady(parameter);
return ResponseEntity.ok(ApiResponse.success(response));
}

@AllUser
@Operation(summary = "결제 승인 API", description = "pgToken과 주문 번호를 json 본문으로 입력받아 최종 승인을 처리합니다.")
@PostMapping("/approve")
public ResponseEntity<ApiResponse<KakaoPayApproveDto.Response>> kakaoPayApprove(
@RequestParam Long userId,
@UserId Long userId,
@RequestBody KakaoPayApproveDto.Request request) {
log.info("카카오페이 결제 최종 승인 API 호출");
KakaoPayApproveDto.Parameter parameter = KakaoPayApproveDtoConverter.toParameter(userId, request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static KakaoPayApproveDto.Parameter toParameter(Long userId, KakaoPayAppr
.userId(userId)
.orderId(request.getOrderId())
.pgToken(request.getPgToken())
.ideaId(request.getIdeaId())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.brainpix.kakaopay.converter;

import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.entity.KakaoPaymentData;
import com.brainpix.post.entity.idea_market.IdeaMarket;
import com.brainpix.user.entity.User;

public class KakaoPayReadyDtoConverter {

Expand All @@ -19,25 +16,10 @@ public static KakaoPayReadyDto.Parameter toParameter(Long userId, KakaoPayReadyD
.build();
}

public static KakaoPayReadyDto.Response toResponse(KakaoPayReadyDto.KakaoApiResponse kakaoApiResponse,
String orderId) {
public static KakaoPayReadyDto.Response toResponse(KakaoPayReadyDto.KakaoApiResponse kakaoApiResponse) {

return KakaoPayReadyDto.Response.builder()
.nextRedirectPcUrl(kakaoApiResponse.getNext_redirect_pc_url())
.build();
}

public static KakaoPaymentData toKakaoPaymentData(KakaoPayReadyDto.KakaoApiResponse kakaoApiResponse,
KakaoPayReadyDto.Parameter parameter,
String orderId, User buyer,
IdeaMarket ideaMarket) {

return KakaoPaymentData.builder()
.tid(kakaoApiResponse.getTid())
.quantity(parameter.getQuantity())
.orderId(orderId)
.buyer(buyer)
.ideaMarket(ideaMarket)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.brainpix.kakaopay.converter;

import com.brainpix.kakaopay.dto.KakaoPaymentDataDto;

public class KakaoPaymentDataDtoConverter {

public static KakaoPaymentDataDto toKakaoPaymentDataDto(String tid, Long buyerId, Long quantity) {

return KakaoPaymentDataDto.builder()
.tid(tid)
.buyerId(buyerId)
.quantity(quantity)
.build();
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/brainpix/kakaopay/dto/KakaoPaymentDataDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.brainpix.kakaopay.dto;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class KakaoPaymentDataDto {

private String tid; // 카카오페이 쪽 주문 고유 번호
private Long buyerId; // 유저 ID
private Long quantity; // 구매 수량
}
43 changes: 0 additions & 43 deletions src/main/java/com/brainpix/kakaopay/entity/KakaoPaymentData.java

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.brainpix.kakaopay.service;

import java.util.concurrent.TimeUnit;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.brainpix.api.code.error.CommonErrorCode;
import com.brainpix.api.exception.BrainPixException;
import com.brainpix.kakaopay.dto.KakaoPayApproveDto;
import com.brainpix.kakaopay.dto.KakaoPayReadyDto;
import com.brainpix.kakaopay.repository.NamedLockRepository;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -17,35 +20,35 @@
@RequiredArgsConstructor
public class KakaoPayFacadeService {

private final NamedLockRepository namedLockRepository;
private final KakaoPayService kakaoPayService;
private final RedissonClient redissonClient;

@Transactional
public KakaoPayReadyDto.Response kakaoPayReady(KakaoPayReadyDto.Parameter parameter) {
return kakaoPayService.kakaoPayReady(parameter);
}

@Transactional
// 락 획득 → 트랜잭션 시작 → 트랜잭션 해제 → 락 해제 (락 해제가 커밋 이후로 보장되어야 하므로 Facade 패턴 적용)
public KakaoPayApproveDto.Response kakaoPayApprove(KakaoPayApproveDto.Parameter parameter) {
String lockName = "LOCK_KAKAO_PAY_APPROVE_" + parameter.getIdeaId();
try {
acquireLock(lockName);
return kakaoPayService.kakaoPayApprove(parameter);
} finally {
releaseLock(lockName);
}
}

private void acquireLock(String lockName) {
log.info("락 요청 lockName : {}", lockName);
Integer result = namedLockRepository.getLock(lockName);
if (result == null || result == 0) {
String lockKey = "lock:idea:" + parameter.getIdeaId();
RLock lock = redissonClient.getLock(lockKey);
boolean isLocked = false;

try {
isLocked = lock.tryLock(5, TimeUnit.SECONDS); // 5초동안 락 획득 대기
if (isLocked) {
log.info("결제 락 획득 성공: {}", lockKey);
return kakaoPayService.kakaoPayApprove(parameter); // 승인 서비스 코드 실행
} else {
log.info("결제 락 획득 실패: {}", lockKey);
throw new BrainPixException(CommonErrorCode.INTERNAL_SERVER_ERROR);
}
} catch (InterruptedException e) {
throw new BrainPixException(CommonErrorCode.INTERNAL_SERVER_ERROR);
} finally {
lock.unlock(); // 락 해제
log.info("결제 락 해제 성공: {}", lockKey);
}
}

private void releaseLock(String lockName) {
log.info("락 반환 lockName : {}", lockName);
namedLockRepository.releaseLock(lockName);
}
}
Loading