generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #41 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> - 예약 스키마 & API 재정의 - 새로운 기능에 맞춘 프론트엔드 페이지 추가 - Controller 이후 응답(성공, 실패) 로그에 Endpoint 추가 - 전환으로 인해 미사용되는 코드 및 테스트 전체 제거 ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> <img width="528" alt="테스트 커버리지" src="attachments/a4899c0a-2919-4993-bd3b-a71bc601137d"> - 예약 & 결제 통합 테스트 작성 완료 - 결제 테스트는 통합 테스트에서는 Client를 mocking하는 방식 + 별도의 Client 슬라이스 테스트로 진행 ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> Reviewed-on: #42 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
import { isLoginRequiredError } from '@_api/apiClient';
|
|
import { confirmPayment } from '@_api/payment/paymentAPI';
|
|
import { PaymentType, type PaymentConfirmRequest } from '@_api/payment/PaymentTypes';
|
|
import { confirmReservation } from '@_api/reservation/reservationAPIV2';
|
|
import '@_css/reservation-v2-1.css';
|
|
import React, { useEffect, useRef } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import { formatDate, formatTime } from 'src/util/DateTimeFormatter';
|
|
|
|
declare global {
|
|
interface Window {
|
|
PaymentWidget: any;
|
|
}
|
|
}
|
|
|
|
const ReservationStep2PageV21: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const paymentWidgetRef = useRef<any>(null);
|
|
const paymentMethodsRef = useRef<any>(null);
|
|
|
|
const { reservationId, themeName, date, startAt, price } = location.state || {};
|
|
|
|
const handleError = (err: any) => {
|
|
if (isLoginRequiredError(err)) {
|
|
alert('로그인이 필요해요.');
|
|
navigate('/login', { state: { from: location } });
|
|
} else {
|
|
const message = err.response?.data?.message || '알 수 없는 오류가 발생했습니다.';
|
|
alert(message);
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (!reservationId) {
|
|
alert('잘못된 접근입니다.');
|
|
navigate('/v2-1/reservation');
|
|
return;
|
|
}
|
|
|
|
const script = document.createElement('script');
|
|
script.src = 'https://js.tosspayments.com/v1/payment-widget';
|
|
script.async = true;
|
|
document.head.appendChild(script);
|
|
|
|
script.onload = () => {
|
|
const widgetClientKey = "test_gck_docs_Ovk5rk1EwkEbP0W43n07xlzm";
|
|
const paymentWidget = window.PaymentWidget(widgetClientKey, window.PaymentWidget.ANONYMOUS);
|
|
paymentWidgetRef.current = paymentWidget;
|
|
|
|
const paymentMethods = paymentWidget.renderPaymentMethods(
|
|
"#payment-method",
|
|
{ value: price },
|
|
{ variantKey: "DEFAULT" }
|
|
);
|
|
paymentMethodsRef.current = paymentMethods;
|
|
};
|
|
}, [reservationId, price, navigate]);
|
|
|
|
const handlePayment = () => {
|
|
if (!paymentWidgetRef.current || !reservationId) {
|
|
alert('결제 위젯이 로드되지 않았거나 예약 정보가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
const generateRandomString = () =>
|
|
crypto.randomUUID().replace(/-/g, '');
|
|
|
|
paymentWidgetRef.current.requestPayment({
|
|
orderId: generateRandomString(),
|
|
orderName: `${themeName} 예약 결제`,
|
|
amount: price,
|
|
}).then((data: any) => {
|
|
const paymentData: PaymentConfirmRequest = {
|
|
paymentKey: data.paymentKey,
|
|
orderId: data.orderId,
|
|
amount: price, // Use the price from component state instead of widget response
|
|
paymentType: data.paymentType || PaymentType.NORMAL,
|
|
};
|
|
|
|
confirmPayment(reservationId, paymentData)
|
|
.then(() => {
|
|
return confirmReservation(reservationId);
|
|
})
|
|
.then(() => {
|
|
alert('결제가 완료되었어요!');
|
|
navigate('/v2-1/reservation/success', {
|
|
state: {
|
|
themeName,
|
|
date,
|
|
startAt,
|
|
}
|
|
});
|
|
})
|
|
.catch(handleError);
|
|
}).catch((error: any) => {
|
|
console.error("Payment request error:", error);
|
|
alert("결제 요청 중 오류가 발생했습니다.");
|
|
});
|
|
};
|
|
|
|
if (!reservationId) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="reservation-v21-container">
|
|
<h2 className="page-title">결제하기</h2>
|
|
<div className="step-section">
|
|
<h3>결제 정보 확인</h3>
|
|
<p><strong>테마:</strong> {themeName}</p>
|
|
<p><strong>날짜:</strong> {formatDate(date)}</p>
|
|
<p><strong>시간:</strong> {formatTime(startAt)}</p>
|
|
<p><strong>금액:</strong> {price.toLocaleString()}원</p>
|
|
</div>
|
|
<div className="step-section">
|
|
<h3>결제 수단</h3>
|
|
<div id="payment-method" className="w-100"></div>
|
|
<div id="agreement" className="w-100"></div>
|
|
</div>
|
|
<div className="next-step-button-container">
|
|
<button onClick={handlePayment} className="next-step-button">
|
|
{price.toLocaleString()}원 결제하기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ReservationStep2PageV21;
|