Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ticketping/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ yarn-debug.log*
yarn-error.log*

package-lock.json

.env
1 change: 1 addition & 0 deletions ticketping/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions ticketping/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AppLayout>
<Routes>
<Route path='/' element={<Main />}></Route>
<Route path='/login' element={<Login />}></Route>
<Route path='/join' element={<VerificationInfo />}></Route>
<Route path='/mypage' element={<LoginRequiredRoute><MyPage /></LoginRequiredRoute>}></Route>
<Route path='/performance/:id' element={<PerformanceDetail />} />
<Route path='/performance/:id/schedule' element={<LoginRequiredRoute><SelectSchedule /></LoginRequiredRoute>} />
<Route path='/performance/:performanceId/schedule/:scheduleId/seat' element={<LoginRequiredRoute><Seat /></LoginRequiredRoute>}></Route>
<Route path='/login' element={<Login />}></Route>
<Route path='/join' element={<VerificationInfo />}></Route>
<Route path='/checkout' element={<LoginRequiredRoute><Checkout /></LoginRequiredRoute>}></Route>
<Route path='/success' element={<LoginRequiredRoute><Success /></LoginRequiredRoute>}></Route>
<Route path='/fail' element={<LoginRequiredRoute><Fail /></LoginRequiredRoute>}></Route>
<Route path='*' element={<NotFound />}></Route>
</Routes>
</AppLayout>
Expand Down
2 changes: 1 addition & 1 deletion ticketping/src/constant/Constants.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 1 addition & 1 deletion ticketping/src/pages/SelectSchedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')));
Expand Down
102 changes: 102 additions & 0 deletions ticketping/src/pages/payment/Checkout.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="checkout-container">
<div className="wrapper">
<div className="box_section">
{/* 결제 UI */}
<div id="payment-method" />
{/* 이용약관 UI */}
<div id="agreement" />
</div>
<button
className="checkout-button"
disabled={!ready}
onClick={requestPayment}
>
결제하기
</button>
</div>
</div>
);
}

export default Checkout;
30 changes: 30 additions & 0 deletions ticketping/src/pages/payment/Fail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useSearchParams } from "react-router-dom";
import '../../style/Fail.css';

export function Fail() {
const [searchParams] = useSearchParams();

return (
<div className="fail-container">
<div className="fail-box_section">
<img width="100px" src="https://static.toss.im/lotties/error-spot-no-loop-space-apng.png" alt="에러 이미지" />
<h2>결제를 실패했어요</h2>

<div className="p-grid typography--p" style={{ marginTop: "50px" }}>
<div className="p-grid-col text--left">
<b>에러메시지</b>
</div>
<div className="p-grid-col text--right" id="message">{`${searchParams.get("message")}`}</div>
</div>
<div className="p-grid typography--p" style={{ marginTop: "10px" }}>
<div className="p-grid-col text--left">
<b>에러코드</b>
</div>
<div className="p-grid-col text--right" id="code">{`${searchParams.get("code")}`}</div>
</div>
</div>
</div>
);
}

export default Fail;
83 changes: 83 additions & 0 deletions ticketping/src/pages/payment/Success.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="success-container">
<div className="success-box_section">
<img
width="100px"
src="https://static.toss.im/illusts/check-blue-spot-ending-frame.png"
alt="결제 성공 이미지"
/>
<h2>결제를 완료했어요</h2>
<div className="success-p-grid typography--p" style={{ marginTop: "50px" }}>
<div className="success-p-grid-col success-text--left">
<b>결제금액</b>
</div>
<div className="success-p-grid-col success-text--right" id="amount">
{`${Number(searchParams.get("amount")).toLocaleString()}원`}
</div>
</div>
<div className="success-p-grid typography--p">
<div className="success-p-grid-col success-text--left">
<b>주문번호</b>
</div>
<div className="success-p-grid-col success-text--right" id="orderId">
{`${searchParams.get("orderId")}`}
</div>
</div>
<div className="success-p-grid typography--p">
<div className="success-p-grid-col success-text--left">
<b>paymentKey</b>
</div>
<div
className="success-p-grid-col success-text--right"
id="paymentKey"
style={{ whiteSpace: "initial", width: "250px" }}
>
{`${searchParams.get("paymentKey")}`}
</div>
</div>
</div>
<div className="success-box_section">
<b>Response Data :</b>
<div id="response" style={{ whiteSpace: "initial" }}>
{responseData && <pre>{JSON.stringify(responseData, null, 4)}</pre>}
</div>
</div>
</div>
);
}

export default Success;
25 changes: 16 additions & 9 deletions ticketping/src/pages/seat/Seat.js
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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: <FrownOutlined style={{ color: "#ff3333" }} />,
});
} else {
alert("알 수 없는 오류가 발생했습니다.");
}
notification.open({
message: "서버 응답이 없습니다. 잠시 후 다시 시도해주세요.",
icon: <FrownOutlined style={{ color: "#ff3333" }} />,
});
}
} finally {
setIsBooking(false);
}
Expand Down
1 change: 0 additions & 1 deletion ticketping/src/style/AppLayout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
35 changes: 35 additions & 0 deletions ticketping/src/style/Checkout.css
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 20 additions & 0 deletions ticketping/src/style/Fail.css
Original file line number Diff line number Diff line change
@@ -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;
}

Loading
Loading