✨ 기능 설명
페이플 파트너 관리자 〉 기본정보에 결제결과 수신 URL (가맹점 미수신 결과) 을 등록했습니다.
https://promptplace.kro.kr/api/prompts/purchases/payple-webhook
브라우저가 결제창에서 돌아오지 못한 결제(창 강제 종료, 네트워크 끊김, 앱 전환 실패 등)를 서버-투-서버 웹훅으로 보완해 결제결과 누락을 방지하는 것이 목적입니다. 현재 해당 경로는 미구현이라 등록만 된 상태입니다.
✨ 왜 기존 /payple-result 를 못 쓰나
src/purchases/routes/purchase.webhook.route.ts:8 의 POST /payple-result 는 PCD_RST_URL(결제창 리턴 URL) 겸용입니다. 운영 .env 에 PURCHASE_RESULT_REDIRECT_URL 이 설정돼 있어 (https://www.promptplace.kr/purchase/result) 컨트롤러가 302 redirect 를 반환합니다 (purchase.webhook.controller.ts:36).
브라우저 리턴엔 맞지만 웹훅 수신엔 200 이어야 하고, 302 를 실패로 판단해 재전송이 반복될 수 있습니다. 그래서 리다이렉트 없이 항상 200 을 주는 전용 경로를 분리합니다.
✨ 구현 중 확인된 문제 2건
1. 웹훅 페이로드의 PCD_PAY_URL 이 빈 문자열
공식 문서의 결제완료 웹훅 예시(카드/계좌 공통)에서 PCD_PAY_URL 은 "" 이고, 대신 최종 결제요청 URL 이 PCD_PAY_COFURL 로 옵니다.
verifyPayplePayment 는 payUrl 이 falsy 면 키 누락으로 던지므로 (purchases/utils/payple.ts:169), 웹훅 페이로드를 그대로 넘기면 재검증 단계에서 100% 실패합니다. PCD_PAY_COFURL 폴백이 필요합니다.
2. 재검증 호스트를 요청 본문에서 받음 (SSRF)
verifyPayplePayment 는 ${payHost}${payUrl} 로 POST 하는데 (payple.ts:175) 두 값 모두 요청 본문에서 옵니다. 지금까지는 브라우저 리턴 경로에서만 쓰여 노출이 제한적이었지만, 인증 없는 공개 웹훅 엔드포인트가 같은 함수를 타면 임의 호스트로 사내 요청을 유도할 수 있습니다. PAYPLE_CPAY_URL 도메인 allowlist 로 막습니다.
✨ 취소완료 웹훅 처리 방침
같은 URL 로 취소완료 이벤트(PCD_PAY_CODE: PAYC0000)도 들어옵니다. 다만 취소 페이로드에는 PCD_USER_DEFINE1(prompt_id/user_id)이 없고 PCD_REFUND_TOTAL 만 있어 기존 결제 처리 로직과 형태가 다릅니다.
환불은 이미 admin-refund.service.ts 워크플로(#533)가 정본이므로, 이번 PR 에서는 취소 이벤트는 로그만 남기고 무시합니다. 파트너 관리자에서 직접 취소한 건을 DB 에 자동 반영하는 건 별도 이슈로 분리합니다.
✨ 개발 목록
✨ 기타 설명 / 질문
- 멱등성은 기존
WebhookService.handlePaypleResult 의 findExistingPurchase 가이드 그대로 재사용합니다 (purchase.webhook.service.ts:26). /complete 와 웹훅이 동시에 도착해도 중복 구매가 생기지 않습니다.
- 해지결과 수신 URL 칸은 비워뒀습니다. 구매 결제가
PCD_PAY_WORK: 'PAY' 단건결제라 PCD_PAYER_ID 빌링키를 저장하지 않고, schema 에 매칭할 컬럼도 없습니다. 간편/정기결제 도입 시 별도 이슈로 다룹니다.
✨ 기능 설명
페이플 파트너 관리자 〉 기본정보에 결제결과 수신 URL (가맹점 미수신 결과) 을 등록했습니다.
브라우저가 결제창에서 돌아오지 못한 결제(창 강제 종료, 네트워크 끊김, 앱 전환 실패 등)를 서버-투-서버 웹훅으로 보완해 결제결과 누락을 방지하는 것이 목적입니다. 현재 해당 경로는 미구현이라 등록만 된 상태입니다.
✨ 왜 기존
/payple-result를 못 쓰나src/purchases/routes/purchase.webhook.route.ts:8의POST /payple-result는PCD_RST_URL(결제창 리턴 URL) 겸용입니다. 운영.env에PURCHASE_RESULT_REDIRECT_URL이 설정돼 있어 (https://www.promptplace.kr/purchase/result) 컨트롤러가 302 redirect 를 반환합니다 (purchase.webhook.controller.ts:36).브라우저 리턴엔 맞지만 웹훅 수신엔 200 이어야 하고, 302 를 실패로 판단해 재전송이 반복될 수 있습니다. 그래서 리다이렉트 없이 항상 200 을 주는 전용 경로를 분리합니다.
✨ 구현 중 확인된 문제 2건
1. 웹훅 페이로드의
PCD_PAY_URL이 빈 문자열공식 문서의 결제완료 웹훅 예시(카드/계좌 공통)에서
PCD_PAY_URL은""이고, 대신 최종 결제요청 URL 이PCD_PAY_COFURL로 옵니다.verifyPayplePayment는payUrl이 falsy 면 키 누락으로 던지므로 (purchases/utils/payple.ts:169), 웹훅 페이로드를 그대로 넘기면 재검증 단계에서 100% 실패합니다.PCD_PAY_COFURL폴백이 필요합니다.2. 재검증 호스트를 요청 본문에서 받음 (SSRF)
verifyPayplePayment는${payHost}${payUrl}로 POST 하는데 (payple.ts:175) 두 값 모두 요청 본문에서 옵니다. 지금까지는 브라우저 리턴 경로에서만 쓰여 노출이 제한적이었지만, 인증 없는 공개 웹훅 엔드포인트가 같은 함수를 타면 임의 호스트로 사내 요청을 유도할 수 있습니다.PAYPLE_CPAY_URL도메인 allowlist 로 막습니다.✨ 취소완료 웹훅 처리 방침
같은 URL 로 취소완료 이벤트(
PCD_PAY_CODE: PAYC0000)도 들어옵니다. 다만 취소 페이로드에는PCD_USER_DEFINE1(prompt_id/user_id)이 없고PCD_REFUND_TOTAL만 있어 기존 결제 처리 로직과 형태가 다릅니다.환불은 이미
admin-refund.service.ts워크플로(#533)가 정본이므로, 이번 PR 에서는 취소 이벤트는 로그만 남기고 무시합니다. 파트너 관리자에서 직접 취소한 건을 DB 에 자동 반영하는 건 별도 이슈로 분리합니다.✨ 개발 목록
POST /api/prompts/purchases/payple-webhook추가 — 리다이렉트 없이 항상 200verifyPayplePayment—PCD_PAY_URL없으면PCD_PAY_COFURL폴백PAYPLE_CPAY_URL도메인)pnpm build통과✨ 기타 설명 / 질문
WebhookService.handlePaypleResult의findExistingPurchase가이드 그대로 재사용합니다 (purchase.webhook.service.ts:26)./complete와 웹훅이 동시에 도착해도 중복 구매가 생기지 않습니다.PCD_PAY_WORK: 'PAY'단건결제라PCD_PAYER_ID빌링키를 저장하지 않고, schema 에 매칭할 컬럼도 없습니다. 간편/정기결제 도입 시 별도 이슈로 다룹니다.