diff --git a/src/app/(with-header)/mypage/dashboard/page.tsx b/src/app/(with-header)/mypage/dashboard/page.tsx
index c3e3645..c6aa7c4 100644
--- a/src/app/(with-header)/mypage/dashboard/page.tsx
+++ b/src/app/(with-header)/mypage/dashboard/page.tsx
@@ -20,11 +20,13 @@ export default function MyDashboardPage() {
// 내 체험 리스트 조회
const { data: activitiesData, isLoading, error } = useMyActivities();
- const { activityOptions, uniqueTitles, handleActivityChange } =
- useActivityOptions(activitiesData, (activityId) => {
+ const { activityOptions, handleActivityChange } = useActivityOptions(
+ activitiesData,
+ (activityId) => {
setSelectedActivityId(activityId);
setSelectedDate('');
- });
+ },
+ );
// 페이지 로드 시 첫 번째 체험 자동 선택
useEffect(() => {
@@ -38,11 +40,8 @@ export default function MyDashboardPage() {
}
}, [activitiesData, selectedActivityId]);
- // 현재 선택된 체험의 제목 찾기
- const selectedActivityTitle =
- activityOptions.find(
- (option) => parseInt(option.value) === selectedActivityId,
- )?.label || '';
+ // 현재 선택된 체험의 ID를 문자열로 변환
+ const selectedActivityValue = selectedActivityId?.toString() || '';
// 날짜 클릭 (모달 열기)
const handleDateClick = (date: string) => {
@@ -118,8 +117,9 @@ export default function MyDashboardPage() {
{/* 체험 선택 드롭다운 */}
{
if (reviewModal.reservationId) {
+ const reservation = allReservations.find(
+ (r) => r.id === reviewModal.reservationId,
+ );
+
createReviewMutation.mutate(
{
reservationId: reviewModal.reservationId,
@@ -109,16 +117,10 @@ export default function MyReservationsPage() {
},
{
onSuccess: () => {
- setReviewModal({
- isOpen: false,
- reservationId: null,
- activityTitle: null,
- activityImage: null,
- activityDate: null,
- activityTime: null,
- headCount: null,
- totalPrice: null,
- });
+ // 성공 후 추가 쿼리 무효화
+ if (reservation?.activity.id) {
+ invalidateActivityQueries(reservation.activity.id);
+ }
},
},
);
diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx
index e6905d2..4fc0495 100644
--- a/src/components/Dropdown.tsx
+++ b/src/components/Dropdown.tsx
@@ -5,7 +5,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import cn from '@lib/cn';
import useOutsideClick from '@hooks/useOutsideClick';
import ChevronIcon from '@assets/svg/chevron';
-import { DropdownProps } from '@/types/dropdownTypes';
+import { DropdownProps, DropdownOption } from '@/types/dropdownTypes';
// import CheckIcon from '@assets/svg/check';
/**
@@ -28,6 +28,7 @@ import { DropdownProps } from '@/types/dropdownTypes';
*/
export default function Dropdown({
options,
+ optionData,
value,
onChange,
placeholder,
@@ -47,10 +48,21 @@ export default function Dropdown({
const dropdownRef = useRef(null);
const buttonRef = useRef(null);
+ // optionData가 제공되면 우선 사용, 없으면 options 사용
+ const useOptionData = !!optionData;
+ const finalOptions = useOptionData
+ ? optionData!
+ : options.map(opt => ({ value: opt, label: opt }));
+
// 내/외부 상태 관리 판별
const isControlled = value !== undefined;
const selectedValue = isControlled ? value : internalValue;
+ // 선택된 값에 해당하는 표시 텍스트 찾기
+ const displayValue = useOptionData
+ ? finalOptions.find(opt => opt.value === selectedValue)?.label || ''
+ : selectedValue;
+
// 외부 클릭 감지
useOutsideClick(dropdownRef, () => {
setIsOpen(false);
@@ -58,11 +70,12 @@ export default function Dropdown({
});
// 값 선택 핸들러
- const handleSelect = (option: T) => {
+ const handleSelect = (optionData: DropdownOption) => {
+ const selectedValue = optionData.value as T;
if (!isControlled) {
- setInternalValue(option);
+ setInternalValue(selectedValue);
}
- onChange?.(option);
+ onChange?.(selectedValue);
setIsOpen(false);
setFocusedIndex(-1);
buttonRef.current?.focus();
@@ -77,7 +90,7 @@ export default function Dropdown({
case ' ':
e.preventDefault();
if (isOpen && focusedIndex >= 0) {
- handleSelect(options[focusedIndex]);
+ handleSelect(finalOptions[focusedIndex]);
} else {
setIsOpen(!isOpen);
}
@@ -88,7 +101,7 @@ export default function Dropdown({
if (!isOpen) {
setIsOpen(true);
} else {
- setFocusedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
+ setFocusedIndex((prev) => (prev < finalOptions.length - 1 ? prev + 1 : 0));
}
break;
@@ -97,7 +110,7 @@ export default function Dropdown({
if (!isOpen) {
setIsOpen(true);
} else {
- setFocusedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
+ setFocusedIndex((prev) => (prev > 0 ? prev - 1 : finalOptions.length - 1));
}
break;
@@ -149,7 +162,7 @@ export default function Dropdown({
truncateText && 'flex-1 truncate text-left',
)}
>
- {selectedValue || placeholder}
+ {displayValue || placeholder}
({
listboxClassName,
)}
>
- {options.map((option, index) => {
- const isSelected = option === selectedValue;
+ {finalOptions.map((optionItem, index) => {
+ const isSelected = optionItem.value === selectedValue;
const isFocused = index === focusedIndex;
return (
({
: 'hover:bg-gray-100',
optionClassName,
)}
- onClick={() => handleSelect(option)}
+ onClick={() => handleSelect(optionItem)}
onMouseEnter={() => setFocusedIndex(index)}
>
{/* 아이콘 영역 */}
@@ -211,7 +224,7 @@ export default function Dropdown({
/>
)}
*/}
- {option}
+ {optionItem.label}
);
})}
diff --git a/src/hooks/useActivityOptions.ts b/src/hooks/useActivityOptions.ts
index 386e1f4..a83abb3 100644
--- a/src/hooks/useActivityOptions.ts
+++ b/src/hooks/useActivityOptions.ts
@@ -15,57 +15,43 @@ export const useActivityOptions = (
) => {
const queryClient = useQueryClient();
- const { activityOptions, uniqueTitles } = useMemo(() => {
+ const activityOptions = useMemo(() => {
if (!activitiesData?.activities) {
- return {
- activityOptions: [] as ActivityOption[],
- uniqueTitles: [] as string[],
- };
+ return [] as ActivityOption[];
}
- const options: ActivityOption[] = activitiesData.activities.map(
+ return activitiesData.activities.map(
(activity: { id: number; title: string }) => ({
value: activity.id.toString(),
label: activity.title,
}),
);
-
- const uniqueTitles = options.map((option) => option.label);
-
- return { activityOptions: options, uniqueTitles };
}, [activitiesData]);
// 체험 변경 시 쿼리 무효화 함수
- const handleActivityChange = (selectedTitle: string): void => {
- const selectedOption = activityOptions.find(
- (option: ActivityOption) => option.label === selectedTitle,
- );
-
- if (selectedOption) {
- const activityId = parseInt(selectedOption.value);
-
- // 관련 쿼리들 무효화
- queryClient.invalidateQueries({
- queryKey: ['reservedSchedules'],
- exact: false,
- });
- queryClient.invalidateQueries({
- queryKey: ['activityReservations'],
- exact: false,
- });
- queryClient.invalidateQueries({
- queryKey: ['monthlyReservationDashboard'],
- exact: false,
- });
-
- // 콜백 호출
- onActivityChange?.(activityId);
- }
+ const handleActivityChange = (selectedValue: string): void => {
+ const activityId = parseInt(selectedValue);
+
+ // 관련 쿼리들 무효화
+ queryClient.invalidateQueries({
+ queryKey: ['reservedSchedules'],
+ exact: false,
+ });
+ queryClient.invalidateQueries({
+ queryKey: ['activityReservations'],
+ exact: false,
+ });
+ queryClient.invalidateQueries({
+ queryKey: ['monthlyReservationDashboard'],
+ exact: false,
+ });
+
+ // 콜백 호출
+ onActivityChange?.(activityId);
};
return {
activityOptions,
- uniqueTitles,
handleActivityChange,
};
};
diff --git a/src/hooks/useMyActivitiesQueries.ts b/src/hooks/useMyActivitiesQueries.ts
index 59d7ae6..27fb0b3 100644
--- a/src/hooks/useMyActivitiesQueries.ts
+++ b/src/hooks/useMyActivitiesQueries.ts
@@ -10,6 +10,7 @@ import {
deleteMyActivity,
} from '@/apis/myActivities';
import { toast } from 'sonner';
+import { AxiosError } from 'axios';
export const MY_ACTIVITIES_QUERY_KEYS = {
ALL: ['my-activities'] as const,
@@ -54,7 +55,25 @@ export const useDeleteMyActivity = () => {
toast.success('체험이 삭제되었습니다.');
},
onError: (error) => {
- toast.error(`체험 삭제 실패: ${error.message}`);
+ const axiosError = error as AxiosError;
+ const status = axiosError.response?.status;
+
+ switch (status) {
+ case 400:
+ toast.error('예약이 있는 체험은 삭제할 수 없습니다.');
+ break;
+ case 401:
+ toast.error('로그인이 필요합니다.');
+ break;
+ case 403:
+ toast.error('삭제 권한이 없습니다.');
+ break;
+ case 404:
+ toast.error('존재하지 않는 체험입니다.');
+ break;
+ default:
+ toast.error('체험 삭제에 실패했습니다. 다시 시도해주세요.');
+ }
},
});
};
diff --git a/src/hooks/useReservationQueries.ts b/src/hooks/useReservationQueries.ts
index 55d4e49..14f67bb 100644
--- a/src/hooks/useReservationQueries.ts
+++ b/src/hooks/useReservationQueries.ts
@@ -74,10 +74,11 @@ export const useCreateReview = () => {
data: CreateReviewRequest;
}) => createReview(reservationId, data),
onSuccess: () => {
- // 예약 리스트 캐시 무효화하여 reviewSubmitted 상태 업데이트
+ // 기존 쿼리 무효화
queryClient.invalidateQueries({
queryKey: RESERVATION_QUERY_KEYS.RESERVATIONS,
});
+
toast.success('후기가 작성되었습니다.');
},
onError: (error) => {
@@ -85,3 +86,25 @@ export const useCreateReview = () => {
},
});
};
+
+// 쿼리 무효화 훅
+export const useInvalidateActivityQueries = () => {
+ const queryClient = useQueryClient();
+
+ return (activityId: number) => {
+ queryClient.invalidateQueries({
+ queryKey: ['popularExperiences'],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ['experiences'],
+ exact: false,
+ });
+ queryClient.invalidateQueries({
+ queryKey: ['activity', activityId.toString()],
+ });
+ queryClient.invalidateQueries({
+ queryKey: ['reviews'],
+ exact: false,
+ });
+ };
+};
diff --git a/src/types/dropdownTypes.ts b/src/types/dropdownTypes.ts
index c1fec3f..b65123b 100644
--- a/src/types/dropdownTypes.ts
+++ b/src/types/dropdownTypes.ts
@@ -1,5 +1,10 @@
import { ClassValue } from 'clsx';
+export interface DropdownOption {
+ value: string;
+ label: string;
+}
+
/**
* 범용 Dropdown 컴포넌트의 props 정의입니다.
* 제네릭 타입 T를 사용하여 다양한 문자열 옵션 타입을 지원합니다.
@@ -16,6 +21,7 @@ import { ClassValue } from 'clsx';
*/
export interface DropdownProps {
options: readonly T[];
+ optionData?: readonly DropdownOption[];
value?: T | '';
onChange?: (value: T) => void;
placeholder?: string;