npm install @tosspayments/tosspayments-sdk
const clientKey =
process.env.NEXT_PUBLIC_TOSS_CLIENT_KEY ??
"test_ck_DpexMgkW36oWNvJzApKEVGbR5ozO";
const handlePayment = async () => {
if (!selected) {
alert("충전 금액을 선택해주세요.");
return;
}
// "100,000원" -> 100000 (숫자)로 변환
const amount = Number(selected.replace(/[^0-9]/g, ""));
try {
const tossPayments = await loadTossPayments(clientKey);
// 결제창 띄우기
const payment = tossPayments.payment({
customerKey: "ANONYMOUS", // 비회원 결제 시 필수값 (랜덤값도 가능)
});
await payment.requestPayment({
method: "CARD", // 결제 수단
amount: {
currency: "KRW",
value: amount,
},
orderId: `CHARGE-${Math.random().toString(36).slice(2, 11)}`,
orderName: `캐시 ${selected} 충전`,
successUrl: `${window.location.origin}/payment/success`,
failUrl: `${window.location.origin}/payment/fail`,
});
} catch (error) {
console.error("결제 요청 에러:", error);
}
};
"use client";
import { useSession } from "next-auth/react"; // 예시: NextAuth 사용 시
// 또는 자신이 사용하는 Auth Hook (zustand, react-query 등)
export default function ChargeModal({ open, onClose }: ModalProps) {
// 1. 로그인된 유저 정보 가져오기
const { data: session } = useSession();
const userIdentifier = session?.user?.id || "ANONYMOUS"; // 유저 고유 ID
const handlePayment = async () => {
// ... (금액 변환 로직 동일)
try {
const tossPayments = await loadTossPayments(clientKey);
// 2. 고유 식별자를 customerKey에 할당
const payment = tossPayments.payment({
customerKey: userIdentifier,
});
await payment.requestPayment({
method: "CARD",
amount: {
currency: "KRW",
value: amount,
},
orderId: `CHARGE-${Date.now()}-${userIdentifier.slice(0, 5)}`, // 주문번호에 유저 정보 살짝 섞기
orderName: `캐시 ${selected} 충전`,
// 3. 성공 시 우리 서버로 유저 정보를 같이 넘기기 위해 세팅 (선택사항)
successUrl: `${window.location.origin}/payment/success?userId=${userIdentifier}`,
failUrl: `${window.location.origin}/payment/fail`,
customerEmail: session?.user?.email, // 유저 이메일 (결제창에 자동 입력됨)
customerName: session?.user?.name, // 유저 이름
});
} catch (error) {
console.error(error);
}
};
// ... (UI 코드)
}
무조건 성공 또는 실패 후 페이지 이동 필수
"use client";
import Modal from "@/shared/ui/Modal";
import { useState } from "react";
const SuccessPage = () => {
const [successModalOpen, setSuccessModalOpen] = useState(true);
return (
<Modal
actions={[{ label: "확인", onClick: () => {}, variant: "green" }]}
title="결제 성공"
description="성공적으로 결제가 완료되었습니다."
open={successModalOpen}
onClose={() => {
setSuccessModalOpen(false);
}}
/>
);
};
export default SuccessPage;
const SuccessPage = () => {
const [successModalOpen, setSuccessModalOpen] = useState(true);
const searchParams = useSearchParams();
useEffect(() => {
// 1. URL에서 토스가 보내준 파라미터 추출
const paymentKey = searchParams.get("paymentKey");
const orderId = searchParams.get("orderId");
const amount = searchParams.get("amount");
const confirmPayment = async () => {
try {
// 2. 내 백엔드 서버(API)로 결제 승인 요청 보내기
// BASE_URL은 아까 적어주신 http://123.142.202.212:8081/api/web 등을 활용하세요.
const response = await axios.post(
`${process.env.NEXT_PUBLIC_API_BASE_URL}/payments/confirm`,
{
paymentKey,
orderId,
amount: Number(amount),
},
);
if (response.status === 200) {
// 서버에서 최종 승인 성공 시 모달 띄우기
setSuccessModalOpen(true);
}
} catch (error: any) {
console.error("승인 실패:", error.response?.data || error.message);
// 승인 실패 시 실패 페이지로 강제 이동하거나 에러 모달 처리
router.push(
`/billing/payment/fail?code=${error.response?.data?.code}&message=${error.response?.data?.message}`,
);
} finally {
setLoading(false);
}
};
if (paymentKey && orderId && amount) {
confirmPayment();
}
}, [searchParams, router]);
if (loading) return <div>결제 승인 처리 중입니다...</div>;
return (
<Modal
actions={[{ label: "확인", onClick: () => {}, variant: "green" }]}
title="결제 성공"
description="성공적으로 결제가 완료되었습니다."
open={successModalOpen}
onClose={() => {
setSuccessModalOpen(false);
}}
/>
);
};
실패
결제 시간 초과
결제 실패
토스에서 제공하는 에러 코드
PAY_PROCESS_ABORTED 사용자가 결제창을 닫음 "결제가 취소되었습니다"
REJECT_CARD_PAYMENT 카드사 거절 (잔액부족 등) "결제에 실패했습니다"
PAY_PROCESS_TIMEOUT 결제 시간 초과 "시간 초과"
PROVIDER_TIMEOUT 카드사/은행 응답 초과 "시간 초과"
에러코드가 토스페이먼츠의 기본 작동 방식이 "브라우저 주소창(URL)에 에러 정보를 담아서 약속된 페이지로 던져주는 것"이기 때문입니다.
ex)만약 failUrl을 https://mysite.com/payment/fail 로 설정했다면, 실제 에러 발생 시 브라우저 주소창은 다음과 같이 변합니다:
https://mysite.com/payment/fail?code=PAY_PROCESS_TIMEOUT&message=결제시간이+초과되었습니다
"use client";
import Modal from "@/shared/ui/Modal";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";
const FailPage = () => {
const searchParams = useSearchParams();
const router = useRouter();
const [failModalOpen, setFailModalOpen] = useState(true);
const errorCode = searchParams.get("code");
const isTimeout =
errorCode === "PAY_PROCESS_TIMEOUT" || errorCode === "PROVIDER_TIMEOUT";
const isAborted = errorCode === "PAY_PROCESS_ABORTED"; // 사용자가 직접 닫음
const handleRetry = () => {
router.push("/billing");
};
return (
<Modal
actions={[{ label: "확인", onClick: () => {}, variant: "white" }]}
title="결제 실패"
description={
isTimeout
? "결제 가능 시간을 초과하였습니다. 다시 시도해주세요."
: isAborted
? "결제가 취소되었습니다."
: "결제 요청에 실패했습니다. 다시 시도해주세요."
}
open={failModalOpen}
onClose={() => {
setFailModalOpen(false);
handleRetry();
}}
/>
);
};
export default FailPage;