diff --git a/components/common/button/index.tsx b/components/common/button/index.tsx
index 1b26158..74ce154 100644
--- a/components/common/button/index.tsx
+++ b/components/common/button/index.tsx
@@ -54,8 +54,7 @@ const ButtonText = styled(Text)<{ sort: ButtonSort }>`
const { colors } = theme;
return css`
- font-weight: 700;
- line-height: 24px;
+ ${theme.typoToken.subhead02}
letter-spacing: 0.5px;
color: ${sort === "primary" ? colors.white : colors.primary_01};
`;
diff --git a/components/common/modal/index.ts b/components/common/modal/index.ts
index 7c4b24b..4377c00 100644
--- a/components/common/modal/index.ts
+++ b/components/common/modal/index.ts
@@ -1,2 +1,4 @@
export * from "./modal.default";
export * from "./modal.portal";
+export * from "./modal.confirm";
+export * from "./modal.reservation";
diff --git a/components/common/modal/modal.confirm.tsx b/components/common/modal/modal.confirm.tsx
new file mode 100644
index 0000000..c60a37d
--- /dev/null
+++ b/components/common/modal/modal.confirm.tsx
@@ -0,0 +1,102 @@
+import { useEffect, type ReactNode } from "react";
+import Image from "next/image";
+import styled, { css } from "styled-components";
+import Button from "@/components/common/button";
+import { SpacerSkleton } from "@/components/common/spacer";
+import Typography from "@/components/common/text/Typography";
+
+interface ConfirmModalProps {
+ isShow: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ title: string;
+ children: ReactNode;
+}
+
+export function ConfirmModal({
+ isShow,
+ onClose,
+ onConfirm,
+ title,
+ children,
+}: ConfirmModalProps) {
+ useEffect(() => {
+ if (isShow) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }, [isShow]);
+
+ if (!isShow) {
+ return null;
+ }
+
+ return (
+
+ e.stopPropagation()}>
+
+
+
+ {title}
+ {children}
+ 확인
+
+
+ );
+}
+
+const ModalTitle = styled(Typography)`
+ margin-top: 0.5rem;
+ margin-bottom: 1.4rem;
+`;
+
+const Backdrop = styled.div`
+ ${({ theme }) => {
+ const { colors } = theme;
+
+ return css`
+ z-index: 9999;
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ background-color: ${colors.secondary["black-50"]};
+ `;
+ }}
+`;
+
+const ConfirmButton = styled(Button)`
+ margin-top: 2rem;
+`;
+
+const Content = styled.div`
+ ${({ theme }) => {
+ const { colors } = theme;
+
+ return css`
+ display: flex;
+ flex-direction: column;
+ padding: 1.5rem 2rem;
+ border-radius: 12px;
+ background-color: ${colors.secondary.white};
+ box-shadow: 0 5px 15px ${colors.secondary["black-30"]};
+ text-align: center;
+ `;
+ }}
+`;
diff --git a/components/domains/detail/RowInfo.tsx b/components/domains/detail/RowInfo.tsx
index ead557f..ca669bc 100644
--- a/components/domains/detail/RowInfo.tsx
+++ b/components/domains/detail/RowInfo.tsx
@@ -1,3 +1,4 @@
+import dayjs from "dayjs";
import styled from "styled-components";
import Typography from "@/components/common/text/Typography";
@@ -12,6 +13,8 @@ export interface RowInfoData {
}
export default function RowInfo({ infoData }: { infoData: RowInfoData }) {
+ console.log(dayjs(infoData?.startDate).format("YYYY-MM-DD"));
+
return (
@@ -24,7 +27,9 @@ export default function RowInfo({ infoData }: { infoData: RowInfoData }) {
{infoData.place}
- {`${infoData.startDate}~${infoData.endDate}`}
+ {`${dayjs(infoData?.startDate).format(
+ "YYYY-MM-DD"
+ )}~${dayjs(infoData?.endDate).format("YYYY-MM-DD")}`}
{`만 ${infoData.ageLimit}세 이상`}
{`총 ${infoData.runningTime}분`}
{`${infoData.interMission}분`}
diff --git a/components/domains/detail/calendar/DaySchedules.tsx b/components/domains/detail/calendar/DaySchedules.tsx
index 44a08a2..5724c8e 100644
--- a/components/domains/detail/calendar/DaySchedules.tsx
+++ b/components/domains/detail/calendar/DaySchedules.tsx
@@ -1,5 +1,6 @@
import type { Dispatch, SetStateAction } from "react";
import format from "date-fns/format";
+import dayjs from "dayjs";
import styled from "styled-components";
import Typography from "@/components/common/text/Typography";
import type { Schedule } from "./type";
@@ -7,28 +8,37 @@ import type { Schedule } from "./type";
type DaySchedulesProps = {
schedules: Schedule[];
onClickSchedule: Dispatch>;
+ selectedDate: Date | null;
clickedId: number | null;
};
export default function DaySchedules({
schedules,
+ selectedDate,
onClickSchedule,
clickedId,
}: DaySchedulesProps) {
return (
- {schedules.map((schedule, index) => {
- return (
-
-
- {`${index + 1}회 | ${format(
- new Date(schedule.startAt),
- "HH:mm"
- )}`}
-
-
- );
- })}
+ {schedules
+ .filter((schedule) => {
+ return (
+ dayjs(schedule.startAt).startOf("day").valueOf() ===
+ selectedDate?.valueOf()
+ );
+ })
+ .map((schedule, index) => {
+ return (
+
+
+ {`${index + 1}회 | ${format(
+ new Date(schedule.startAt),
+ "HH:mm"
+ )}`}
+
+
+ );
+ })}
);
}
diff --git a/components/domains/detail/calendar/ReservationCalendar.tsx b/components/domains/detail/calendar/ReservationCalendar.tsx
index 6b339ab..3620031 100644
--- a/components/domains/detail/calendar/ReservationCalendar.tsx
+++ b/components/domains/detail/calendar/ReservationCalendar.tsx
@@ -1,18 +1,30 @@
import { useState } from "react";
-import styled, { css } from "styled-components";
+import styled from "styled-components";
import Button from "@/components/common/button";
import { CustomCalendar } from "@/components/common/calendar";
+import { ConfirmModal } from "@/components/common/modal";
import Typography from "@/components/common/text/Typography";
import CustomHeader from "@/components/domains/detail/calendar/CustomHeader";
import DaySchedules from "@/components/domains/detail/calendar/DaySchedules";
import ReservedSeat from "@/components/domains/detail/calendar/ResearvedSeat";
-import type { ReservationCalendarProps } from "@/components/domains/detail/calendar/type";
+import type {
+ ReservationCalendarProps,
+ Schedule,
+} from "@/components/domains/detail/calendar/type";
+import { ReservationModal } from "@/components/domains/detail/reservation/ReservationModal";
+import ReservationNotice from "@/components/domains/detail/reservation/ReservationNotice";
+
+type ReserveModalCase = "NOTICE" | "COMPLETION" | "RESERVATION" | null;
+
+const convertScheduleData = (schedules: Schedule[]) => {
+ return schedules.map((schedule) => {
+ return new Date(schedule.startAt);
+ });
+};
export default function ReservationCalendar({
- schedules,
+ schedules = [],
}: ReservationCalendarProps) {
- // TODO: Link Calendar Function
-
const [date, setDate] = useState(null);
const [clickedScheduleId, setClickedScheduleId] = useState(
null
@@ -20,16 +32,23 @@ export default function ReservationCalendar({
const [availableSeatCount, setAvailableSeatCount] = useState(
null
);
+ const [modalStatus, setModalStatus] =
+ useState("RESERVATION");
const onChangeDate = (date: Date | null) => {
setDate(date);
};
+ const closeModal = () => {
+ setModalStatus(null);
+ };
+
return (
(
)}
@@ -37,13 +56,46 @@ export default function ReservationCalendar({
onChangeDate={onChangeDate}
/>
- 예매하기
+ {
+ setModalStatus("NOTICE");
+ }}
+ >
+ 예매하기
+
+ {
+ setModalStatus("RESERVATION");
+ }}
+ >
+
+
+
+ 티켓 구매가 완료되었습니다.
+
구매하신 티켓은 MY PAGE에서 확인 가능합니다.
+
+ {
+ setModalStatus("COMPLETION");
+ }}
+ onClose={closeModal}
+ />
);
}
diff --git a/components/domains/detail/reservation/ReservationModal.tsx b/components/domains/detail/reservation/ReservationModal.tsx
new file mode 100644
index 0000000..18a5f47
--- /dev/null
+++ b/components/domains/detail/reservation/ReservationModal.tsx
@@ -0,0 +1,109 @@
+import { useEffect, useState } from "react";
+import Image from "next/image";
+import styled, { css } from "styled-components";
+import { SpacerSkleton } from "@/components/common/spacer";
+import ReservationSummaryPanel from "@/components/domains/detail/reservation/ReservationSummaryPanel";
+import TicketQuantitySelect from "@/components/domains/detail/reservation/TicketQuantitySelect";
+import ReservationStep from "./ReservationStep";
+
+interface Props {
+ isShow: boolean;
+ onClose: () => void;
+ onReserve: () => void;
+}
+
+export function ReservationModal({ isShow, onClose, onReserve }: Props) {
+ const [ticketCount, setTicketCount] = useState(1);
+
+ useEffect(() => {
+ if (isShow) {
+ document.body.style.overflow = "hidden";
+ } else {
+ document.body.style.overflow = "";
+ }
+
+ return () => {
+ document.body.style.overflow = "";
+ };
+ }, [isShow]);
+
+ if (!isShow) {
+ return null;
+ }
+
+ return (
+
+ e.stopPropagation()}>
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const Wrapper = styled.div`
+ width: 45rem;
+`;
+const ContentSection = styled.div`
+ display: flex;
+`;
+
+const TopSection = styled(SpacerSkleton)`
+ padding: 8px 16px;
+ border-bottom: 1px solid #e8e8e8;
+`;
+
+const Backdrop = styled.div`
+ ${({ theme }) => {
+ const { colors } = theme;
+
+ return css`
+ z-index: 9999;
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ background-color: ${colors.secondary["black-50"]};
+ `;
+ }}
+`;
+
+const Content = styled.div`
+ ${({ theme }) => {
+ const { colors } = theme;
+
+ return css`
+ border-radius: 12px;
+ background-color: ${colors.secondary.white};
+ box-shadow: 0 5px 15px ${colors.secondary["black-30"]};
+ `;
+ }}
+`;
diff --git a/components/domains/detail/reservation/ReservationNotice.tsx b/components/domains/detail/reservation/ReservationNotice.tsx
new file mode 100644
index 0000000..59f30fb
--- /dev/null
+++ b/components/domains/detail/reservation/ReservationNotice.tsx
@@ -0,0 +1,21 @@
+import styled from 'styled-components';
+
+export default function ReservationNotice() {
+ return 예매자는 본 안내페이지의 모든 내용을 숙지 및 동의한 것으로 간주합니다.{" "}
+
+
- 관람 연령/티켓수령/공연관람 안내 미숙지로 인한 책임은 관람자
+ 본인에게 있으며,
+
관련 사유로 예매 티켓의 취소/변경/환불은 일체 불가하오니 각별히
+ 유의하시기 바랍니다.
+
+
- 모든 할인은 실 관람자 기준으로 적용되며, 관람회차의 티켓 수령
+ 시 반드시 할인 대상 본인이
매표소에 방문하여 증빙자료를 제시해 주셔야
+ 합니다.
+
+
*Play NEWniverse는 작품 유통 외의 작품 제작과 관련에는 책임이
+ 없음을 알립니다.
+}
+
+const Notice = styled.p`
+ text-align: left;
+`
diff --git a/components/domains/detail/reservation/ReservationStep.tsx b/components/domains/detail/reservation/ReservationStep.tsx
new file mode 100644
index 0000000..dc01c71
--- /dev/null
+++ b/components/domains/detail/reservation/ReservationStep.tsx
@@ -0,0 +1,73 @@
+import Image from "next/image";
+import styled, { css } from "styled-components";
+import Typography from "@/components/common/text/Typography";
+
+export type ReservationStepProps = {
+ step: "PRICE_SELECT" | "SEAT_SELECT";
+ purchaseLimitCount?: number;
+ isAlert?: boolean;
+};
+
+export default function ReservationStep({
+ step,
+ purchaseLimitCount = 2,
+ isAlert,
+}: ReservationStepProps) {
+ return (
+
+
+ 좌석 선택
+
+ 가격 선택
+
+
+ {`1인당 ${purchaseLimitCount}매까지 구매 가능합니다.`}
+
+
+ );
+}
+
+const Container = styled.div`
+ padding: 12px 24px;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.secondary[150]};
+`;
+
+const Wrapper = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const ArrowIcon = styled(Image)`
+ margin: 0 12px;
+`;
+
+const AlertMessage = styled(Typography)<{ isAlert?: boolean }>`
+ ${({ theme }) => {
+ const { colors, isAlert, typoToken } = theme;
+
+ return css`
+ color: ${isAlert ? colors.system.red : colors.secondary[500]};
+ ${typoToken.subhead04}
+ `;
+ }}
+`;
+
+const Step = styled(Typography)<{ isActive: boolean }>`
+ ${({ theme, isActive }) => {
+ const { colors, typoToken } = theme;
+
+ return css`
+ ${typoToken.subhead02}
+ color: ${isActive ? colors.secondary[400] : colors.secondary.black}
+ `;
+ }}
+`;
diff --git a/components/domains/detail/reservation/ReservationSummaryPanel.tsx b/components/domains/detail/reservation/ReservationSummaryPanel.tsx
new file mode 100644
index 0000000..0af9f5d
--- /dev/null
+++ b/components/domains/detail/reservation/ReservationSummaryPanel.tsx
@@ -0,0 +1,106 @@
+import styled from "styled-components";
+import Button from "@/components/common/button";
+import Typography from "@/components/common/text/Typography";
+
+type ReservationSummaryPanelProps = {
+ ticketPrice: number;
+ discount: number;
+ reservationFee: number;
+ ticketCount: number;
+ onClickReserveButton: () => void;
+};
+
+export default function ReservationSummaryPanel({
+ ticketPrice = 0,
+ discount = 0,
+ reservationFee = 0,
+ ticketCount = 0,
+ onClickReserveButton,
+}: ReservationSummaryPanelProps) {
+ return (
+
+
+
+ 결제 금액
+
+
+
+ 기본가
+ {ticketPrice * ticketCount}원
+
+
+ 가격 할인
+ {discount}원
+
+
+ 예매수수료
+ {reservationFee}원
+
+
+ 총금액
+
+ {ticketPrice * ticketCount - discount - reservationFee}원
+
+
+
+ 결제
+
+ );
+}
+
+const Container = styled.div`
+ width: 240px;
+ height: 560px;
+ border-left: 1px solid ${({ theme }) => theme.colors.secondary[150]};
+ padding: 8px 16px 20px 16px;
+ display: flex;
+ flex-direction: column;
+`;
+
+const LogoIcon = styled.div`
+ height: 50px;
+ width: 90px;
+ object-fit: contain;
+ margin: 8px auto 16px auto;
+ background-repeat: no-repeat;
+ background-image: url("/icon/plavnewniverse_black_logo.svg");
+`;
+
+const DataWrapper = styled.div`
+ padding: 0 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 4px;
+`;
+
+const Wrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ background-color: ${({ theme }) => theme.colors.secondary[100]};
+ border-radius: 4px;
+ padding: 16px 0;
+`;
+
+const TotalCount = styled.div`
+ padding: 16px 12px 0 12px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-top: 1px solid ${({ theme }) => theme.colors.secondary[150]};
+`;
+
+const ReservationButton = styled(Button)`
+ margin-top: auto;
+ width: 100%;
+`;
diff --git a/components/domains/detail/reservation/TicketQuantitySelect.tsx b/components/domains/detail/reservation/TicketQuantitySelect.tsx
new file mode 100644
index 0000000..f4c6a50
--- /dev/null
+++ b/components/domains/detail/reservation/TicketQuantitySelect.tsx
@@ -0,0 +1,71 @@
+import { useMemo } from "react";
+import styled from "styled-components";
+import { Field } from "@/components/common/field";
+import Typography from "@/components/common/text/Typography";
+
+type TicketQuantitySelectProps = {
+ purchaseLimitCount?: number;
+ ticketPrice?: number;
+ ticketCount: number;
+ setTicketCount: (count: number) => void;
+};
+
+export default function TicketQuantitySelect({
+ purchaseLimitCount = 2,
+ ticketPrice = 0,
+ ticketCount = 0,
+ setTicketCount,
+}: TicketQuantitySelectProps) {
+ const selectOptions = useMemo(() => {
+ return Array.from({ length: purchaseLimitCount }, (_, index) => ({
+ label: `${index + 1}매`,
+ value: index + 1,
+ }));
+ }, [purchaseLimitCount]);
+
+ return (
+
+
+ 티켓 선택
+
+ {ticketCount}
+ 매 선택
+
+
+
+ 기본가
+
+ {`${ticketPrice}원`}
+
+ {
+ setTicketCount(Number(count));
+ }}
+ placeholder={`${ticketCount}`}
+ />
+
+
+
+
+ );
+}
+
+const Container = styled.div``;
+
+const Wrapper = styled.div`
+ padding: 16px 24px;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.secondary[150]};
+ display: flex;
+ justify-content: space-between;
+`;
+
+const SelectedTicketCount = styled.div`
+ display: flex;
+ align-items: center;
+`;