diff --git a/ticketping/src/App.js b/ticketping/src/App.js
index 1aebc8a..3189afe 100644
--- a/ticketping/src/App.js
+++ b/ticketping/src/App.js
@@ -3,12 +3,13 @@ import { Routes, Route } from 'react-router-dom'
import LoginRequiredRoute from './utils/LoginRequiredRoute';
import Main from './pages/Main';
import PerformanceDetail from './pages/PerformanceDetail'
+import SelectSchedule from './pages/SelectSchedule';
+import Seat from './pages/seat/Seat'
import AppLayout from './component/AppLayout';
import Login from './pages/Login';
import VerificationInfo from './pages/Join';
import MyPage from './pages/MyPage';
import NotFound from './pages/NotFound';
-import SelectSchedule from './pages/SelectSchedule';
function App() {
return (
@@ -16,10 +17,11 @@ function App() {
}>
}>
- } />
+ } />
+ } />
+ }>
}>
}>
- } />
}>
diff --git a/ticketping/src/pages/SelectSchedule.js b/ticketping/src/pages/SelectSchedule.js
index 835bc66..cf63f98 100644
--- a/ticketping/src/pages/SelectSchedule.js
+++ b/ticketping/src/pages/SelectSchedule.js
@@ -29,7 +29,7 @@ export default function SelectSchedule() {
}, [id, headers]);
const disabledDate = (current) => {
- const hasSchedule = schedules.some(schedule => schedule.startTime.startsWith(current.format('YYYY-MM-DD')));
+ const hasSchedule = schedules.some(schedule => schedule.startDate.startsWith(current.format('YYYY-MM-DD')));
return !hasSchedule;
};
@@ -39,7 +39,7 @@ export default function SelectSchedule() {
const daysWithSchedules = schedules
.filter(schedule => {
- const scheduleDate = dayjs(schedule.startTime);
+ const scheduleDate = dayjs(schedule.startDate);
return (
scheduleDate.year() === year &&
scheduleDate.month() === month
@@ -83,7 +83,7 @@ export default function SelectSchedule() {
};
const onDateSelect = (date) => {
- const schedule = schedules.find(schedule => schedule.startTime.startsWith(date.format('YYYY-MM-DD')));
+ const schedule = schedules.find(schedule => schedule.startDate.startsWith(date.format('YYYY-MM-DD')));
if (schedule) {
setSelectedDateId(schedule.id);
}
diff --git a/ticketping/src/pages/seat/Seat.js b/ticketping/src/pages/seat/Seat.js
new file mode 100644
index 0000000..e47badf
--- /dev/null
+++ b/ticketping/src/pages/seat/Seat.js
@@ -0,0 +1,148 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { axiosInstance } from "../../api";
+import { useAppContext } from "../../store";
+import SeatLayout from './SeatLayout';
+import '../../style/Seat.css';
+
+function Seat() {
+ const { performanceId, scheduleId } = useParams();
+ const { store: { jwtToken } } = useAppContext();
+ const [seats, setSeats] = useState([]);
+ const [selectedSeat, setSelectedSeat] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [isBooking, setIsBooking] = useState(false);
+
+ // rows, columns, performanceName은 performance 정보 받아온 거에 맞춰서 바꾸기
+ const rows = 10;
+ const columns = 5;
+ const performanceName = "햄릿";
+ const grades = ['S', 'S', 'A', 'A', 'B', 'B', 'B', 'B', 'B', 'B', 'B'];
+ const headers = { Authorization: jwtToken };
+
+ useEffect(() => {
+ const fetchSeats = async () => {
+ try {
+ const response = await axiosInstance.get(
+ `/api/v1/schedules/${scheduleId}/seats`,
+ { headers }
+ );
+
+ const seatData = response.data.data.map((seat) => ({
+ row: seat.row,
+ col: seat.col,
+ grade: seat.seatGrade,
+ price: seat.cost,
+ reserved: seat.seatStatus !== "AVAILABLE",
+ seatId: seat.seatId,
+ }));
+
+ const sortedSeats = seatData.sort((a, b) => {
+ if (a.row === b.row) {
+ return a.col - b.col;
+ }
+ return a.row - b.row;
+ });
+
+ setSeats(sortedSeats);
+ } catch (error) {
+ console.error('Error fetching seat data:', error);
+ setError("좌석 정보를 불러오는데 실패하였습니다!");
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchSeats();
+ }, []);
+
+ const handleSeatSelect = (seatId) => {
+ setSelectedSeat(selectedSeat === seatId ? null : seatId);
+ };
+
+ const handleBooking = async () => {
+ if (selectedSeat === null) return;
+
+ const selectedSeatData = seats[selectedSeat];
+ if (!selectedSeatData) {
+ alert("선택된 좌석이 유효하지 않습니다.");
+ return;
+ }
+
+ const bookingData = {
+ seatId: selectedSeatData.seatId,
+ scheduleId: scheduleId,
+ };
+
+ try {
+ setIsBooking(true);
+ await axiosInstance.post(
+ `http://localhost:10001/api/v1/orders?performanceId=${performanceId}`,
+ bookingData,
+ { headers }
+ );
+ alert("예매 성공!");
+ } catch (error) {
+ console.error("Error during booking:", error);
+ if (error.response) {
+ alert(error.response.data.message || "서버 오류가 발생했습니다.");
+ } else if (error.request) {
+ alert("서버 응답이 없습니다. 잠시 후 다시 시도해주세요.");
+ } else {
+ alert("알 수 없는 오류가 발생했습니다.");
+ }
+ } finally {
+ setIsBooking(false);
+ }
+ };
+
+ if (isLoading) {
+ return
Loading seats...
;
+ }
+
+ if (error) {
+ console.error("Error state:", error);
+ return {error}
;
+ }
+
+ return (
+
+
+
+
+
+
+
{performanceName}
+ {selectedSeat !== null ? (
+
+
+ 좌석: {seats[selectedSeat].row}열 {seats[selectedSeat].col}행
+
+
등급: {seats[selectedSeat].grade}
+
가격: {seats[selectedSeat].price}원
+
+ ) : (
+
선택된 좌석이 없습니다.
+ )}
+
+
+
+
+ );
+}
+
+export default Seat;
diff --git a/ticketping/src/pages/seat/SeatLayout.js b/ticketping/src/pages/seat/SeatLayout.js
new file mode 100644
index 0000000..57ed74e
--- /dev/null
+++ b/ticketping/src/pages/seat/SeatLayout.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import '../../style/SeatLayout.css';
+
+function SeatLayout({ rows, columns, grades, seats, selectedSeat, onSeatSelect }) {
+ const handleSeatClick = (seatId) => {
+ if (!seats[seatId].reserved) {
+ onSeatSelect(seatId);
+ }
+ };
+
+ const seatLayout = [];
+
+ seatLayout.push(
+
+ 공연장
+
+ );
+
+ for (let i = 1; i <= rows; i++) {
+ const row = [];
+ row.push(
+
+ {grades[i - 1]}
+
+ );
+
+ for (let j = 1; j <= columns; j++) {
+ const seat = seats.find((s) => s.row === i && s.col === j);
+ const isReserved = seat?.reserved || false;
+ const isSelected = seat && selectedSeat === seats.indexOf(seat);
+
+ row.push(
+ handleSeatClick(seats.indexOf(seat))}
+ title={seat ? `등급: ${seat.grade}, 가격: ${seat.price}원` : ''}
+ >
+ {i}-{j}
+
+ );
+ }
+
+ seatLayout.push(
+
+ {row}
+
+ );
+ }
+
+ return {seatLayout}
;
+}
+
+export default SeatLayout;
diff --git a/ticketping/src/style/Seat.css b/ticketping/src/style/Seat.css
new file mode 100644
index 0000000..3e2c36b
--- /dev/null
+++ b/ticketping/src/style/Seat.css
@@ -0,0 +1,62 @@
+.seat-page {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin: 20px;
+}
+
+.seat-layout-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ min-height: 550px;
+}
+
+.page-detail {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-left: 50px;
+ width: 300px;
+ height: 550px;
+ min-width: 55vb;
+ position: relative;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.order-detail {
+ width: 300px;
+ height: 400px;
+ margin: 20px;
+ padding: 20px;
+ border-radius: 8px;
+}
+
+.booking-button {
+ position: absolute;
+ bottom: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ height: 50px;
+ width: 80%;
+ background-color: #4796eb;
+ color: white;
+ border: none;
+ cursor: pointer;
+ font-size: 1.1rem;
+ transition: background-color 0.3s;
+}
+
+.booking-button:hover {
+ background-color: #4a1cef;
+ cursor: not-allowed;
+ opacity: 1;
+}
+
+.booking-button:disabled {
+ background-color: #d0d0d0;
+ cursor: not-allowed;
+ opacity: 1;
+}
+
diff --git a/ticketping/src/style/SeatLayout.css b/ticketping/src/style/SeatLayout.css
new file mode 100644
index 0000000..182dcbd
--- /dev/null
+++ b/ticketping/src/style/SeatLayout.css
@@ -0,0 +1,54 @@
+.stage {
+ width: 100%;
+ text-align: center;
+ background-color: #9c9b9b;
+ color: white;
+ font-size: 1.2rem;
+ padding: 10px 0;
+ margin-bottom: 20px;
+}
+
+.seat-layout-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.seat-row {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ width: 100%;
+}
+
+.seat {
+ width: 50px;
+ height: 50px;
+ background-color: #e0e0e0;
+ border: 1px solid #b0b0b0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ cursor: pointer;
+}
+
+.seat.reserved {
+ background-color: #7e7b7b;
+ cursor: not-allowed;
+}
+
+.seat.selected {
+ background-color: #4caf50;
+}
+
+.seat:hover:not(.reserved):not(.selected) {
+ background-color: #c0c0c0;
+}
+
+.seat-grade {
+ width: 50px;
+ text-align: center;
+ font-weight: bold;
+ color: #333;
+}