티스토리 뷰
'use client';
import { useReserveStore } from '@/store/reserveClassStore';
import { PaymentWidgetInstance, loadPaymentWidget } from '@tosspayments/payment-widget-sdk';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useRef } from 'react';
import { useAsync } from 'react-use';
const clientKey = 'test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm' as string;
export default function PaymentPageasync() {
const router = useRouter();
const searchParams = useSearchParams();
const customerKey = searchParams.get('customerKey');
const price = parseInt(searchParams.get('price') || '', 10) || 0;
const orderId = searchParams.get('orderId');
const classId = searchParams.get('classId') || crypto.randomUUID();
const paymentWidgetRef = useRef<PaymentWidgetInstance | null>(null);
const paymentMethodsWidgetRef = useRef<ReturnType<PaymentWidgetInstance['renderPaymentMethods']> | null>(null);
const title = searchParams.get('title');
const { reserveInfo } = useReserveStore();
useAsync(async () => {
if (customerKey === 'null' || !customerKey) {
alert('유저 정보가 존재하지 않습니다. 로그인 상태를 확인해주세요.');
router.replace(`/`);
return;
}
//초기화
const paymentWidget = await loadPaymentWidget(clientKey, customerKey);
if (!loadPaymentWidget) {
return console.log('요청실패');
}
//결제위젯 렌더링
const paymentMethodsWidget = paymentWidget.renderPaymentMethods(
'#payment-widget',
{ value: price },
{ variantKey: 'DEFAULT' }
);
//이용약관 렌더링
paymentWidget.renderAgreement('#agreement');
paymentWidgetRef.current = paymentWidget;
paymentMethodsWidgetRef.current = paymentMethodsWidget;
}, []);
useEffect(() => {
const paymentMethodsWidget = paymentMethodsWidgetRef.current;
if (paymentMethodsWidget == null) {
return;
}
//금액 업데이트
paymentMethodsWidget.updateAmount(price);
}, [price]);
return (
<div className=" responsiveHeight mx-auto my-20 flex max-w-2xl items-center justify-center px-4">
<div className="mt-4 flex flex-col gap-2">
<h1 className="flex w-full justify-center text-lg font-semibold text-button-default-color md:text-2xl">
확인 및 결제
</h1>
<p className="text-grey-600 mb-4">
결제 수단을 선택하고 결제를 진행해주세요. 환불금은 예약 취소 후 2-3일 내에 결제한 카드로 입금됩니다. 아래
버튼을 눌러 예약을 결제하세요.
</p>
{/* {(paymentWidget === null || paymentMethodsWidgetRef === null) && ''} */}
<div id="payment-widget" className="w-full" />
<div id="agreement" className="w-full" />
<div className="flex w-full items-center justify-center">
<p className=" text-red-600">
모바일의 경우 테스트 결제가 진행되지 않습니다. PC버전에서 시도해주시면 감사하겠습니다!
</p>
</div>
<button
type="button"
className="hover:button-hover-color mt-8 rounded-md bg-button-default-color px-5 py-2 text-white"
onClick={async () => {
if (customerKey === 'null' || !customerKey) {
alert('유저 정보가 존재하지 않습니다. 로그인 상태를 확인해주세요.');
router.replace('/');
}
const paymentWidget = paymentWidgetRef.current;
try {
await paymentWidget?.requestPayment({
orderId: orderId as string,
orderName: title as string,
// 라우트 핸들러로 예약 정보 전송
successUrl: `${window.location.origin}/api/payment?classId=${reserveInfo.classId}&reserveQuantity=${reserveInfo.reserveQuantity}&timeId=${reserveInfo.timeId}&userId=${reserveInfo.userId}`,
//fail 시 보여줄 페이지 만들기
failUrl: `${window.location.origin}/fail?orderId=${orderId}&classId=${classId}`
});
} catch (error: any) {
console.log('failed to paymentWidget', error);
}
}}
>
결제하기
</button>
</div>
</div>
);
}
1. 결제 위젯 로딩 및 초기화:
Toss Payments SDK를 사용하여 결제 위젯을 로드하고, 사용자의 고객 키를 기반으로 결제 위젯을 초기화
const paymentWidget = await loadPaymentWidget(clientKey, customerKey); if (!loadPaymentWidget) { return console.log('요청실패'); }
2. 결제 수단 렌더링:
초기화된 결제 위젯을 통해 사용자가 선택할 수 있는 결제 수단을 웹 페이지에 렌더링
const paymentMethodsWidget = paymentWidget.renderPaymentMethods( '#payment-widget', { value: price }, { variantKey: 'DEFAULT' } );
3. 이용 약관 동의 처리
사용자가 결제를 진행하기 전에 이용 약관에 동의할 수 있도록 별도의 약관 동의 섹션을 렌더링paymentWidget.renderAgreement('#agreement'); paymentWidgetRef.current = paymentWidget; paymentMethodsWidgetRef.current = paymentMethodsWidget; }, []);
4. 동적 금액 업데이트
사용자가 다른 금액을 선택하거나 변경하는 경우, 결제 위젯의 금액을 실시간으로 업데이트하여 반영
useEffect(() => { const paymentMethodsWidget = paymentMethodsWidgetRef.current; if (paymentMethodsWidget == null) { return; } //금액 업데이트 paymentMethodsWidget.updateAmount(price); }, [price]);
5. 결제 요청 처리
최종적으로 사용자가 결제하기 버튼을 클릭할 때, 결제를 요청하고 성공 또는 실패에 따라 적절한 URL로 리디렉션
<div className=" responsiveHeight mx-auto my-20 flex max-w-2xl items-center justify-center px-4"> <div className="mt-4 flex flex-col gap-2"> <h1 className="flex w-full justify-center text-lg font-semibold text-button-default-color md:text-2xl"> 확인 및 결제 </h1> <p className="text-grey-600 mb-4"> 결제 수단을 선택하고 결제를 진행해주세요. 환불금은 예약 취소 후 2-3일 내에 결제한 카드로 입금됩니다. 아래 버튼을 눌러 예약을 결제하세요. </p> {/* {(paymentWidget === null || paymentMethodsWidgetRef === null) && ''} */} <div id="payment-widget" className="w-full" /> <div id="agreement" className="w-full" /> <div className="flex w-full items-center justify-center"> <p className=" text-red-600"> 모바일의 경우 테스트 결제가 진행되지 않습니다. PC버전에서 시도해주시면 감사하겠습니다! </p> </div> <button type="button" className="hover:button-hover-color mt-8 rounded-md bg-button-default-color px-5 py-2 text-white" onClick={async () => { if (customerKey === 'null' || !customerKey) { alert('유저 정보가 존재하지 않습니다. 로그인 상태를 확인해주세요.'); router.replace('/'); } const paymentWidget = paymentWidgetRef.current; try { await paymentWidget?.requestPayment({ orderId: orderId as string, orderName: title as string, // 라우트 핸들러로 예약 정보 전송 successUrl: `${window.location.origin}/api/payment?classId=${reserveInfo.classId}&reserveQuantity=${reserveInfo.reserveQuantity}&timeId=${reserveInfo.timeId}&userId=${reserveInfo.userId}`, //fail 시 보여줄 페이지 만들기 failUrl: `${window.location.origin}/fail?orderId=${orderId}&classId=${classId}` }); } catch (error: any) { console.log('failed to paymentWidget', error); } }} > 결제하기 </button> </div> </div>
api 코드
아래 코드는 다른 팀원분이 작성해 주셨는데 이해한대로 주석을 달아보았다!
import { redirect } from 'next/dist/server/api-utils';
import { insertNewReservation } from '../reserve/insertNewReservation';
import { NextRequest, NextResponse } from 'next/server';
//http get 요청처리, request 객체를 인자로 받음
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const orderId = searchParams.get('orderId');
const paymentKey = searchParams.get('paymentKey');
const amount = searchParams.get('amount');
const reserveQuantity = searchParams.get('reserveQuantity');
const timeId = searchParams.get('timeId');
const userId = searchParams.get('userId');
const classId = searchParams.get('classId');
if (!process.env.NEXT_PUBLIC_BASE_URL) {
console.log('NEXT_PUBLIC_BASE_URL is undefined');
return;
}
//params로 값을 받을 때 하나라도 없으면 결제 실패페이지로 리다이렉트 오류처리를 위한 필수 검증
if (!orderId || !classId || !amount || !reserveQuantity || !timeId || !userId) {
// 값이 없으면 실패 페이지로 리다이렉트
return NextResponse.redirect(new URL(`${process.env.NEXT_PUBLIC_BASE_URL}/reserve/fail`));
}
//tosspayment api에 결제를 확인하는 post 요청.
//요청 헤더에는 인증, 컨텐츠 유형 설정
const response = await fetch('https://api.tosspayments.com/v1/payments/confirm', {
method: 'POST',
body: JSON.stringify({ orderId: orderId, amount: amount, paymentKey: paymentKey }),
headers: {
Authorization: `Basic ${Buffer.from(`${process.env.TOSS_SECRET_KEY}:`).toString('base64')}`,
'Content-Type': 'application/json'
}
});
//tosspayment api 응답을 json 형태로 파싱
const res = await response.json();
//결제 성공 시 예약 데이터를 데이터베이스에 저장
if (response.ok) {
try {
await insertNewReservation({
reserveId: res.orderId,
classId,
reservePrice: res.totalAmount,
reserveQuantity: Number(reserveQuantity),
timeId,
userId
});
//성공 시 예약 성공 페이지로 리다이렉트
//NextResponse.redirect는 서버 측에서 클라이언트를 다른 페이지로 안내할 때 주로 사용
return NextResponse.redirect(new URL(`${process.env.NEXT_PUBLIC_BASE_URL}/reserve/success/${res.orderId}`));
} catch (error) {
console.log('라우트 핸들러의 insertNewReservation 오류 => ', error);
//실패 시 실패 페이지로 리다이렉트
return NextResponse.redirect(new URL(`${process.env.NEXT_PUBLIC_BASE_URL}/reserve/fail`));
}
} else {
//실패 시
return NextResponse.redirect(
new URL(`${process.env.NEXT_PUBLIC_BASE_URL}/reserve/fail?code=${res.code}&statusText=${res.message}`)
);
}
}
'프론트앤드 면접 준비' 카테고리의 다른 글
개발자 이력서 작성 방법 (0) | 2024.05.07 |
---|
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Warning: validateDOMNesting(...): <li> cannot appear as a descendant of <li>
- Warning: A component is changing an uncontrolled input to be controlled.
- axios CRUD
- 별점만들기
- styled component 조건부 사용방법
- axios 설치하기
- 에러모음집
- readme 이미지 추가 방법
- readme 작성해야 하는 이유
- 영화 별점
- readme작성해보기
- axiosinstance 사용 시 토큰 사용 법
- styled component GlobalStyle 사용방법
- 영화별점만들기
- 유효성검사 css
- readme 역할
- readme 작성 방법
- axios instance 작성하기
- Fetch와 Axios 의 장단점
- simple Icon 사용방법
- styled component 사용방법
- nextjs 토큰 만료처리하기
- Warning: Each child in a list should have a unique "key" prop.
- 유효성검사
- 별점 색채우기
- styled component 설치방법
- git cache
- axios 사용하기
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
글 보관함