From c84b2acbadbe8bc562172bc0e58d8cf896f536dd Mon Sep 17 00:00:00 2001 From: choyoungseo20 Date: Sat, 14 Jun 2025 22:19:50 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 +- .../global/api_payload/ErrorCode.java | 4 + .../global/api_payload/SuccessCode.java | 8 + .../payment/controller/PaymentController.java | 120 +++++++ .../payment/converter/PaymentConverter.java | 41 +++ .../payment/domain/Payment.java | 34 ++ .../payment/dto/PaymentDto.java | 146 ++++++++ .../payment/repository/PaymentRepository.java | 17 + .../payment/scheduler/PaymentScheduler.java | 18 + .../payment/service/PaymentService.java | 314 ++++++++++++++++++ .../silverbridgeX_user/user/domain/User.java | 4 + src/main/resources/application.yml | 4 + src/main/resources/templates/success.html | 104 ++++++ 13 files changed, 815 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/controller/PaymentController.java create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/converter/PaymentConverter.java create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/domain/Payment.java create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/dto/PaymentDto.java create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/repository/PaymentRepository.java create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/scheduler/PaymentScheduler.java create mode 100644 src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java create mode 100644 src/main/resources/templates/success.html diff --git a/build.gradle b/build.gradle index cb4156a..6590ed3 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' testImplementation 'org.springframework.security:spring-security-test' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // batch implementation 'org.springframework.boot:spring-boot-starter-batch' @@ -68,7 +69,6 @@ dependencies { // swaggerDoc implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' - } tasks.named('test') { diff --git a/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java b/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java index d97b30b..757ac71 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java +++ b/src/main/java/com/example/silverbridgeX_user/global/api_payload/ErrorCode.java @@ -34,6 +34,10 @@ public enum ErrorCode implements BaseCode { // Recommend Activity RECOMMEND_ACTIVITY_NOT_FOUND(HttpStatus.NOT_FOUND, "RECOMMEND_4041", "존재하지 않는 추천입니다."), + // Payment + TID_NOT_EXIST(HttpStatus.BAD_REQUEST, "PAYMENT_4001", "tid가 존재하지 않습니다."), + TID_SID_UNSUPPORTED(HttpStatus.BAD_REQUEST, "PAYMENT_4002", "지원되지 않는 tid, sid 입니다."), + SID_NOT_EXIST(HttpStatus.BAD_REQUEST, "PAYMENT_4003", "sid가 존재하지 않습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java b/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java index c580150..c5e7828 100644 --- a/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java +++ b/src/main/java/com/example/silverbridgeX_user/global/api_payload/SuccessCode.java @@ -30,6 +30,14 @@ public enum SuccessCode implements BaseCode { // MatchRequest MATCH_REQUEST_SUCCESS(HttpStatus.OK, "MATCH_REQUEST_2001", "매치 신청이 완료되었습니다."), + + // Payment + PAYMENT_URL_CREATE_SUCCESS(HttpStatus.OK, "PAYMENT_2001", "카카오페이 URL을 생성하였습니다."), + PAYMENT_CANCEL_SUCCESS(HttpStatus.OK, "PAYMENT_2002", "결제 취소가 완료되었습니다."), + PAYMENT_SUBSCRIBE_SUCCESS(HttpStatus.OK, "PAYMENT_2003", "구독이 완료되었습니다."), + PAYMENT_SUBSCRIBE_CANCEL_SUCCESS(HttpStatus.OK, "PAYMENT_2004", "구독 취소가 완료되었습니다."), + PAYMENT_VIEW_SUBSCRIBE_STATUS_SUCCESS(HttpStatus.OK, "PAYMENT_2005", "구독 상태를 반환 완료하였습니다."), + ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/example/silverbridgeX_user/payment/controller/PaymentController.java b/src/main/java/com/example/silverbridgeX_user/payment/controller/PaymentController.java new file mode 100644 index 0000000..2a44364 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/controller/PaymentController.java @@ -0,0 +1,120 @@ +package com.example.silverbridgeX_user.payment.controller; + +import com.example.silverbridgeX_user.global.api_payload.ApiResponse; +import com.example.silverbridgeX_user.global.api_payload.SuccessCode; +import com.example.silverbridgeX_user.payment.converter.PaymentConverter; +import com.example.silverbridgeX_user.payment.domain.Payment; +import com.example.silverbridgeX_user.payment.dto.PaymentDto; +import com.example.silverbridgeX_user.payment.service.PaymentService; +import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.jwt.CustomUserDetails; +import com.example.silverbridgeX_user.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.ModelAndView; + +@RestController +@RequestMapping("/payment") +@RequiredArgsConstructor +public class PaymentController { + + private final PaymentService paymentService; + private final UserService userService; + + @PostMapping("/ready") + @Operation(summary = "카카오페이 URL 생성 API", description = "카카오페이 URL을 생성하는 API 입니다.") + public ApiResponse readyToKakaoPay(@AuthenticationPrincipal CustomUserDetails customUserDetails) { + PaymentDto.KakaoReadyResponse kakaoReadyResponse = paymentService.kakaoPayReady(); + + User user = userService.findByUserName(customUserDetails.getUsername()); + Long userId = user.getId(); + + paymentService.saveTid(userId, kakaoReadyResponse.getTid()); + + return ApiResponse.onSuccess(SuccessCode.PAYMENT_URL_CREATE_SUCCESS, kakaoReadyResponse); + } + + @GetMapping("/success") + public ModelAndView afterPayRequest(@RequestParam("pg_token") String pgToken) { + PaymentDto.KakaoApproveResponse kakaoApproveResponse = paymentService.approveResponse(pgToken); + + ModelAndView modelAndView = new ModelAndView("success"); // "success"는 템플릿 파일 이름 + modelAndView.addObject("paymentInfo", kakaoApproveResponse); + + paymentService.saveSid(kakaoApproveResponse); + + return modelAndView; + } + + @GetMapping("/fail") + public String fail() { + return "fail"; + } + + @GetMapping("/cancel") + public ApiResponse refund(@AuthenticationPrincipal CustomUserDetails customUserDetails) { + + User user = userService.findByUserName(customUserDetails.getUsername()); + Long userId = user.getId(); + + Payment kakaoPay = paymentService.getKakaoPayInfo(userId); + + PaymentDto.KakaoCancelResponse kakaoCancelResponse = paymentService.cancelResponse(kakaoPay.getTid()); + + paymentService.cancelPay(userId); + + return ApiResponse.onSuccess(SuccessCode.PAYMENT_CANCEL_SUCCESS, kakaoCancelResponse); + } + + @PostMapping("/subscribe") + public ApiResponse subscribePayRequest(@AuthenticationPrincipal CustomUserDetails customUserDetails) { + User user = userService.findByUserName(customUserDetails.getUsername()); + Long userId = user.getId(); + + Payment kakaoPay = paymentService.getKakaoPayInfo(userId); + + PaymentDto.KakaoApproveResponse kakaoApproveResponse = paymentService.approveSubscribeResponse(kakaoPay.getSid()); + + paymentService.savePayInfo(userId, kakaoApproveResponse); + + return ApiResponse.onSuccess(SuccessCode.PAYMENT_SUBSCRIBE_SUCCESS, kakaoApproveResponse); + } + + @PostMapping("/subscribe/cancel") + @Operation(summary = "카카오페이 구독 취소 API", description = "카카오페이 구독을 취소하는 API 입니다.") + public ApiResponse subscribeCancelRequest(@AuthenticationPrincipal CustomUserDetails customUserDetails) { + User user = userService.findByUserName(customUserDetails.getUsername()); + Long userId = user.getId(); + + Payment kakaoPay = paymentService.getKakaoPayInfo(userId); + + PaymentDto.KakaoSubscribeCancelResponse kakaoSubscribeCancelResponse = paymentService.subscribeCancelResponse(kakaoPay.getSid()); + + return ApiResponse.onSuccess(SuccessCode.PAYMENT_URL_CREATE_SUCCESS, kakaoSubscribeCancelResponse); + } + + @GetMapping("/subscribe/status") + @Operation(summary = "카카오페이 구독 상태 확인 API", description = "카카오페이 구독 상태를 확인하는 API 입니다.") + public ApiResponse subscribeStatusRequest(@AuthenticationPrincipal CustomUserDetails customUserDetails) { + User user = userService.findByUserName(customUserDetails.getUsername()); + Long userId = user.getId(); + + PaymentDto.KakaoSubscribeStatusResponse kakaoSubscribeStatusResponse = new PaymentDto.KakaoSubscribeStatusResponse(); + + boolean isLogExist = false; + if (paymentService.getKakaoPayLog(userId)) { + Payment kakaoPay = paymentService.getKakaoPayInfo(userId); + + if (kakaoPay.getSid() == null || kakaoPay.getSid().isEmpty()) {} + else { + isLogExist = true; + + kakaoSubscribeStatusResponse = paymentService.subscribeStatusResponse(kakaoPay.getSid()); + } + } + + return ApiResponse.onSuccess(SuccessCode.PAYMENT_VIEW_SUBSCRIBE_STATUS_SUCCESS, PaymentConverter.toKakaoPayStatus(isLogExist, kakaoSubscribeStatusResponse)); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/payment/converter/PaymentConverter.java b/src/main/java/com/example/silverbridgeX_user/payment/converter/PaymentConverter.java new file mode 100644 index 0000000..50afd23 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/converter/PaymentConverter.java @@ -0,0 +1,41 @@ +package com.example.silverbridgeX_user.payment.converter; + +import com.example.silverbridgeX_user.payment.domain.Payment; +import com.example.silverbridgeX_user.payment.dto.PaymentDto; +import com.example.silverbridgeX_user.user.domain.User; + +public class PaymentConverter { + + public static Payment toKakaoPayTid(String tid, User user) { + return Payment.builder() + .user(user) + .tid(tid) + .build(); + + } + + public static Payment toKakaoPay(String tid, String sid, User user) { + return Payment.builder() + .user(user) + .tid(tid) + .sid(sid) + .build(); + + } + + public static PaymentDto.KakaoPayStatus toKakaoPayStatus(boolean isLogExist, PaymentDto.KakaoSubscribeStatusResponse kakaoSubscribeStatusResponse) { + String status = kakaoSubscribeStatusResponse.getStatus(); + + String last_approved_at; + if(kakaoSubscribeStatusResponse.getLast_approved_at() == null || kakaoSubscribeStatusResponse.getLast_approved_at().isEmpty()) { + last_approved_at = kakaoSubscribeStatusResponse.getCreated_at(); + } + else last_approved_at = kakaoSubscribeStatusResponse.getLast_approved_at(); + + return PaymentDto.KakaoPayStatus.builder() + .isLogExist(isLogExist) + .status(status) + .last_approved_at(last_approved_at) + .build(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/payment/domain/Payment.java b/src/main/java/com/example/silverbridgeX_user/payment/domain/Payment.java new file mode 100644 index 0000000..d5c231d --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/domain/Payment.java @@ -0,0 +1,34 @@ +package com.example.silverbridgeX_user.payment.domain; + +import com.example.silverbridgeX_user.global.entity.BaseEntity; +import com.example.silverbridgeX_user.user.domain.User; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Payment extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String tid; + + private String sid; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + public void updateTid(String tid) { this.tid = tid; } + + public void updateSid(String sid) { this.sid = sid; } + + public void updatePayInfo(String tid, String sid) { + this.tid = tid; + this.sid = sid; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/payment/dto/PaymentDto.java b/src/main/java/com/example/silverbridgeX_user/payment/dto/PaymentDto.java new file mode 100644 index 0000000..181345a --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/dto/PaymentDto.java @@ -0,0 +1,146 @@ +package com.example.silverbridgeX_user.payment.dto; + +import lombok.*; + +public class PaymentDto { + + @Getter + @Setter + @ToString + public static class Amount { + private int total; + private int tax_free; + private int tax; + private int point; + private int discount; + private int green_deposit; + } + + @Data + public static class KakaoReadyResponse { + private String tid; + private String next_redirect_app_url; + private String next_redirect_mobile_url; + private String next_redirect_pc_url; + private String android_app_scheme; + private String ios_app_scheme; + private String created_at; + } + + @Getter + @Setter + @ToString + public static class KakaoApproveResponse { + private String aid; + private String tid; + private String cid; + private String sid; + private String partner_order_id; + private String partner_user_id; + private String payment_method_type; + private Amount amount; + private String item_name; + private String item_code; + private int quantity; + private String created_at; + private String approved_at; + private String payload; + } + + @Getter + @Setter + @ToString + public static class KakaoCancelResponse { + private String aid; + private String tid; + private String cid; + private String status; + private String partner_order_id; + private String partner_user_id; + private String payment_method_type; + private Amount amount; + private ApprovedCancelAmount approved_cancel_amount; + private CanceledAmount canceled_amount; + private CancelAvailableAmount cancel_available_amount; + private String item_name; + private String item_code; + private int quantity; + private String created_at; + private String approved_at; + private String canceled_at; + private String payload; + } + + @Getter + @Setter + @ToString + public static class KakaoSubscribeCancelResponse { + private String cid; + private String sid; + private String status; + private String created_at; + private String last_approved_at; + private String inactivated_at; + } + + @Getter + @Setter + @ToString + public static class KakaoSubscribeStatusResponse { + private boolean available; + private String cid; + private String sid; + private String status; + private String item_name; + private String payment_method_type; + private String created_at; + private String last_approved_at; + private String use_point_status; + } + + @Getter + @Setter + @ToString + public static class ApprovedCancelAmount { + private int total; + private int tax_free; + private int vat; + private int point; + private int discount; + private int green_deposit; + } + + @Getter + @Setter + @ToString + public static class CanceledAmount { + private int total; + private int tax_free; + private int vat; + private int point; + private int discount; + private int green_deposit; + } + + @Getter + @Setter + @ToString + public static class CancelAvailableAmount { + private int total; + private int tax_free; + private int vat; + private int point; + private int discount; + private int green_deposit; + } + + @Builder + @Getter + @Setter + @ToString + public static class KakaoPayStatus { + private boolean isLogExist; + private String status; + private String last_approved_at; + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/payment/repository/PaymentRepository.java b/src/main/java/com/example/silverbridgeX_user/payment/repository/PaymentRepository.java new file mode 100644 index 0000000..5c131c3 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/repository/PaymentRepository.java @@ -0,0 +1,17 @@ +package com.example.silverbridgeX_user.payment.repository; + +import com.example.silverbridgeX_user.payment.domain.Payment; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface PaymentRepository extends JpaRepository { + + boolean existsByUser_Id(Long userId); + Optional findByUser_Id(Long userId); + Optional findByTid(String tid); + @Query("SELECT k FROM Payment k JOIN FETCH k.user WHERE k.sid IS NOT NULL") + List findAllWithMemberAndSidNotNull(); +} diff --git a/src/main/java/com/example/silverbridgeX_user/payment/scheduler/PaymentScheduler.java b/src/main/java/com/example/silverbridgeX_user/payment/scheduler/PaymentScheduler.java new file mode 100644 index 0000000..4fb576e --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/scheduler/PaymentScheduler.java @@ -0,0 +1,18 @@ +package com.example.silverbridgeX_user.payment.scheduler; + +import com.example.silverbridgeX_user.payment.service.PaymentService; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PaymentScheduler { + + private final PaymentService paymentService; + + @Scheduled(cron = "0 0 14 * * ?") + public void regularPayment() { + paymentService.regularPayment(); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java b/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java new file mode 100644 index 0000000..59f0739 --- /dev/null +++ b/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java @@ -0,0 +1,314 @@ +package com.example.silverbridgeX_user.payment.service; + +import com.example.silverbridgeX_user.global.api_payload.ErrorCode; +import com.example.silverbridgeX_user.global.exception.GeneralException; +import com.example.silverbridgeX_user.payment.converter.PaymentConverter; +import com.example.silverbridgeX_user.payment.domain.Payment; +import com.example.silverbridgeX_user.payment.dto.PaymentDto; +import com.example.silverbridgeX_user.payment.repository.PaymentRepository; +import com.example.silverbridgeX_user.user.domain.User; +import com.example.silverbridgeX_user.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +@RequiredArgsConstructor +@Transactional +@EnableScheduling +public class PaymentService { + + private RestTemplate restTemplate = new RestTemplate(); + private PaymentDto.KakaoReadyResponse kakaoReadyResponse; + private final PaymentRepository paymentRepository; + private final UserRepository userRepository; + + @Value("${kakaopay.secret_key}") + private String secretKey; + + @Value("${kakaopay.cid}") + private String cid; + + private HttpHeaders getHeaders() { + HttpHeaders httpHeaders = new HttpHeaders(); + String auth = "SECRET_KEY " + secretKey; + httpHeaders.set("Authorization", auth); + httpHeaders.set("Content-Type", "application/json"); + return httpHeaders; + } + + public PaymentDto.KakaoReadyResponse kakaoPayReady() { + Map parameters = new HashMap<>(); + + parameters.put("cid", cid); + parameters.put("partner_order_id", "ORDER_ID"); + parameters.put("partner_user_id", "USER_ID"); + parameters.put("item_name", "은빛동행 구독"); + parameters.put("quantity", "1"); + parameters.put("total_amount", "9900"); + parameters.put("vat_amount", "200"); + parameters.put("tax_free_amount", "0"); + parameters.put("approval_url", "http://localhost:8080/payment/success"); // http://15.165.17.95/user/payment/success http://localhost:8080/payment/success + parameters.put("fail_url", "http://localhost:8080/payment/fail"); // http://15.165.17.95/user/payment/fail http://localhost:8080/payment/fail + parameters.put("cancel_url", "http://localhost:8080/payment/cancel"); // http://15.165.17.95/user/payment/cancel http://localhost:8080/payment/cancel + + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + kakaoReadyResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/ready", + requestEntity, + PaymentDto.KakaoReadyResponse.class); + return kakaoReadyResponse; + } + + public PaymentDto.KakaoApproveResponse approveResponse(String pgToken) { + Map parameters = new HashMap<>(); + parameters.put("cid", cid); + parameters.put("tid", kakaoReadyResponse.getTid()); + parameters.put("partner_order_id", "ORDER_ID"); + parameters.put("partner_user_id", "USER_ID"); + parameters.put("pg_token", pgToken); + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + PaymentDto.KakaoApproveResponse kakaoApproveResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/approve", + requestEntity, + PaymentDto.KakaoApproveResponse.class); + return kakaoApproveResponse; + } + + public PaymentDto.KakaoCancelResponse cancelResponse(String tid) { + if (tid == null || tid.isEmpty()) { + throw new GeneralException(ErrorCode.TID_NOT_EXIST); + } + + Map parameters = new HashMap<>(); + parameters.put("cid", cid); + parameters.put("tid", tid); + parameters.put("cancel_amount", "9900"); + parameters.put("cancel_tax_free_amount", "0"); + parameters.put("cancel_vat_amount", "0"); + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + PaymentDto.KakaoCancelResponse kakaoCancelResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/cancel", + requestEntity, + PaymentDto.KakaoCancelResponse.class); + return kakaoCancelResponse; + } + + public Payment getKakaoPayInfo(Long userId) { + Payment kakaoPay = paymentRepository.findByUser_Id(userId).orElseThrow(() -> new GeneralException(ErrorCode.TID_NOT_EXIST)); + + return kakaoPay; + } + + public boolean getKakaoPayLog(Long userId) { + return paymentRepository.existsByUser_Id(userId); + } + + public boolean getPremiumState(Long userId) { + boolean isPremium = false; + + if (paymentRepository.existsByUser_Id(userId)) { + Payment kakaoPay = paymentRepository.findByUser_Id(userId).orElseThrow(() -> new GeneralException(ErrorCode.TID_NOT_EXIST)); + + if (kakaoPay.getSid() == null || kakaoPay.getSid().isEmpty()) {} + else { + Map parameters = new HashMap<>(); + parameters.put("cid", cid); + parameters.put("sid", kakaoPay.getSid()); + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + PaymentDto.KakaoSubscribeStatusResponse kakaoSubscribeStatusResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/manage/subscription/status", + requestEntity, + PaymentDto.KakaoSubscribeStatusResponse.class); + + if (kakaoSubscribeStatusResponse.getStatus().equals("ACTIVE")) { + isPremium = true; + } + else { + String last_approved_at; + if(kakaoSubscribeStatusResponse.getLast_approved_at() == null || kakaoSubscribeStatusResponse.getLast_approved_at().isEmpty()) { + last_approved_at = kakaoSubscribeStatusResponse.getCreated_at(); + } + else last_approved_at = kakaoSubscribeStatusResponse.getLast_approved_at(); + + DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + LocalDateTime lastApprovedAt = LocalDateTime.parse(last_approved_at, formatter); + + LocalDateTime oneMonthLater = lastApprovedAt.plusMonths(1).withHour(14).withMinute(0).withSecond(0); + + LocalDateTime now = LocalDateTime.now(); + + if (now.isBefore(oneMonthLater)) { + isPremium = true; + } + } + } + + } + + return isPremium; + } + + public PaymentDto.KakaoApproveResponse approveSubscribeResponse(String sid) { + if (sid == null || sid.isEmpty()) { + throw new GeneralException(ErrorCode.SID_NOT_EXIST); + } + + Map parameters = new HashMap<>(); + parameters.put("cid", cid); + parameters.put("sid", sid); + parameters.put("partner_order_id", "ORDER_ID"); + parameters.put("partner_user_id", "USER_ID"); + parameters.put("item_name", "BodyCheck 구독"); + parameters.put("quantity", "1"); + parameters.put("total_amount", "4900"); + parameters.put("vat_amount", "200"); + parameters.put("tax_free_amount", "0"); + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + PaymentDto.KakaoApproveResponse kakaoApproveResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/subscription", + requestEntity, + PaymentDto.KakaoApproveResponse.class); + return kakaoApproveResponse; + } + + public PaymentDto.KakaoSubscribeCancelResponse subscribeCancelResponse(String sid) { + if (sid == null || sid.isEmpty()) { + throw new GeneralException(ErrorCode.SID_NOT_EXIST); + } + + Map parameters = new HashMap<>(); + parameters.put("cid", cid); + parameters.put("sid", sid); + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + PaymentDto.KakaoSubscribeCancelResponse kakaoSubscribeCancelResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/manage/subscription/inactive", + requestEntity, + PaymentDto.KakaoSubscribeCancelResponse.class); + return kakaoSubscribeCancelResponse; + } + + public PaymentDto.KakaoSubscribeStatusResponse subscribeStatusResponse(String sid) { + if (sid == null || sid.isEmpty()) { + throw new GeneralException(ErrorCode.SID_NOT_EXIST); + } + + Map parameters = new HashMap<>(); + parameters.put("cid", cid); + parameters.put("sid", sid); + + HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); + + PaymentDto.KakaoSubscribeStatusResponse kakaoSubscribeStatusResponse = restTemplate.postForObject( + "https://open-api.kakaopay.com/online/v1/payment/manage/subscription/status", + requestEntity, + PaymentDto.KakaoSubscribeStatusResponse.class); + return kakaoSubscribeStatusResponse; + } + + public void saveTid(Long userId, String tid) { + Payment kakaoPay; + if (paymentRepository.existsByUser_Id(userId)) { + kakaoPay = paymentRepository.findByUser_Id(userId).orElseThrow(() -> new GeneralException(ErrorCode.TID_SID_UNSUPPORTED)); + kakaoPay.updateTid(tid); + } + else { + User user = userRepository.findById(userId).orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND)); + kakaoPay = PaymentConverter.toKakaoPayTid(tid, user); + } + paymentRepository.save(kakaoPay); + } + + public void saveSid(PaymentDto.KakaoApproveResponse kakaoApproveResponse) { + + Payment kakaoPay = paymentRepository.findByTid(kakaoApproveResponse.getTid()).orElseThrow(() -> new GeneralException(ErrorCode.TID_NOT_EXIST)); + + kakaoPay.updateSid(kakaoApproveResponse.getSid()); + + paymentRepository.save(kakaoPay); + } + + public void savePayInfo(Long userId, PaymentDto.KakaoApproveResponse kakaoApproveResponse) { + Payment kakaoPay; + if (paymentRepository.existsByUser_Id(userId)) { + kakaoPay = paymentRepository.findByUser_Id(userId).orElseThrow(() -> new GeneralException(ErrorCode.TID_SID_UNSUPPORTED)); + kakaoPay.updatePayInfo(kakaoApproveResponse.getTid(), kakaoApproveResponse.getSid()); + } + else { + User member = userRepository.findById(userId).orElseThrow(() -> new GeneralException(ErrorCode.USER_NOT_FOUND)); + kakaoPay = PaymentConverter.toKakaoPay(kakaoApproveResponse.getTid(), kakaoApproveResponse.getSid(), member); + } + paymentRepository.save(kakaoPay); + } + + public void cancelPay(Long userId) { + Payment kakaoPay = paymentRepository.findByUser_Id(userId).orElseThrow(() -> new GeneralException(ErrorCode.TID_NOT_EXIST)); + + paymentRepository.delete(kakaoPay); + } + + public void regularPayment() { + List kakaoPayList = paymentRepository.findAllWithMemberAndSidNotNull(); + + kakaoPayList.stream() + .forEach(kakaoPay -> { + if (kakaoPay.getSid() != null && !kakaoPay.getSid().isEmpty()) { + PaymentDto.KakaoSubscribeStatusResponse kakaoSubscribeStatusResponse = subscribeStatusResponse(kakaoPay.getSid()); + + // "ACTIVE" 상태인지 확인 + if (kakaoSubscribeStatusResponse.getStatus().equals("ACTIVE")) { + String lastApprovedAtStr = kakaoSubscribeStatusResponse.getLast_approved_at(); + if (lastApprovedAtStr == null || lastApprovedAtStr.isEmpty()) { + lastApprovedAtStr = kakaoSubscribeStatusResponse.getCreated_at(); + } + + // last_approved_at을 LocalDate로 변환 + DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME; + LocalDate lastApprovedAt = LocalDate.parse(lastApprovedAtStr, formatter); + + LocalDate today = LocalDate.now(); + + // 결제일과 오늘의 일(day)이 같고, 마지막 결제일이 이번 달이 아닌 경우에만 결제 수행 + if (today.getDayOfMonth() == lastApprovedAt.getDayOfMonth() && + (today.getYear() != lastApprovedAt.getYear() || today.getMonthValue() != lastApprovedAt.getMonthValue())) { + + PaymentDto.KakaoApproveResponse approveResponse = approveSubscribeResponse(kakaoPay.getSid()); + + savePayInfo(kakaoPay.getUser().getId(), approveResponse); + } +// if (today.getDayOfMonth() == lastApprovedAt.getDayOfMonth()) { +// KakaoPayDTO.KakaoApproveResponse approveResponse = approveSubscribeResponse(kakaoPay.getSid()); +// +// savePayInfo(kakaoPay.getMember().getId(), approveResponse); +// } + } + } + }); + +// System.out.println("정기 결제 작업 완료"); + } +} diff --git a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java index e4c6559..2bdea90 100644 --- a/src/main/java/com/example/silverbridgeX_user/user/domain/User.java +++ b/src/main/java/com/example/silverbridgeX_user/user/domain/User.java @@ -2,6 +2,7 @@ import com.example.silverbridgeX_user.global.entity.BaseEntity; import com.example.silverbridgeX_user.matching.domain.MatchRequest; +import com.example.silverbridgeX_user.payment.domain.Payment; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -56,6 +57,9 @@ public class User extends BaseEntity { @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) private MatchRequest matchRequest; + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL) + private Payment payment; + @JdbcTypeCode(SqlTypes.JSON) private List preferredKeywords; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f0f31d5..1dcd684 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,6 +32,10 @@ logging: org.springframework.batch: DEBUG org.springframework.jdbc.datasource.init: DEBUG +kakaopay: + secret_key: ${kakao.secret.key} + cid: ${kakao.cid} + --- # local profile diff --git a/src/main/resources/templates/success.html b/src/main/resources/templates/success.html new file mode 100644 index 0000000..370f5d6 --- /dev/null +++ b/src/main/resources/templates/success.html @@ -0,0 +1,104 @@ + + + + + + 결제 완료 + + + +
+
은빛동행 결제
+
카카오페이 결제가 완료되었습니다.
+ +
+
+ 구매처 + +
+
+ 결제일시 + +
+
+ 상품명 + +
+
+ 결제금액 + +
+
+ + +
+ + From 4dd60bc6f714a7913f516a3e83f20d1c0abf6da1 Mon Sep 17 00:00:00 2001 From: choyoungseo20 Date: Sat, 14 Jun 2025 22:20:44 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../silverbridgeX_user/payment/service/PaymentService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java b/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java index 59f0739..d551b99 100644 --- a/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java +++ b/src/main/java/com/example/silverbridgeX_user/payment/service/PaymentService.java @@ -60,9 +60,9 @@ public PaymentDto.KakaoReadyResponse kakaoPayReady() { parameters.put("total_amount", "9900"); parameters.put("vat_amount", "200"); parameters.put("tax_free_amount", "0"); - parameters.put("approval_url", "http://localhost:8080/payment/success"); // http://15.165.17.95/user/payment/success http://localhost:8080/payment/success - parameters.put("fail_url", "http://localhost:8080/payment/fail"); // http://15.165.17.95/user/payment/fail http://localhost:8080/payment/fail - parameters.put("cancel_url", "http://localhost:8080/payment/cancel"); // http://15.165.17.95/user/payment/cancel http://localhost:8080/payment/cancel + parameters.put("approval_url", "http://15.165.17.95/user/payment/success"); // http://15.165.17.95/user/payment/success http://localhost:8080/payment/success + parameters.put("fail_url", "http://15.165.17.95/user/payment/fail"); // http://15.165.17.95/user/payment/fail http://localhost:8080/payment/fail + parameters.put("cancel_url", "http://15.165.17.95/user/payment/cancel"); // http://15.165.17.95/user/payment/cancel http://localhost:8080/payment/cancel HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders());