diff --git a/src/content/docs/_redir.yaml b/src/content/docs/_redir.yaml index 9f8109618..1e82f2a61 100644 --- a/src/content/docs/_redir.yaml +++ b/src/content/docs/_redir.yaml @@ -247,6 +247,27 @@ - old: /ko/ready/3-v2 new: /ko/ready/readme?v=v2#integration-identifiers +- old: /ko/auth/guide/def + new: /ko/authpay/guide#definition +- old: /ko/auth/guide/1 + new: /ko/authpay/guide?v=v1#sdk-installation +- old: /ko/auth/guide/2 + new: /ko/authpay/guide?v=v1#sdk-initialization +- old: /ko/auth/guide/3 + new: /ko/authpay/guide?v=v1#request-payment +- old: /ko/auth/guide/4/readme + new: /ko/authpay/guide?v=v1#handle-result +- old: /ko/auth/guide/4/iframe + new: /ko/authpay/guide?v=v1#handle-callback +- old: /ko/auth/guide/4/redirect + new: /ko/authpay/guide?v=v1#handle-redirect +- old: /ko/auth/guide/5/readme + new: /ko/authpay/guide?v=v1#complete +- old: /ko/auth/guide/5/post + new: /ko/authpay/guide?v=v1#complete +- old: /ko/v2-payment/authpay + new: /ko/authpay/guide?v=v2 + # 신규 API 문서 - old: /ko/api/api new: https://developers.portone.io/api/rest-v1 diff --git a/src/content/docs/ko/_nav.yaml b/src/content/docs/ko/_nav.yaml index b086398a2..c12029229 100644 --- a/src/content/docs/ko/_nav.yaml +++ b/src/content/docs/ko/_nav.yaml @@ -6,21 +6,7 @@ - label: 결제 연동 systemVersion: v1 items: - - slug: /ko/auth/guide/readme - items: - - /ko/auth/guide/def - - /ko/auth/guide/1 - - /ko/auth/guide/2 - - /ko/auth/guide/3 - - slug: /ko/auth/guide/4/readme - items: - - /ko/auth/guide/4/iframe - - /ko/auth/guide/4/redirect - - slug: /ko/auth/guide/5/readme - items: - - /ko/auth/guide/5/pre - - /ko/auth/guide/5/post - - /ko/auth/guide/6 + - slug: /ko/authpay/guide - slug: /ko/auth/guide-1/readme items: - slug: /ko/auth/guide-1/bill/readme @@ -303,7 +289,7 @@ systemVersion: v2 items: - /ko/v2-payment/v2 - - /ko/v2-payment/authpay + - /ko/authpay/guide - /ko/v2-payment/key-in - slug: /ko/v2-payment/billing-key/readme items: diff --git a/src/content/docs/ko/authpay/_assets/authpay-example.png b/src/content/docs/ko/authpay/_assets/authpay-example.png new file mode 100644 index 000000000..20912b465 Binary files /dev/null and b/src/content/docs/ko/authpay/_assets/authpay-example.png differ diff --git a/src/content/docs/ko/authpay/_assets/authpay-flow.png b/src/content/docs/ko/authpay/_assets/authpay-flow.png new file mode 100644 index 000000000..63d3db63b Binary files /dev/null and b/src/content/docs/ko/authpay/_assets/authpay-flow.png differ diff --git a/src/content/docs/ko/authpay/_components/v2-sdk-installation.mdx b/src/content/docs/ko/authpay/_components/v2-sdk-installation.mdx new file mode 100644 index 000000000..6dc106a06 --- /dev/null +++ b/src/content/docs/ko/authpay/_components/v2-sdk-installation.mdx @@ -0,0 +1,61 @@ +import Tab from "~/components/gitbook/tabs/Tab.astro"; +import Tabs from "~/components/gitbook/tabs/Tabs.astro"; +import Hint from "~/components/Hint.astro"; +import * as prose from '~/components/prose'; + +export const components = prose; + +포트원 V2 SDK는 npm 레지스트리와 CDN을 통해 배포되고 있습니다. + +- npm, yarn 등 패키지 매니저를 사용한다면 의존 + 대상으로 [@portone/browser-sdk](https://www.npmjs.com/package/@portone/browser-sdk)를 추가하세요. + +- 패키지 매니저를 사용하지 않는다면 ` + ``` + + ` + ``` + + + + + + + +### 2. 결제 요청하기 + + + + #### SDK 초기화하기 + + 포트원 SDK를 사용하여 결제창을 호출하려면, 먼저 포트원 SDK를 초기화하여야 합니다. + + 먼저, 관리자 콘솔의 결제 연동 페이지에서 **고객사 식별코드**를 확인해 주세요. + + 그리고 결제창을 호출할 페이지에서 다음과 같이 포트원 SDK를 초기화합니다. + + + 아래 초기화 함수를 2회 이상 중복 호출하지 않도록 주의해 주세요. + + + ```ts + IMP.init("고객사 식별코드"); // 예: 'imp00000000' + ``` + + ##### 하위 상점에서 SDK 초기화하기 + + 하위 상점에서 SDK를 초기화하려면, `IMP.init()` 함수 대신 `IMP.agency()` 함수를 사용합니다. + + ```ts + IMP.agency("고객사 식별코드", "티어코드"); // 예: 'imp00000000', '123' + ``` + +
+ #### 결제창 불러오기 +
+ + SDK의 `IMP.request_pay()` 함수를 호출하여 결제 수단에 따른 결제창을 열 수 있습니다. + + 아래와 같이 [결제 요청 파라미터](../sdk/javascript-sdk/payrq)를 `request_pay()` 함수의 + 첫 인자로 설정하여 호출합니다. + + ```ts + IMP.request_pay( + { + pg: "{PG사 코드}.{상점 ID}", + pay_method: "card", + merchant_uid: `payment-${crypto.randomUUID()}`, // 주문 고유 번호 + name: "노르웨이 회전 의자", + amount: 64900, + buyer_email: "gildong@gmail.com", + buyer_name: "홍길동", + buyer_tel: "010-4242-4242", + buyer_addr: "서울특별시 강남구 신사동", + buyer_postcode: "01181", + }, + function (response) { + // 결제 종료 시 호출되는 콜백 함수 + // response.imp_uid 값으로 결제 단건조회 API를 호출하여 결제 결과를 확인하고, + // 결제 결과를 처리하는 로직을 작성합니다. + }, + ); + ``` + + +
+ + + SDK의 `PortOne.requestPayment()` 함수를 호출하여 결제 수단에 따른 결제창을 열 수 있습니다. + + 먼저, 관리자 콘솔의 결제 연동 페이지에서 **Store ID**와 사용할 채널의 **채널 키**를 확인해 주세요. + + 그리고 아래와 같이 [결제 요청 파라미터](../v2-payment/v2-sdk/payment-request)를 + `requestPayment()` 함수의 첫 인자로 설정하여 호출합니다. + + ```ts + const response = await PortOne.requestPayment({ + // Store ID 설정 + storeId: "store-4ff4af41-85e3-4559-8eb8-0d08a2c6ceec", + // 채널 키 설정 + channelKey: "channel-key-893597d6-e62d-410f-83f9-119f530b4b11", + paymentId: `payment-${crypto.randomUUID()}`, + orderName: "나이키 와플 트레이너 2 SD", + totalAmount: 1000, + currency: "CURRENCY_KRW", + payMethod: "CARD", + }); + ``` + +
+ + + + + **주문 고유 번호(`merchant_uid`) 관련 유의사항** + + + + **주문 고유 번호(`paymentId`) 관련 유의사항** + + + + - 주문 고유 번호는 개별 결제 요청을 구분하기 위해 사용되는 문자열입니다. + + - 따라서 주문 고유 번호는 결제 요청 시 항상 **고유한 값**으로 채번되어야 하며, + 결제 완료 이후 결제 기록 조회나 위변조 대사 작업 시 사용되기 때문에 + 고객사 **DB 상에 별도로 저장**해야 합니다. + + +### 3. 결제 결과 처리하기 + +결제창이 활성화되는 방식에 따라 결제 결과를 획득하는 방법이 상이합니다. + +일반적으로 PC 환경에서는 iframe 또는 팝업 방식으로 페이지 이동 없이 결제창이 활성화되며, +따라서 SDK의 반환값을 통해서 결제 결과를 받아 볼 수 있습니다. +반면, 모바일 환경에서는 일반적으로 새로운 페이지로 리다이렉트되는 방식으로 결제창이 활성화되고, +SDK의 반환값 대신 URL의 [쿼리 문자열](https://en.wikipedia.org/wiki/Query_string) 형태로 +결제 결과를 받아볼 수 있습니다. + + + 결제창이 활성화되는 방식은 `windowType` 파라미터를 통해 명시적으로 설정할 수 있습니다. + + +#### SDK 반환값으로 처리하기 + + + + **`request_pay()`** 함수의 두 번째 인자인 **callback** 함수를 통해 결제 결과를 확인할 수 있습니다. + + ```ts + IMP.request_pay( + { + /* 파라미터 생략 */ + }, + async (response) => { + if (response.error_code != null) { + return alert(`결제에 실패하였습니다. 에러 내용: ${response.error_msg}`); + } + + // 고객사 서버에서 /payment/complete 엔드포인트를 구현해야 합니다. + // (다음 목차에서 설명합니다) + const notified = await fetch(`${SERVER_BASE_URL}/payment/complete`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + // imp_uid와 merchant_uid, 주문 정보를 서버에 전달합니다 + body: JSON.stringify({ + imp_uid: response.imp_uid, + merchant_uid: response.merchant_uid, + // 주문 정보... + }), + }); + }, + ); + ``` + + 결제가 완료되면 반환되는 응답 객체([response](../sdk/javascript-sdk-old/readme))의 + 에러 여부에 따라 처리 로직을 콜백 함수에 작성합니다. + 요청이 성공했을 경우에 결제번호(`imp_uid`)와 주문번호(`merchant_uid`)를 + 서버에 전달하는 로직을 위와 같이 작성합니다. + + + 최종 결제 결과 처리는 반드시 [웹훅](../result/webhook)을 이용하여 + 안정적으로 처리해 주셔야 합니다. + + 웹훅 연동을 생략하시는 경우 결제 결과를 정상적으로 수신받지 못하는 상황이 발생합니다. + + + + + `PortOne.requestPayment()` 함수의 반환값을 통해 결제 요청의 결과를 확인할 수 있습니다. + + `code != null`이면 결제 과정에서 오류가 발생한 것이므로 적절히 처리하여야 합니다. + + 결제가 성공한 경우 `paymentId`를 서버에 전달하여 서버 측에서 결제 완료 처리를 진행하도록 합니다. + (가상 계좌 결제의 경우 결제가 아직 완료되지 않은 상태일 수 있습니다) + + ```ts + async function requestPayment() { + const response = await PortOne.requestPayment({ + /* 파라미터 생략 */ + }); + + if (response.code != null) { + // 오류 발생 + return alert(response.message); + } + + // 고객사 서버에서 /payment/complete 엔드포인트를 구현해야 합니다. + // (다음 목차에서 설명합니다) + const notified = await fetch(`${SERVER_BASE_URL}/payment/complete`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + // paymentId와 주문 정보를 서버에 전달합니다 + body: JSON.stringify({ + paymentId: paymentId, + // 주문 정보... + }), + }); + } + ``` + + 결과값에 들어 있는 필드는 다음과 같습니다. + + |필드명 |설명 |비고 | + |-----------|----------|------------| + |`paymentId`|결제 건 ID|공통 | + |`code` |오류 코드 |실패 시 포함| + |`message` |오류 문구 |실패 시 포함| + + + +#### URL 쿼리 문자열로 처리하기 + +모바일 환경에서의 결제는 대부분 리다이렉트 방식으로 이루어집니다. +리다이렉트 방식에서는 브라우저가 결제창으로 리다이렉트되었다가, +결제창에서의 작업이 끝나면 지정한 +`m_redirect_url``redirectUrl`로 +다시 리다이렉트됩니다. +이 경우에는 함수 호출 결과를 이용할 수 없고, +결제 성공 여부 등은 [쿼리 문자열](https://en.wikipedia.org/wiki/Query_string)로 전달받게 됩니다. + + + + ```ts + IMP.request_pay({ + /* 기타 파라미터 생략 */ + m_redirect_url: `${BASE_URL}/payment-redirect`, + }); // 리다이렉트 방식의 경우 콜백은 실행되지 않습니다. + ``` + + + + ```ts + PortOne.requestPayment({ + /* 기타 파라미터 생략 */ + redirectUrl: `${BASE_URL}/payment-redirect`, + }); + ``` + + + +쿼리 문자열로 전달되는 내용은 다음과 같습니다. + + + + |키 |설명 |비고 | + |--------------|---------------------|------------| + |`imp_uid` |포트원 결제 ID |공통 | + |`merchant_uid`|고객사 주문 고유 번호|공통 | + |`error_code` |오류 코드 |실패 시 포함| + |`error_msg` |오류 문구 |실패 시 포함| + + 예를 들어 `merchant_uid`가 `payment-39ecfa97`, `m_redirect_url`이 `https://example.com/payment-redirect`인 경우, + 결제 성공 시에 `https://example.com/payment-redirect?merchant_uid=payment-39ecfa97`로 리다이렉트됩니다. + + + + |키 |설명 |비고 | + |------------|----------|------------| + |`payment_id`|결제 건 ID|공통 | + |`code` |오류 코드 |실패 시 포함| + |`message` |오류 문구 |실패 시 포함| + + 예를 들어 `paymentId`가 `payment-39ecfa97`, `redirectUrl`이 `https://example.com/payment-redirect`인 경우, + 결제 성공 시에 `https://example.com/payment-redirect?payment_id=payment-39ecfa97`로 리다이렉트됩니다. + + + +### 4. 결제 완료 처리하기 + + + + `imp_uid`와 `merchant_uid`를 서버에 전달하면, 서버는 포트원의 [결제 조회 API](/api/rest-v1/payment#get%20%2Fpayments%2F%7Bimp_uid%7D)를 + 호출하여 해당 결제 건의 상태를 확인하고 결제 완료 처리를 진행하여야 합니다. + + + + `paymentId`를 서버에 전달하면, 서버는 포트원의 [결제 조회 API](/api/rest-v2/payment#get%20%2Fpayments%2F%7BpaymentId%7D)를 + 호출하여 해당 결제 건의 상태를 확인하고 결제 완료 처리를 진행하여야 합니다. + + + + + **결제 검증 필수** + + 인증 결제의 흐름상 결제 금액 등 정보가 고객의 브라우저 측에서 처리되므로, + 의도한 결제 내용이 맞는지 서버 측에서 꼭 확인하여야 위변조를 막을 수 있습니다. + + +예시로, 위에서 사용했던 `/payment/complete` 엔드포인트를 다음과 같이 구현할 수 있습니다. + + + + ```ts title="Express" + // JSON 요청을 처리하기 위해 body-parser 미들웨어 세팅 + app.use(bodyParser.json()); + + // POST 요청을 받는 /payments/complete + app.post("/payment/complete", async (req, res) => { + try { + // 요청의 body로 imp_uid와 merchant_uid가 전달되기를 기대합니다. + const { imp_uid, merchant_uid } = req.body; + + // 1. 포트원 API 엑세스 토큰 발급 + const tokenResponse = await fetch("https://api.iamport.kr/users/getToken", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + imp_key: "imp_apikey", // REST API 키 + imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW", // REST API Secret + }), + }); + if (!tokenResponse.ok) + throw new Error(`tokenResponse: ${tokenResponse.statusText}`); + const { access_token } = await tokenResponse.json(); + + // 2. 포트원 결제내역 단건조회 API 호출 + const paymentResponse = await fetch( + `https://api.iamport.kr/payments/${imp_uid}`, + { headers: { Authorization: access_token } }, + ); + if (!paymentResponse.ok) + throw new Error(`paymentResponse: ${paymentResponse.statusText}`); + const payment = await paymentResponse.json(); + + // 3. 고객사 내부 주문 데이터의 가격과 실제 지불된 금액을 비교합니다. + const order = await OrderService.findById(merchant_uid); + if (order.amount === payment.amount) { + switch (payment.status) { + case "ready": { + // 가상 계좌가 발급된 상태입니다. + // 계좌 정보를 이용해 원하는 로직을 구성하세요. + break; + } + case "paid": { + // 모든 금액을 지불했습니다! 완료 시 원하는 로직을 구성하세요. + break; + } + } + } else { + // 결제 금액이 불일치하여 위/변조 시도가 의심됩니다. + } + } catch (e) { + // 결제 검증에 실패했습니다. + res.status(400).send(e); + } + }); + ``` + + + + [PORTONE\_API\_SECRET](/docs/ko/ready/readme?v=v2#4-2-v2-api-secret-%EB%B0%9C%EA%B8%89%ED%95%98%EA%B8%B0) + 은 V2 전용 시크릿으로, 포트원 콘솔 내 결제연동 탭에서 발급받을 수 있습니다. + + ```ts title="Express" + // JSON 요청을 처리하기 위해 body-parser 미들웨어 세팅 + app.use(bodyParser.json()); + + // POST 요청을 받는 /payments/complete + app.post("/payment/complete", async (req, res) => { + try { + // 요청의 body로 paymentId가 전달되기를 기대합니다. + const { paymentId, orderId } = req.body; + + // 1. 포트원 결제내역 단건조회 API 호출 + const paymentResponse = await fetch( + `https://api.portone.io/payments/${paymentId}`, + { headers: { Authorization: `PortOne ${PORTONE_API_SECRET}` } }, + ); + if (!paymentResponse.ok) + throw new Error(`paymentResponse: ${paymentResponse.statusText}`); + const payment = await paymentResponse.json(); + + // 2. 고객사 내부 주문 데이터의 가격과 실제 지불된 금액을 비교합니다. + const order = await OrderService.findById(orderId); + if (order.amount === payment.amount.total) { + switch (payment.status) { + case "VIRTUAL_ACCOUNT_ISSUED": { + // 가상 계좌가 발급된 상태입니다. + // 계좌 정보를 이용해 원하는 로직을 구성하세요. + break; + } + case "PAID": { + // 모든 금액을 지불했습니다! 완료 시 원하는 로직을 구성하세요. + break; + } + } + } else { + // 결제 금액이 불일치하여 위/변조 시도가 의심됩니다. + } + } catch (e) { + // 결제 검증에 실패했습니다. + res.status(400).send(e); + } + }); + ``` + +