From af4899b6b11f58379751ba855ed8a6ae5456ef36 Mon Sep 17 00:00:00 2001 From: rivertw777 <105557972+rivertw777@users.noreply.github.com> Date: Fri, 27 Dec 2024 23:16:37 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20TOSS=20=EA=B2=B0=EC=A0=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ticketping/.gitignore | 2 + ticketping/package.json | 1 + ticketping/src/App.js | 10 ++- ticketping/src/constant/Constants.js | 2 +- ticketping/src/pages/SelectSchedule.js | 2 +- ticketping/src/pages/payment/Checkout.js | 102 +++++++++++++++++++++++ ticketping/src/pages/payment/Fail.js | 30 +++++++ ticketping/src/pages/payment/Success.js | 83 ++++++++++++++++++ ticketping/src/pages/seat/Seat.js | 25 ++++-- ticketping/src/style/AppLayout.css | 1 - ticketping/src/style/Checkout.css | 35 ++++++++ ticketping/src/style/Fail.css | 20 +++++ ticketping/src/style/Success.css | 33 ++++++++ 13 files changed, 332 insertions(+), 14 deletions(-) create mode 100644 ticketping/src/pages/payment/Checkout.js create mode 100644 ticketping/src/pages/payment/Fail.js create mode 100644 ticketping/src/pages/payment/Success.js create mode 100644 ticketping/src/style/Checkout.css create mode 100644 ticketping/src/style/Fail.css create mode 100644 ticketping/src/style/Success.css diff --git a/ticketping/.gitignore b/ticketping/.gitignore index e9e922d..bc544c5 100644 --- a/ticketping/.gitignore +++ b/ticketping/.gitignore @@ -23,3 +23,5 @@ yarn-debug.log* yarn-error.log* package-lock.json + +.env \ No newline at end of file diff --git a/ticketping/package.json b/ticketping/package.json index 4bab163..6c3af25 100644 --- a/ticketping/package.json +++ b/ticketping/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@tosspayments/tosspayments-sdk": "^2.3.4", "antd": "^5.22.5", "axios": "^1.7.9", "axios-hooks": "^5.0.2", diff --git a/ticketping/src/App.js b/ticketping/src/App.js index 3189afe..df3b0f8 100644 --- a/ticketping/src/App.js +++ b/ticketping/src/App.js @@ -10,18 +10,24 @@ import Login from './pages/Login'; import VerificationInfo from './pages/Join'; import MyPage from './pages/MyPage'; import NotFound from './pages/NotFound'; +import Checkout from './pages/payment/Checkout'; +import Success from './pages/payment/Success'; +import Fail from './pages/payment/Fail'; function App() { return ( }> + }> + }> }> } /> } /> }> - }> - }> + }> + }> + }> }> diff --git a/ticketping/src/constant/Constants.js b/ticketping/src/constant/Constants.js index 06ed342..d23ab1a 100644 --- a/ticketping/src/constant/Constants.js +++ b/ticketping/src/constant/Constants.js @@ -1 +1 @@ -export const API_HOST = process.env.REACT_APP_API_HOST || "http://localhost:10001"; +export const API_HOST = process.env.REACT_APP_API_HOST; \ No newline at end of file diff --git a/ticketping/src/pages/SelectSchedule.js b/ticketping/src/pages/SelectSchedule.js index c6f86be..b17365a 100644 --- a/ticketping/src/pages/SelectSchedule.js +++ b/ticketping/src/pages/SelectSchedule.js @@ -29,7 +29,7 @@ export default function SelectSchedule() { }; fetchSchedules(); - }, [id, headers]); + }, []); const disabledDate = (current) => { const hasSchedule = schedules.some(schedule => schedule.startDate.startsWith(current.format('YYYY-MM-DD'))); diff --git a/ticketping/src/pages/payment/Checkout.js b/ticketping/src/pages/payment/Checkout.js new file mode 100644 index 0000000..f40d6f3 --- /dev/null +++ b/ticketping/src/pages/payment/Checkout.js @@ -0,0 +1,102 @@ +import { loadTossPayments } from "@tosspayments/tosspayments-sdk"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; +import { axiosInstance } from "../../api"; +import { useAppContext } from "../../store"; +import '../../style/Checkout.css'; + +export function Checkout() { + const location = useLocation(); + const { order } = location.state || {}; + const { store: { jwtToken } } = useAppContext(); + const headers = { Authorization: jwtToken }; + + const clientKey = process.env.REACT_APP_TOSS_CLIENT_KEY; + const customerKey = order.userId; + const [amount, setAmount] = useState({ + currency: "KRW", + value: order.price, + }); + const [ready, setReady] = useState(false); + const [widgets, setWidgets] = useState(null); + + useEffect(() => { + async function fetchPaymentWidgets() { + try { + const tossPayments = await loadTossPayments(clientKey); + const widgets = tossPayments.widgets({ + customerKey, + }); + setWidgets(widgets); + } catch (error) { + console.error("Error fetching payment widget:", error); + } + } + + fetchPaymentWidgets(); + }, [clientKey, customerKey]); + + useEffect(() => { + async function renderPaymentWidgets() { + if (widgets == null) { + return; + } + + await widgets.setAmount(amount); + await widgets.renderPaymentMethods({ + selector: "#payment-method", + variantKey: "DEFAULT", + }); + await widgets.renderAgreement({ + selector: "#agreement", + variantKey: "AGREEMENT", + }); + + setReady(true); + } + + renderPaymentWidgets(); + }, [widgets]); + + const requestPayment = async () => { + if (!widgets) return; + + try { + // 주문 검증 API 호출 + const response = await axiosInstance.post(`/api/v1/orders/${order.id}/validate`, null, { headers }); + + if (response.status == 200) { + await widgets.requestPayment({ + orderId: order.id, + orderName: "공연: " + order.performanceName, + successUrl: window.location.origin + "/success", + failUrl: window.location.origin + "/fail", + }); + } + } catch (error) { + console.error("Error requesting payment:", error); + } + }; + + return ( +
+
+
+ {/* 결제 UI */} +
+ {/* 이용약관 UI */} +
+
+ +
+
+ ); +} + +export default Checkout; \ No newline at end of file diff --git a/ticketping/src/pages/payment/Fail.js b/ticketping/src/pages/payment/Fail.js new file mode 100644 index 0000000..06488e4 --- /dev/null +++ b/ticketping/src/pages/payment/Fail.js @@ -0,0 +1,30 @@ +import { useSearchParams } from "react-router-dom"; +import '../../style/Fail.css'; + +export function Fail() { + const [searchParams] = useSearchParams(); + + return ( +
+
+ 에러 이미지 +

결제를 실패했어요

+ +
+
+ 에러메시지 +
+
{`${searchParams.get("message")}`}
+
+
+
+ 에러코드 +
+
{`${searchParams.get("code")}`}
+
+
+
+ ); +} + +export default Fail; diff --git a/ticketping/src/pages/payment/Success.js b/ticketping/src/pages/payment/Success.js new file mode 100644 index 0000000..710827d --- /dev/null +++ b/ticketping/src/pages/payment/Success.js @@ -0,0 +1,83 @@ +import React, { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { axiosInstance } from "../../api"; +import { useAppContext } from "../../store"; +import '../../style/Success.css'; + +export function Success() { + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const { store: { jwtToken } } = useAppContext(); + const headers = { Authorization: jwtToken }; + const [responseData, setResponseData] = useState(null); + + useEffect(() => { + async function confirm() { + const requestData = { + orderId: searchParams.get("orderId"), + amount: searchParams.get("amount"), + paymentKey: searchParams.get("paymentKey"), + }; + + try { + const response = await axiosInstance.post("/api/v1/payments/confirm", requestData, { headers }); + setResponseData(response.data.data); + } catch (error) { + const errorMessage = error.response?.data?.message || "Unknown error"; + const errorCode = error.response?.data?.code || error.response?.data?.status || "Unknown code"; + navigate(`/fail?code=${errorCode}&message=${errorMessage}`); + } + } + + confirm(); + }, [searchParams]); + + return ( +
+
+ 결제 성공 이미지 +

결제를 완료했어요

+
+
+ 결제금액 +
+
+ {`${Number(searchParams.get("amount")).toLocaleString()}원`} +
+
+
+
+ 주문번호 +
+
+ {`${searchParams.get("orderId")}`} +
+
+
+
+ paymentKey +
+
+ {`${searchParams.get("paymentKey")}`} +
+
+
+
+ Response Data : +
+ {responseData &&
{JSON.stringify(responseData, null, 4)}
} +
+
+
+ ); +} + +export default Success; diff --git a/ticketping/src/pages/seat/Seat.js b/ticketping/src/pages/seat/Seat.js index b4c5822..e51af78 100644 --- a/ticketping/src/pages/seat/Seat.js +++ b/ticketping/src/pages/seat/Seat.js @@ -1,12 +1,15 @@ import React, { useState, useEffect } from 'react'; -import { useParams, useLocation } from 'react-router-dom'; +import { useParams, useLocation, useNavigate } from 'react-router-dom'; import { axiosInstance } from "../../api"; import { useAppContext } from "../../store"; +import { notification } from "antd"; +import { FrownOutlined } from "@ant-design/icons"; import SeatLayout from './SeatLayout'; import '../../style/Seat.css'; function Seat() { const location = useLocation(); + const navigate = useNavigate(); const { performance } = location.state || {}; const { performanceId, scheduleId } = useParams(); const { store: { jwtToken } } = useAppContext(); @@ -79,21 +82,25 @@ function Seat() { try { setIsBooking(true); - await axiosInstance.post( + const response = await axiosInstance.post( `http://localhost:10001/api/v1/orders?performanceId=${performanceId}`, bookingData, { headers } - ); - alert("예매 성공!"); + ); + navigate('/checkout', { state: { order: response.data.data } }); } catch (error) { console.error("Error during booking:", error); if (error.response) { - alert(error.response.data.message || "서버 오류가 발생했습니다."); - } else if (error.request) { - alert("서버 응답이 없습니다. 잠시 후 다시 시도해주세요."); + notification.open({ + message: error.response.data.message || "서버 오류가 발생했습니다.", + icon: , + }); } else { - alert("알 수 없는 오류가 발생했습니다."); - } + notification.open({ + message: "서버 응답이 없습니다. 잠시 후 다시 시도해주세요.", + icon: , + }); + } } finally { setIsBooking(false); } diff --git a/ticketping/src/style/AppLayout.css b/ticketping/src/style/AppLayout.css index d2c0fa0..e582387 100644 --- a/ticketping/src/style/AppLayout.css +++ b/ticketping/src/style/AppLayout.css @@ -13,7 +13,6 @@ justify-content: space-between; align-items: center; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 4px 8px rgba(0, 0, 0, 0.1); - border-bottom: 1px solid white; position: relative; } diff --git a/ticketping/src/style/Checkout.css b/ticketping/src/style/Checkout.css new file mode 100644 index 0000000..e8fdaad --- /dev/null +++ b/ticketping/src/style/Checkout.css @@ -0,0 +1,35 @@ +.checkout-container { + display: flex; + justify-content: center; + height: 660px; + background-color: white; +} + +.wrapper { + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + width: 400px; +} + +.box_section { + margin-bottom: 20px; +} + +.checkout-button { + width: 20%; + background-color: #4A90E2; + color: white; + border-radius: 5px; +} + +.checkout-button:disabled { + background-color: #ccc; + cursor: not-allowed; +} + +.checkable { + display: flex; + align-items: center; +} diff --git a/ticketping/src/style/Fail.css b/ticketping/src/style/Fail.css new file mode 100644 index 0000000..5eb806d --- /dev/null +++ b/ticketping/src/style/Fail.css @@ -0,0 +1,20 @@ +.fail-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 20px auto; + padding: 20px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 700px; +} + +.fail-box_section { + width: 600px; + text-align: left; + margin-bottom: 20px; +} + diff --git a/ticketping/src/style/Success.css b/ticketping/src/style/Success.css new file mode 100644 index 0000000..1476173 --- /dev/null +++ b/ticketping/src/style/Success.css @@ -0,0 +1,33 @@ +.success-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + margin: 20px auto; + padding: 20px; + border: 1px solid #e0e0e0; + border-radius: 8px; + background-color: #f9f9f9; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + width: 1100px; +} + +.success-box_section { + width: 600px; + text-align: left; + margin-bottom: 20px; +} + +.success-p-grid { + display: flex; + justify-content: space-between; + margin-top: 10px; +} + +.success-text--left { + text-align: left; +} + +.success-text--right { + text-align: right; +}