diff --git a/src/components/calendar/ListBody/index.tsx b/src/components/calendar/ListBody/index.tsx index daeeed2b..54bb8b0b 100644 --- a/src/components/calendar/ListBody/index.tsx +++ b/src/components/calendar/ListBody/index.tsx @@ -3,14 +3,14 @@ import classNames from 'classnames/bind'; import ListCard from '@/components/calendar/ListCard'; import EmptyCard from '@/components/layout/empty/EmptyCard'; -import { MonthlySchedule } from '@/types'; +import { MonthlyReservationResponse } from '@/types'; import styles from './ListBody.module.scss'; const cx = classNames.bind(styles); type ListBodyProps = { - schedules?: MonthlySchedule[]; + schedules?: MonthlyReservationResponse[]; onClick: (date: string) => void; }; diff --git a/src/components/calendar/ModalCard/ModalCard.module.scss b/src/components/calendar/ModalCard/ModalCard.module.scss new file mode 100644 index 00000000..a2107411 --- /dev/null +++ b/src/components/calendar/ModalCard/ModalCard.module.scss @@ -0,0 +1,32 @@ +.modal-card { + &-container { + @include column-flexbox(start, stretch, 0.8rem); + + padding: 1.2rem; + background-color: $black80; + border-radius: 0.8rem; + } + + &-text { + @include column-flexbox(start, stretch, 0.4rem); + + &-line { + @include flexbox(start, center, 0.8rem); + } + + &-title { + @include text-style(14, $gray30); + + display: inline-block; + width: 4rem; + } + + &-value { + @include text-style(14, $white); + } + } + + &-button { + @include flexbox(end, center, 0.8rem); + } +} diff --git a/src/components/calendar/ModalCard/index.tsx b/src/components/calendar/ModalCard/index.tsx new file mode 100644 index 00000000..14137505 --- /dev/null +++ b/src/components/calendar/ModalCard/index.tsx @@ -0,0 +1,52 @@ +import { Fragment } from 'react'; + +import classNames from 'classnames/bind'; + +import Badge from '@/components/commons/Badge'; +import { BaseButton } from '@/components/commons/buttons'; + +import { MyReservationsStatus } from '@/types'; + +import styles from './ModalCard.module.scss'; + +const cx = classNames.bind(styles); + +type ModalCardProps = { + nickName: string; + headCount: number; + status: MyReservationsStatus; + onClickButton: (text: string) => void; +}; + +const ModalCard = ({ nickName, headCount, status, onClickButton }: ModalCardProps) => { + return ( +
+
+
+ 닉네임 + {nickName} +
+
+ 인원 + {headCount}명 +
+
+
+ {status === 'pending' ? ( + + onClickButton('거절')}> + 거절 + + onClickButton('승인')}> + 승인 + + + ) : ( + + )} +
+
+ ); +}; + +export default ModalCard; diff --git a/src/components/calendar/ModalContents/ModalContents.module.scss b/src/components/calendar/ModalContents/ModalContents.module.scss index e69de29b..7e82a232 100644 --- a/src/components/calendar/ModalContents/ModalContents.module.scss +++ b/src/components/calendar/ModalContents/ModalContents.module.scss @@ -0,0 +1,72 @@ +.schedule-modal { + &-container { + @include column-flexbox(start, stretch, 1.6rem); + + @include responsive(T) { + justify-content: space-between; + height: 100%; + } + } + + &-date { + @include column-flexbox(start, stretch, 0.8rem); + + &-title { + @include text-style(16, $white, bold); + } + + &-korean { + @include text-style(14, $gray10); + } + } + + &-reservation { + @include column-flexbox(start, stretch, 0.8rem); + + @include responsive(T) { + flex-grow: 1; + } + + &-title { + @include flexbox(start, center, 0.4rem); + @include text-style(16, $white, bold); + } + + &-count { + @include text-style(14, $primary, bold); + } + + &-card { + @include column-flexbox(start, stretch, 0.8rem); + + overflow-y: auto; + max-height: 23.2rem; + + &::-webkit-scrollbar { + width: 0.6rem; + } + + &::-webkit-scrollbar-track { + background: $opacity-white-5; + border-radius: 9.9rem; + } + + &::-webkit-scrollbar-thumb { + background-color: $gray10; + border-radius: 9.9rem; + } + + &.scroll { + padding-right: 0.8rem; + } + } + } + + &-close { + @include responsive(T) { + margin: 0; + } + + margin: 0 auto; + } +} diff --git a/src/components/calendar/ModalContents/index.tsx b/src/components/calendar/ModalContents/index.tsx index d62264d3..7b188bd6 100644 --- a/src/components/calendar/ModalContents/index.tsx +++ b/src/components/calendar/ModalContents/index.tsx @@ -1,11 +1,112 @@ +import { MouseEventHandler, useState } from 'react'; + import classNames from 'classnames/bind'; +import { getDateStringKR, getScheduleDropdownOption, getStatusCountByScheduleId } from '@/utils'; + +import ModalCard from '@/components/calendar/ModalCard'; +import { BaseButton } from '@/components/commons/buttons'; +import Dropdown from '@/components/commons/Dropdown'; +import Tab from '@/components/commons/Tab'; +import EmptyCard from '@/components/layout/empty/EmptyCard'; +import reservationDetailMockDataConfirmed from '@/constants/mockData/reservationDetailMockDataConfirmed.json'; +import reservationDetailMockDataDeclined from '@/constants/mockData/reservationDetailMockDataDeclined.json'; +import reservationDetailMockDataPending from '@/constants/mockData/reservationDetailMockDataPending.json'; +import reservationDetailMockDataPendingNoData from '@/constants/mockData/reservationDetailMockDataPendingNoData.json'; +import DailyScheduleMockData from '@/constants/mockData/reservedScheduleMockData.json'; +import { useDeviceType } from '@/hooks/useDeviceType'; + +import { DailyReservationResponse, DetailReservationResponse, MyReservationsStatus, StatusTabOptions } from '@/types'; + import styles from './ModalContents.module.scss'; const cx = classNames.bind(styles); -const ModalContents = () => { - return
ModalContents
; +type ModalContentsProps = { + gameId: number; + activeDate: string; + onClickCloseButton: MouseEventHandler; + onClickCardButton: (text: string) => void; +}; + +const ModalContents = ({ gameId, activeDate, onClickCloseButton, onClickCardButton }: ModalContentsProps) => { + const DailyMockData: Record = { + '2024-03-27': DailyScheduleMockData['2024-03-27'], + '2024-03-30': DailyScheduleMockData['2024-03-30'], + '2024-04-01': DailyScheduleMockData['2024-04-01'], + }; + + const ReservationMockData: Record = { + '1-0-confirmed': reservationDetailMockDataConfirmed as DetailReservationResponse, + '1-0-declined': reservationDetailMockDataDeclined as DetailReservationResponse, + '1-0-pending': reservationDetailMockDataPending as DetailReservationResponse, + '1-1-pending': reservationDetailMockDataPendingNoData as DetailReservationResponse, + }; + + const dropdownOptions = getScheduleDropdownOption(DailyMockData[activeDate]); + const statusCountByScheduleId = getStatusCountByScheduleId(DailyMockData[activeDate]); + + const [scheduleId, setScheduleId] = useState(dropdownOptions[0].value as number); + + const statusCount = statusCountByScheduleId[scheduleId]; + + const statusTabOptions: StatusTabOptions[] = [ + { id: 'pending', text: '신청', count: statusCount.pending }, + { id: 'confirmed', text: '승인', count: statusCount.confirmed }, + { id: 'declined', text: '거절', count: statusCount.declined }, + ]; + + const [selectedStatus, setSelectedStatus] = useState(statusTabOptions[0].id); + + const currentDeviceType = useDeviceType(); + + const { totalCount, reservations } = ReservationMockData[`${gameId}-${scheduleId}-${selectedStatus}`]; + + const handleChangeTabId = (selectedId: string | number) => { + setSelectedStatus(selectedId as MyReservationsStatus); + }; + + const handleChangeScheduleId = (value: string | number) => { + setScheduleId(value as number); + }; + + return ( +
+ +
+

예약 날짜

+ {getDateStringKR(activeDate)} + +
+
+

+ 예약 내역 + {totalCount} +

+ {totalCount !== 0 ? ( +
    2 })}> + {reservations.map(({ nickname, headCount, status, id }) => ( +
  • + +
  • + ))} +
+ ) : ( + + )} +
+
+ + 닫기 + +
+
+ ); }; export default ModalContents; diff --git a/src/components/calendar/index.tsx b/src/components/calendar/index.tsx index b3fffe7e..ca6c3ee1 100644 --- a/src/components/calendar/index.tsx +++ b/src/components/calendar/index.tsx @@ -2,18 +2,21 @@ import { useEffect, useState } from 'react'; import classNames from 'classnames/bind'; -import { getCurrentDate, scheduleListToObjectByDate } from '@/utils'; +import { getCurrentDate, getScheduleByDate } from '@/utils'; import CalendarBody from '@/components/calendar/CalendarBody'; import CalendarHeader from '@/components/calendar/CalendarHeader'; import ListBody from '@/components/calendar/ListBody'; +import ModalContents from '@/components/calendar/ModalContents'; +import { CommonModal, ConfirmModal, ModalButton } from '@/components/commons/modals'; import MockMonthlySchedule1 from '@/constants/mockData/myScheduleMockDataMonth1.json'; import MockMonthlySchedule2 from '@/constants/mockData/myScheduleMockDataMonth2.json'; import MockMonthlySchedule3 from '@/constants/mockData/myScheduleMockDataMonth3.json'; import MockMonthlySchedule4 from '@/constants/mockData/myScheduleMockDataMonth4.json'; import { useDeviceType } from '@/hooks/useDeviceType'; +import useMultiState from '@/hooks/useMultiState'; -import { MonthlySchedule } from '@/types'; +import { MonthlyReservationResponse } from '@/types'; import styles from './Calendar.module.scss'; @@ -26,7 +29,7 @@ const getMonthlyMockData = (year: number, month: number) => { const yearText = year.toString(); const monthText = month.toString().padStart(2, '0'); - const MonthlyMockData: Record = { + const MonthlyMockData: Record = { '2024/01': MockMonthlySchedule1, '2024/02': MockMonthlySchedule2, '2024/03': MockMonthlySchedule3, @@ -46,9 +49,11 @@ const Calendar = ({ gameId }: CalendarProps) => { const [isCalendar, setIsCalendar] = useState(true); const [currentYear, setCurrentYear] = useState(today.year); const [currentMonth, setCurrentMonth] = useState(today.month); - const [monthlySchedule, setMonthlySchedule] = useState([]); + const [monthlySchedule, setMonthlySchedule] = useState([]); const [activeDate, setActiveDate] = useState(''); + const [confirmText, setConfirmText] = useState('승인'); + const { multiState, toggleClick } = useMultiState(['scheduleModal, confirmModal']); const currentDeviceType = useDeviceType(); const handleChangeMonth = (addNumber: number) => { @@ -67,6 +72,12 @@ const Calendar = ({ gameId }: CalendarProps) => { const handleScheduleClick = (date: string) => { setActiveDate(date); + toggleClick('scheduleModal'); + }; + + const handleConfirmClick = (text: string) => { + setConfirmText(text); + toggleClick('confirmModal'); }; useEffect(() => { @@ -79,30 +90,59 @@ const Calendar = ({ gameId }: CalendarProps) => { setMonthlySchedule(scheduleData); }, [currentYear, currentMonth]); - console.log(gameId); - console.log(activeDate); - return ( -
- - {isCalendar ? ( - +
+ - ) : ( - - )} -
+ {isCalendar ? ( + + ) : ( + + )} +
+ toggleClick('scheduleModal')} + title={'예약 정보'} + isResponsive + renderContent={ + toggleClick('scheduleModal')} + onClickCardButton={handleConfirmClick} + /> + } + /> + toggleClick('confirmModal')} + state='WARNING' + title={`예약 신청을 ${confirmText}하시겠습니까?`} + desc={`한번 ${confirmText}한 예약은 되돌릴 수 없습니다.`} + renderButton={ + <> + toggleClick('confirmModal')}> + 확인 + + toggleClick('confirmModal')}>취소 + + } + /> + ); }; diff --git a/src/components/commons/Dropdown/Dropdown.module.scss b/src/components/commons/Dropdown/Dropdown.module.scss index cfcc2409..f77865ed 100644 --- a/src/components/commons/Dropdown/Dropdown.module.scss +++ b/src/components/commons/Dropdown/Dropdown.module.scss @@ -21,10 +21,15 @@ cursor: pointer; + overflow: hidden; + width: 100%; height: 4.8rem; padding: 0 5.2rem 0 1.6rem; + text-overflow: ellipsis; + white-space: nowrap; + background: $input-background; border: 0.1rem solid $input-stroke; border-radius: 0.8rem; diff --git a/src/components/commons/modals/CommonModal.module.scss b/src/components/commons/modals/CommonModal.module.scss index 874fccc5..4652852b 100644 --- a/src/components/commons/modals/CommonModal.module.scss +++ b/src/components/commons/modals/CommonModal.module.scss @@ -66,6 +66,47 @@ &-content { width: 100%; } + + &.responsive { + @include responsive(T) { + position: fixed; + inset: 0; + + width: 100vw; + max-height: 100vh; + + background-color: $black70; + + &::before, + &::after { + display: none; + } + + .modal-inner { + height: 100%; + } + + .modal-mobile-nav { + width: 100%; + height: 5.6rem; + border-bottom: 0.1rem solid $opacity-white-10; + } + + .modal-header-title { + padding-top: 2.4rem; + } + + .modal-content { + height: 100%; + } + } + } + + &:not(.responsive) { + .modal-mobile-nav { + display: none; + } + } } .body-open { diff --git a/src/components/commons/modals/CommonModal.tsx b/src/components/commons/modals/CommonModal.tsx index d3999e0b..635227d0 100644 --- a/src/components/commons/modals/CommonModal.tsx +++ b/src/components/commons/modals/CommonModal.tsx @@ -1,20 +1,27 @@ +import Image from 'next/image'; + import { ReactNode, MouseEventHandler } from 'react'; import classNames from 'classnames/bind'; import ReactModal from 'react-modal'; +import { SVGS } from '@/constants'; + import styles from './CommonModal.module.scss'; const cx = classNames.bind(styles); +const { url, alt } = SVGS.arrow.chevron; + type CommonModalProps = { openModal: boolean; onClose: MouseEventHandler; title: string; renderContent: ReactNode; + isResponsive?: boolean; }; -export const CommonModal = ({ openModal, onClose, title, renderContent }: CommonModalProps) => { +export const CommonModal = ({ openModal, onClose, title, renderContent, isResponsive }: CommonModalProps) => { return (
+
+ +

{title}

{renderContent}
diff --git a/src/components/layout/empty/EmptyCard/EmptyCard.module.scss b/src/components/layout/empty/EmptyCard/EmptyCard.module.scss index 6f057011..58125192 100644 --- a/src/components/layout/empty/EmptyCard/EmptyCard.module.scss +++ b/src/components/layout/empty/EmptyCard/EmptyCard.module.scss @@ -9,4 +9,8 @@ background-color: $black70; border: 0.1rem dashed $gray50; border-radius: 0.8rem; + + &.small { + height: 11.2rem; + } } diff --git a/src/components/layout/empty/EmptyCard/index.tsx b/src/components/layout/empty/EmptyCard/index.tsx index 47d426c9..375656ae 100644 --- a/src/components/layout/empty/EmptyCard/index.tsx +++ b/src/components/layout/empty/EmptyCard/index.tsx @@ -10,13 +10,14 @@ const cx = classNames.bind(styles); type EmptyCardProps = { text: string; + isSmall?: boolean; }; const { url, alt } = SVGS.empty; -const EmptyCard = ({ text }: EmptyCardProps) => { +const EmptyCard = ({ text, isSmall }: EmptyCardProps) => { return ( -
+
{alt}
{text}
diff --git a/src/constants/mockData/reservationDetailMockData.json b/src/constants/mockData/reservationDetailMockDataConfirmed.json similarity index 74% rename from src/constants/mockData/reservationDetailMockData.json rename to src/constants/mockData/reservationDetailMockDataConfirmed.json index a7a34d58..d2799b2d 100644 --- a/src/constants/mockData/reservationDetailMockData.json +++ b/src/constants/mockData/reservationDetailMockDataConfirmed.json @@ -1,18 +1,18 @@ { "cursorId": 0, - "totalCount": 3, + "totalCount": 1, "reservations": [ { "id": 1, - "nickname": "tester", + "nickname": "testerc", "userId": 0, - "teamId": "test", + "teamId": "testerc", "activityId": 0, "scheduleId": 0, - "status": "pending", + "status": "confirmed", "reviewSubmitted": false, "totalPrice": 0, - "headCount": 2, + "headCount": 1, "date": "2024-03-27", "startTime": "09:00", "endTime": "12:00", diff --git a/src/constants/mockData/reservationDetailMockDataDeclined.json b/src/constants/mockData/reservationDetailMockDataDeclined.json new file mode 100644 index 00000000..c8035b4b --- /dev/null +++ b/src/constants/mockData/reservationDetailMockDataDeclined.json @@ -0,0 +1,40 @@ +{ + "cursorId": 0, + "totalCount": 2, + "reservations": [ + { + "id": 1, + "nickname": "testerd", + "userId": 0, + "teamId": "testd", + "activityId": 0, + "scheduleId": 0, + "status": "declined", + "reviewSubmitted": false, + "totalPrice": 0, + "headCount": 10, + "date": "2024-03-27", + "startTime": "09:00", + "endTime": "12:00", + "createdAt": "2024-03-27T02:35:20.905Z", + "updatedAt": "2024-03-27T02:35:20.905Z" + }, + { + "id": 2, + "nickname": "testerd2", + "userId": 0, + "teamId": "testd2", + "activityId": 0, + "scheduleId": 0, + "status": "declined", + "reviewSubmitted": false, + "totalPrice": 0, + "headCount": 11, + "date": "2024-03-27", + "startTime": "09:00", + "endTime": "12:00", + "createdAt": "2024-03-27T02:35:20.905Z", + "updatedAt": "2024-03-27T02:35:20.905Z" + } + ] +} diff --git a/src/constants/mockData/reservationDetailMockDataPending.json b/src/constants/mockData/reservationDetailMockDataPending.json new file mode 100644 index 00000000..d5d4efe3 --- /dev/null +++ b/src/constants/mockData/reservationDetailMockDataPending.json @@ -0,0 +1,57 @@ +{ + "cursorId": 0, + "totalCount": 3, + "reservations": [ + { + "id": 1, + "nickname": "testerp", + "userId": 0, + "teamId": "testp", + "activityId": 0, + "scheduleId": 0, + "status": "pending", + "reviewSubmitted": false, + "totalPrice": 0, + "headCount": 2, + "date": "2024-03-27", + "startTime": "09:00", + "endTime": "12:00", + "createdAt": "2024-03-27T02:35:20.905Z", + "updatedAt": "2024-03-27T02:35:20.905Z" + }, + { + "id": 2, + "nickname": "testerp2", + "userId": 0, + "teamId": "testp2", + "activityId": 0, + "scheduleId": 0, + "status": "pending", + "reviewSubmitted": false, + "totalPrice": 0, + "headCount": 3, + "date": "2024-03-27", + "startTime": "09:00", + "endTime": "12:00", + "createdAt": "2024-03-27T02:35:20.905Z", + "updatedAt": "2024-03-27T02:35:20.905Z" + }, + { + "id": 3, + "nickname": "testerp3", + "userId": 0, + "teamId": "testp3", + "activityId": 0, + "scheduleId": 0, + "status": "pending", + "reviewSubmitted": false, + "totalPrice": 0, + "headCount": 4, + "date": "2024-03-27", + "startTime": "09:00", + "endTime": "12:00", + "createdAt": "2024-03-27T02:35:20.905Z", + "updatedAt": "2024-03-27T02:35:20.905Z" + } + ] +} diff --git a/src/constants/mockData/reservationDetailMockDataPendingNoData.json b/src/constants/mockData/reservationDetailMockDataPendingNoData.json new file mode 100644 index 00000000..ee594a7a --- /dev/null +++ b/src/constants/mockData/reservationDetailMockDataPendingNoData.json @@ -0,0 +1,5 @@ +{ + "cursorId": 0, + "totalCount": 0, + "reservations": [] +} diff --git a/src/types/schedule.ts b/src/types/schedule.ts index b0fb91f9..48dbaa6b 100644 --- a/src/types/schedule.ts +++ b/src/types/schedule.ts @@ -1,4 +1,4 @@ -import { ReservationResponse } from '@/types'; +import { MyReservationsStatus, MyReservationsStatusKR, ReservationResponse } from '@/types'; export type AvailableSchedule = { date: string; @@ -18,22 +18,32 @@ export type DailyReservationCount = { pending: number; }; -export type MonthlySchedule = { +export type MonthlyReservationResponse = { date: string; reservations: MonthlyReservationCount; }; -export type DailySchedule = { +export type DailyReservationResponse = { scheduleId: number; startTime: string; endTime: string; count: DailyReservationCount; }; -export type DetailSchedule = { +export type DetailReservationResponse = { cursorId: 0; totalCount: 0; - reservations: Omit[]; + reservations: ReservationDetail[]; +}; + +export type ReservationDetail = Omit & { + nickname: string; }; export type ReservationsByDate = Record; + +export type StatusTabOptions = { + id: MyReservationsStatus; + text: MyReservationsStatusKR; + count: number; +}; diff --git a/src/utils/dataMap.ts b/src/utils/dataMap.ts index c12ae395..e9921535 100644 --- a/src/utils/dataMap.ts +++ b/src/utils/dataMap.ts @@ -1,6 +1,11 @@ import { PRICE_TO_POST_TYPES } from '@/constants'; -import { MonthlySchedule, ReservationsByDate } from '@/types'; +import { + DailyReservationCount, + DailyReservationResponse, + MonthlyReservationResponse, + ReservationsByDate, +} from '@/types'; import { formatCategoryToGameNameKR } from './gameFormatter'; @@ -29,10 +34,35 @@ export const splitDescByDelimiter = (inputString: string) => { }; }; -export const scheduleListToObjectByDate = (scheduleData: MonthlySchedule[] | undefined) => { +export const getScheduleByDate = (scheduleData: MonthlyReservationResponse[] | undefined) => { const result: ReservationsByDate = {}; - scheduleData?.forEach((schedule) => (result[schedule.date] = schedule.reservations)); + scheduleData?.forEach((schedule) => { + result[schedule.date] = schedule.reservations; + }); + + return result; +}; + +export const getStatusCountByScheduleId = (scheduleData: DailyReservationResponse[] | undefined) => { + const result: { [id: number]: DailyReservationCount } = {}; + + scheduleData?.forEach((schedule) => { + result[schedule.scheduleId] = schedule.count; + }); + + return result; +}; + +export const getScheduleDropdownOption = (scheduleData: DailyReservationResponse[] | undefined) => { + const result: { title: string; value: number | string }[] = []; + + scheduleData?.forEach((schedule) => { + result.push({ + title: `${schedule.startTime} - ${schedule.endTime}`, + value: schedule.scheduleId, + }); + }); return result; }; diff --git a/src/utils/getDate.ts b/src/utils/getDate.ts index c3a0e41c..c4cdc5fa 100644 --- a/src/utils/getDate.ts +++ b/src/utils/getDate.ts @@ -126,3 +126,5 @@ export const getMonthString = (month: number) => dayjs() .month(month - 1) .format('MMMM'); + +export const getDateStringKR = (date: string) => dayjs(date).format('YYYY년 M월 D일');