diff --git a/src/app/(global)/(mypage)/myCreateExperiences/page.tsx b/src/app/(global)/(mypage)/myCreateExperiences/page.tsx index 8308a3c..d595220 100644 --- a/src/app/(global)/(mypage)/myCreateExperiences/page.tsx +++ b/src/app/(global)/(mypage)/myCreateExperiences/page.tsx @@ -1,9 +1,9 @@ 'use client'; import AvailableTimeSlots from '@/src/components/pages/myCreateExperiences/AvailableTimeSlots'; -import Dropdown from '@/src/components/pages/myCreateExperiences/Dropdown'; import UploadBannerImage from '@/src/components/pages/myCreateExperiences/UploadBannerImage'; import Button from '@/src/components/primitives/Button'; +import Dropdown from '@/src/components/primitives/Dropdown'; import FormInput from '@/src/components/primitives/input/FormInput'; import AlertModal from '@/src/components/primitives/modal/AlertModal'; import ConfirmModal from '@/src/components/primitives/modal/ConfirmModal'; @@ -37,6 +37,14 @@ interface ExperiencesFormData { } export default function MyCreateExperiencesPage() { + const dropdownItems = [ + { id: 1, title: '문화 ∙ 예술' }, + { id: 2, title: '식음료' }, + { id: 3, title: '스포츠' }, + { id: 4, title: '투어' }, + { id: 5, title: '관광' }, + { id: 6, title: '웰빙' }, + ]; const { register, handleSubmit, @@ -137,14 +145,7 @@ export default function MyCreateExperiencesPage() { render={({ field, fieldState }) => ( diff --git a/src/app/(global)/(mypage)/myInfo/_components/myReservationStatus.tsx b/src/app/(global)/(mypage)/myInfo/_components/myReservationStatus.tsx deleted file mode 100644 index c23cbb6..0000000 --- a/src/app/(global)/(mypage)/myInfo/_components/myReservationStatus.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MyReservationStatusPage() { - return ( -
-

예약 현황

-
- ); -} diff --git a/src/app/(global)/(mypage)/myInfo/page.tsx b/src/app/(global)/(mypage)/myInfo/page.tsx index 6e59669..488f462 100644 --- a/src/app/(global)/(mypage)/myInfo/page.tsx +++ b/src/app/(global)/(mypage)/myInfo/page.tsx @@ -10,7 +10,7 @@ import { TabContext } from './pageContext'; import LeftSidebar from '@/src/components/pages/sidebar/LeftSidebar'; import MyExperiencesPage from './_components/myExperiences'; import MyInfoPage from './_components/myInfo'; -import MyReservationStatusPage from './_components/myReservationStatus'; +import MyReservationStatusPage from '@/src/components/pages/myReservationStatus/myReservationStatus'; import MyReservation from '@/src/components/pages/myReservation/MyReservation'; export interface ISidebarButtons { @@ -53,7 +53,10 @@ export default function MypageLayout() { return ( -
+
@@ -32,7 +33,7 @@ export default function MyExperienceCard({ data }: { data: IActivity }) {
행사 이미지 URL diff --git a/src/components/pages/myReservationStatus/ReservationCalendar.tsx b/src/components/pages/myReservationStatus/ReservationCalendar.tsx index 0890311..c470391 100644 --- a/src/components/pages/myReservationStatus/ReservationCalendar.tsx +++ b/src/components/pages/myReservationStatus/ReservationCalendar.tsx @@ -12,10 +12,12 @@ import { useReservationStore } from '@/src/store/ReservationStore'; export default function ReservationCalendar({ schedule, }: { - schedule: IReservedSchedule[]; + schedule: IReservedSchedule[] | null; }) { - const { displayController } = useReservationStore(); - const daysArray = getDaysArray(new Date()); + const displayController = useReservationStore( + (state) => state.displayController + ); + const daysArray = getDaysArray(displayController.dateToDisplay); const yoils = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; const handleLeftClick = () => { diff --git a/src/components/pages/myReservationStatus/ReservationDay.tsx b/src/components/pages/myReservationStatus/ReservationDay.tsx index 4d29aa8..519fa24 100644 --- a/src/components/pages/myReservationStatus/ReservationDay.tsx +++ b/src/components/pages/myReservationStatus/ReservationDay.tsx @@ -5,7 +5,10 @@ import { IScheduleCount } from '@/src/types/scheduleType'; import { dateToCalendarDate } from '@/src/utils/dateParser'; import { format } from 'date-fns'; import DayModal from './modal/DayModal'; -import { useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { useBreakPoint } from '@/src/hooks/useBreakPoint'; +import PopupWrapper from '../../primitives/popup/PopupWrapper'; +import ResponsiveDayModal from './modal/ResponsiveDayModal'; export default function ReservationDay({ date, @@ -21,6 +24,8 @@ export default function ReservationDay({ const currentMonth = format(displayMonth, 'MM'); const calendarDate = dateToCalendarDate(date); const [isModalVisible, setIsModalVisible] = useState(false); + const modalRef = useRef(null); + const { isLg } = useBreakPoint(); const prevNextMonthClasses = calendarDate.month !== currentMonth ? 'text-gray-200' : 'text-black'; @@ -32,6 +37,17 @@ export default function ReservationDay({ (calendarDate.yoil === 0 || calendarDate.yoil === 6) && 'text-red-500'; + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(e.target as Node)) + setIsModalVisible(false); + }; + + document.addEventListener('mousedown', handleClickOutside); + + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + if (!schedule) { return (
)} - {isModalVisible && ( - + {isLg && isModalVisible ? ( + + ) : ( + + + )}
); diff --git a/src/components/pages/myReservationStatus/modal/DayModal.tsx b/src/components/pages/myReservationStatus/modal/DayModal.tsx index 0113d8e..3016fd1 100644 --- a/src/components/pages/myReservationStatus/modal/DayModal.tsx +++ b/src/components/pages/myReservationStatus/modal/DayModal.tsx @@ -1,25 +1,230 @@ import Tabs from '@/src/components/primitives/Tabs'; -import { IScheduleCount } from '@/src/types/scheduleType'; +import { IScheduleCount, TScheduleStatus } from '@/src/types/scheduleType'; +import { ComponentPropsWithRef, useEffect, useState } from 'react'; +import CloseIcon from '@/public/images/icons/DeleteIcon.svg'; +import Dropdown from '@/src/components/primitives/Dropdown'; +import { useActivityIdStore } from '@/src/store/ReservationStore'; +import { patchReservationStatus } from '@/src/services/pages/myReservationStatus/myActivities'; +import { format } from 'date-fns'; +import LoadingSpinner from '@/src/components/primitives/LoadingSpinner'; +import { MyReservationItem } from '@/src/types/myReservationType'; +import ExperienceButton from '../../myExperiences/ExperienceButton'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { reservationQueries } from '@/src/services/primitives/queries'; + +interface IProps extends ComponentPropsWithRef<'div'> { + setIsModalVisible: (state: boolean) => void; + selectedDate: Date; + schedule: IScheduleCount; +} export default function DayModal({ - setIsVisible, + setIsModalVisible, + selectedDate, schedule, + ...props +}: IProps) { + const activityId = useActivityIdStore((state) => state.activityId); + const [status, setStatus] = useState('pending'); + const [scheduleId, setScheduleId] = useState(null); + + const { data: timeSchedule } = useQuery( + reservationQueries.timeScheduleOptions(activityId, scheduleId, status) + ); + const { data: daySchedule } = useQuery( + reservationQueries.dayScheduleOptions( + activityId, + format(selectedDate, 'yyyy-MM-dd') + ) + ); + + useEffect(() => { + const body = document.getElementById('my-info-body'); + body?.classList.toggle('overflow-y-scroll'); + + return () => { + body?.classList.toggle('overflow-hidden'); + }; + }, [activityId, selectedDate]); + + return ( +
+ {daySchedule ? ( + +
+

+ {selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월{' '} + {selectedDate.getDate()}일 +

+ +
+ + setStatus('pending')} value='pending'> + 신청 {schedule.pending} + + setStatus('confirmed')} + value='confirmed' + > + 승인 {schedule.confirmed} + + setStatus('declined')} + value='declined' + > + 거절 + + +

예약 시간

+ { + return { + id: el.scheduleId, + title: `${el.startTime} ~ ${el.endTime}`, + }; + })} + label='' + value={'time'} + onChange={setScheduleId} + /> + {timeSchedule && ( + <> + + el.status === 'pending' + )} + activitiyId={activityId} + scheduleId={scheduleId!} + /> + + + el.status === 'confirmed' + )} + activitiyId={activityId} + scheduleId={scheduleId!} + /> + + + el.status === 'declined' + )} + activitiyId={activityId} + scheduleId={scheduleId!} + /> + + + )} +
+ ) : ( + + )} +
+ ); +} + +function TabsContent({ + data, + activitiyId, + scheduleId, }: { - setIsVisible: (state: boolean) => void; - schedule: IScheduleCount; + data: MyReservationItem[]; + activitiyId: number; + scheduleId: number; }) { + const queryClient = useQueryClient(); + const handleConfirmButtonClick = (reservationId: number) => { + patchReservationStatus(activitiyId, reservationId, 'confirmed'); + queryClient.invalidateQueries({ + queryKey: [ + reservationQueries.timeSchedule(scheduleId), + reservationQueries.daySchedule(activitiyId), + ], + }); + }; + const handleDeclineButtonClick = (reservationId: number) => { + patchReservationStatus(activitiyId, reservationId, 'declined'); + queryClient.invalidateQueries({ + queryKey: [ + reservationQueries.timeSchedule(scheduleId), + reservationQueries.daySchedule(activitiyId), + ], + }); + }; return ( -
- - - 신청 - 승인 - 거절 - - 므아는 므아다 - 지경은 므아지경이다 - 개굴풋푸~ - -
+ <> +

예약 내역

+ {data.map((el) => ( +
+
+ 닉네임 +

{el.nickname}

+ 인원 +

{el.headCount}

+
+ +
+ ))} + ); } + +function ReservationChip({ + data, + handleConfirmButtonClick, + handleDeclineButtonClick, +}: { + data: MyReservationItem; + handleConfirmButtonClick: (id: number) => void; + handleDeclineButtonClick: (id: number) => void; +}) { + switch (data.status) { + case 'confirmed': + return ( +
+

예약 승인

+
+ ); + case 'declined': + return ( +
+

예약 거절

+
+ ); + default: + return ( +
+ handleConfirmButtonClick(data.id)} + > + 승인하기 + + handleDeclineButtonClick(data.id)} + > + 거절하기 + +
+ ); + } +} diff --git a/src/components/pages/myReservationStatus/modal/ResponsiveDayModal.tsx b/src/components/pages/myReservationStatus/modal/ResponsiveDayModal.tsx new file mode 100644 index 0000000..8803796 --- /dev/null +++ b/src/components/pages/myReservationStatus/modal/ResponsiveDayModal.tsx @@ -0,0 +1,231 @@ +import Tabs from '@/src/components/primitives/Tabs'; +import { IScheduleCount, TScheduleStatus } from '@/src/types/scheduleType'; +import { ComponentPropsWithRef, useEffect, useState } from 'react'; +import CloseIcon from '@/public/images/icons/DeleteIcon.svg'; +import Dropdown from '@/src/components/primitives/Dropdown'; +import { useActivityIdStore } from '@/src/store/ReservationStore'; +import { patchReservationStatus } from '@/src/services/pages/myReservationStatus/myActivities'; +import { format } from 'date-fns'; +import LoadingSpinner from '@/src/components/primitives/LoadingSpinner'; +import { MyReservationItem } from '@/src/types/myReservationType'; +import ExperienceButton from '../../myExperiences/ExperienceButton'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { reservationQueries } from '@/src/services/primitives/queries'; + +interface IProps extends ComponentPropsWithRef<'div'> { + setIsModalVisible: (state: boolean) => void; + selectedDate: Date; + schedule: IScheduleCount; +} + +export default function ResponsiveDayModal({ + setIsModalVisible, + selectedDate, + schedule, + ...props +}: IProps) { + const activityId = useActivityIdStore((state) => state.activityId); + const [status, setStatus] = useState('pending'); + const [scheduleId, setScheduleId] = useState(null); + + const { data: timeSchedule } = useQuery( + reservationQueries.timeScheduleOptions(activityId, scheduleId, status) + ); + const { data: daySchedule } = useQuery( + reservationQueries.dayScheduleOptions( + activityId, + format(selectedDate, 'yyyy-MM-dd') + ) + ); + + useEffect(() => { + const body = document.getElementById('my-info-body'); + body?.classList.toggle('overflow-y-scroll'); + + return () => { + body?.classList.toggle('overflow-hidden'); + }; + }, []); + + return ( +
+ {daySchedule ? ( + +
+

+ {selectedDate.getFullYear()}년 {selectedDate.getMonth() + 1}월{' '} + {selectedDate.getDate()}일 +

+ +
+ + setStatus('pending')} value='pending'> + 신청 {schedule.pending} + + setStatus('confirmed')} + value='confirmed' + > + 승인 {schedule.confirmed} + + setStatus('declined')} + value='declined' + > + 거절 + + +
+
+

예약 시간

+ { + return { + id: el.scheduleId, + title: `${el.startTime} ~ ${el.endTime}`, + }; + })} + label='' + value={'time'} + onChange={setScheduleId} + /> +
+ {timeSchedule && ( + <> + + el.status === 'pending' + )} + activitiyId={activityId} + scheduleId={scheduleId!} + /> + + + el.status === 'confirmed' + )} + activitiyId={activityId} + scheduleId={scheduleId!} + /> + + + el.status === 'declined' + )} + activitiyId={activityId} + scheduleId={scheduleId!} + /> + + + )} +
+
+ ) : ( + + )} +
+ ); +} + +function TabsContent({ + data, + activitiyId, + scheduleId, +}: { + data: MyReservationItem[]; + activitiyId: number; + scheduleId: number; +}) { + const queryClient = useQueryClient(); + const handleConfirmButtonClick = (reservationId: number) => { + patchReservationStatus(activitiyId, reservationId, 'confirmed'); + queryClient.invalidateQueries({ + queryKey: [ + reservationQueries.timeSchedule(scheduleId), + reservationQueries.daySchedule(activitiyId), + ], + }); + }; + const handleDeclineButtonClick = (reservationId: number) => { + patchReservationStatus(activitiyId, reservationId, 'declined'); + queryClient.invalidateQueries({ + queryKey: [ + reservationQueries.timeSchedule(scheduleId), + reservationQueries.daySchedule(activitiyId), + ], + }); + }; + return ( + <> +

예약 내역

+ {data.map((el) => ( +
+
+ 닉네임 +

{el.nickname}

+ 인원 +

{el.headCount}

+
+ +
+ ))} + + ); +} + +function ReservationChip({ + data, + handleConfirmButtonClick, + handleDeclineButtonClick, +}: { + data: MyReservationItem; + handleConfirmButtonClick: (id: number) => void; + handleDeclineButtonClick: (id: number) => void; +}) { + switch (data.status) { + case 'confirmed': + return ( +
+

예약 승인

+
+ ); + case 'declined': + return ( +
+

예약 거절

+
+ ); + default: + return ( +
+ handleConfirmButtonClick(data.id)} + > + 승인하기 + + handleDeclineButtonClick(data.id)} + > + 거절하기 + +
+ ); + } +} diff --git a/src/components/pages/myReservationStatus/myReservationStatus.tsx b/src/components/pages/myReservationStatus/myReservationStatus.tsx new file mode 100644 index 0000000..dc644f6 --- /dev/null +++ b/src/components/pages/myReservationStatus/myReservationStatus.tsx @@ -0,0 +1,87 @@ +'use client'; + +import Dropdown from '../../primitives/Dropdown'; +import LoadingSpinner from '../../primitives/LoadingSpinner'; +import Image from 'next/image'; +import BackBtn from '../../primitives/mypage/BackBtn'; +import { Activity } from '@/src/types/activityType'; +import ReservationCalendar from './ReservationCalendar'; +import { + useActivityIdStore, + useReservationStore, +} from '@/src/store/ReservationStore'; +import { format } from 'date-fns'; +import { useQuery } from '@tanstack/react-query'; +import { queries, reservationQueries } from '@/src/services/primitives/queries'; +import { useShallow } from 'zustand/shallow'; + +export default function MyReservationStatusPage() { + const { data: activities, isPending } = useQuery( + queries.myExperiencesOptions() + ); + + if (isPending || !activities) return ; + + return ( +
+
+
+ +

예약 현황

+
+

+ 내 체험에 예약된 내역들을 한 눈에 확인할 수 있습니다. +

+
+ {activities && activities.totalCount !== 0 ? ( + + ) : ( +
+ 찾을 수 없습니다 + + 아직 등록한 체험이 없어요 + +
+ )} +
+ ); +} + +function DropdownAndCalendar({ data }: { data: Activity[] }) { + const { activityId, setActivityId } = useActivityIdStore( + useShallow((state) => ({ + activityId: state.activityId, + setActivityId: state.setActivityId, + })) + ); + // const [schedule, setSchedule] = useState(null); + const selectedDate = useReservationStore( + (state) => state.displayController.dateToDisplay + ); + + const { data: schedule } = useQuery( + reservationQueries.monthScheduleOptions( + activityId, + format(selectedDate, 'yyyy'), + format(selectedDate, 'MM') + ) + ); + + return ( + <> + + {schedule && } + + ); +} diff --git a/src/components/pages/myCreateExperiences/Dropdown.tsx b/src/components/primitives/Dropdown.tsx similarity index 56% rename from src/components/pages/myCreateExperiences/Dropdown.tsx rename to src/components/primitives/Dropdown.tsx index fe14600..8c5f7b9 100644 --- a/src/components/pages/myCreateExperiences/Dropdown.tsx +++ b/src/components/primitives/Dropdown.tsx @@ -1,13 +1,19 @@ 'use client'; +import { cn } from '@/src/utils/cn'; import Image from 'next/image'; import { useEffect, useRef, useState } from 'react'; +interface DropdownItem { + id: number; + title: string; +} + interface DropdownProps { label: string; - items: string[]; + items: DropdownItem[]; value: string | null; - onChange: (value: string) => void; + onChange: (id: number) => void; error?: string; } @@ -18,7 +24,27 @@ export default function Dropdown({ onChange, }: DropdownProps) { const [isOpen, setIsOpen] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [contentUp, setContentUp] = useState(false); const dropdownRef = useRef(null); + const contentRef = useRef(null); + + useEffect(() => { + if (isOpen && dropdownRef.current && contentRef.current) { + const dropdownRect = dropdownRef.current.getBoundingClientRect(); + const contentHeight = contentRef.current.offsetHeight; + const viewportHeight = window.innerHeight; + + const spaceBelow = viewportHeight - dropdownRect.bottom; + const spaceAbove = dropdownRect.top; + + if (spaceBelow < contentHeight && spaceAbove > contentHeight) { + setContentUp(true); + } else { + setContentUp(false); + } + } + }, [isOpen]); useEffect(() => { function handleClickOutside(event: MouseEvent) { @@ -44,7 +70,7 @@ export default function Dropdown({ className='inline-flex w-full justify-between items-center border border-gray-100 rounded-2xl px-5 py-4 font-medium text-gray-400 mt-2.5' > - {value ?? '카테고리를 선택해 주세요'} + {selectedItem?.title ?? '카테고리를 선택해 주세요'} {isOpen && ( -
+
{items.map((item) => ( ))}
diff --git a/src/components/primitives/Tabs.tsx b/src/components/primitives/Tabs.tsx index da31d1b..67005b8 100644 --- a/src/components/primitives/Tabs.tsx +++ b/src/components/primitives/Tabs.tsx @@ -40,14 +40,19 @@ function List({ children }: { children: React.ReactNode }) { function Trigger({ children, value, + onClick, }: { children: React.ReactNode; value: string; + onClick: () => void; }) { const { value: selectedValue, setValue } = useContext(TabsContext); return (