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;
+}