diff --git a/.vscode/settings.json b/.vscode/settings.json index b752fa4e..29508367 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,5 +37,6 @@ "YONGMIN", "JAEHYUN", "Kakao" - ] + ], + "claudeCodeChat.permissions.yoloMode": false } diff --git a/src/domain/Reservation/components/reservation-calendar/DayCell.tsx b/src/domain/Reservation/components/reservation-calendar/DayCell.tsx index 7ca4d26c..49e785a2 100644 --- a/src/domain/Reservation/components/reservation-calendar/DayCell.tsx +++ b/src/domain/Reservation/components/reservation-calendar/DayCell.tsx @@ -1,34 +1,22 @@ 'use client'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import dayjs from 'dayjs'; -import { X } from 'lucide-react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -import { - getReservationsBySchedule, - getSchedulesByDate, - type ScheduleItem, - updateReservationStatus, -} from '@/domain/Reservation/services/reservation-calendar'; +import { useCallback } from 'react'; + +import DayCellContent from '@/domain/Reservation/components/reservation-calendar/DayCellContent'; +import ReservationModalContent from '@/domain/Reservation/components/reservation-calendar/ReservationModalContent'; +import { useDayCellStyles } from '@/domain/Reservation/hooks/useDayCellStyles'; +import { useModalState } from '@/domain/Reservation/hooks/useModalState'; +import { useReservationCounts } from '@/domain/Reservation/hooks/useReservationCounts'; +import { useReservationMutations } from '@/domain/Reservation/hooks/useReservationMutations'; +import { useReservationQueries } from '@/domain/Reservation/hooks/useReservationQueries'; import { BottomSheet } from '@/shared/components/ui/bottom-sheet'; import Popover from '@/shared/components/ui/popover'; -import Tabs from '@/shared/components/ui/tabs'; -import { useToast } from '@/shared/hooks/useToast'; - -import type { - Reservation, - ReservationItem, - ReservationStatus, -} from '../../types/reservation'; -import { getColorClassByStatus, STATUS_LABELS } from '../../utils/reservation'; -import ReservationDetail from './ReservationDetail'; interface DayCellProps { day: dayjs.Dayjs; isCurrentMonth: boolean; isToday: boolean; isLastRow: boolean; - reservation: Reservation | null; selectedActivityId: number | null; displayMode?: 'popover' | 'bottomsheet'; // ๐Ÿ”ฅ UI ๋ชจ๋“œ ์„ ํƒ } @@ -38,379 +26,66 @@ export default function DayCell({ isCurrentMonth, isToday, isLastRow, - reservation, selectedActivityId, displayMode = 'popover', // ๐Ÿ”ฅ ๊ธฐ๋ณธ๊ฐ’์€ popover }: DayCellProps) { - const queryClient = useQueryClient(); - const [selectedScheduleId, setSelectedScheduleId] = useState( - null, - ); - // BottomSheet์šฉ ์ƒํƒœ - const [isOpen, setIsOpen] = useState(false); - const { showSuccess } = useToast(); - - const [activeTab, setActiveTab] = useState< - 'pending' | 'confirmed' | 'declined' - >('pending'); - const styles = useMemo(() => { - const cellClasses = ` - relative flex aspect-square cursor-pointer flex-col items-center justify-start p-1 md:p-2 text-center font-size-14 - hover:bg-gray-50 - ${!isLastRow ? 'border-b-[0.05rem] border-gray-100' : ''} - ${!isCurrentMonth ? 'bg-neutral-200 text-gray-400 opacity-50' : ''} - ${isToday ? 'border-blue-300 bg-blue-100' : ''} - `; - - const dayOfWeek = day.day(); - const dateClasses = `font-size-14 ${ - dayOfWeek === 0 - ? 'text-red-500' - : dayOfWeek === 6 - ? 'text-blue-500' - : isCurrentMonth - ? 'text-gray-900' - : '' - }`; - - return { cellClasses, dateClasses }; - }, [day, isCurrentMonth, isToday, isLastRow]); - - // 1. ๋‚ ์งœ๋ณ„ ์Šค์ผ€์ค„ ์กฐํšŒ (useQuery) - const { data: schedules = [] } = useQuery({ - queryKey: ['schedules', selectedActivityId, day.format('YYYY-MM-DD')], - queryFn: () => - getSchedulesByDate(selectedActivityId!, day.format('YYYY-MM-DD')), - enabled: !!selectedActivityId, - }); + const { isOpen, setIsOpen, activeTab, setActiveTab, handleClose } = + useModalState(); - useEffect(() => { - if (schedules && schedules.length > 0 && !selectedScheduleId) { - // pending ์˜ˆ์•ฝ์ด ์žˆ๋Š” ์ฒซ ๋ฒˆ์งธ ์Šค์ผ€์ค„ ์ฐพ๊ธฐ - const scheduleWithPending = schedules.find((s) => s.count.pending > 0); - // pending์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„, ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์Šค์ผ€์ค„ ์„ ํƒ - const targetSchedule = scheduleWithPending || schedules[0]; - setSelectedScheduleId(targetSchedule.scheduleId); - } - }, [schedules, selectedScheduleId]); + const styles = useDayCellStyles({ day, isCurrentMonth, isToday, isLastRow }); - // 2. ๋ชจ๋“  ์Šค์ผ€์ค„์˜ ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ (ํ†ตํ•ฉ ๋ฒ„์ „) - const { - data: reservationsByStatus = { pending: [], confirmed: [], declined: [] }, - } = useQuery<{ - pending: ReservationItem[]; - confirmed: ReservationItem[]; - declined: ReservationItem[]; - }>({ - queryKey: [ - 'allReservationsByDate', + const { schedules, reservationsByStatus, setSelectedScheduleId } = + useReservationQueries({ selectedActivityId, - day.format('YYYY-MM-DD'), - ], - queryFn: async () => { - if (!schedules || schedules.length === 0) { - return { pending: [], confirmed: [], declined: [] }; - } + day, + }); - // ๋ชจ๋“  ์Šค์ผ€์ค„์˜ ์˜ˆ์•ฝ์„ ๋ณ‘๋ ฌ๋กœ ์กฐํšŒ - const allPendingPromises = schedules.map((schedule) => - getReservationsBySchedule( - selectedActivityId!, - schedule.scheduleId, - 'pending', - ), - ); - const allConfirmedPromises = schedules.map((schedule) => - getReservationsBySchedule( - selectedActivityId!, - schedule.scheduleId, - 'confirmed', - ), - ); - const allDeclinedPromises = schedules.map((schedule) => - getReservationsBySchedule( - selectedActivityId!, - schedule.scheduleId, - 'declined', - ), - ); - - const [pendingResults, confirmedResults, declinedResults] = - await Promise.all([ - Promise.all(allPendingPromises), - Promise.all(allConfirmedPromises), - Promise.all(allDeclinedPromises), - ]); - - return { - pending: pendingResults - .flat() - .filter((item): item is ReservationItem => item !== null), - confirmed: confirmedResults - .flat() - .filter((item): item is ReservationItem => item !== null), - declined: declinedResults - .flat() - .filter((item): item is ReservationItem => item !== null), - }; - }, - enabled: !!selectedActivityId && !!schedules?.length, - }); - - // 3. 'ํ•˜๋‚˜ ์Šน์ธ ํ›„ ๋‚˜๋จธ์ง€ ๊ฑฐ์ ˆ' ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ „์šฉ ๋ฎคํ…Œ์ด์…˜ - const { mutate: approveAndDecline, isPending: isApproving } = useMutation({ - mutationFn: async (variables: { - reservationId: number; - scheduleId: number; - reservationsToDecline: ReservationItem[]; - }) => { - const { reservationId, reservationsToDecline } = variables; - await updateReservationStatus({ - activityId: selectedActivityId!, - reservationId, - status: 'confirmed', - }); - await Promise.all( - reservationsToDecline.map((r) => - updateReservationStatus({ - activityId: selectedActivityId!, - reservationId: r.id, - status: 'declined', - }), - ), - ); - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [ - 'allReservationsByDate', - selectedActivityId, - day.format('YYYY-MM-DD'), - ], - }); - queryClient.invalidateQueries({ queryKey: ['schedules'] }); - queryClient.invalidateQueries({ queryKey: ['reservationDashboard'] }); - }, - onError: (error) => console.error('์˜ˆ์•ฝ ์Šน์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error), + const { handleApprove, handleReject, isLoading } = useReservationMutations({ + selectedActivityId, + day, + reservationsByStatus, }); - // 4. '๋‹จ์ผ ๊ฑฐ์ ˆ' ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฎคํ…Œ์ด์…˜ - const { mutate: reject, isPending: isRejecting } = useMutation({ - mutationFn: (variables: { reservationId: number }) => - updateReservationStatus({ - activityId: selectedActivityId!, - reservationId: variables.reservationId, - status: 'declined', - }), - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: [ - 'allReservationsByDate', - selectedActivityId, - day.format('YYYY-MM-DD'), - ], - }); - queryClient.invalidateQueries({ queryKey: ['schedules'] }); - queryClient.invalidateQueries({ queryKey: ['reservationDashboard'] }); - }, - onError: (error) => console.error('๊ฑฐ์ ˆ ์‹คํŒจ:', error), - }); - - // ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋“ค - const handleApprove = useCallback( - (reservationId: number, scheduleId: number) => { - if (isApproving) return; - const reservationsToDecline = reservationsByStatus.pending.filter( - (r: ReservationItem) => - r.scheduleId === scheduleId && r.id !== reservationId, - ); - approveAndDecline({ reservationId, scheduleId, reservationsToDecline }); - showSuccess('์Šน์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - }, - [approveAndDecline, isApproving, reservationsByStatus.pending, showSuccess], - ); - - const handleReject = useCallback( - (reservationId: number) => { - if (isRejecting) return; - reject({ reservationId }); - showSuccess('๊ฑฐ์ ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); - }, - [reject, isRejecting, showSuccess], - ); - const handleTimeSlotSelect = useCallback(async (scheduleId: number) => { setSelectedScheduleId(scheduleId); }, []); - const handleClose = useCallback(() => { - setIsOpen(false); - }, []); - - const reservationCounts = useMemo(() => { - const counts = schedules?.reduce( - (acc, schedule) => { - acc.pending += schedule.count.pending; - acc.confirmed += schedule.count.confirmed; - acc.declined += schedule.count.declined; - return acc; - }, - { pending: 0, confirmed: 0, declined: 0 }, - ) ?? { pending: 0, confirmed: 0, declined: 0 }; - - return counts; - }, [schedules, day]); - - const displayItems = useMemo(() => { - const items: { status: ReservationStatus; count: number }[] = []; - if (reservationCounts.pending > 0) { - items.push({ status: 'pending', count: reservationCounts.pending }); - } - if (reservationCounts.confirmed > 0) { - items.push({ status: 'confirmed', count: reservationCounts.confirmed }); - } - if (reservationCounts.declined > 0) { - items.push({ status: 'declined', count: reservationCounts.declined }); - } - return items; - }, [reservationCounts]); - - const totalReservations = - reservationCounts.pending + - reservationCounts.confirmed + - reservationCounts.declined; + const { reservationCounts, displayItems, totalReservations } = + useReservationCounts({ + schedules, + day, + }); // ๊ณตํ†ต ์…€ UI const cellContent = ( -
setIsOpen(true)} - > - {/* ๋ฐ˜์‘ํ˜• ์•Œ๋ฆผ ์  */} - {displayItems.length > 0 && ( -
- )} - - {/* ๋ฐ˜์‘ํ˜• ๋‚ ์งœ ํฐํŠธ */} -
- {day.format('D')} -
- -
- {/* --- ๋ชจ๋ฐ”์ผ ๋ทฐ (md ์‚ฌ์ด์ฆˆ ๋ฏธ๋งŒ) --- */} - {displayItems.length > 0 && ( -
-
- {STATUS_LABELS[displayItems[0].status]} {displayItems[0].count}๊ฑด -
-
- )} - - {/* --- ๋ฐ์Šคํฌํ†ฑ ๋ทฐ (md ์‚ฌ์ด์ฆˆ ์ด์ƒ) --- */} -
- {displayItems.map((item, index) => ( -
- {STATUS_LABELS[item.status]} {item.count}๊ฑด -
- ))} -
-
-
+ /> ); + // ๊ณตํ†ต ์ฝ˜ํ…์ธ  UI const contentUI = ( -
-
-
-

- {day.format('YY๋…„ M์›” D์ผ')} -

- - {totalReservations}๊ฐœ์˜ ์˜ˆ์•ฝ - -
- -
- - - setActiveTab(value as 'pending' | 'confirmed' | 'declined') - } - > - - - ์‹ ์ฒญ {reservationCounts.pending} - - - ์Šน์ธ {reservationCounts.confirmed} - - - ๊ฑฐ์ ˆ {reservationCounts.declined} - - - - - - - - - - - - - - - -
+ ); // ๐Ÿ”ฅ displayMode์— ๋”ฐ๋ผ ๋‹ค๋ฅธ UI ๋ Œ๋”๋ง diff --git a/src/domain/Reservation/components/reservation-calendar/DayCellContent.tsx b/src/domain/Reservation/components/reservation-calendar/DayCellContent.tsx new file mode 100644 index 00000000..231190ee --- /dev/null +++ b/src/domain/Reservation/components/reservation-calendar/DayCellContent.tsx @@ -0,0 +1,72 @@ +import dayjs from 'dayjs'; + +import { ReservationStatus } from '@/domain/Reservation/types/reservation'; +import { + getColorClassByStatus, + STATUS_LABELS, +} from '@/domain/Reservation/utils/reservation'; + +interface DisplayItem { + status: ReservationStatus; + count: number; +} + +interface DayCellContentProps { + day: dayjs.Dayjs; + cellClasses: string; + dateClasses: string; + displayItems: DisplayItem[]; + onClick: () => void; +} + +export default function DayCellContent({ + day, + cellClasses, + dateClasses, + displayItems, + onClick, +}: DayCellContentProps) { + return ( +
+ {/* ๋ฐ˜์‘ํ˜• ์•Œ๋ฆผ ์  */} + {displayItems.length > 0 && ( +
+ )} + + {/* ๋ฐ˜์‘ํ˜• ๋‚ ์งœ ํฐํŠธ */} +
+ {day.format('D')} +
+ +
+ {/* --- ๋ชจ๋ฐ”์ผ ๋ทฐ (md ์‚ฌ์ด์ฆˆ ๋ฏธ๋งŒ) --- */} + {displayItems.length > 0 && ( +
+
+ {STATUS_LABELS[displayItems[0].status]} {displayItems[0].count}๊ฑด +
+
+ )} + + {/* --- ๋ฐ์Šคํฌํ†ฑ ๋ทฐ (md ์‚ฌ์ด์ฆˆ ์ด์ƒ) --- */} +
+ {displayItems.map((item, index) => ( +
+ {STATUS_LABELS[item.status]} {item.count}๊ฑด +
+ ))} +
+
+
+ ); +} diff --git a/src/domain/Reservation/components/reservation-calendar/ReservationCalendar.tsx b/src/domain/Reservation/components/reservation-calendar/ReservationCalendar.tsx index b7ecd8f9..50fec21d 100644 --- a/src/domain/Reservation/components/reservation-calendar/ReservationCalendar.tsx +++ b/src/domain/Reservation/components/reservation-calendar/ReservationCalendar.tsx @@ -27,8 +27,11 @@ export default function ReservationCalendar({ // ๐Ÿ”ฅ ๋ฏธ๋””์–ด์ฟผ๋ฆฌ ๊ฒฐ๊ณผ์— ๋”ฐ๋ผ displayMode ๊ฒฐ์ • const displayMode = isDesktop ? 'popover' : 'bottomsheet'; - const { today, days, getReservationForDate, prevMonth, nextMonth } = - useCalendar(monthlyReservations, currentDate, onMonthChange); + const { today, days, prevMonth, nextMonth } = useCalendar( + monthlyReservations, + currentDate, + onMonthChange, + ); return (
= days.length - 7} - reservation={getReservationForDate(day)} selectedActivityId={selectedActivityId} displayMode={displayMode} // ๐Ÿ”ฅ displayMode prop ์ถ”๊ฐ€ /> diff --git a/src/domain/Reservation/components/reservation-calendar/ReservationDashboard.tsx b/src/domain/Reservation/components/reservation-calendar/ReservationDashboard.tsx index c4bc2852..26b678ee 100644 --- a/src/domain/Reservation/components/reservation-calendar/ReservationDashboard.tsx +++ b/src/domain/Reservation/components/reservation-calendar/ReservationDashboard.tsx @@ -54,7 +54,6 @@ export default function ReservationDashboard({ setCurrentDate(newDate); }; - if (isLoading) return ; if (isError) return
์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
; if (!initialActivities.length) return ( @@ -73,12 +72,16 @@ export default function ReservationDashboard({ />
- + {isLoading ? ( + + ) : ( + + )}
); diff --git a/src/domain/Reservation/components/reservation-calendar/ReservationModalContent.tsx b/src/domain/Reservation/components/reservation-calendar/ReservationModalContent.tsx new file mode 100644 index 00000000..c54b5f45 --- /dev/null +++ b/src/domain/Reservation/components/reservation-calendar/ReservationModalContent.tsx @@ -0,0 +1,134 @@ +import dayjs from 'dayjs'; +import { X } from 'lucide-react'; +import { Dispatch, SetStateAction } from 'react'; + +import ReservationDetail from '@/domain/Reservation/components/reservation-calendar/ReservationDetail'; +import type { ScheduleItem } from '@/domain/Reservation/services/reservation-calendar'; +import { ReservationItem } from '@/domain/Reservation/types/reservation'; +import Tabs from '@/shared/components/ui/tabs'; + +interface ReservationModalContentProps { + day: dayjs.Dayjs; + totalReservations: number; + reservationCounts: { + pending: number; + confirmed: number; + declined: number; + }; + activeTab: 'pending' | 'confirmed' | 'declined'; + setActiveTab: (tab: 'pending' | 'confirmed' | 'declined') => void; + schedules: ScheduleItem[] | null; + reservationsByStatus: { + pending: ReservationItem[]; + confirmed: ReservationItem[]; + declined: ReservationItem[]; + }; + handleApprove: (reservationId: number, scheduleId: number) => void; + handleReject: (reservationId: number) => void; + handleTimeSlotSelect: (scheduleId: number) => Promise; + isLoading: boolean; + setIsOpen: Dispatch>; + onClose: () => void; +} + +export default function ReservationModalContent({ + day, + totalReservations, + reservationCounts, + schedules, + reservationsByStatus, + activeTab, + setActiveTab, + handleApprove, + handleReject, + handleTimeSlotSelect, + isLoading, + setIsOpen, + onClose, +}: ReservationModalContentProps) { + return ( +
+
+
+

+ {day.format('YY๋…„ M์›” D์ผ')} +

+ + {totalReservations}๊ฐœ์˜ ์˜ˆ์•ฝ + +
+ +
+ + + setActiveTab(value as 'pending' | 'confirmed' | 'declined') + } + > + + + ์‹ ์ฒญ {reservationCounts.pending} + + + ์Šน์ธ {reservationCounts.confirmed} + + + ๊ฑฐ์ ˆ {reservationCounts.declined} + + + + + + + + + + + + + + + +
+ ); +} diff --git a/src/domain/Reservation/components/reservation-calendar/ReservationSkeleton.tsx b/src/domain/Reservation/components/reservation-calendar/ReservationSkeleton.tsx index a6b69580..4e91ff6f 100644 --- a/src/domain/Reservation/components/reservation-calendar/ReservationSkeleton.tsx +++ b/src/domain/Reservation/components/reservation-calendar/ReservationSkeleton.tsx @@ -9,7 +9,6 @@ const WEEKDAY_SKELETONS = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; export default function ReservationCalendarSkeleton() { return (
-
{ + return useMemo(() => { + const cellClasses = ` + relative flex aspect-square cursor-pointer flex-col items-center justify-start p-1 md:p-2 text-center font-size-14 + hover:bg-gray-50 + ${!isLastRow ? 'border-b-[0.05rem] border-gray-100' : ''} + ${!isCurrentMonth ? 'bg-neutral-200 text-gray-400 opacity-50' : ''} + ${isToday ? 'border-blue-300 bg-blue-100' : ''} + `; + + const dayOfWeek = day.day(); + const dateClasses = `font-size-14 ${ + dayOfWeek === 0 + ? 'text-red-500' + : dayOfWeek === 6 + ? 'text-blue-500' + : isCurrentMonth + ? 'text-gray-900' + : '' + }`; + return { cellClasses, dateClasses }; + }, [day, isCurrentMonth, isToday, isLastRow]); +}; diff --git a/src/domain/Reservation/hooks/useModalState.ts b/src/domain/Reservation/hooks/useModalState.ts new file mode 100644 index 00000000..278467dd --- /dev/null +++ b/src/domain/Reservation/hooks/useModalState.ts @@ -0,0 +1,26 @@ +import { useCallback, useState } from 'react'; + +export const useModalState = () => { + const [isOpen, setIsOpen] = useState(false); + + const [activeTab, setActiveTab] = useState< + 'pending' | 'confirmed' | 'declined' + >('pending'); + + const handleClose = useCallback(() => { + setIsOpen(false); + }, []); + + const handleOpen = useCallback(() => { + setIsOpen(true); + }, []); + + return { + isOpen, + setIsOpen, + activeTab, + setActiveTab, + handleClose, + handleOpen, + }; +}; diff --git a/src/domain/Reservation/hooks/useReservationCounts.ts b/src/domain/Reservation/hooks/useReservationCounts.ts new file mode 100644 index 00000000..e25b3739 --- /dev/null +++ b/src/domain/Reservation/hooks/useReservationCounts.ts @@ -0,0 +1,57 @@ +import { Dayjs } from 'dayjs'; +import { useMemo } from 'react'; + +import type { ScheduleItem } from '@/domain/Reservation/services/reservation-calendar'; +import type { ReservationStatus } from '@/domain/Reservation/types/reservation'; + +interface UseReservationCountsProps { + schedules: ScheduleItem[] | null; + day: Dayjs; +} + +export function useReservationCounts({ + schedules, + day, +}: UseReservationCountsProps) { + const reservationCounts = useMemo(() => { + const counts = schedules?.reduce( + (acc, schedule) => { + acc.pending += schedule.count.pending; + acc.confirmed += schedule.count.confirmed; + acc.declined += schedule.count.declined; + return acc; + }, + { pending: 0, confirmed: 0, declined: 0 }, + ) ?? { pending: 0, confirmed: 0, declined: 0 }; + + return counts; + }, [schedules, day]); + + const displayItems = useMemo(() => { + const items: { status: ReservationStatus; count: number }[] = []; + if (reservationCounts.pending > 0) { + items.push({ status: 'pending', count: reservationCounts.pending }); + } + if (reservationCounts.confirmed > 0) { + items.push({ status: 'confirmed', count: reservationCounts.confirmed }); + } + if (reservationCounts.declined > 0) { + items.push({ status: 'declined', count: reservationCounts.declined }); + } + return items; + }, [reservationCounts]); + + const totalReservations = useMemo(() => { + return ( + reservationCounts.pending + + reservationCounts.confirmed + + reservationCounts.declined + ); + }, [reservationCounts]); + + return { + reservationCounts, + displayItems, + totalReservations, + }; +} diff --git a/src/domain/Reservation/hooks/useReservationMutations.ts b/src/domain/Reservation/hooks/useReservationMutations.ts new file mode 100644 index 00000000..5e7cceb1 --- /dev/null +++ b/src/domain/Reservation/hooks/useReservationMutations.ts @@ -0,0 +1,115 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { useCallback } from 'react'; + +import { updateReservationStatus } from '@/domain/Reservation/services/reservation-calendar'; +import type { ReservationItem } from '@/domain/Reservation/types/reservation'; +import { useToast } from '@/shared/hooks/useToast'; + +interface UseReservationMutationsProps { + selectedActivityId: number | null; + day: dayjs.Dayjs; + reservationsByStatus: { + pending: ReservationItem[]; + confirmed: ReservationItem[]; + declined: ReservationItem[]; + }; +} + +export const useReservationMutations = ({ + selectedActivityId, + day, + reservationsByStatus, +}: UseReservationMutationsProps) => { + const queryClient = useQueryClient(); + const { showSuccess } = useToast(); + + // ํ•˜๋‚˜ ์Šน์ธ ํ›„ ๋‚˜๋จธ์ง€ ๊ฑฐ์ ˆ ์ฒ˜๋ฆฌ + const { mutate: approveAndDecline, isPending: isApproving } = useMutation({ + mutationFn: async (variables: { + reservationId: number; + scheduleId: number; + reservationsToDecline: ReservationItem[]; + }) => { + const { reservationId, reservationsToDecline } = variables; + await updateReservationStatus({ + activityId: selectedActivityId!, + reservationId, + status: 'confirmed', + }); + await Promise.all( + reservationsToDecline.map((r) => + updateReservationStatus({ + activityId: selectedActivityId!, + reservationId: r.id, + status: 'declined', + }), + ), + ); + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ + 'allReservationsByDate', + selectedActivityId, + day.format('YYYY-MM-DD'), + ], + }); + queryClient.invalidateQueries({ queryKey: ['schedules'] }); + queryClient.invalidateQueries({ queryKey: ['reservationDashboard'] }); + }, + onError: (error) => console.error('์˜ˆ์•ฝ ์Šน์ธ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error), + }); + + // 4. '๋‹จ์ผ ๊ฑฐ์ ˆ' ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฎคํ…Œ์ด์…˜ + const { mutate: reject, isPending: isRejecting } = useMutation({ + mutationFn: (variables: { reservationId: number }) => + updateReservationStatus({ + activityId: selectedActivityId!, + reservationId: variables.reservationId, + status: 'declined', + }), + onSuccess: () => { + showSuccess('๊ฑฐ์ ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + queryClient.invalidateQueries({ + queryKey: [ + 'allReservationsByDate', + selectedActivityId, + day.format('YYYY-MM-DD'), + ], + }); + queryClient.invalidateQueries({ queryKey: ['schedules'] }); + queryClient.invalidateQueries({ queryKey: ['reservationDashboard'] }); + }, + onError: (error) => console.error('๊ฑฐ์ ˆ ์‹คํŒจ:', error), + }); + + // ํ•ธ๋“ค๋Ÿฌ ํ•จ์ˆ˜๋“ค + const handleApprove = useCallback( + (reservationId: number, scheduleId: number) => { + if (isApproving) return; + const reservationsToDecline = reservationsByStatus.pending.filter( + (r: ReservationItem) => + r.scheduleId === scheduleId && r.id !== reservationId, + ); + approveAndDecline({ reservationId, scheduleId, reservationsToDecline }); + showSuccess('์Šน์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + }, + [approveAndDecline, isApproving, reservationsByStatus.pending, showSuccess], + ); + + const handleReject = useCallback( + (reservationId: number) => { + if (isRejecting) return; + reject({ reservationId }); + showSuccess('๊ฑฐ์ ˆ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + }, + [reject, isRejecting, showSuccess], + ); + + return { + handleApprove, + handleReject, + isLoading: isApproving || isRejecting, + }; +}; diff --git a/src/domain/Reservation/hooks/useReservationQueries.ts b/src/domain/Reservation/hooks/useReservationQueries.ts new file mode 100644 index 00000000..4ee5ef39 --- /dev/null +++ b/src/domain/Reservation/hooks/useReservationQueries.ts @@ -0,0 +1,111 @@ +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import { useEffect, useState } from 'react'; + +import { + getReservationsBySchedule, + getSchedulesByDate, + ScheduleItem, +} from '@/domain/Reservation/services/reservation-calendar'; +import { ReservationItem } from '@/domain/Reservation/types/reservation'; + +interface UseReservationQueriesProps { + selectedActivityId: number | null; + day: dayjs.Dayjs; +} + +export const useReservationQueries = ({ + selectedActivityId, + day, +}: UseReservationQueriesProps) => { + const [selectedScheduleId, setSelectedScheduleId] = useState( + null, + ); + + // 1. ๋‚ ์งœ๋ณ„ ์Šค์ผ€์ค„ ์กฐํšŒ (useQuery) + const { data: schedules = [] } = useQuery({ + queryKey: ['schedules', selectedActivityId, day.format('YYYY-MM-DD')], + queryFn: () => + getSchedulesByDate(selectedActivityId!, day.format('YYYY-MM-DD')), + enabled: !!selectedActivityId, + }); + + useEffect(() => { + if (schedules && schedules.length > 0 && !selectedScheduleId) { + // pending ์˜ˆ์•ฝ์ด ์žˆ๋Š” ์ฒซ ๋ฒˆ์งธ ์Šค์ผ€์ค„ ์ฐพ๊ธฐ + const scheduleWithPending = schedules.find((s) => s.count.pending > 0); + // pending์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„, ์—†์œผ๋ฉด ์ฒซ ๋ฒˆ์งธ ์Šค์ผ€์ค„ ์„ ํƒ + const targetSchedule = scheduleWithPending || schedules[0]; + setSelectedScheduleId(targetSchedule.scheduleId); + } + }, [schedules, selectedScheduleId]); + + // 2. ๋ชจ๋“  ์Šค์ผ€์ค„์˜ ์˜ˆ์•ฝ ๋ชฉ๋ก ์กฐํšŒ (ํ†ตํ•ฉ ๋ฒ„์ „) + const { + data: reservationsByStatus = { pending: [], confirmed: [], declined: [] }, + } = useQuery<{ + pending: ReservationItem[]; + confirmed: ReservationItem[]; + declined: ReservationItem[]; + }>({ + queryKey: [ + 'allReservationsByDate', + selectedActivityId, + day.format('YYYY-MM-DD'), + ], + queryFn: async () => { + if (!schedules || schedules.length === 0) { + return { pending: [], confirmed: [], declined: [] }; + } + + // ๋ชจ๋“  ์Šค์ผ€์ค„์˜ ์˜ˆ์•ฝ์„ ๋ณ‘๋ ฌ๋กœ ์กฐํšŒ + const allPendingPromises = schedules.map((schedule) => + getReservationsBySchedule( + selectedActivityId!, + schedule.scheduleId, + 'pending', + ), + ); + const allConfirmedPromises = schedules.map((schedule) => + getReservationsBySchedule( + selectedActivityId!, + schedule.scheduleId, + 'confirmed', + ), + ); + const allDeclinedPromises = schedules.map((schedule) => + getReservationsBySchedule( + selectedActivityId!, + schedule.scheduleId, + 'declined', + ), + ); + + const [pendingResults, confirmedResults, declinedResults] = + await Promise.all([ + Promise.all(allPendingPromises), + Promise.all(allConfirmedPromises), + Promise.all(allDeclinedPromises), + ]); + + return { + pending: pendingResults + .flat() + .filter((item): item is ReservationItem => item !== null), + confirmed: confirmedResults + .flat() + .filter((item): item is ReservationItem => item !== null), + declined: declinedResults + .flat() + .filter((item): item is ReservationItem => item !== null), + }; + }, + enabled: !!selectedActivityId && !!schedules?.length, + }); + return { + schedules, + reservationsByStatus, + selectedScheduleId, + setSelectedScheduleId, + }; +};