diff --git a/ticketping/src/App.js b/ticketping/src/App.js index 3f66abd..d480e4d 100644 --- a/ticketping/src/App.js +++ b/ticketping/src/App.js @@ -1,6 +1,7 @@ import React from 'react'; import { Routes, Route } from 'react-router-dom' import Main from './pages/Main'; +import PerformanceDetail from './pages/PerformanceDetail' import AppLayout from './component/AppLayout'; import Login from './pages/Login'; import VerificationInfo from './pages/Join'; @@ -11,6 +12,7 @@ function App() { }> + } /> }> }> }> diff --git a/ticketping/src/api.js b/ticketping/src/api.js index 2f38894..48dfe36 100644 --- a/ticketping/src/api.js +++ b/ticketping/src/api.js @@ -1,5 +1,5 @@ import Axios from "axios"; -import { API_HOST } from "./Constants"; +import { API_HOST } from "./constant/Constants"; export const axiosInstance = Axios.create({ baseURL: API_HOST, diff --git a/ticketping/src/Constants.js b/ticketping/src/constant/Constants.js similarity index 100% rename from ticketping/src/Constants.js rename to ticketping/src/constant/Constants.js diff --git a/ticketping/src/pages/Join.js b/ticketping/src/pages/Join.js index 0a798db..e14f89f 100644 --- a/ticketping/src/pages/Join.js +++ b/ticketping/src/pages/Join.js @@ -3,7 +3,7 @@ import { Card, Form, Input, Button, notification } from "antd"; import { SmileOutlined, FrownOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import { axiosInstance } from "../api"; -import "./Join.css"; +import "../style/Join.css"; export default function Join() { const navigate = useNavigate(); diff --git a/ticketping/src/pages/Login.js b/ticketping/src/pages/Login.js index 520924b..1edc978 100644 --- a/ticketping/src/pages/Login.js +++ b/ticketping/src/pages/Login.js @@ -4,7 +4,7 @@ import { SmileOutlined, FrownOutlined } from "@ant-design/icons"; import { useNavigate } from "react-router-dom"; import { axiosInstance } from "../api"; import { useAppContext, setToken } from "../store"; -import "./Login.css"; +import "../style/Login.css"; export default function Login() { const navigate = useNavigate(); diff --git a/ticketping/src/pages/Main.js b/ticketping/src/pages/Main.js index 1f20adb..7efa6ae 100644 --- a/ticketping/src/pages/Main.js +++ b/ticketping/src/pages/Main.js @@ -1,10 +1,111 @@ -import React from 'react'; +import React, { useState, useEffect } from "react"; +import { axiosInstance } from "../api"; +import { Link } from "react-router-dom"; +import "../style/Main.css"; + +const ITEMS_PER_LOAD = 10; + +function Main() { + const [performances, setPerformances] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [hasMore, setHasMore] = useState(true); + const [currentPage, setCurrentPage] = useState(0); + const [error, setError] = useState(null); + + const fetchPerformances = async (page) => { + if (isLoading || !hasMore || error) return; + setIsLoading(true); + + try { + const response = await axiosInstance.get("http://localhost:10001/api/v1/performances", { + params: { + page, + size: ITEMS_PER_LOAD, + }, + }); + + const { content, last } = response.data.data; + + if (content.length === 0 || last) { + setHasMore(false); + } + + const newPerformances = content.map((item) => ({ + id: item.id, + name: item.name, + startDate: item.startDate, + endDate: item.endDate, + venue: item.performanceHallName, + poster: item.posterUrl, + })); + + setPerformances((prev) => [...prev, ...newPerformances]); + setCurrentPage(page); + setError(null); + } catch (err) { + console.error("Error fetching performances:", err); + if (err.response) { + setError(err.response.data.message || "서버 오류가 발생했습니다."); + } else if (err.request) { + setError("서버 응답이 없습니다. 잠시 후 다시 시도해주세요."); + } else { + setError("알 수 없는 오류가 발생했습니다."); + } + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + const handleScroll = () => { + if ( + window.innerHeight + document.documentElement.scrollTop >= + document.documentElement.offsetHeight - 100 && + !isLoading && + hasMore && + !error + ) { + fetchPerformances(currentPage + 1); + } + }; + + window.addEventListener("scroll", handleScroll); + + if (currentPage === 0 && performances.length === 0 && !error) { + fetchPerformances(0); + } + + return () => window.removeEventListener("scroll", handleScroll); + }, [currentPage, isLoading, hasMore, error]); -function Main(props) { return ( -
+
+

공연 목록

+ {error &&
{error}
} + {performances.length === 0 && !isLoading && !error && ( +
현재 공연이 존재하지 않습니다.
+ )} +
+ {performances.map((performance) => ( +
+ +
+ {performance.name} +
+ +
+

{performance.name}

+

+ {performance.startDate} ~ {performance.endDate} +

+

{performance.venue}

+
+
+ ))} +
+ {isLoading &&
Loading...
}
); } -export default Main; \ No newline at end of file +export default Main; diff --git a/ticketping/src/pages/PerformanceDetail.js b/ticketping/src/pages/PerformanceDetail.js new file mode 100644 index 0000000..472bd9b --- /dev/null +++ b/ticketping/src/pages/PerformanceDetail.js @@ -0,0 +1,133 @@ +// PerformanceDetail.js +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { axiosInstance } from "../api"; +import "../style/PerformanceDetail.css"; + +function PerformanceDetail() { + const { id } = useParams(); + const [performance, setPerformance] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [timeRemaining, setTimeRemaining] = useState(null); + + useEffect(() => { + const fetchPerformance = async () => { + console.log("Fetching performance details for ID:", id); + try { + setIsLoading(true); + const response = await axiosInstance.get(`http://localhost:10001/api/v1/performances/${id}`); + console.log("Performance data fetched successfully:", response.data); + setPerformance(response.data.data); + } catch (err) { + console.error("Error fetching performance:", err); + if (err.response && err.response.status === 404) { + setError("존재하지 않는 공연입니다!"); + } else { + setError("공연 정보를 불러오는 데 실패했습니다."); + } + } finally { + setIsLoading(false); + } + }; + + fetchPerformance(); + }, [id]); + + useEffect(() => { + if (performance) { + const updateRemainingTime = () => { + console.log("Updating remaining time..."); + const now = new Date(); + const bookingDate = new Date(performance.reservationStartDate); + const endBookingDate = new Date(performance.reservationEndDate); + + if (now < bookingDate) { + const diff = bookingDate - now; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((diff % (1000 * 60)) / 1000); + + if (days > 0) { + setTimeRemaining(`예매까지 D-${days}`); + } else { + setTimeRemaining(`예매까지 ${hours}시간 ${minutes}분 ${seconds}초`); + } + } else if (now >= bookingDate && now <= endBookingDate) { + setTimeRemaining("공연 예매하기"); + } else { + setTimeRemaining("예매 마감"); + } + }; + + updateRemainingTime(); + const interval = setInterval(updateRemainingTime, 1000); + + return () => clearInterval(interval); + } + }, [performance]); + + const formatPrice = (price) => { + console.log("Formatting price:", price); + return price.toLocaleString(); + }; + + if (isLoading) { + console.log("Loading state active..."); + return

로딩 중...

; + } + + if (error) { + console.error("Error state:", error); + return

{error}

; + } + + console.log("Rendering performance details:", performance); + + return ( +
+
+ {performance.name} +
+
+

{performance.name}

+ + + + + + + + + + + + + + + + + + + + + + + +
등급{performance.grade}세 이상
관람시간{performance.runTime}분
가격 + {performance.seatCostResponses.map((seat, index) => ( +
+ {seat.seatGrade} {formatPrice(seat.cost)}원 +
+ ))} +
공연 날짜 + {performance.startDate} ~ {performance.endDate} +
공연장{performance.performanceHallName}
+ +
+
+ ); +} + +export default PerformanceDetail; diff --git a/ticketping/src/pages/Join.css b/ticketping/src/style/Join.css similarity index 100% rename from ticketping/src/pages/Join.css rename to ticketping/src/style/Join.css diff --git a/ticketping/src/pages/Login.css b/ticketping/src/style/Login.css similarity index 100% rename from ticketping/src/pages/Login.css rename to ticketping/src/style/Login.css diff --git a/ticketping/src/style/Main.css b/ticketping/src/style/Main.css new file mode 100644 index 0000000..97bdffb --- /dev/null +++ b/ticketping/src/style/Main.css @@ -0,0 +1,71 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background-color: #f4f4f4; +} + +.container { + max-width: 1400px; + margin: 20px auto; + padding: 10px; + text-align: center; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, 250px); + gap: 5px; + justify-content: center; +} + +.poster-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 250px; + height: 400px; + overflow: hidden; +} + +.poster { + position: relative; + width: 230px; + height: 300px; + transition: transform 0.3s ease; +} + +.poster:hover { + transform: scale(1.05); +} + +.poster-info { + margin-top: 10px; +} + +.poster img { + width: 100%; + height: 100%; + object-fit: fill; +} + + +.poster-info h3 { + font-size: 16px; + margin: 5px 0; + color: #333; +} + +.poster-info p { + font-size: 14px; + color: #555; + margin: 3px 0; +} + +.loading { + text-align: center; + margin: 20px 0; + font-size: 16px; + color: #555; +} \ No newline at end of file diff --git a/ticketping/src/style/PerformanceDetail.css b/ticketping/src/style/PerformanceDetail.css new file mode 100644 index 0000000..bf254fe --- /dev/null +++ b/ticketping/src/style/PerformanceDetail.css @@ -0,0 +1,91 @@ +.performance-detail { + display: flex; + justify-content: center; + align-items: center; + width: 80%; + min-width: 60vb; + margin: 20px auto; +} + +.poster-detail { + display: flex; + justify-content: center; +} + +.poster-detail img { + width: 400px; + height: 550px; +} + +.details { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-left: 50px; + width: 50%; + height: 550px; + min-width: 55vb; + position: relative; +} + +.details h2 { + margin: 0; + font-size: 2rem; + font-weight: bold; + width: 100%; + padding-bottom: 20px; + border-bottom: #333 solid; +} + +.performance-table { + margin-top: 30px; + width: 100%; + border-collapse: collapse; +} + +.performance-table th, +.performance-table td { + text-align: left; + padding: 10px 12px; + vertical-align: top; +} + +.performance-table th { + font-weight: bold; + width: 150px; + color: #333; +} + +.performance-table td { + color: #555; +} + +.performance-table td.price-cell { + background-color: #f2f2f2; + padding: 15px 20px; +} + +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; +} + +button:hover { + background-color: #0056b3; +} + +button:disabled { + background-color: #ccc; + cursor: not-allowed; + color: #666; +}