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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CountdownTimer from "./CountdownTimer";
import DateSelector from "./DateSelector";
import RoundSelector from "./RoundSelector";
import type { Performance } from "@/types/performance";
import { useRouter } from "next/navigation";

interface TicketingControlsProps {
performance?: Performance;
Expand All @@ -19,10 +20,13 @@ export default function TicketingControls({
const [selectedDate, setSelectedDate] = useState<number | null>(null);
const [selectedRound, setSelectedRound] = useState<string | null>(null);

const router = useRouter();

const handleConfirm = () => {
if (selectedDate && selectedRound) {
// TODO: 예매 처리
console.log("예매 확정:", { selectedDate, selectedRound });
router.push("/waiting-queue");
}
};

Expand Down
30 changes: 30 additions & 0 deletions frontend/app/api/mock/result/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const GET = async () => {
const selectedSeats = [
{
seatInfoId: "25016072:25001454:001:14821",
seatGrade: "S",
seatGradeName: "S석",
floor: "",
rowNo: "101구역 10열",
seatNo: "8",
salesPrice: 154000,
posLeft: 103.294,
posTop: 129.235,
isExposable: true,
},
{
seatInfoId: "25016072:25001454:001:14822",
seatGrade: "S",
seatGradeName: "S석",
floor: "",
rowNo: "101구역 10열",
seatNo: "9",
salesPrice: 154000,
posLeft: 106.294,
posTop: 129.235,
isExposable: true,
},
];

return Response.json(selectedSeats, { status: 200 });
};
2 changes: 2 additions & 0 deletions frontend/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import Header from "./_source/components/Header";
import Ticketing from "./_source/components/ticketing/Ticketing";

export const dynamic = "force-dynamic";

export default async function Home() {
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/reservations/_source/components/Reservation.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ReservationProvider } from "../contexts/ReservationProvider";
import ReservationHeader from "./ReservationHeader";
import ReservationTimeTracker from "./ReservationTimeTracker";
import ReservationStage from "./stage/ReservationStage";
import ReservationSidebar from "./sidebar/ReservationSidebar";

export default function Reservation() {
return (
<ReservationProvider>
<ReservationTimeTracker />
<div className="h-screen flex flex-col overflow-hidden">
<ReservationHeader />
<div className="flex-1 flex overflow-hidden min-h-0">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client";

import { useEffect, useRef } from "react";

export default function ReservationTimeTracker() {
const effectRan = useRef(false);

useEffect(() => {
if (effectRan.current === false) {
const queueEnterTime = sessionStorage.getItem("timeQueueEnter");

if (queueEnterTime) {
const newQueueDuration = (Date.now() - Number(queueEnterTime)) / 1000;
const oldQueueDuration =
Number(sessionStorage.getItem("timeQueueDuration")) || 0;
const totalQueueDuration = oldQueueDuration + newQueueDuration;
sessionStorage.setItem(
"timeQueueDuration",
totalQueueDuration.toString()
);
}

sessionStorage.setItem("timeReservationEnter", Date.now().toString());

return () => {
effectRan.current = true;
};
}
}, []);

return null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ export function ReservationProvider({ children }: ReservationProviderProps) {
remove: handleRemoveSeat,
reset: handleResetSeats,
} = useSelection<string, Seat>(new Map(), { max: RESERVATION_LIMIT });

const router = useRouter();

const handleClickReserve = () => {
try {
throw new Error("예매 실패");
// throw new Error("예매 실패");
router.push("/result");
} catch (e) {
console.error(e);
Expand Down
20 changes: 20 additions & 0 deletions frontend/app/result/_source/components/CompleteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"use client";

import { useRouter } from "next/navigation";

export default function CompleteButton() {
const router = useRouter();

const handleClickComplete = () => {
router.push("/");
};

return (
<button
onClick={handleClickComplete}
className="w-full py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
완료
</button>
);
}
13 changes: 13 additions & 0 deletions frontend/app/result/_source/components/ResultHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Trophy } from "lucide-react";

export default function ResultHeader() {
return (
<div className="py-6">
<div className="w-16 h-16 bg-gradient-to-br from-yellow-500 to-orange-500 rounded-full flex items-center justify-center mx-auto mb-6">
<Trophy className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl mb-2 text-center">예매 완료!</h3>
<p className="text-gray-500 text-center">티켓팅 결과를 확인하세요</p>
</div>
);
}
17 changes: 17 additions & 0 deletions frontend/app/result/_source/components/TicketResult.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import ResultHeader from "./ResultHeader";
import CompleteButton from "./CompleteButton";
import ResultDetails from "./details/ResultDetails";

export default function TicketResult() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 p-4">
<div className="bg-white rounded-2xl max-w-2xl w-full px-8 py-4 shadow-xl">
<ResultHeader />
<div className="max-w-md mx-auto space-y-4">
<ResultDetails />
<CompleteButton />
</div>
</div>
</div>
);
}
28 changes: 28 additions & 0 deletions frontend/app/result/_source/components/details/ResultDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import dynamic from "next/dynamic";
import TimeLog from "./TimeLog";
import UserRank from "./UserRank";

const SelectedSeats = dynamic(() => import("./SelectedSeats"), {
ssr: false,
loading: () => <div>결과를 불러오는 중입니다.</div>,
});

export default function ResultDetails() {
return (
<>
<ErrorBoundary
fallback={<div>결과를 불러오는 중 오류가 발생했습니다.</div>}
>
<Suspense fallback={<div>결과를 불러오는 중입니다.</div>}>
<SelectedSeats />
<TimeLog />
<UserRank />
</Suspense>
</ErrorBoundary>
</>
);
}
26 changes: 26 additions & 0 deletions frontend/app/result/_source/components/details/SelectedSeats.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"use client";

import { CheckCircle } from "lucide-react";
import { useResultQuery } from "../../queries/result";

export default function SelectedSeats() {
const { data: seats } = useResultQuery();

return (
<div className="bg-green-50 border border-green-200 rounded-xl p-6 text-center">
<CheckCircle className="w-12 h-12 text-green-600 mx-auto mb-3" />
<p className="text-green-800 mb-3">성공적으로 티켓을 예매했습니다!</p>

{seats.map((seat) => (
<div
key={seat.seatInfoId}
className="flex justify-center items-center text-sm"
>
<span className="text-gray-600">
{seat.seatGradeName} {seat.rowNo} {seat.seatNo}번
</span>
</div>
))}
</div>
);
}
36 changes: 36 additions & 0 deletions frontend/app/result/_source/components/details/TimeLog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { Clock } from "lucide-react";
import { useTimeLog } from "../../hooks/useTimeLog";

export default function TimeLog() {
const { timeLog, totalTime } = useTimeLog();

return (
<div className="bg-white border border-gray-200 rounded-xl p-6">
<h4 className="mb-4 flex items-center gap-2">
<Clock className="w-5 h-5 text-gray-400" />
단계별 소요 시간
</h4>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-gray-600">대기열 통과</span>
<span>{timeLog.queue?.toFixed(1)}초</span>
</div>
{/* 보안 문자 섹션 개발 후 적용 */}
{/* <div className="flex justify-between items-center">
<span className="text-gray-600">보안문자 입력</span>
<span>{timeLog.captcha?.toFixed(1)}초</span>
</div> */}
<div className="flex justify-between items-center">
<span className="text-gray-600">좌석 선택</span>
<span>{timeLog.seats?.toFixed(1)}초</span>
</div>
<div className="border-t border-gray-200 pt-3 flex justify-between items-center">
<span>총 소요 시간</span>
<span className="text-purple-600">{totalTime.toFixed(1)}초</span>
</div>
</div>
</div>
);
}
13 changes: 13 additions & 0 deletions frontend/app/result/_source/components/details/UserRank.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function UserRank() {
const userRank = 1000;

return (
<div className="bg-gradient-to-br from-purple-50 to-pink-50 border border-purple-200 rounded-xl p-6 text-center">
<p className="text-gray-600 mb-2">전체 사용자 중</p>
<p className="text-3xl text-purple-600 mb-2">{userRank}위</p>
<p className="text-sm text-gray-500">
상위 {((userRank / 10000) * 100).toFixed(1)}%
</p>
</div>
);
}
53 changes: 53 additions & 0 deletions frontend/app/result/_source/hooks/useTimeLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client";

import { useEffect, useRef, useState } from "react";
import { ITimeLog } from "../types/resultType";

export function useTimeLog() {
const [timeLog, setTimeLog] = useState<ITimeLog>({});
const [totalTime, setTotalTime] = useState(0);
const effectRan = useRef(false);

useEffect(() => {
if (effectRan.current === false) {
const reservationEnterTime = sessionStorage.getItem(
"timeReservationEnter"
);
if (reservationEnterTime) {
const newReservationDuration =
(Date.now() - Number(reservationEnterTime)) / 1000;
const oldReservationDuration =
Number(sessionStorage.getItem("timeReservationDuration")) || 0;
const totalReservationDuration =
oldReservationDuration + newReservationDuration;
sessionStorage.setItem(
"timeReservationDuration",
totalReservationDuration.toString()
);
}

const queueDuration =
Number(sessionStorage.getItem("timeQueueDuration")) || 0;
const reservationDuration =
Number(sessionStorage.getItem("timeReservationDuration")) || 0;
const newTimeLog: ITimeLog = {
queue: queueDuration,
seats: reservationDuration,
};

// eslint-disable-next-line react-hooks/set-state-in-effect
setTimeLog(newTimeLog);
setTotalTime(Object.values(newTimeLog).reduce((a, b) => a + b, 0));

return () => {
effectRan.current = true;
sessionStorage.removeItem("timeQueueEnter");
sessionStorage.removeItem("timeQueueDuration");
sessionStorage.removeItem("timeReservationEnter");
sessionStorage.removeItem("timeReservationDuration");
};
}
}, []);

return { timeLog, totalTime };
}
13 changes: 13 additions & 0 deletions frontend/app/result/_source/queries/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { api } from "@/lib/api";
import { Seat } from "@/types/seat";

export const useResultQuery = (resultId: string = "") => {
return useSuspenseQuery({
queryKey: ["result", resultId],

queryFn: async () => {
return api.get<Seat[]>(`/result`);
},
});
};
3 changes: 3 additions & 0 deletions frontend/app/result/_source/types/resultType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ITimeLog {
[key: string]: number;
}
5 changes: 5 additions & 0 deletions frontend/app/result/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import TicketResult from "./_source/components/TicketResult";

export default function TicketResultPage() {
return <TicketResult />;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import WaitingProgress from "./WaitingProgress";
import WaitingHeader from "./WaitingHeader";
import WaitingNotice from "./WaitingNotice";
import WaitingQueueTimeTracker from "./WaitingQueueTimeTracker";

export default function WaitingQueue() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-50 to-pink-50 p-4">
<div className="bg-white rounded-2xl max-w-2xl w-full p-8 shadow-xl">
<WaitingQueueTimeTracker />
<div className="py-8">
<WaitingHeader />
<div className="max-w-md mx-auto">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"use client";

import { useEffect } from "react";

export default function WaitingQueueTimeTracker() {
useEffect(() => {
sessionStorage.setItem("timeQueueEnter", Date.now().toString());
}, []);

return null;
}
Loading
Loading