From 8bd5db3658c725015a23eaaa5a02fea112757a55 Mon Sep 17 00:00:00 2001 From: mii2026 Date: Wed, 18 Dec 2024 20:57:05 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat]=20Main(=EA=B3=B5=EC=97=B0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D),=20PerformanceDetail(=EA=B3=B5=EC=97=B0=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80)=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ticketping/src/App.js | 2 + ticketping/src/pages/Main.js | 90 +++++++++++++++++++++- ticketping/src/pages/PerformanceDetail.js | 86 +++++++++++++++++++++ ticketping/src/style/Main.css | 73 ++++++++++++++++++ ticketping/src/style/PerformanceDetail.css | 85 ++++++++++++++++++++ 5 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 ticketping/src/pages/PerformanceDetail.js create mode 100644 ticketping/src/style/Main.css create mode 100644 ticketping/src/style/PerformanceDetail.css 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/pages/Main.js b/ticketping/src/pages/Main.js index 1f20adb..5ef29f4 100644 --- a/ticketping/src/pages/Main.js +++ b/ticketping/src/pages/Main.js @@ -1,10 +1,92 @@ -import React from 'react'; +import React, { useState, useEffect } from "react"; +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(1); + + const fetchPerformances = async (page) => { + setIsLoading(true); + try { + const response = await new Promise((resolve) => { + setTimeout(() => { + const start = (page - 1) * ITEMS_PER_LOAD; + + const newPerformances = Array.from({ length: ITEMS_PER_LOAD }, (_, i) => ({ + id: start + i + 1, + name: `공연 ${start + i + 1}`, + startDate: "2024-01-01", + endDate: "2024-01-10", + venue: `공연장 ${start + i + 1}`, + poster: "https://via.placeholder.com/300x400", + })).filter((_, idx) => start + idx < 10); + + resolve(newPerformances); + }, 1000); + }); + + if (response.length === 0) { + setHasMore(false); + } else { + setPerformances((prev) => [...prev, ...response]); + setCurrentPage(page); + } + } catch (error) { + console.error("Error fetching performances:", error); + } + setIsLoading(false); + }; + + useEffect(() => { + const handleScroll = () => { + if ( + window.innerHeight + document.documentElement.scrollTop >= + document.documentElement.offsetHeight - 100 && + !isLoading && + hasMore + ) { + fetchPerformances(currentPage + 1); + } + }; + + window.addEventListener("scroll", handleScroll); + + if (currentPage === 1 && performances.length === 0) { + fetchPerformances(1); + } + + return () => window.removeEventListener("scroll", handleScroll); + }, [performances, currentPage, isLoading, hasMore]); -function Main(props) { return ( -
+
+

공연 목록

+
+ {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..1fecd4f --- /dev/null +++ b/ticketping/src/pages/PerformanceDetail.js @@ -0,0 +1,86 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import "../style/PerformanceDetail.css"; + +const performances = Array.from({ length: 2 }, (_, i) => ({ + id: i + 1, + name: `공연 ${i + 1}`, + grade: `등급 ${i + 1}`, + duration: "120분", + price: "VIP: 100,000원 / 일반: 50,000원", + startDate: "2024-01-01", + endDate: "2024-01-10", + venue: `공연장 ${i + 1}`, + bookingDate: "2024-01-05", + poster: "https://via.placeholder.com/300x400", +})); + +function PerformanceDetail() { + const { id } = useParams(); + const performance = performances.find((performance) => performance.id === parseInt(id)); + + if (!performance) { + return

공연을 찾을 수 없습니다!

; + } + + // 현재 날짜, 예매 가능 날짜, 공연 종료 날짜 설정 + const today = new Date(); + const bookingDate = new Date(performance.bookingDate); + const endDate = new Date(performance.endDate); + + // 조건 설정 + const isBeforeBooking = today.getTime() < bookingDate.getTime(); // 예매 날짜 이전 + const isAfterEnd = today.getTime() > endDate.getTime(); // 공연 종료 날짜 이후 + const isAfterBooking = today.getTime() > bookingDate.getTime(); // 예매 가능 날짜 이후 + const isBookingAvailable = !isBeforeBooking && !isAfterEnd; // 예매 가능 여부 + + // D-Day 계산 + const daysUntilBooking = Math.ceil((bookingDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); // 남은 일 수 + + // 버튼 텍스트 설정 + const buttonText = isBeforeBooking + ? `D-${daysUntilBooking}` + : isAfterEnd + ? "예매 불가" + : isAfterBooking && !isBookingAvailable + ? "예약 만료" + : "공연 예매하기"; + + return ( +
+
+ {performance.name} +
+
+

{performance.name}

+ + + + + + + + + + + + + + + + + + + + + + + +
등급{performance.grade}
상영 시간{performance.duration}
등급별 가격{performance.price}
공연 날짜{performance.startDate} ~ {performance.endDate}
공연장{performance.venue}
+ +
+
+ ); +} + +export default PerformanceDetail; diff --git a/ticketping/src/style/Main.css b/ticketping/src/style/Main.css new file mode 100644 index 0000000..e3fbc01 --- /dev/null +++ b/ticketping/src/style/Main.css @@ -0,0 +1,73 @@ +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; + text-align: center; +} + +.poster { + position: relative; + overflow: hidden; + width: 230px; + height: 300px; + transition: transform 0.3s ease; +} + +.poster:hover { + transform: scale(1.05); +} + +.poster img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.poster-info { + padding: 10px; + border-radius: 5px; +} + +.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..37859d1 --- /dev/null +++ b/ticketping/src/style/PerformanceDetail.css @@ -0,0 +1,85 @@ +.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: 8px 12px; +} + +.performance-table th { + font-weight: bold; + width: 150px; + color: #333; +} + +.performance-table td { + color: #555; +} + +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; +} From 908e1e9e3214f70bba03b721fc9145dc19c2ab7e Mon Sep 17 00:00:00 2001 From: mii2026 Date: Thu, 19 Dec 2024 19:40:49 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[feat]=20=EA=B3=B5=EC=97=B0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D,=20=EA=B3=B5=EC=97=B0=20=EC=84=B8=EB=B6=80=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20axios=20api=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ticketping/src/pages/Main.js | 71 +++++++---- ticketping/src/pages/PerformanceDetail.js | 141 ++++++++++++++------- ticketping/src/style/Main.css | 14 +- ticketping/src/style/PerformanceDetail.css | 10 +- 4 files changed, 153 insertions(+), 83 deletions(-) diff --git a/ticketping/src/pages/Main.js b/ticketping/src/pages/Main.js index 5ef29f4..7efa6ae 100644 --- a/ticketping/src/pages/Main.js +++ b/ticketping/src/pages/Main.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { axiosInstance } from "../api"; import { Link } from "react-router-dom"; import "../style/Main.css"; @@ -8,38 +9,51 @@ function Main() { const [performances, setPerformances] = useState([]); const [isLoading, setIsLoading] = useState(false); const [hasMore, setHasMore] = useState(true); - const [currentPage, setCurrentPage] = useState(1); + 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 new Promise((resolve) => { - setTimeout(() => { - const start = (page - 1) * ITEMS_PER_LOAD; - - const newPerformances = Array.from({ length: ITEMS_PER_LOAD }, (_, i) => ({ - id: start + i + 1, - name: `공연 ${start + i + 1}`, - startDate: "2024-01-01", - endDate: "2024-01-10", - venue: `공연장 ${start + i + 1}`, - poster: "https://via.placeholder.com/300x400", - })).filter((_, idx) => start + idx < 10); - resolve(newPerformances); - }, 1000); + try { + const response = await axiosInstance.get("http://localhost:10001/api/v1/performances", { + params: { + page, + size: ITEMS_PER_LOAD, + }, }); - if (response.length === 0) { + 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 { - setPerformances((prev) => [...prev, ...response]); - setCurrentPage(page); + setError("알 수 없는 오류가 발생했습니다."); } - } catch (error) { - console.error("Error fetching performances:", error); + } finally { + setIsLoading(false); } - setIsLoading(false); }; useEffect(() => { @@ -48,7 +62,8 @@ function Main() { window.innerHeight + document.documentElement.scrollTop >= document.documentElement.offsetHeight - 100 && !isLoading && - hasMore + hasMore && + !error ) { fetchPerformances(currentPage + 1); } @@ -56,16 +71,20 @@ function Main() { window.addEventListener("scroll", handleScroll); - if (currentPage === 1 && performances.length === 0) { - fetchPerformances(1); + if (currentPage === 0 && performances.length === 0 && !error) { + fetchPerformances(0); } return () => window.removeEventListener("scroll", handleScroll); - }, [performances, currentPage, isLoading, hasMore]); + }, [currentPage, isLoading, hasMore, error]); return (

공연 목록

+ {error &&
{error}
} + {performances.length === 0 && !isLoading && !error && ( +
현재 공연이 존재하지 않습니다.
+ )}
{performances.map((performance) => (
diff --git a/ticketping/src/pages/PerformanceDetail.js b/ticketping/src/pages/PerformanceDetail.js index 1fecd4f..472bd9b 100644 --- a/ticketping/src/pages/PerformanceDetail.js +++ b/ticketping/src/pages/PerformanceDetail.js @@ -1,55 +1,94 @@ -import React from "react"; +// PerformanceDetail.js +import React, { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; +import { axiosInstance } from "../api"; import "../style/PerformanceDetail.css"; -const performances = Array.from({ length: 2 }, (_, i) => ({ - id: i + 1, - name: `공연 ${i + 1}`, - grade: `등급 ${i + 1}`, - duration: "120분", - price: "VIP: 100,000원 / 일반: 50,000원", - startDate: "2024-01-01", - endDate: "2024-01-10", - venue: `공연장 ${i + 1}`, - bookingDate: "2024-01-05", - poster: "https://via.placeholder.com/300x400", -})); - function PerformanceDetail() { - const { id } = useParams(); - const performance = performances.find((performance) => performance.id === parseInt(id)); + const { id } = useParams(); + const [performance, setPerformance] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [timeRemaining, setTimeRemaining] = useState(null); - if (!performance) { - return

공연을 찾을 수 없습니다!

; - } + 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); - // 현재 날짜, 예매 가능 날짜, 공연 종료 날짜 설정 - const today = new Date(); - const bookingDate = new Date(performance.bookingDate); - const endDate = new Date(performance.endDate); + if (days > 0) { + setTimeRemaining(`예매까지 D-${days}`); + } else { + setTimeRemaining(`예매까지 ${hours}시간 ${minutes}분 ${seconds}초`); + } + } else if (now >= bookingDate && now <= endBookingDate) { + setTimeRemaining("공연 예매하기"); + } else { + setTimeRemaining("예매 마감"); + } + }; - // 조건 설정 - const isBeforeBooking = today.getTime() < bookingDate.getTime(); // 예매 날짜 이전 - const isAfterEnd = today.getTime() > endDate.getTime(); // 공연 종료 날짜 이후 - const isAfterBooking = today.getTime() > bookingDate.getTime(); // 예매 가능 날짜 이후 - const isBookingAvailable = !isBeforeBooking && !isAfterEnd; // 예매 가능 여부 + updateRemainingTime(); + const interval = setInterval(updateRemainingTime, 1000); - // D-Day 계산 - const daysUntilBooking = Math.ceil((bookingDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); // 남은 일 수 + 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}

; + } - // 버튼 텍스트 설정 - const buttonText = isBeforeBooking - ? `D-${daysUntilBooking}` - : isAfterEnd - ? "예매 불가" - : isAfterBooking && !isBookingAvailable - ? "예약 만료" - : "공연 예매하기"; + console.log("Rendering performance details:", performance); return (
- {performance.name} + {performance.name}

{performance.name}

@@ -57,27 +96,35 @@ function PerformanceDetail() { 등급 - {performance.grade} + {performance.grade}세 이상 - 상영 시간 - {performance.duration} + 관람시간 + {performance.runTime}분 - 등급별 가격 - {performance.price} + 가격 + + {performance.seatCostResponses.map((seat, index) => ( +
+ {seat.seatGrade} {formatPrice(seat.cost)}원 +
+ ))} + 공연 날짜 - {performance.startDate} ~ {performance.endDate} + + {performance.startDate} ~ {performance.endDate} + 공연장 - {performance.venue} + {performance.performanceHallName} - +
); diff --git a/ticketping/src/style/Main.css b/ticketping/src/style/Main.css index e3fbc01..97bdffb 100644 --- a/ticketping/src/style/Main.css +++ b/ticketping/src/style/Main.css @@ -27,31 +27,29 @@ body { width: 250px; height: 400px; overflow: hidden; - text-align: center; } .poster { position: relative; - overflow: hidden; width: 230px; height: 300px; transition: transform 0.3s ease; } .poster:hover { - transform: scale(1.05); + transform: scale(1.05); +} + +.poster-info { + margin-top: 10px; } .poster img { width: 100%; height: 100%; - object-fit: cover; + object-fit: fill; } -.poster-info { - padding: 10px; - border-radius: 5px; -} .poster-info h3 { font-size: 16px; diff --git a/ticketping/src/style/PerformanceDetail.css b/ticketping/src/style/PerformanceDetail.css index 37859d1..bf254fe 100644 --- a/ticketping/src/style/PerformanceDetail.css +++ b/ticketping/src/style/PerformanceDetail.css @@ -25,7 +25,7 @@ width: 50%; height: 550px; min-width: 55vb; - position: relative; /* 버튼 고정을 위한 설정 */ + position: relative; } .details h2 { @@ -46,7 +46,8 @@ .performance-table th, .performance-table td { text-align: left; - padding: 8px 12px; + padding: 10px 12px; + vertical-align: top; } .performance-table th { @@ -59,6 +60,11 @@ color: #555; } +.performance-table td.price-cell { + background-color: #f2f2f2; + padding: 15px 20px; +} + button { position: absolute; bottom: 20px; From c5bf93be4bbc2d4f5ec92de7aaf1cd59535aef16 Mon Sep 17 00:00:00 2001 From: mii2026 Date: Thu, 19 Dec 2024 19:45:51 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[refactor]=20=ED=8F=B4=EB=8D=94=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ticketping/src/api.js | 2 +- ticketping/src/{ => constant}/Constants.js | 0 ticketping/src/pages/Join.js | 2 +- ticketping/src/pages/Login.js | 2 +- ticketping/src/{pages => style}/Join.css | 0 ticketping/src/{pages => style}/Login.css | 0 6 files changed, 3 insertions(+), 3 deletions(-) rename ticketping/src/{ => constant}/Constants.js (100%) rename ticketping/src/{pages => style}/Join.css (100%) rename ticketping/src/{pages => style}/Login.css (100%) 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/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