diff --git a/apps/what-today/src/pages/mypage/main/index.tsx b/apps/what-today/src/pages/mypage/main/index.tsx index a55e42a8..a96ef4a1 100644 --- a/apps/what-today/src/pages/mypage/main/index.tsx +++ b/apps/what-today/src/pages/mypage/main/index.tsx @@ -1,9 +1,13 @@ import { useQueries, useQuery } from '@tanstack/react-query'; import { + Button, + EmptyLogo, MypageProfileHeader, MypageSummaryCard, OngoingExperienceCard, + OngoingExperienceCardSkeleton, UpcomingSchedule, + UpcomingScheduleItemSkeleton, useToast, } from '@what-today/design-system'; import dayjs from 'dayjs'; @@ -18,6 +22,36 @@ import type { monthlyScheduleResponse } from '@/schemas/myActivities'; import type { MyReservationsResponse } from '@/schemas/myReservations'; import { useWhatTodayStore } from '@/stores'; +function NoResultUpcoming() { + const navigate = useNavigate(); + + return ( +
+ + +
+ ); +} + +function NoResultOngoing() { + const navigate = useNavigate(); + + return ( +
+ + +
+ ); +} + export default function MyPage() { const navigate = useNavigate(); @@ -30,7 +64,7 @@ export default function MyPage() { const month = dayjs().format('MM'); // 등록한 체험 갯수 - const { data: activityData } = useInfiniteMyActivitiesQuery(MAX_PAGE_SIZE); + const { data: activityData, isLoading: isLoadingActivities } = useInfiniteMyActivitiesQuery(MAX_PAGE_SIZE); const totalActivity = activityData?.pages[0]?.totalCount; // 이번달 예약 승인 대기 갯수 @@ -68,7 +102,7 @@ export default function MyPage() { const reviewRequired = completedData?.reservations.filter((res) => res.reviewSubmitted === false).length ?? 0; // 다가오는 체험 데이터 - const { data: confirmedData } = useQuery({ + const { data: confirmedData, isLoading: isLoadingConfirmed } = useQuery({ queryKey: ['reservations', 'confirmed'], queryFn: () => fetchMyReservations({ @@ -80,6 +114,54 @@ export default function MyPage() { enabled: Boolean(user), }); + const sortedReservations = [...(confirmedData?.reservations ?? [])].sort((a, b) => + dayjs(a.date).isAfter(b.date) ? 1 : -1, + ); + + let upcomingScheduleContent = null; + + if (isLoadingConfirmed) { + // 1. 로딩 중: 스켈레톤 3개 + upcomingScheduleContent = ( + <> + + + + + ); + } else if (sortedReservations.length > 0) { + // 2. 데이터 있음: 날짜별 그룹으로 렌더링 + upcomingScheduleContent = (() => { + let prevDate: string | null = null; + + return sortedReservations.map((res, idx, arr) => { + const showDateLabel = res.date !== prevDate; + const isLast = idx === arr.length - 1; + prevDate = res.date; + + return ( +
navigate(`/activities/${res.activity.id}`)} + > + {showDateLabel &&

{res.date}

} + +
+ ); + }); + })(); + } else { + // 3. 데이터 없음 + upcomingScheduleContent = ; + } + // 이번 달 모집 중인 체험 const reservationAvailableResults = useQueries({ queries: activityIds.map((id) => ({ @@ -90,14 +172,52 @@ export default function MyPage() { enabled: !!id, })), }); + + // useQueries 로딩 상태 + const isLoadingAvailableQueries = + reservationAvailableResults.length === 0 || // 아직 activityIds 준비 전 + reservationAvailableResults.some((q) => q.isLoading || q.isFetching); + const availableActivityIds = reservationAvailableResults .map((result, index) => ({ data: result.data, activityId: activityIds[index] })) .filter(({ data }) => Array.isArray(data) && data.length > 0) .map(({ activityId }) => activityId); + // 1. useInfiniteMyActivitiesQuery에서 받은 모든 pages를 펼침 const allActivities = activityData?.pages.flatMap((page) => page.activities) ?? []; // 2. 예약 가능 activityId와 일치하는 항목만 필터링 const availableActivities = allActivities.filter((activity) => availableActivityIds.includes(activity.id)); + // 3. 최종 스켈레톤 노출 여부 + const isLoadingAvailable = isLoadingActivities || isLoadingAvailableQueries; + // 4. 모집 중인 체험에 띄울 콘텐츠 결정 (스켈레톤 UI or 데이터 없음 or 실제 데이터) + let ongoingExperienceContent = null; + if (isLoadingAvailable) { + ongoingExperienceContent = ( + <> + + + + + + ); + } else if (availableActivities.length > 0) { + ongoingExperienceContent = ( + <> + {availableActivities.map((act) => ( + navigate(`/activities/${id}`)} + /> + ))} + + ); + } else { + ongoingExperienceContent = ; + } const handleLogout = () => { logoutUser(); @@ -110,15 +230,14 @@ export default function MyPage() { }; return (
- {/* */} -
+
-
+
@@ -138,22 +257,31 @@ export default function MyPage() { />
-
-

{`${dayjs().format('M')}월 모집 중인 체험`}

- navigate('/experiences/create')} - onClickActivity={(id) => navigate(`/activities/${id}`)} - /> +
+

{`${dayjs().format('M')}월 모집 중인 체험`}

+
+
+ {/* flex로 한 줄로 나열해두고 overflow-x-auto를 부모 너비가 같이 늘어났음 */} + {ongoingExperienceContent} +
+
-
-

다가오는 일정

- navigate('/')} - onClickReservation={(id) => navigate(`/activities/${id}`)} - /> + +
+

다가오는 일정

+ +
+
+ {/* 왼쪽 타임라인 */} + {sortedReservations.length > 0 && ( +
+
+
+
+ )} +
{upcomingScheduleContent}
+
+
diff --git a/packages/design-system/src/components/MypageProfileHeader.tsx b/packages/design-system/src/components/MypageProfileHeader.tsx index 1d27fb29..cb445db0 100644 --- a/packages/design-system/src/components/MypageProfileHeader.tsx +++ b/packages/design-system/src/components/MypageProfileHeader.tsx @@ -11,31 +11,31 @@ interface MypageProfileHeaderProps { export default function MypageProfileHeader({ name, email, profileImageUrl, onLogoutClick }: MypageProfileHeaderProps) { return ( -
-
+
+
{profileImageUrl ? ( 프로필 이미지 ) : ( - + )}
-
+
-

{name}

-

{email}

+

{name}

+

{email}

diff --git a/packages/design-system/src/components/MypageSummaryCard.tsx b/packages/design-system/src/components/MypageSummaryCard.tsx index ee51a27a..ad8ab5fd 100644 --- a/packages/design-system/src/components/MypageSummaryCard.tsx +++ b/packages/design-system/src/components/MypageSummaryCard.tsx @@ -12,7 +12,7 @@ function SummaryRoot({ children, className }: { children: React.ReactNode; class return (
@@ -23,9 +23,9 @@ function SummaryRoot({ children, className }: { children: React.ReactNode; class function SummaryItem({ count, label, onClick, countClassName, labelClassName }: SummaryItemProps) { return ( -
-

{count}

-

{label}

+
+

{count}

+

{label}

); } diff --git a/packages/design-system/src/components/OngoingExperienceCard.tsx b/packages/design-system/src/components/OngoingExperienceCard.tsx index da9fb87a..11e954fa 100644 --- a/packages/design-system/src/components/OngoingExperienceCard.tsx +++ b/packages/design-system/src/components/OngoingExperienceCard.tsx @@ -1,8 +1,3 @@ -import { twMerge } from 'tailwind-merge'; - -import Button from './button'; -import { EmptyLogo } from './logos'; - interface Activity { id: number; bannerImageUrl: string; @@ -10,42 +5,24 @@ interface Activity { price: number; } -interface OngoingExperienceCardProps { - className?: string; - activities: Activity[]; +interface OngoingExperienceCardProps extends Activity { onClickActivity: (id: number) => void; - onClick: () => void; } export default function OngoingExperienceCard({ - className, - activities, + id, + bannerImageUrl, + title, + price, onClickActivity, - onClick, }: OngoingExperienceCardProps) { - const flex = activities.length === 0 ? 'justify-center' : ''; return ( -
- {activities.length === 0 ? ( -
- - -
- ) : ( - activities.map((act) => { - return ( -
onClickActivity(act.id)}> - -
-

{act.title}

-

₩{act.price.toLocaleString()} / 인

-
-
- ); - }) - )} +
onClickActivity(id)}> + {title} +
+

{title}

+

₩ {price.toLocaleString()} / 인

+
); } diff --git a/packages/design-system/src/components/UpcomingSchedule.tsx b/packages/design-system/src/components/UpcomingSchedule.tsx index 27b82d94..c4155ca0 100644 --- a/packages/design-system/src/components/UpcomingSchedule.tsx +++ b/packages/design-system/src/components/UpcomingSchedule.tsx @@ -1,13 +1,4 @@ -import { twMerge } from 'tailwind-merge'; - -import Button from './button'; -import { EmptyLogo } from './logos'; - -function ScheduleDateLabel({ date }: { date: string }) { - return

{date}

; -} - -function ScheduleItem({ +export default function UpcomingSchedule({ title, price, headCount, @@ -35,71 +26,3 @@ function ScheduleItem({
); } - -interface Reservation { - id: number; - activity: { id: number; title: string; bannerImageUrl: string }; - date: string; - headCount: number; - totalPrice: number; - startTime: string; - endTime: string; -} - -interface UpcomingScheduleProps { - className?: string; - reservation: Reservation[]; - onClickReservation: (id: number) => void; - onClick?: () => void; -} -export default function UpcomingSchedule({ - className, - reservation, - onClickReservation, - onClick, -}: UpcomingScheduleProps) { - const flex = reservation.length === 0 ? 'justify-center' : ''; - return ( -
- {/*
-
-
-
*/} -
- {reservation.length === 0 ? ( -
- - -
- ) : ( - (() => { - let prevDate: string | null = null; - return reservation.map((res, idx) => { - const showDateLabel = res.date !== prevDate; - const isLast = idx === reservation.length - 1; - prevDate = res.date; - return ( -
onClickReservation(res.activity.id)} - > - {showDateLabel && } - -
- ); - }); - })() - )} -
-
- ); -} diff --git a/packages/design-system/src/components/skeleton/OngoingExperienceCardSkeleton.tsx b/packages/design-system/src/components/skeleton/OngoingExperienceCardSkeleton.tsx new file mode 100644 index 00000000..3da6377b --- /dev/null +++ b/packages/design-system/src/components/skeleton/OngoingExperienceCardSkeleton.tsx @@ -0,0 +1,19 @@ +export default function OngoingExperienceCardSkeleton() { + return ( +
+ {/* 이미지 영역 스켈레톤 */} +
+ + {/* 하단 정보 카드 스켈레톤 */} +
+ {/* 타이틀 두 줄 가짜 라인 */} +
+
+
+
+ {/* 가격 라인 */} +
+
+
+ ); +} diff --git a/packages/design-system/src/components/skeleton/UpcomingScheduleItemSkeleton.tsx b/packages/design-system/src/components/skeleton/UpcomingScheduleItemSkeleton.tsx new file mode 100644 index 00000000..6600df56 --- /dev/null +++ b/packages/design-system/src/components/skeleton/UpcomingScheduleItemSkeleton.tsx @@ -0,0 +1,27 @@ +export default function UpcomingScheduleItemSkeleton() { + return ( +
+ {/* 날짜 라벨 스켈레톤 */} +
+ + {/* 카드 스켈레톤 (UpcomingSchedule 레이아웃 매칭) */} +
+ {/* 좌측 텍스트 영역 */} +
+ {/* 제목 */} +
+ {/* 시간 */} +
+ {/* 가격/인원 */} +
+
+
+
+
+ + {/* 우측 이미지 영역 */} +
+
+
+ ); +} diff --git a/packages/design-system/src/components/skeleton/index.tsx b/packages/design-system/src/components/skeleton/index.tsx index 015d3644..a9f05cea 100644 --- a/packages/design-system/src/components/skeleton/index.tsx +++ b/packages/design-system/src/components/skeleton/index.tsx @@ -1,2 +1,4 @@ export { default as ExperienceCardSkeleton } from './ExperienceCardSkeleton'; +export { default as OngoingExperienceCardSkeleton } from './OngoingExperienceCardSkeleton'; +export { default as UpcomingScheduleItemSkeleton } from './UpcomingScheduleItemSkeleton'; export { default as NotificationCardSkeleton } from './NotificationCardSkeleton';