Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bf647ae
fix: 예약 현황 드롭다운 텍스트 오버플로우 해결
LeeCh0129 Aug 3, 2025
4025fd5
refactor: Mypage 레이아웃 안정성 개선
LeeCh0129 Aug 3, 2025
f2e5d7d
fix: 예약 내역 상태별 버튼표시 로직 수정 및 만료상태 추가
LeeCh0129 Aug 3, 2025
7b0223f
fix: 예약후기 작성 모달 반응형 및 오버레이 수정
LeeCh0129 Aug 3, 2025
094a47c
fix: 내 정보 페ㅐ이지 유효성 검사 및 UX 개선
LeeCh0129 Aug 3, 2025
214a21d
refactor: 내 정보 수정 alert -> 토스트 메시지로 교체
LeeCh0129 Aug 3, 2025
7807799
refactor: 마이페이지(정보, 내역, 관리, 현황) 스켈레톤 UI 개선
LeeCh0129 Aug 3, 2025
892b6e4
fix: 예약 현황 탭별 시간대 필터링 및 실시간 업데이트, 토스트 메시지 개선
LeeCh0129 Aug 3, 2025
6ac0463
fix: 예약내역 페이지 스켈레톤UI 개선
LeeCh0129 Aug 3, 2025
46594eb
fix: 체험 삭제 시 쿼리 무효화 로직 및 토스트 메시지 개선
LeeCh0129 Aug 3, 2025
3a20ffb
refactor: 마이페이지 관련 alert->토스트 메시지로 교체
LeeCh0129 Aug 3, 2025
806f4b2
fix: 예약 현황 페이지 체험 변경 시 쿼리 무효화 로직 추가
LeeCh0129 Aug 3, 2025
d55662c
feat: 내 예약 카드 클릭 시 체험 상세페이지로 이동
LeeCh0129 Aug 3, 2025
9bfe31d
feat: 내 체험 관리 카드 클릭 시 체험 상세페이지로 이동
LeeCh0129 Aug 3, 2025
3193b65
fix: 예약 완료 후 예약 내역 페이지 쿼리 무효화 로직 추가
LeeCh0129 Aug 4, 2025
48bfc8d
Merge branch 'develop' into fix/128
LeeCh0129 Aug 4, 2025
a0b5114
fix: 빌드에러 해결 - 사용하지 않는 변수 제거
LeeCh0129 Aug 4, 2025
a0ccff2
fix: 빌드에러 해결 - 사용하지 않는 변수 제거(2)
LeeCh0129 Aug 4, 2025
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
13 changes: 8 additions & 5 deletions src/app/(with-header)/activities/[id]/hooks/useDeleteActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { privateInstance } from '@/apis/privateInstance';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AxiosError } from 'axios';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';

const deleteActivity = async (id: string) => {
const response = await privateInstance.delete(`/deleteActivity/${id}`);
Expand All @@ -15,9 +16,12 @@ export const useDeleteActivity = () => {
return useMutation({
mutationFn: deleteActivity,
onSuccess: (_data) => {
queryClient.invalidateQueries({ queryKey: ['activity'] }); // 내 체험 관리
queryClient.invalidateQueries({ queryKey: ['experiences'], exact: false }); // 모든 체험 리스트
queryClient.invalidateQueries({ queryKey: ['popularExperiences'] }); // 인기 체험
queryClient.invalidateQueries({ queryKey: ['activity'] });
queryClient.invalidateQueries({
queryKey: ['experiences'],
exact: false,
});
queryClient.invalidateQueries({ queryKey: ['popularExperiences'] });
router.push(`/`);
},
onError: (error: AxiosError) => {
Expand All @@ -27,8 +31,7 @@ export const useDeleteActivity = () => {

console.error('전체 에러:', error);

alert(
//토스트로 대체
toast.error(
responseData?.error ||
responseData?.message ||
error.message ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export default function ActivityCard({

const { id, title, price, bannerImageUrl, rating, reviewCount } = activity;

const handleCardClick = () => {
router.push(`/activities/${id}`);
};

const handleEdit = () => {
router.push(`/myactivity/${id}`);
};
Expand All @@ -31,7 +35,10 @@ export default function ActivityCard({
};

return (
<div className='flex h-128 w-full max-w-792 flex-row rounded-3xl border border-gray-300 bg-white sm:h-156 lg:h-204'>
<div
className='flex h-128 w-full max-w-792 cursor-pointer flex-row rounded-3xl border border-gray-300 bg-white transition-shadow hover:shadow-md sm:h-156 lg:h-204'
onClick={handleCardClick}
>
Comment on lines +38 to +41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

접근성 개선이 필요합니다

정적 분석 도구에서 지적한 대로 접근성 문제가 있습니다. 키보드 네비게이션과 적절한 역할(role) 속성이 필요합니다.

다음과 같이 수정하여 접근성을 개선하세요:

-    <div
-      className='flex h-128 w-full max-w-792 cursor-pointer flex-row rounded-3xl border border-gray-300 bg-white transition-shadow hover:shadow-md sm:h-156 lg:h-204'
-      onClick={handleCardClick}
-    >
+    <div
+      className='flex h-128 w-full max-w-792 cursor-pointer flex-row rounded-3xl border border-gray-300 bg-white transition-shadow hover:shadow-md sm:h-156 lg:h-204'
+      onClick={handleCardClick}
+      onKeyDown={(e) => {
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          handleCardClick();
+        }
+      }}
+      role="button"
+      tabIndex={0}
+      aria-label={`${title} 활동 상세보기`}
+    >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className='flex h-128 w-full max-w-792 cursor-pointer flex-row rounded-3xl border border-gray-300 bg-white transition-shadow hover:shadow-md sm:h-156 lg:h-204'
onClick={handleCardClick}
>
<div
className='flex h-128 w-full max-w-792 cursor-pointer flex-row rounded-3xl border border-gray-300 bg-white transition-shadow hover:shadow-md sm:h-156 lg:h-204'
onClick={handleCardClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleCardClick();
}
}}
role="button"
tabIndex={0}
aria-label={`${title} 활동 상세보기`}
>
🧰 Tools
🪛 Biome (2.1.2)

[error] 38-41: Static Elements should not be interactive.

To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.

(lint/a11y/noStaticElementInteractions)


[error] 38-41: Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.

Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.

(lint/a11y/useKeyWithClickEvents)

🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/activities/components/ActivityCard.tsx around
lines 38 to 41, the div acting as a clickable card lacks accessibility features.
To fix this, replace the div with a semantic interactive element like a button
or add role="button" to the div, ensure it is focusable by adding tabIndex={0},
and handle keyboard events such as onKeyDown to trigger handleCardClick on Enter
or Space key presses. This will improve keyboard navigation and accessibility
compliance.

{/* 이미지 영역 */}
<div className='relative h-full w-128 flex-shrink-0 overflow-hidden rounded-l-3xl sm:w-156 lg:w-204'>
<Image src={bannerImageUrl} alt={title} fill className='object-cover' />
Expand Down Expand Up @@ -68,7 +75,10 @@ export default function ActivityCard({
{/* 더보기 옵션 */}
<div className='relative'>
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(!isMenuOpen);
}}
className='flex h-40 w-40 items-center justify-center rounded-full hover:bg-gray-100'
>
<MoreOptionsIcon size={40} />
Expand All @@ -78,18 +88,27 @@ export default function ActivityCard({
<>
<div
className='fixed inset-0 z-40'
onClick={() => setIsMenuOpen(false)}
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(false);
}}
/>
{/* 드롭다운 메뉴 */}
<div className='absolute top-full right-0 z-50 w-120 rounded-md border border-gray-300 bg-white shadow-lg sm:w-160'>
<button
onClick={handleEdit}
onClick={(e) => {
e.stopPropagation();
handleEdit();
}}
className='flex h-50 w-full items-center justify-center border-b border-gray-300 px-4 py-3 text-center text-base font-medium text-gray-900 hover:bg-gray-50 sm:h-62 sm:px-46 sm:py-18 sm:text-lg'
>
수정하기
</button>
Comment on lines 98 to 106
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버튼 타입 명시가 필요합니다

정적 분석에서 지적한 대로 버튼 요소에 명시적인 type 속성이 필요합니다.

                  <button
+                    type="button"
                    onClick={(e) => {
                      e.stopPropagation();
                      handleEdit();
                    }}

동일하게 삭제 버튼에도 적용하세요:

                  <button
+                    type="button"
                    onClick={(e) => {
                      e.stopPropagation();
                      handleDelete();
                    }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={handleEdit}
onClick={(e) => {
e.stopPropagation();
handleEdit();
}}
className='flex h-50 w-full items-center justify-center border-b border-gray-300 px-4 py-3 text-center text-base font-medium text-gray-900 hover:bg-gray-50 sm:h-62 sm:px-46 sm:py-18 sm:text-lg'
>
수정하기
</button>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleEdit();
}}
className='flex h-50 w-full items-center justify-center border-b border-gray-300 px-4 py-3 text-center text-base font-medium text-gray-900 hover:bg-gray-50 sm:h-62 sm:px-46 sm:py-18 sm:text-lg'
>
수정하기
</button>
🤖 Prompt for AI Agents
In src/app/(with-header)/mypage/activities/components/ActivityCard.tsx around
lines 98 to 106, the button element lacks an explicit type attribute, which can
cause unintended form submissions. Add type="button" to this button and also to
the delete button elsewhere in the file to explicitly specify their behavior and
avoid default submit actions.

<button
onClick={handleDelete}
onClick={(e) => {
e.stopPropagation();
handleDelete();
}}
className='flex h-50 w-full items-center justify-center px-4 py-3 text-center text-base font-medium text-gray-900 hover:bg-gray-50 sm:h-62 sm:px-46 sm:py-18 sm:text-lg'
>
삭제하기
Expand Down
60 changes: 57 additions & 3 deletions src/app/(with-header)/mypage/activities/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,35 @@ export default function MyActivitiesPage() {
{[1, 2, 3].map((i) => (
<div
key={i}
className='rounded-24 h-204 w-792 animate-pulse bg-gray-200'
/>
className='flex h-128 w-full max-w-792 flex-row rounded-3xl border border-gray-300 bg-white sm:h-156 lg:h-204'
>
{/* 이미지 영역 스켈레톤 */}
<div className='h-full w-128 flex-shrink-0 animate-pulse rounded-l-3xl bg-gray-200 sm:w-156 lg:w-204'></div>

{/* 콘텐츠 영역 스켈레톤 */}
<div className='flex w-0 flex-grow flex-col justify-start px-12 py-10 sm:px-16 sm:py-12 lg:px-24 lg:py-14'>
{/* 별점 및 리뷰 스켈레톤 */}
<div className='flex items-center gap-6'>
<div className='flex items-center gap-2'>
<div className='h-4 w-4 animate-pulse rounded bg-gray-200 sm:h-5 sm:w-5'></div>
<div className='h-4 w-8 animate-pulse rounded bg-gray-200 sm:h-5 sm:w-10'></div>
<div className='h-4 w-12 animate-pulse rounded bg-gray-200 sm:h-5 sm:w-16'></div>
</div>
</div>

{/* 제목 스켈레톤 */}
<div className='mt-2 h-4 w-40 animate-pulse rounded bg-gray-200 sm:mt-4 sm:h-5 sm:w-48 lg:mt-6 lg:h-6 lg:w-56'></div>

{/* 가격 + 더보기 버튼 영역 스켈레톤 */}
<div className='mt-auto flex items-center justify-between'>
{/* 가격 스켈레톤 */}
<div className='h-6 w-32 animate-pulse rounded bg-gray-200 sm:h-6 sm:w-36 lg:h-7 lg:w-40'></div>

{/* 더보기 버튼 스켈레톤 */}
<div className='h-40 w-40 animate-pulse rounded-full bg-gray-200'></div>
</div>
</div>
</div>
))}
</div>
</div>
Expand Down Expand Up @@ -157,7 +184,34 @@ export default function MyActivitiesPage() {

{/* 무한 스크롤 로딩 */}
{isFetchingNextPage && (
<div className='rounded-24 h-204 w-792 animate-pulse bg-gray-200' />
<div className='flex h-128 w-full max-w-792 flex-row rounded-3xl border border-gray-300 bg-white sm:h-156 lg:h-204'>
{/* 이미지 영역 스켈레톤 */}
<div className='h-full w-128 flex-shrink-0 animate-pulse rounded-l-3xl bg-gray-200 sm:w-156 lg:w-204'></div>

{/* 콘텐츠 영역 스켈레톤 */}
<div className='flex w-0 flex-grow flex-col justify-start px-12 py-10 sm:px-16 sm:py-12 lg:px-24 lg:py-14'>
{/* 별점 및 리뷰 스켈레톤 */}
<div className='flex items-center gap-6'>
<div className='flex items-center gap-2'>
<div className='h-4 w-4 animate-pulse rounded bg-gray-200 sm:h-5 sm:w-5'></div>
<div className='h-4 w-8 animate-pulse rounded bg-gray-200 sm:h-5 sm:w-10'></div>
<div className='h-4 w-12 animate-pulse rounded bg-gray-200 sm:h-5 sm:w-16'></div>
</div>
</div>

{/* 제목 스켈레톤 */}
<div className='mt-2 h-4 w-40 animate-pulse rounded bg-gray-200 sm:mt-4 sm:h-5 sm:w-48 lg:mt-6 lg:h-6 lg:w-56'></div>

{/* 가격 + 더보기 버튼 영역 스켈레톤 */}
<div className='mt-auto flex items-center justify-between'>
{/* 가격 스켈레톤 */}
<div className='h-6 w-32 animate-pulse rounded bg-gray-200 sm:h-6 sm:w-36 lg:h-7 lg:w-40'></div>

{/* 더보기 버튼 스켈레톤 */}
<div className='h-40 w-40 animate-pulse rounded-full bg-gray-200'></div>
</div>
</div>
</div>
)}
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
useReservedSchedules,
useActivityReservations,
Expand All @@ -9,14 +10,15 @@ import {
import { DashboardFilterOption } from '@/types/dashboardTypes';
import { DASHBOARD_TAB_OPTIONS } from '@/constants/dashboardConstants';
import {
createTimeSlotOptions,
createFilteredTimeSlotOptions,
getScheduleIdFromTimeSlot,
getSelectedTimeSlotValue,
} from '@/utils/timeSlotUtils';
import Dropdown from '@/components/Dropdown';
import ReservationActionButtons from './ReservationActionButtons';
import dayjs from 'dayjs';
import CloseIcon from '@assets/svg/close';
import { toast } from 'sonner';

interface Props {
isOpen: boolean;
Expand Down Expand Up @@ -50,13 +52,28 @@ export default function ReservationInfoModal({

// 예약 상태 업데이트
const updateReservationMutation = useUpdateActivityReservationStatus();
const queryClient = useQueryClient();

const timeSlotOptions = createTimeSlotOptions(schedules);
const timeSlotOptions = createFilteredTimeSlotOptions(schedules, activeTab);
const selectedTimeSlotValue = getSelectedTimeSlotValue(
schedules,
selectedScheduleId,
);

// 탭 변경 시 현재 선택된 시간대가 유효한지 확인하고 초기화
useEffect(() => {
if (selectedScheduleId && schedules) {
const currentSchedule = schedules.find(
(schedule) => schedule.scheduleId === selectedScheduleId,
);

// 현재 선택된 시간대가 새 탭에서 유효하지 않으면 초기화
if (!currentSchedule || currentSchedule.count[activeTab] === 0) {
setSelectedScheduleId(null);
}
}
}, [activeTab, schedules, selectedScheduleId]);

// 예약 승인/거절 처리
const handleReservationAction = async (
reservationId: number,
Expand All @@ -69,7 +86,7 @@ export default function ReservationInfoModal({
data: { status },
});
} catch {
alert('예약 처리에 실패했습니다.');
toast.error('예약 처리에 실패했습니다.');
}
};

Expand Down
24 changes: 9 additions & 15 deletions src/app/(with-header)/mypage/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useActivityOptions } from '@/hooks/useActivityOptions';
import EmptyDashboard from './components/EmptyDashboard';
import ReservationDashboardCalendar from './components/ReservationDashboardCalendar';
import ReservationInfoModal from './components/ReservationInfoModal';
import CalendarSkeleton from './components/CalendarSkeleton';

export default function MyDashboardPage() {
const [selectedActivityId, setSelectedActivityId] = useState<number | null>(
Expand All @@ -19,7 +20,11 @@ export default function MyDashboardPage() {

// 내 체험 리스트 조회
const { data: activitiesData, isLoading, error } = useMyActivities();
const { activityOptions, uniqueTitles } = useActivityOptions(activitiesData);
const { activityOptions, uniqueTitles, handleActivityChange } =
useActivityOptions(activitiesData, (activityId) => {
setSelectedActivityId(activityId);
setSelectedDate('');
});

// 페이지 로드 시 첫 번째 체험 자동 선택
useEffect(() => {
Expand All @@ -33,18 +38,6 @@ export default function MyDashboardPage() {
}
}, [activitiesData, selectedActivityId]);

// 체험 선택 -> 제목으로 ID 찾기
const handleActivityChange = (selectedTitle: string) => {
const selectedOption = activityOptions.find(
(option) => option.label === selectedTitle,
);

if (selectedOption) {
setSelectedActivityId(parseInt(selectedOption.value));
setSelectedDate('');
}
};

// 현재 선택된 체험의 제목 찾기
const selectedActivityTitle =
activityOptions.find(
Expand Down Expand Up @@ -76,7 +69,7 @@ export default function MyDashboardPage() {
<div className='mb-48 h-56 w-full max-w-792 animate-pulse rounded-md bg-gray-200' />

{/* 달력 스켈레톤 */}
<div className='rounded-24 h-600 w-full animate-pulse bg-gray-200' />
<CalendarSkeleton calendarWeeksLength={6} />
</div>
);
}
Expand Down Expand Up @@ -129,7 +122,8 @@ export default function MyDashboardPage() {
value={selectedActivityTitle}
onChange={handleActivityChange}
placeholder='체험을 선택하세요'
className='h-56'
className='h-56 min-w-0'
truncateText={true}
/>
</div>

Expand Down
27 changes: 6 additions & 21 deletions src/app/(with-header)/mypage/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

import { ProfileNavigation } from './components';
import useResponsiveRouting from '@/hooks/useResponsiveRouting';
import { useMyProfile } from '@/hooks/useMyPageQueries';

export default function MyPageLayout({
children,
}: {
children: React.ReactNode;
}) {
const { mounted } = useResponsiveRouting();
const { isLoading, error } = useMyProfile();

// mounted + API 로딩 상태 모두 체크
if (!mounted || isLoading) {
// mounted 상태만 체크
if (!mounted) {
return (
<div className='min-h-screen bg-gray-100'>
<div className='mx-auto max-w-1200 px-20 py-24 lg:py-72'>
Expand All @@ -36,28 +34,15 @@ export default function MyPageLayout({
</div>
</div>
</div>
{/* 메인 스켈레톤 */}
<div className='flex-grow animate-pulse rounded bg-gray-200'></div>
{/* 메인 영역 */}
<div className='min-w-0 flex-grow'></div>
</div>
</div>
</div>
);
}

if (error) {
return (
<div className='flex min-h-screen items-center justify-center bg-gray-100'>
<div className='text-center'>
<h2 className='mb-2 text-xl font-bold text-red-500'>
로그인이 필요합니다
</h2>
<p className='text-gray-600'>다시 로그인해주세요.</p>
</div>
</div>
);
}

// API 로딩 완료 + mounted 상태일 때만 실행
// mounted 상태일 때만 실행
return (
<div className='min-h-screen bg-gray-100'>
<div className='mx-auto max-w-1200 px-20 py-24 lg:py-72'>
Expand All @@ -66,7 +51,7 @@ export default function MyPageLayout({
<ProfileNavigation />

{/* 우측 메인 콘텐츠 섹션 */}
<div className='flex-grow'>{children}</div>
<div className='min-w-0 flex-grow'>{children}</div>
</div>
</div>
</div>
Expand Down
Loading