From 61a1565357f0d2fe962a64522e9a691bd9539b7c Mon Sep 17 00:00:00 2001 From: yujin-fe Date: Thu, 15 Jan 2026 17:51:05 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EB=8B=AC=EB=A0=A5=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20?= =?UTF-8?q?=EB=85=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/myActivities.ts | 18 +++ .../myactivities-reservations/page.tsx | 1 + .../CalendarComponents.tsx | 33 +++- .../ReservationManagementCalendar.tsx | 150 ++++++++---------- 4 files changed, 115 insertions(+), 87 deletions(-) diff --git a/src/api/myActivities.ts b/src/api/myActivities.ts index dfe556a..90dbe04 100644 --- a/src/api/myActivities.ts +++ b/src/api/myActivities.ts @@ -8,6 +8,7 @@ import { ResponseMyActivities, } from '@/types/myactivities'; import type { + ReservationDashboardRes, ReservationListResponse, ReservationStatusType, ReservedScheduleList, @@ -38,6 +39,23 @@ export const deleteMyActivities = async (activityId: number) => { }); }; +//체험 예약 현황 월별 예약 조회 +interface GetMonthlyReservationsParams { + year: string; + month: string; +} +export const getMonthlyReservations = ( + activityId: number, + params: GetMonthlyReservationsParams +) => { + return apiFetch( + `/my-activities/${activityId}/reservation-dashboard`, + { + params, + } + ); +}; + //날짜별 예약 정보 조회 export const getDailyReservationInfo = ( activityId: number, diff --git a/src/app/(common)/(mypage)/myactivities-reservations/page.tsx b/src/app/(common)/(mypage)/myactivities-reservations/page.tsx index 2d0c038..084bac4 100644 --- a/src/app/(common)/(mypage)/myactivities-reservations/page.tsx +++ b/src/app/(common)/(mypage)/myactivities-reservations/page.tsx @@ -60,6 +60,7 @@ export default function Page() { )} { setLocation(slotInfo.box || null); setSelectedDate(slotInfo.start); diff --git a/src/components/reservation-management/CalendarComponents.tsx b/src/components/reservation-management/CalendarComponents.tsx index bab944f..2eca4d4 100644 --- a/src/components/reservation-management/CalendarComponents.tsx +++ b/src/components/reservation-management/CalendarComponents.tsx @@ -12,12 +12,20 @@ import type { import CalendarBadge from '../Badge/CalendarBadge'; -import { mockEvents } from './ReservationManagementCalendar'; - import ic_next from '@/assets/icons/activities/ic-calender-next.svg'; import ic_prev from '@/assets/icons/activities/ic-calender-prev.svg'; import { cn } from '@/util/cn'; -export const Toolbar = ({ date, onNavigate }: ToolbarProps) => { + +interface CustomToolbarProps extends ToolbarProps { + onClickNextMonth?: () => void; + onClickPrevMonth?: () => void; +} +export const Toolbar = ({ + date, + onNavigate, + onClickNextMonth, + onClickPrevMonth, +}: CustomToolbarProps) => { const customLabel = moment(date).format('YYYY년 M월'); return (
@@ -25,12 +33,18 @@ export const Toolbar = ({ date, onNavigate }: ToolbarProps) => { {customLabel} -
@@ -44,8 +58,15 @@ export const Toolbar = ({ date, onNavigate }: ToolbarProps) => { }; //TODO: 데이터 바꾸기 -export const MyDateHeader = ({ date, isOffRange }: DateHeaderProps) => { - const hasEvent = mockEvents.some( +interface MyDateHeaderProps extends DateHeaderProps { + event: CalendarEventData[]; +} +export const MyDateHeader = ({ + date, + isOffRange, + event, +}: MyDateHeaderProps) => { + const hasEvent = event.some( (e) => e.start?.toDateString() === date.toDateString() ); const day = date.getDate(); diff --git a/src/components/reservation-management/ReservationManagementCalendar.tsx b/src/components/reservation-management/ReservationManagementCalendar.tsx index 942797f..be39ab3 100644 --- a/src/components/reservation-management/ReservationManagementCalendar.tsx +++ b/src/components/reservation-management/ReservationManagementCalendar.tsx @@ -1,6 +1,7 @@ 'use client'; import 'react-big-calendar/lib/css/react-big-calendar.css'; import './calendar.css'; +import { useQuery } from '@tanstack/react-query'; import moment from 'moment'; import { useEffect, useState } from 'react'; import { Calendar, momentLocalizer, type SlotInfo } from 'react-big-calendar'; @@ -15,6 +16,7 @@ import { } from './CalendarComponents'; import type { CalendarEventData } from './CalendarComponents'; +import { getMonthlyReservations } from '@/api/myActivities'; import type { ReservationDashboardRes } from '@/types/reserved-schedule'; moment.locale('ko'); @@ -24,105 +26,85 @@ moment.updateLocale('en', { weekdaysMin: ['S', 'M', 'T', 'W', 'T', 'F', 'S'], }); -//TODO: 이벤트(내 체험 월별 예약 조회) api 가져오기 프롭스 data 가공 -export const mockEvents: CalendarEventData[] = [ - { - title: '완료 1', - start: new Date(2026, 0, 9), - end: new Date(2026, 0, 9), - allDay: true, - status: 'completed', - count: 1, - }, - { - title: '대기 1', - start: new Date(2026, 0, 9), - end: new Date(2026, 0, 9), - allDay: true, - status: 'pending', - count: 1, - }, - { - title: '완료 2', - start: new Date(2026, 0, 12), - end: new Date(2026, 0, 12), - allDay: true, - status: 'completed', - count: 2, - }, - { - title: '확정 1', - start: new Date(2026, 0, 15), - end: new Date(2026, 0, 15), - allDay: true, - status: 'confirmed', - count: 1, - }, - { - title: '대기 2', - start: new Date(2026, 0, 15), - end: new Date(2026, 0, 15), - allDay: true, - status: 'pending', - count: 2, - }, - { - title: '대기 2', - start: new Date(2026, 0, 15), - end: new Date(2026, 0, 15), - allDay: true, - status: 'pending', - count: 2, - }, - { - title: '대기 2', - start: new Date(2026, 0, 15), - end: new Date(2026, 0, 15), - allDay: true, - status: 'pending', - count: 2, - }, - { - title: '대기 2', - start: new Date(2026, 0, 15), - end: new Date(2026, 0, 15), - allDay: true, - status: 'pending', - count: 2, - }, - { - title: '대기 2', - start: new Date(2026, 0, 15), - end: new Date(2026, 0, 15), - allDay: true, - status: 'pending', - count: 2, - }, -]; interface ReservationManagementCalendarProps { - data?: ReservationDashboardRes; + activityId: number; onSelectSlot: (slotInfo: SlotInfo) => void; } export default function ReservationManagementCalendar({ - data, + activityId, onSelectSlot, }: ReservationManagementCalendarProps) { const [currentDate, setCurrentDate] = useState(null); + + const currentYear = currentDate ? moment(currentDate).format('YYYY') : '2026'; + const currentMonth = currentDate ? moment(currentDate).format('MM') : '01'; + useEffect(() => { setCurrentDate(new Date()); }, []); + const { data: MonthlyReservationData } = useQuery({ + queryKey: ['MonthlyReservationData', currentMonth, activityId], + queryFn: () => + getMonthlyReservations(activityId, { + year: currentYear.toString(), + month: currentMonth.toString(), + }), + enabled: !!activityId, + }); + if (!currentDate) return null; + + const convertApiToEvent = (apiData: ReservationDashboardRes[]) => { + return apiData.flatMap((item) => { + const event: CalendarEventData[] = []; + const date = new Date(item.date); + if (item.reservations.completed > 0) { + event.push({ + title: '', + start: date, + end: date, + status: 'completed', + count: item.reservations.completed, + }); + } + if (item.reservations.confirmed > 0) { + event.push({ + title: '', + start: date, + end: date, + status: 'confirmed', + count: item.reservations.confirmed, + }); + } + if (item.reservations.pending > 0) { + event.push({ + title: '', + start: date, + end: date, + status: 'pending', + count: item.reservations.pending, + }); + } + return event; + }); + }; + + const event = MonthlyReservationData + ? convertApiToEvent(MonthlyReservationData) + : []; + if (!MonthlyReservationData) return null; + return (
- date={currentDate} onNavigate={setCurrentDate} formats={{ weekdayFormat: 'dd', }} localizer={localizer} - events={mockEvents} + events={event} startAccessor="start" endAccessor="end" onSelectSlot={onSelectSlot} @@ -140,13 +122,19 @@ export default function ReservationManagementCalendar({ }} style={{ height: 'fit-content' }} components={{ - toolbar: Toolbar, + toolbar: (props) => ( + console.log(currentDate)} + onClickPrevMonth={() => console.log(currentDate)} + /> + ), event: CalendarEvent, dateCellWrapper: MyDateCellWrapper, showMore: ShowMore, month: { header: MonthHeader, - dateHeader: MyDateHeader, + dateHeader: (props) => , }, }} /> From 8cf8016a4c93691df6d59b28866b7f08d4751f32 Mon Sep 17 00:00:00 2001 From: yujin-fe Date: Sat, 17 Jan 2026 09:36:25 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=98=88=EC=95=BD=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B1=B0=EC=A0=88=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/myActivities.ts | 16 +++ .../components/DailyReservationStatus.tsx | 108 ++++++++++++------ .../components/ReservationListByStatus.tsx | 41 +++---- .../ReservationManagementCalendar.tsx | 1 - src/types/reserved-schedule.ts | 4 + 5 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/api/myActivities.ts b/src/api/myActivities.ts index 90dbe04..9f3d395 100644 --- a/src/api/myActivities.ts +++ b/src/api/myActivities.ts @@ -12,6 +12,7 @@ import type { ReservationListResponse, ReservationStatusType, ReservedScheduleList, + UpdateReservationApprovalReq, } from '@/types/reserved-schedule'; //내 체험 수정 @@ -87,3 +88,18 @@ export const getReservationBySchedule = ( } ); }; + +//체험 예약 승인, 거절 업데이트 +export const updateReservationApproval = ( + activityId: number, + reservationId: number, + req: UpdateReservationApprovalReq +) => { + return apiFetch( + `/my-activities/${activityId}/reservations/${reservationId}`, + { + method: 'PATCH', + body: req, + } + ); +}; diff --git a/src/app/(common)/(mypage)/myactivities-reservations/components/DailyReservationStatus.tsx b/src/app/(common)/(mypage)/myactivities-reservations/components/DailyReservationStatus.tsx index 0d4c473..0a34fc6 100644 --- a/src/app/(common)/(mypage)/myactivities-reservations/components/DailyReservationStatus.tsx +++ b/src/app/(common)/(mypage)/myactivities-reservations/components/DailyReservationStatus.tsx @@ -1,11 +1,15 @@ 'use client'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import moment from 'moment'; import { useEffect, useState } from 'react'; import ReservationListByStatus from './ReservationListByStatus'; -import { getDailyReservationInfo } from '@/api/myActivities'; +import { + getDailyReservationInfo, + getReservationBySchedule, + updateReservationApproval, +} from '@/api/myActivities'; import Button from '@/components/Button'; import { DropDownTrigger, @@ -16,7 +20,10 @@ import { import Tab from '@/components/Tab'; import useClickOutside from '@/hooks/useClickOutside'; import useWindowSize from '@/hooks/useWindowSize'; -import type { ReservationStatusType } from '@/types/reserved-schedule'; +import type { + ReservationStatusType, + UpdateReservationApprovalReq, +} from '@/types/reserved-schedule'; import { cn } from '@/util/cn'; export interface BoxType { clientX: number; @@ -44,6 +51,7 @@ export default function DailyReservationStatus({ box, onClose, }: DailyReservationStatusProps) { + const queryClient = useQueryClient(); const [isMounted, setIsMounted] = useState(false); const formatDate = date ? moment(date).format('YYYY-MM-DD') : ''; const params = { @@ -53,13 +61,12 @@ export default function DailyReservationStatus({ const [time, setTime] = useState(''); const [status, setStatus] = useState('pending'); const popupCloseRef = useClickOutside(onClose); - const { data: dailyReservationData } = useQuery({ - queryKey: ['DailyReservationStatus', activityId, formatDate], - queryFn: () => getDailyReservationInfo(activityId, params), - enabled: !!date && !!activityId && !!box, - }); - - console.log(box?.x, box?.clientX, box?.y, box?.clientY); + const { data: dailyReservationData, refetch: dailyReservationRefetch } = + useQuery({ + queryKey: ['DailyReservationStatus', activityId, formatDate], + queryFn: () => getDailyReservationInfo(activityId, params), + enabled: !!date && !!activityId && !!box, + }); useEffect(() => { if (!isMounted) { @@ -75,43 +82,68 @@ export default function DailyReservationStatus({ } }, [dailyReservationData]); - if (!dailyReservationData) return null; - const startToEndTimes = dailyReservationData.map( + const startToEndTimes = dailyReservationData?.map( (schedule) => `${schedule.startTime} - ${schedule.endTime}` ); - const filteredStatus = dailyReservationData.filter( + const filteredStatus = dailyReservationData?.filter( (data) => `${data.startTime} - ${data.endTime}` === time ); + const scheduleId = filteredStatus?.[0]?.scheduleId ?? null; + + const reservationListDataParams = { + scheduleId: scheduleId ?? 0, + status, + }; + + const { data: reservationListData, refetch: reservationListRefetch } = + useQuery({ + queryKey: ['ReservationListByStatus', status, scheduleId], + queryFn: () => + getReservationBySchedule(activityId, reservationListDataParams), + enabled: !!scheduleId && scheduleId > 0, + }); + + const handleReservation = async ( + reservationId: number, + status: ReservationStatusType + ) => { + const req: UpdateReservationApprovalReq = { + status, + }; + try { + await updateReservationApproval(activityId, reservationId, req); + await Promise.all([dailyReservationRefetch(), reservationListRefetch()]); + } catch (error) { + alert(error); + } + }; + const getDesktopPosition = () => { if (!isMounted) return; if (!box || !screenWidth || screenWidth < 1024) return undefined; const POPUP_WIDTH = 375; - const POPUP_HEIGHT = dailyReservationData.length === 0 ? 400 : 580; + const POPUP_HEIGHT = dailyReservationData?.length === 0 ? 400 : 580; let left = box.clientX + 20; let top = box.clientY + 20; - // 오른쪽이 짤릴 때 if (left + POPUP_WIDTH > window.innerWidth) { left = left - POPUP_WIDTH - 40; } - // 아래가 짤릴 때 if (top + POPUP_HEIGHT > window.innerHeight) { - console.log('짤림'); top = box.y; top = top - POPUP_HEIGHT + 20; } - console.log('left, top', left, top); return { left, top, }; }; - + if (!startToEndTimes || !filteredStatus) return null; return ( <> {isMounted && screenWidth && screenWidth < 1024 && ( @@ -133,10 +165,10 @@ export default function DailyReservationStatus({
{formatDate} - {dailyReservationData.length === 0 ? ( + {dailyReservationData?.length === 0 ? (
해당 날짜에 예약 내역이 없습니다. @@ -149,9 +181,9 @@ export default function DailyReservationStatus({ 예약 시간 - + - {startToEndTimes.map((time) => ( + {startToEndTimes?.map((time) => ( setTime(selected)}> @@ -161,17 +193,25 @@ export default function DailyReservationStatus({
- - setStatus(STATUS_TO_EN[status] ?? 'pending') - } - /> - + {filteredStatus[0] && ( + <> + + setStatus(STATUS_TO_EN[status] ?? 'pending') + } + /> + {reservationListData && scheduleId ? ( + + ) : null} + + )} )}
diff --git a/src/components/reservation-management/ReservationManagementCalendar.tsx b/src/components/reservation-management/ReservationManagementCalendar.tsx index be39ab3..d494601 100644 --- a/src/components/reservation-management/ReservationManagementCalendar.tsx +++ b/src/components/reservation-management/ReservationManagementCalendar.tsx @@ -93,7 +93,6 @@ export default function ReservationManagementCalendar({ const event = MonthlyReservationData ? convertApiToEvent(MonthlyReservationData) : []; - if (!MonthlyReservationData) return null; return (
diff --git a/src/types/reserved-schedule.ts b/src/types/reserved-schedule.ts index d83937c..b3cf913 100644 --- a/src/types/reserved-schedule.ts +++ b/src/types/reserved-schedule.ts @@ -47,3 +47,7 @@ export interface ReservationListResponse { totalCount: number; cursorId: number | null; } + +export type UpdateReservationApprovalReq = { + status: ReservationStatusType; +}; From 987c587b5d3cc898448120ac9882750f32ca0795 Mon Sep 17 00:00:00 2001 From: yujin-fe Date: Sat, 17 Jan 2026 09:37:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20TODO=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/reservation-management/CalendarComponents.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/reservation-management/CalendarComponents.tsx b/src/components/reservation-management/CalendarComponents.tsx index 2eca4d4..3c9ce30 100644 --- a/src/components/reservation-management/CalendarComponents.tsx +++ b/src/components/reservation-management/CalendarComponents.tsx @@ -57,7 +57,6 @@ export const Toolbar = ({ ); }; -//TODO: 데이터 바꾸기 interface MyDateHeaderProps extends DateHeaderProps { event: CalendarEventData[]; } From b0af6dddab34ac36bcc87797836643835a5006e1 Mon Sep 17 00:00:00 2001 From: yujin-fe Date: Sat, 17 Jan 2026 09:52:51 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=90=9B=20Fix:=20console.log=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ReservationListByStatus.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/(common)/(mypage)/myactivities-reservations/components/ReservationListByStatus.tsx b/src/app/(common)/(mypage)/myactivities-reservations/components/ReservationListByStatus.tsx index 5a2fd25..6ac07a6 100644 --- a/src/app/(common)/(mypage)/myactivities-reservations/components/ReservationListByStatus.tsx +++ b/src/app/(common)/(mypage)/myactivities-reservations/components/ReservationListByStatus.tsx @@ -53,11 +53,9 @@ export default function ReservationListByStatus({ key={reservation.id} data={reservation} onConfirm={() => { - console.log('onConfirm'); handleReservation(reservation.id, 'confirmed'); }} onDecline={() => { - console.log('onDecline'); handleReservation(reservation.id, 'declined'); }} />