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