Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/api/myActivities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
ReservationListResponse,
ReservationStatusType,
ReservedScheduleList,
UpdateReservationApprovalReq,
} from '@/types/reserved-schedule';

//내 체험 수정
Expand Down Expand Up @@ -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,
}
);
};
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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 = {
Expand All @@ -53,13 +61,12 @@ export default function DailyReservationStatus({
const [time, setTime] = useState('');
const [status, setStatus] = useState<ReservationStatusType>('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) {
Expand All @@ -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);
}
Comment on lines +115 to +120
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

PR 설명에 언급된 간헐적인 데이터 불일치 문제를 해결하고 코드를 개선하기 위해, refetch를 직접 호출하는 대신 queryClient.invalidateQueries를 사용하는 것을 권장합니다. invalidateQueries는 특정 쿼리 키와 일치하는 쿼리를 무효화하여 react-query가 데이터를 다시 가져오게 하는, 더 안정적이고 선언적인 방법입니다. useQueryClient가 이미 선언되어 있으니 바로 활용할 수 있습니다.

또한, catch 블록에서 alert()로 에러를 표시하는 것은 사용자 경험에 좋지 않습니다. 일반적인 API 오류는 토스트 알림과 같은 전역 메커니즘으로 처리하는 것이 좋습니다. (Rule: Handle generic, non-field-specific API errors with a global mechanism like a toast notification, rather than assigning the error to an arbitrary input field.) 토스트 메시지 등 더 나은 방식의 피드백을 제공하는 것을 고려해보세요.

    try {
      await updateReservationApproval(activityId, reservationId, req);
      queryClient.invalidateQueries({ queryKey: ['DailyReservationStatus', activityId, formatDate] });
      queryClient.invalidateQueries({ queryKey: ['ReservationListByStatus'] });
    } catch (error) {
      console.error('예약 상태 업데이트 실패:', error);
      // TODO: 사용자에게 토스트 메시지 등으로 에러 알림
    }
References
  1. Handle generic, non-field-specific API errors with a global mechanism like a toast notification, rather than assigning the error to an arbitrary input field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/gemini
이것도 해봤는데 안되던데 결국 데이터 다시 불러오는건 똑같잖아

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yujin-fe님, 좋은 질문입니다. invalidateQueriesrefetch 모두 최종적으로는 데이터를 다시 불러오는 결과를 가져오는 것은 맞습니다. 하지만 react-query의 핵심 개념과 더 나은 데이터 관리 측면에서 invalidateQueries를 사용하는 것이 권장됩니다.

주요 차이점은 다음과 같습니다:

  • queryClient.invalidateQueries (권장): 이는 선언적인 방식입니다. 특정 queryKey에 해당하는 모든 쿼리를 '오래된(stale)' 상태로 표시하라고 react-query에게 지시합니다. react-query는 이 지시를 받고 해당 쿼리를 구독하고 있는 모든 컴포넌트에서 데이터를 자동으로 다시 가져오도록 관리합니다. 이는 여러 컴포넌트가 동일한 데이터를 사용하고 있을 때 데이터 일관성을 유지하는 데 매우 효과적입니다. 캐시 관리와 최적화는 react-query가 담당하게 됩니다.
  • refetch: 이는 특정 useQuery 훅 인스턴스에 대해 데이터를 즉시 다시 가져오도록 강제하는 명령적인 방식입니다. 해당 refetch를 호출한 특정 쿼리 인스턴스만 영향을 받으며, 다른 곳에서 동일한 queryKey를 사용하는 쿼리들은 자동으로 업데이트되지 않을 수 있습니다.

만약 invalidateQueries가 작동하지 않는다고 느끼셨다면, queryKey가 정확히 일치하는지 확인해 볼 필요가 있습니다. react-queryqueryKey를 기반으로 쿼리를 식별하므로, invalidateQueries에 전달하는 queryKeyuseQuery에서 사용된 queryKey와 정확히 일치해야 합니다.

데이터 일관성 문제를 해결하고 react-query의 이점을 최대한 활용하기 위해 invalidateQueries 방식을 다시 고려해 보시는 것을 추천합니다.

또한, catch 블록에서 alert() 대신 토스트 알림과 같은 사용자 친화적인 방식으로 에러를 처리하는 것도 사용자 경험을 위해 중요합니다. 이 부분도 함께 개선하는 것을 권장합니다.

    try {
      await updateReservationApproval(activityId, reservationId, req);
      queryClient.invalidateQueries({ queryKey: ['DailyReservationStatus', activityId, formatDate] });
      queryClient.invalidateQueries({ queryKey: ['ReservationListByStatus'] });
    } catch (error) {
      console.error('예약 상태 업데이트 실패:', error);
      // TODO: 사용자에게 토스트 메시지 등으로 에러 알림
    }
Suggested change
try {
await updateReservationApproval(activityId, reservationId, req);
await Promise.all([dailyReservationRefetch(), reservationListRefetch()]);
} catch (error) {
alert(error);
}
try {
await updateReservationApproval(activityId, reservationId, req);
queryClient.invalidateQueries({ queryKey: ['DailyReservationStatus', activityId, formatDate] });
queryClient.invalidateQueries({ queryKey: ['ReservationListByStatus'] });
} catch (error) {
console.error('예약 상태 업데이트 실패:', error);
// TODO: 사용자에게 토스트 메시지 등으로 에러 알림
}

};

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 && (
Expand All @@ -133,10 +165,10 @@ export default function DailyReservationStatus({
<div
className={cn(
'flex flex-col gap-4.5',
dailyReservationData.length === 0 ? 'h-100' : 'h-full'
dailyReservationData?.length === 0 ? 'h-100' : 'h-full'
)}>
<span className="bold text-[20px] text-gray-950">{formatDate}</span>
{dailyReservationData.length === 0 ? (
{dailyReservationData?.length === 0 ? (
<div className="flex flex-1 items-center justify-center lg:min-h-auto">
<span className="text-[20px] font-medium">
해당 날짜에 예약 내역이 없습니다.
Expand All @@ -149,9 +181,9 @@ export default function DailyReservationStatus({
예약 시간
</span>
<DropDown>
<DropDownTrigger placeholder={time || startToEndTimes[0]} />
<DropDownTrigger placeholder={time || startToEndTimes?.[0]} />
<DropDownList>
{startToEndTimes.map((time) => (
{startToEndTimes?.map((time) => (
<DropDownItem
key={time}
onSelect={(selected) => setTime(selected)}>
Expand All @@ -161,17 +193,25 @@ export default function DailyReservationStatus({
</DropDownList>
</DropDown>
</div>
<Tab
data={filteredStatus[0]}
onClick={(status) =>
setStatus(STATUS_TO_EN[status] ?? 'pending')
}
/>
<ReservationListByStatus
activityId={activityId}
scheduleId={filteredStatus[0]?.scheduleId}
status={status}
/>
{filteredStatus[0] && (
<>
<Tab
data={filteredStatus[0]}
onClick={(status) =>
setStatus(STATUS_TO_EN[status] ?? 'pending')
}
/>
{reservationListData && scheduleId ? (
<ReservationListByStatus
reservationListData={reservationListData}
handleReservation={handleReservation}
activityId={activityId}
scheduleId={scheduleId}
status={status}
/>
) : null}
</>
)}
</>
)}
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { useQuery } from '@tanstack/react-query';

import ReservationListCard from './ReservationListCard';

import { getReservationBySchedule } from '@/api/myActivities';
import type { ReservationStatusType } from '@/types/reserved-schedule';
import type {
ReservationListResponse,
ReservationStatusType,
} from '@/types/reserved-schedule';

interface ReservationListByStatusProps {
activityId: number;
scheduleId: number;
status: ReservationStatusType;
handleReservation: (
reservationId: number,
status: ReservationStatusType
) => Promise<void>;
reservationListData: ReservationListResponse;
}

const STATUS_TO_KO = {
Expand All @@ -18,25 +23,15 @@ const STATUS_TO_KO = {
};

export default function ReservationListByStatus({
activityId,
scheduleId,
handleReservation,
status,
reservationListData,
}: ReservationListByStatusProps) {
const params = {
scheduleId,
status,
};
const { data: ReservationListData } = useQuery({
queryKey: ['ReservationListByStatus', scheduleId, status],
queryFn: () => getReservationBySchedule(activityId, params),
enabled: scheduleId !== undefined,
});

if (!ReservationListData) {
if (!reservationListData) {
return null;
}

const { reservations } = ReservationListData;
const { reservations } = reservationListData;

if (reservations.length === 0) {
return (
Expand All @@ -57,8 +52,12 @@ export default function ReservationListByStatus({
status={status}
key={reservation.id}
data={reservation}
onConfirm={() => console.log('confirm')}
onDecline={() => console.log('decline')}
onConfirm={() => {
handleReservation(reservation.id, 'confirmed');
}}
onDecline={() => {
handleReservation(reservation.id, 'declined');
}}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ export const Toolbar = ({
);
};

//TODO: 데이터 바꾸기
interface MyDateHeaderProps extends DateHeaderProps {
event: CalendarEventData[];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export default function ReservationManagementCalendar({
const event = MonthlyReservationData
? convertApiToEvent(MonthlyReservationData)
: [];
if (!MonthlyReservationData) return null;

return (
<div className="md:shadow-calendar bg-background h-fit w-full rounded-3xl pt-5 pb-2.5">
Expand Down
4 changes: 4 additions & 0 deletions src/types/reserved-schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ export interface ReservationListResponse {
totalCount: number;
cursorId: number | null;
}

export type UpdateReservationApprovalReq = {
status: ReservationStatusType;
};