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
8 changes: 5 additions & 3 deletions ticketping/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ 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 (
<AppLayout>
<Routes>
<Route path='/' element={<Main />}></Route>
<Route path='/mypage' element={<LoginRequiredRoute><MyPage /></LoginRequiredRoute>}></Route>
<Route path="/performance/:id" element={<PerformanceDetail />} />
<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="/performance/:id/schedule" element={<LoginRequiredRoute><SelectSchedule /></LoginRequiredRoute>} />
<Route path='*' element={<NotFound />}></Route>
</Routes>
</AppLayout>
Expand Down
6 changes: 3 additions & 3 deletions 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() {
}, [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;
};

Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
148 changes: 148 additions & 0 deletions ticketping/src/pages/seat/Seat.js
Original file line number Diff line number Diff line change
@@ -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 <div className="seat-page">Loading seats...</div>;
}

if (error) {
console.error("Error state:", error);
return <p>{error}</p>;
}

return (
<div className="seat-page">
<div className="seat-layout-container">
<SeatLayout
rows={rows}
columns={columns}
grades={grades}
seats={seats}
selectedSeat={selectedSeat}
onSeatSelect={handleSeatSelect}
/>
</div>
<div className="page-detail">
<div className="order-detail">
<h2>{performanceName}</h2>
{selectedSeat !== null ? (
<div className="seat-info">
<p>
좌석: {seats[selectedSeat].row}열 {seats[selectedSeat].col}행
</p>
<p>등급: {seats[selectedSeat].grade}</p>
<p>가격: {seats[selectedSeat].price}원</p>
</div>
) : (
<p>선택된 좌석이 없습니다.</p>
)}
</div>
<button
className="booking-button"
onClick={handleBooking}
disabled={selectedSeat === null || isBooking}
>
{isBooking ? "예약 중..." : "예매하기"}
</button>
</div>
</div>
);
}

export default Seat;
56 changes: 56 additions & 0 deletions ticketping/src/pages/seat/SeatLayout.js
Original file line number Diff line number Diff line change
@@ -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(
<div key="stage" className="stage">
공연장
</div>
);

for (let i = 1; i <= rows; i++) {
const row = [];
row.push(
<div key={`grade-${i}`} className="seat-grade">
{grades[i - 1]}
</div>
);

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(
<div
key={`${i}-${j}`}
className={`seat ${isReserved ? 'reserved' : ''} ${
isSelected ? 'selected' : ''
}`}
onClick={() => handleSeatClick(seats.indexOf(seat))}
title={seat ? `등급: ${seat.grade}, 가격: ${seat.price}원` : ''}
>
{i}-{j}
</div>
);
}

seatLayout.push(
<div key={`row-${i}`} className="seat-row">
{row}
</div>
);
}

return <div className="seat-layout-wrapper">{seatLayout}</div>;
}

export default SeatLayout;
62 changes: 62 additions & 0 deletions ticketping/src/style/Seat.css
Original file line number Diff line number Diff line change
@@ -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;
}

54 changes: 54 additions & 0 deletions ticketping/src/style/SeatLayout.css
Original file line number Diff line number Diff line change
@@ -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;
}
Loading