> 토스 페이먼츠 해보기
@tosspayments/tosspayments-sdk 설치
- promote-page.tsx
import { Hero } from "~/common/components/hero";
import { Route } from "./+types/promote-page";
import SelectPair from "~/common/components/select-pair";
import { Calendar } from "~/common/components/ui/calendar";
import { useEffect, useRef, useState } from "react";
import { Label } from "~/common/components/ui/label";
import { DateRange } from "react-day-picker";
import { DateTime } from "luxon";
import { Button } from "~/common/components/ui/button";
import {
loadTossPayments,
TossPaymentsWidgets,
} from "@tosspayments/tosspayments-sdk";
export const meta: Route.MetaFunction = () => {
return [
{ title: "Promote Product | ProductHunt Clone" },
{ name: "description", content: "Promote your product" },
];
};
export default function PromotePage() {
const [promotionPeriod, setPromotionPeriod] = useState<
DateRange | undefined
>();
const totalDays =
promotionPeriod?.from && promotionPeriod.to
? DateTime.fromJSDate(promotionPeriod.to).diff(
DateTime.fromJSDate(promotionPeriod.from),
"days"
).days
: 0;
const widgets = useRef<TossPaymentsWidgets | null>(null);
const initedToss = useRef<boolean>(false);
useEffect(() => {
const initToss = async () => {
if (initedToss.current) return;
initedToss.current = true;
const toss = await loadTossPayments(
"test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm"
);
widgets.current = await toss.widgets({
customerKey: "1111111",
});
await widgets.current.setAmount({
value: 0,
currency: "KRW",
});
await widgets.current.renderPaymentMethods({
selector: "#toss-payment-methods",
});
await widgets.current.renderAgreement({
selector: "#toss-payment-agreement",
});
};
initToss();
}, []);
useEffect(() => {
const updateAmount = async () => {
if (widgets.current) {
await widgets.current.setAmount({
value: totalDays * 20000,
currency: "KRW",
});
}
};
updateAmount();
}, [promotionPeriod]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const product = formData.get("product") as string;
if (!product || !promotionPeriod?.to || !promotionPeriod?.from) return;
await widgets.current?.requestPayment({
orderId: crypto.randomUUID(),
orderName: `WeMake Promotion`,
customerEmail: "nico@nomadcoders.co",
customerName: "Nico",
customerMobilePhone: "01012345678",
metadata: {
product,
promotionFrom: DateTime.fromJSDate(promotionPeriod.from).toISO(),
promotionTo: DateTime.fromJSDate(promotionPeriod.to).toISO(),
},
successUrl: `${window.location.href}/success`,
failUrl: `${window.location.href}/fail`,
});
};
return (
<div>
<Hero
title="Promote Your Product"
subtitle="Boost your product's visibility."
/>
<form onSubmit={handleSubmit} className="grid grid-cols-6 gap-10">
<div className="col-span-3 mx-auto w-1/2 flex flex-col gap-10 items-start">
<SelectPair
required
label="Select a product"
description="Select the product you want to promote."
name="product"
placeholder="Select a product"
options={[
{
label: "AI Dark Mode Maker",
value: "ai-dark-mode-maker",
},
]}
/>
<div className="flex flex-col gap-2 items-center w-full">
<Label className="flex flex-col gap-1">
Select a range of dates for promotion{" "}
<small className="text-muted-foreground text-center ">
Minimum duration is 3 days.
</small>
</Label>
<Calendar
mode="range"
selected={promotionPeriod}
onSelect={setPromotionPeriod}
min={3}
disabled={{ before: new Date() }}
/>
</div>
</div>
<aside className="col-span-3 px-20 flex flex-col items-center">
<div id="toss-payment-methods" className="w-full" />
<div id="toss-payment-agreement" />
<Button className="w-full" disabled={totalDays === 0}>
Checkout (
{(totalDays * 20000).toLocaleString("ko-KR", {
style: "currency",
currency: "KRW",
})}
)
</Button>
</aside>
</form>
</div>
);
}
- promote-success-page.tsx
import { z } from "zod";
import type { Route } from "./+types/promote-success-page";
const paramsSchema = z.object({
paymentType: z.string(),
orderId: z.string().uuid(),
paymentKey: z.string(),
amount: z.coerce.number(),
});
const TOSS_SECRET_KEY = "test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6";
export const loader = async ({ request }: Route.LoaderArgs) => {
const url = new URL(request.url);
const { success, data } = paramsSchema.safeParse(
Object.fromEntries(url.searchParams)
);
if (!success) {
return new Response(null, { status: 400 });
}
const encryptedSecretKey = `Basic ${Buffer.from(
TOSS_SECRET_KEY + ":"
).toString("base64")}`;
const response = await fetch(
"https://api.tosspayments.com/v1/payments/confirm",
{
method: "POST",
headers: {
Authorization: encryptedSecretKey,
"Content-Type": "application/json",
},
body: JSON.stringify({
orderId: data.orderId,
paymentKey: data.paymentKey,
amount: data.amount,
}),
}
);
const responseData = await response.json();
return Response.json(responseData);
};
결제에 성공 하면 아래와 같은 json 값을 받을 수 있는데, 특정 데이터들을 db에 테이블을 새로 만들어서 값을 저장해주면 된다.
(결제 내역 확인 하는 부분에 쓰면 될듯)
근데 이렇게 하면 예상치 못한 문제로 /success url로 이동못하는 경우에는 결제는 됐는데, 해당 데이터를 못받을 수가 있다. 이럴 때는 토스 관리자 계정에서 웹훅?을 같이 하라는데.. 해보진 않아서 정확히 무슨 의미인지는 지금 단계에서는 잘 모르겠다. 나중에 실제 결제모듈 연동할때 확인해보자.

'코딩강의 > Maker 마스터클래스(노마드코더)' 카테고리의 다른 글
| #14 Deployment (6) | 2025.05.30 |
|---|---|
| #12 Transactional Emails (0) | 2025.05.27 |
| #11 GPT & CRON Jobs (0) | 2025.05.23 |
| #10 DMs (0) | 2025.05.21 |
| #9 Fetchers (0) | 2025.05.14 |