diff --git a/src/components/interactive-docs/PgSelect.tsx b/src/components/interactive-docs/PgSelect.tsx index e896881c7..b417283fc 100644 --- a/src/components/interactive-docs/PgSelect.tsx +++ b/src/components/interactive-docs/PgSelect.tsx @@ -10,7 +10,7 @@ import kpnLogo from "~/assets/pg-circle/kpn.png"; import ksnetLogo from "~/assets/pg-circle/ksnet.png"; import naverLogo from "~/assets/pg-circle/naver.png"; import niceLogo from "~/assets/pg-circle/nice.png"; -import smatroLogo from "~/assets/pg-circle/smartro.png"; +import smartro from "~/assets/pg-circle/smartro.png"; import tossLogo from "~/assets/pg-circle/toss.png"; import { type Pg, useInteractiveDocs } from "~/state/interactive-docs"; @@ -21,7 +21,7 @@ export type PgSelectOption = { const PgOptions = { nice: { label: "나이스페이먼츠", icon: niceLogo }, - smatro: { label: "스마트로", icon: smatroLogo }, + smartro: { label: "스마트로", icon: smartro }, toss: { label: "토스페이먼츠", icon: tossLogo }, kpn: { label: "한국결제네트웍스", icon: kpnLogo }, inicis: { label: "이니시스", icon: inicisLogo }, diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/Preview.tsx b/src/routes/(root)/opi/ko/quick-guide/_preview/Preview.tsx index cf66c7600..ef19bdc54 100644 --- a/src/routes/(root)/opi/ko/quick-guide/_preview/Preview.tsx +++ b/src/routes/(root)/opi/ko/quick-guide/_preview/Preview.tsx @@ -44,15 +44,19 @@ export function Preview() { const requestPayment = async () => { if (paymentStatus().status === "PENDING") return undefined; + const paymentId = crypto + .getRandomValues(new Uint32Array(1))[0]! + .toString(16) + .padStart(8, "0"); const request = match(untrack(() => params)) .with( { pg: { name: "toss", payMethods: "card" } }, () => ({ storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", - paymentId: crypto.randomUUID(), - orderName: "테스트 결제", - totalAmount: 100, + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, currency: "CURRENCY_KRW", channelKey: "channel-key-ebe7daa6-4fe4-41bd-b17d-3495264399b5", payMethod: "CARD", @@ -64,9 +68,9 @@ export function Preview() { () => ({ storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", - paymentId: crypto.randomUUID(), - orderName: "테스트 결제", - totalAmount: 100, + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, currency: "CURRENCY_KRW", channelKey: "channel-key-ebe7daa6-4fe4-41bd-b17d-3495264399b5", payMethod: "VIRTUAL_ACCOUNT", @@ -82,9 +86,9 @@ export function Preview() { () => ({ storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", - paymentId: crypto.randomUUID(), - orderName: "테스트 결제", - totalAmount: 100, + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, currency: "CURRENCY_KRW", channelKey: "channel-key-4ca6a942-3ee0-48fb-93ef-f4294b876d28", payMethod: "CARD", @@ -97,9 +101,9 @@ export function Preview() { () => ({ storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", - paymentId: crypto.randomUUID(), - orderName: "테스트 결제", - totalAmount: 100, + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, currency: "CURRENCY_KRW", channelKey: "channel-key-e6c31df1-5559-4b4a-9b2c-a35793d14db2", payMethod: "VIRTUAL_ACCOUNT", @@ -111,6 +115,198 @@ export function Preview() { redirectUrl: "https://sdk-playground.portone.io/", }) satisfies PortOne.PaymentRequest, ) + .with( + { pg: { name: "smartro", payMethods: "card" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-c4a4b281-a1e5-40c9-8140-f055262bcefd", + payMethod: "CARD", + card: {}, + redirectUrl: "https://sdk-playground.portone.io/", + customer: { + phoneNumber: "01012341234", + }, + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "smartro", payMethods: "virtualAccount" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-c4a4b281-a1e5-40c9-8140-f055262bcefd", + payMethod: "VIRTUAL_ACCOUNT", + virtualAccount: { + accountExpiry: { + validHours: 1, + }, + }, + customer: { + phoneNumber: "01012341234", + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "inicis", payMethods: "card" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-fc5f33bb-c51e-4ac7-a0df-4dc40330046d", + payMethod: "CARD", + card: {}, + customer: { + fullName: "포트원", + email: "example@portone.io", + phoneNumber: "01012341234", + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "inicis", payMethods: "virtualAccount" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-fc5f33bb-c51e-4ac7-a0df-4dc40330046d", + payMethod: "VIRTUAL_ACCOUNT", + virtualAccount: { + accountExpiry: { + validHours: 1, + }, + }, + customer: { + fullName: "포트원", + email: "example@portone.io", + phoneNumber: "01012341234", + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "kcp", payMethods: "card" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-a79920e0-a898-49f0-aab7-50aa6834848f", + payMethod: "CARD", + card: {}, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "kcp", payMethods: "virtualAccount" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-a79920e0-a898-49f0-aab7-50aa6834848f", + payMethod: "VIRTUAL_ACCOUNT", + virtualAccount: { + accountExpiry: { + validHours: 1, + }, + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "kpn", payMethods: "card" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-bcbb1622-ff80-49d5-adef-49191fda8ede", + payMethod: "CARD", + card: {}, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "kpn", payMethods: "virtualAccount" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-bcbb1622-ff80-49d5-adef-49191fda8ede", + payMethod: "VIRTUAL_ACCOUNT", + virtualAccount: { + accountExpiry: { + validHours: 1, + }, + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "ksnet", payMethods: "card" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-4a5daa34-aecb-44af-aaad-e42384acfb6e", + payMethod: "CARD", + card: {}, + customer: { + fullName: "포트원", + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) + .with( + { pg: { name: "ksnet", payMethods: "virtualAccount" } }, + () => + ({ + storeId: "store-e4038486-8d83-41a5-acf1-844a009e0d94", + paymentId, + orderName: "나이키 멘즈 조이라이드 플라이니트", + totalAmount: 1000, + currency: "CURRENCY_KRW", + channelKey: "channel-key-4a5daa34-aecb-44af-aaad-e42384acfb6e", + payMethod: "VIRTUAL_ACCOUNT", + virtualAccount: { + accountExpiry: { + validHours: 1, + }, + }, + customer: { + fullName: "포트원", + }, + redirectUrl: "https://sdk-playground.portone.io/", + }) satisfies PortOne.PaymentRequest, + ) .exhaustive(); setPaymentStatus({ status: "PENDING" }); @@ -123,7 +319,7 @@ export function Preview() { .with({ code: P.nonNullable }, () => { setPaymentStatus({ status: "FAILED" }); }) - .with({ code: P.nullish }, () => { + .with(P.not({ code: P.nonNullable }), () => { setPaymentStatus({ status: "PAID" }); }) .exhaustive(), diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/backend/node/index.ts b/src/routes/(root)/opi/ko/quick-guide/_preview/backend/express/index.ts similarity index 100% rename from src/routes/(root)/opi/ko/quick-guide/_preview/backend/node/index.ts rename to src/routes/(root)/opi/ko/quick-guide/_preview/backend/express/index.ts diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/backend/node/server.js.ts b/src/routes/(root)/opi/ko/quick-guide/_preview/backend/express/server.js.ts similarity index 77% rename from src/routes/(root)/opi/ko/quick-guide/_preview/backend/node/server.js.ts rename to src/routes/(root)/opi/ko/quick-guide/_preview/backend/express/server.js.ts index 95329e68a..a064dad75 100644 --- a/src/routes/(root)/opi/ko/quick-guide/_preview/backend/node/server.js.ts +++ b/src/routes/(root)/opi/ko/quick-guide/_preview/backend/express/server.js.ts @@ -12,8 +12,9 @@ ${({ section }) => section("server:import-portone-sdk")` const PortOne = require("@portone/server-sdk") `} -const portOne = PortOne.PortOneClient(process.env.V2_API_SECRET) +const portone = PortOne.PortOneClient(process.env.V2_API_SECRET) +${({ section }) => section("server:complete-payment:verify-payment")` function verifyPayment(payment) { if (payment.customData == null) return false const customData = JSON.parse(payment.customData) @@ -25,6 +26,7 @@ function verifyPayment(payment) { payment.currency === item.currency ) } +`} const paymentStore = new Map() async function syncPayment(paymentId) { @@ -34,14 +36,16 @@ async function syncPayment(paymentId) { }) } const payment = paymentStore.get(paymentId) + ${({ section }) => section("server:complete-payment:get-payment")` let actualPayment try { - actualPayment = await portOne.payment.getPayment(paymentId) + actualPayment = await portone.payment.getPayment(paymentId) } catch (e) { if (e instanceof PortOne.Errors.PortOneError) return false throw e } if (actualPayment == null) return false + `} switch (actualPayment.status) { case "PAID": if (!verifyPayment(actualPayment)) return false @@ -49,9 +53,11 @@ async function syncPayment(paymentId) { payment.status = "PAID" console.info("결제 성공", actualPayment) break + ${({ when }) => when(({ pg }) => pg.payMethods === "virtualAccount")` case "VIRTUAL_ACCOUNT_ISSUED": payment.status = "VIRTUAL_ACCOUNT_ISSUED" break + `} default: return false } @@ -60,12 +66,14 @@ async function syncPayment(paymentId) { const app = express() +${({ section }) => section("server:webhook:raw-body")` app.use( "/api/payment/webhook", bodyParser.text({ type: "application/json", }), ) +`} app.use(bodyParser.json()) const items = new Map([ @@ -87,6 +95,7 @@ app.get("/api/item", (req, res) => { }) }) +${({ section }) => section("server:complete-payment")` app.post("/api/payment/complete", async (req, res, next) => { try { const { paymentId } = req.body @@ -101,11 +110,15 @@ app.post("/api/payment/complete", async (req, res, next) => { next(e) } }) +`} +${({ section }) => section("server:webhook")` app.post("/api/payment/webhook", async (req, res, next) => { try { + ${({ section }) => section("server:webhook:verify")` + let webhook try { - await PortOne.Webhook.verify( + webhook = await PortOne.Webhook.verify( process.env.V2_WEBHOOK_SECRET, req.body, req.headers, @@ -115,16 +128,17 @@ app.post("/api/payment/webhook", async (req, res, next) => { return res.status(400).end() throw e } - const { - type, - data: { paymentId }, - } = JSON.parse(req.body) - if (type.startsWith("Transaction.")) await syncPayment(paymentId) + `} + ${({ section }) => section("server:webhook:complete-payment")` + if ('data' in webhook && 'paymentId' in webhook.data) + await syncPayment(webhook.data.paymentId) + `} res.status(200).end() } catch (e) { next(e) } }) +`} const server = app.listen(8080, "localhost", () => { console.log("server is running on", server.address()) diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/backend/index.ts b/src/routes/(root)/opi/ko/quick-guide/_preview/backend/index.ts index e341544e3..2652fccdd 100644 --- a/src/routes/(root)/opi/ko/quick-guide/_preview/backend/index.ts +++ b/src/routes/(root)/opi/ko/quick-guide/_preview/backend/index.ts @@ -1 +1 @@ -export { files as node } from "./node"; +export { files as express } from "./express"; diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/frontend/react/app.jsx.ts b/src/routes/(root)/opi/ko/quick-guide/_preview/frontend/react/app.jsx.ts index abf890b65..16d89bdc9 100644 --- a/src/routes/(root)/opi/ko/quick-guide/_preview/frontend/react/app.jsx.ts +++ b/src/routes/(root)/opi/ko/quick-guide/_preview/frontend/react/app.jsx.ts @@ -1,7 +1,28 @@ import { code } from "~/components/interactive-docs/index.jsx"; +import type { Pg } from "~/state/interactive-docs"; import type { Params, Sections } from "../../type"; +function isCustomerRequired(params: Params) { + return ( + isCustomerPhoneNumberRequired(params) || isCustomerEmailRequired(params) + ); +} + +function isCustomerNameRequired(params: Params) { + return (["ksnet", "inicis"] satisfies Pg[] as Pg[]).includes(params.pg.name); +} + +function isCustomerPhoneNumberRequired(params: Params) { + return (["smartro", "inicis"] satisfies Pg[] as Pg[]).includes( + params.pg.name, + ); +} + +function isCustomerEmailRequired(params: Params) { + return (["inicis"] satisfies Pg[] as Pg[]).includes(params.pg.name); +} + export default code<{ params: Params; sections: Sections; @@ -24,6 +45,7 @@ export function App() { status: "IDLE", }) + ${({ section }) => section("client:fetch-item")` useEffect(() => { async function loadItem() { const response = await fetch("/api/item") @@ -32,6 +54,7 @@ export function App() { loadItem().catch((error) => console.error(error)) }, []) + `} if (item == null) { return ( @@ -45,19 +68,12 @@ export function App() { e.preventDefault() setPaymentStatus({ status: "PENDING" }) ${({ section }) => section("client:request-payment")` - ${({ section }) => section("client:payment-id-description")` + ${({ section }) => section("client:payment-id")` const paymentId = randomId() `} const payment = await PortOne.requestPayment({ storeId: VITE_STORE_ID, - ${({ when }) => when(({ smartRouting }) => smartRouting === false)` channelKey: VITE_CHANNEL_KEY, - `} - ${({ when }) => when(({ smartRouting }) => smartRouting === true)` - ${({ section }) => section("client:smart-routing:channel-group-id")` - channelGroupId: VITE_CHANNEL_GROUP_ID, - `} - `} paymentId, orderName: item.name, totalAmount: item.price, @@ -68,13 +84,30 @@ export function App() { ${({ when }) => when(({ pg }) => pg.payMethods === "virtualAccount")` payMethod: "VIRTUAL_ACCOUNT", `} + ${({ when }) => when(isCustomerRequired)` + ${({ section }) => section("client:customer-data")` + customer: { + ${({ when }) => when(isCustomerNameRequired)` + fullName: '포트원', + `} + ${({ when }) => when(isCustomerPhoneNumberRequired)` + phoneNumber: '01012341234', + `} + ${({ when }) => when(isCustomerEmailRequired)` + email: 'example@portone.io', + `} + }, + `} + `} + ${({ section }) => section("client:custom-data")` customData: { item: item.id, }, + `} }) `} ${({ section }) => section("client:handle-payment-error")` - if (payment.code != null) { + if (payment.code !== undefined) { setPaymentStatus({ status: "FAILED", message: payment.message, @@ -93,15 +126,19 @@ export function App() { }), }) if (completeResponse.ok) { + ${({ section }) => section("client:handle-payment-status:paid")` const paymentComplete = await completeResponse.json() setPaymentStatus({ status: paymentComplete.status, }) + `} } else { + ${({ section }) => section("client:handle-payment-status:failed")` setPaymentStatus({ status: "FAILED", message: await completeResponse.text(), }) + `} } `} } @@ -141,7 +178,6 @@ export function App() { - ${({ section }) => section("client:handle-payment-status:failed")` {paymentStatus.status === "FAILED" && (
@@ -153,8 +189,6 @@ export function App() {
)} - `} - ${({ section }) => section("client:handle-payment-status:paid")`

결제 성공

@@ -164,10 +198,10 @@ export function App() { 닫기
- `} - ${({ section }) => section( - "client:handle-payment-status:virtual-account-issued", - )` + ${({ when }) => when(({ pg }) => pg.payMethods === "virtualAccount")` + ${({ section }) => section( + "client:handle-payment-status:virtual-account-issued", + )`

가상계좌 발급 완료

@@ -176,7 +210,8 @@ export function App() { -
+ + `} `} ) diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/index.tsx b/src/routes/(root)/opi/ko/quick-guide/_preview/index.tsx index 3998b5c3c..47bc54ec0 100644 --- a/src/routes/(root)/opi/ko/quick-guide/_preview/index.tsx +++ b/src/routes/(root)/opi/ko/quick-guide/_preview/index.tsx @@ -16,6 +16,6 @@ export const { Section, InteractiveDoc, Condition, Toggle } = smartRouting: false, pg: { name: "toss", payMethods: "card" }, }, - initialSelectedExample: ["react", "node"], + initialSelectedExample: ["react", "express"], preview: Preview, }); diff --git a/src/routes/(root)/opi/ko/quick-guide/_preview/type.ts b/src/routes/(root)/opi/ko/quick-guide/_preview/type.ts index 03f00133e..b2905ef86 100644 --- a/src/routes/(root)/opi/ko/quick-guide/_preview/type.ts +++ b/src/routes/(root)/opi/ko/quick-guide/_preview/type.ts @@ -7,6 +7,21 @@ export const pgOptions = { nice: { payMethods: ["card", "virtualAccount"], }, + smartro: { + payMethods: ["card", "virtualAccount"], + }, + kpn: { + payMethods: ["card", "virtualAccount"], + }, + inicis: { + payMethods: ["card", "virtualAccount"], + }, + ksnet: { + payMethods: ["card", "virtualAccount"], + }, + kcp: { + payMethods: ["card", "virtualAccount"], + }, } as const satisfies PgOptions; export type Params = { @@ -16,12 +31,21 @@ export type Params = { export type Sections = | "client:import-portone-sdk" + | "client:fetch-item" | "client:request-payment" - | "client:payment-id-description" + | "client:payment-id" + | "client:customer-data" + | "client:custom-data" | "client:handle-payment-error" | "client:request-server-side-verification" | "client:handle-payment-status:failed" | "client:handle-payment-status:paid" | "client:handle-payment-status:virtual-account-issued" - | "client:smart-routing:channel-group-id" - | "server:import-portone-sdk"; + | "server:import-portone-sdk" + | "server:complete-payment" + | "server:complete-payment:get-payment" + | "server:complete-payment:verify-payment" + | "server:webhook" + | "server:webhook:raw-body" + | "server:webhook:verify" + | "server:webhook:complete-payment"; diff --git a/src/routes/(root)/opi/ko/quick-guide/payment.mdx b/src/routes/(root)/opi/ko/quick-guide/payment.mdx index 208b7da5b..042936edd 100644 --- a/src/routes/(root)/opi/ko/quick-guide/payment.mdx +++ b/src/routes/(root)/opi/ko/quick-guide/payment.mdx @@ -7,7 +7,9 @@ versionVariants: v1: /opi/ko/integration/ready/readme?v=v1 --- -import {Condition, InteractiveDoc, Section, Toggle} from "./_preview"; +import Hint from "~/components/Hint"; + +import { Condition, InteractiveDoc, Section } from "./_preview"; {/* client:import-portone-sdk client:request-payment @@ -20,68 +22,124 @@ client:handle-payment-status:paid */}
- ## PortOne SDK 불러오기 + ## 포트원 브라우저 SDK 불러오기 + + 포트원 브라우저 SDK를 불러옵니다. +
- PortOne SDK를 불러오기 위한 코드입니다. +
+ ## 상품 정보 불러오기 + + 서버로부터 결제할 상품의 정보를 불러옵니다.
## 결제 요청 - 결제 요청을 위한 코드입니다. + 포트원 브라우저 SDK를 사용하여 결제를 요청합니다. +
+ +
+ ## Payment ID 설정 - - smartRouting === true}> -
- ## `channelGroupId` 설정 + Payment ID는 결제 요청 시 사용하는 고유한 ID입니다. 포트원에서 제공하지 않고 수동으로 생성해야 합니다. - 스마트라우팅 기능을 사용하기 위해 `channelGroupId`를 설정하는 코드입니다. -
-
-
+ + 결제대행사에 따라 Payment ID의 형식에 제한이 있을 수 있습니다. +
-
- ## Payment ID 설명 + ['ksnet', 'inicis', 'smartro'].includes(name)}> +
+ ## 고객 정보 설정 - Payment ID는 결제 요청 시 사용하는 고유한 ID입니다. + 결제 시 요구되는 고객 정보를 입력합니다. +
+
+ +
+ ## Custom Data 설정 + + Custom Data에는 임의의 데이터를 저장할 수 있습니다. 서버에서 결제건 조회 시에 확인할 수 있으며, 상품 정보를 전달하여 서버가 인식한 상품 정보와 일치하는지 확인할 수 있습니다.
## 결제 오류 처리 - 결제 오류를 처리하는 코드입니다. + 결제 중 오류가 발생하여 결제가 완료되지 않은 경우를 처리합니다.
- pg.name === "toss" && pg.payMethods === "card"}> -
- ## 토스 카드 결제 조건 테스트 +
+ ## 서버 측으로 결제 완료 요청 - 토스 카드 결제 조건 테스트 -
- + 완료된 결제의 Payment ID를 서버로 전송하여 결제 상태를 반영합니다. +
-
- ## 서버 측 검증 요청 +
+ ## 결제 완료 상태 처리 - 서버 측 검증 요청을 위한 코드입니다. + 서버로부터 검증 후 결제가 완료된 경우를 처리합니다.
## 결제 실패 상태 처리 - 결제 실패 상태를 처리하는 코드입니다. + 서버로부터 검증 결과를 획득하여, 결제가 최종적으로 실패한 경우를 처리합니다.
-
- ## 결제 완료 상태 처리 + pg.payMethods === "virtualAccount" }> +
+ ## 가상계좌 발급 상태 처리 - 결제 완료 상태를 처리하는 코드입니다. -
+ 서버로부터 검증 후 가상계좌가 발급된 경우를 처리합니다. +
+
- ## PortOne 서버 SDK 불러오기 + ## 포트원 서버 SDK 불러오기 + + 포트원 서버 SDK를 불러옵니다. +
+ +
+ ## 결제 완료 요청 + + 완료된 결제의 실제 상태를 조회해 시스템에 반영합니다. 브라우저 SDK를 통해 결제하는 경우 모든 결제 과정이 브라우저에서 진행되므로 결제가 조작되는 것을 막기 위해 서버에서 검증이 필요합니다. +
+ +
+ ## 결제 정보 조회 + + 브라우저에서 전송한 Payment ID를 통해 실제 결제 상태를 조회합니다. +
+ +
+ ## 결제 정보 일치 검증 + + 포트원에 전달한 Custom Data로 조회한 상품 정보와 결제 정보가 일치하는지 검증합니다. +
+ +
+ ## 웹훅 수신 + + 결제 상태의 변화를 실시간으로 확인해야 한다면 웹훅을 사용할 수 있습니다. +
+ +
+ ## HTTP Body 수신 설정 + + 웹훅 내용을 검증하기 위해서는 HTTP Body를 문자열 형태로 수신해야 합니다. +
+ +
+ ## 웹훅 검증 + + 수신한 웹훅이 위조되지 않았는지 포트원 서버 SDK를 사용하여 검증합니다. +
+ +
+ ## 결제 상태 업데이트 - PortOne 서버 SDK를 불러오기 위한 코드입니다. + 검증된 웹훅 결과를 바탕으로 결제 상태를 업데이트합니다.
diff --git a/src/state/interactive-docs/index.tsx b/src/state/interactive-docs/index.tsx index ace9040f6..7ca08dc62 100644 --- a/src/state/interactive-docs/index.tsx +++ b/src/state/interactive-docs/index.tsx @@ -34,7 +34,7 @@ export type Tab = { export type PayMethod = "card" | "virtualAccount"; export type Pg = | "nice" - | "smatro" + | "smartro" | "toss" | "kpn" | "inicis" @@ -108,12 +108,12 @@ const [InteractiveDocsProvider, useInteractiveDocs] = createContextProvider( hybrid: string[]; }>({ frontend: ["react", "html"], - backend: ["node", "python"], + backend: ["express", "fastapi", "flask"], hybrid: ["nextjs"], }); const [selectedLanguage, setSelectedLanguage] = createSignal< [frontend: string, backend: string] | string - >(["react", "node"]); + >(["react", "express"]); createEffect(() => { setSelectedLanguage([languages().frontend[0], languages().backend[0]]); }); @@ -245,11 +245,11 @@ const [InteractiveDocsProvider, useInteractiveDocs] = createContextProvider( setPgOptions: (_) => {}, languages: () => ({ frontend: ["react", "html"], - backend: ["node", "python"], + backend: ["express", "fastapi", "flask"], hybrid: ["nextjs"], }), setLanguages: (_) => {}, - selectedLanguage: () => ["react", "node"], + selectedLanguage: () => ["react", "express"], setSelectedLanguage: (_) => {}, params: { pg: {