From c76bfdbe951b214fc1f1ccd19953e1990b38cfd4 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sat, 13 Jun 2026 11:44:54 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Implement=20query=20key=20to=20?= =?UTF-8?q?filter=20event=20detail=20data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/constants/queryKeys.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/constants/queryKeys.ts diff --git a/src/constants/queryKeys.ts b/src/constants/queryKeys.ts new file mode 100644 index 0000000..f16445b --- /dev/null +++ b/src/constants/queryKeys.ts @@ -0,0 +1,25 @@ +import type { GuestsParams } from '@/types/events'; + +type GuestListFilters = Omit; + +export const queryKeys = { + events: { + detailRoot: ['events', 'detail'] as const, + detailBase: (eventId: string) => + [...queryKeys.events.detailRoot, eventId] as const, + detail: (eventId: string, registrationId?: string | null) => + [ + ...queryKeys.events.detailBase(eventId), + registrationId ?? 'viewer', + ] as const, + guests: { + all: (eventId: string) => ['events', 'guests', eventId] as const, + list: (eventId: string, filters: GuestListFilters) => + [...queryKeys.events.guests.all(eventId), filters] as const, + }, + }, + legacy: { + myEvents: ['myEvents'] as const, + myRegistrations: ['myRegistrations'] as const, + }, +}; From 4c0d5ea0a2fabb8d6afe93dbdca6dfb353d5e209 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sat, 13 Jun 2026 11:45:56 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9C=A8=20Add=20hook=20to=20invalidate=20?= =?UTF-8?q?event=20registration=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useInvalidateEventRegistration.ts | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/hooks/useInvalidateEventRegistration.ts diff --git a/src/hooks/useInvalidateEventRegistration.ts b/src/hooks/useInvalidateEventRegistration.ts new file mode 100644 index 0000000..218331f --- /dev/null +++ b/src/hooks/useInvalidateEventRegistration.ts @@ -0,0 +1,27 @@ +import { queryKeys } from '@/constants/queryKeys'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +export default function useInvalidateEventRegistration() { + const queryClient = useQueryClient(); + + return useCallback( + async (eventId: string) => { + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: queryKeys.events.detailBase(eventId), + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.events.guests.all(eventId), + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.legacy.myRegistrations, + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.legacy.myEvents, + }), + ]); + }, + [queryClient] + ); +} From 2026d1d31ac962da232900339cdb340da5ce9456 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sat, 13 Jun 2026 11:47:14 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20event=20det?= =?UTF-8?q?ail=20and=20guest=20hooks=20to=20use=20TanStack=20Query?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useEventDetail.ts | 294 +++++++++++++++++++-------------- src/hooks/useInfiniteGuests.ts | 7 +- 2 files changed, 170 insertions(+), 131 deletions(-) diff --git a/src/hooks/useEventDetail.ts b/src/hooks/useEventDetail.ts index a59e9ae..4eda866 100644 --- a/src/hooks/useEventDetail.ts +++ b/src/hooks/useEventDetail.ts @@ -1,25 +1,72 @@ import { deleteEvent, getEventDetail } from '@/api/events/event'; -import { getGuests, joinEvent } from '@/api/events/registrations'; +import { joinEvent } from '@/api/events/registrations'; import { deleteRegistration, getRegistrationDetail, patchRegistration, } from '@/api/registrations/registration'; +import { queryKeys } from '@/constants/queryKeys'; import useAuthStore from '@/hooks/useAuthStore'; +import useInvalidateEventRegistration from '@/hooks/useInvalidateEventRegistration'; import type { EventDetailResponse, JoinEventRequest } from '@/types/events'; -import type { Guest } from '@/types/schemas'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { useCallback, useState } from 'react'; +import { useEffect } from 'react'; import { useSearchParams } from 'react-router'; +interface JoinEventVariables { + eventId: string; + data: JoinEventRequest; +} + +const getMergedEventDetail = async ( + eventId: string, + effectiveRegId?: string | null +): Promise => { + const eventRes = await getEventDetail(eventId); + let mergedData = eventRes.data; + + if (mergedData.viewer.status !== 'NONE' || !effectiveRegId) { + return mergedData; + } + + try { + const regRes = await getRegistrationDetail(effectiveRegId); + const canCancel = + regRes.status === 'CONFIRMED' || regRes.status === 'WAITLISTED'; + + // 비로그인 신청자는 이벤트 상세 응답만으로 본인 상태를 알 수 없어서 + // 저장해 둔 regId로 신청 상세를 병합해 같은 화면 상태를 구성합니다. + mergedData = { + ...mergedData, + viewer: { + ...mergedData.viewer, + status: regRes.status, + name: regRes.guestName, + waitlistPosition: regRes.waitlistPosition, + registrationPublicId: regRes.registrationPublicId, + reservationEmail: regRes.reservationEmail, + }, + capabilities: { + ...mergedData.capabilities, + apply: regRes.status === 'CANCELED', + wait: false, + cancel: canCancel, + }, + }; + } catch (regError) { + // regId가 만료되었거나 잘못된 경우에는 공개 상세만 보여줘야 합니다. + console.error('Failed to fetch guest registration info:', regError); + } + + return mergedData; +}; + export default function useEventDetail(id?: string) { - const [loading, setLoading] = useState(false); - const [isDeleted, setIsDeleted] = useState(false); - const [data, setData] = useState(null); const [searchParams] = useSearchParams(); - const [guests, setGuests] = useState([]); + const queryClient = useQueryClient(); + const invalidateEventRegistration = useInvalidateEventRegistration(); - // 1. 유저 식별용 ID 확보 (URL 파라미터 우선, 없으면 Zustand 스토어) const urlRegId = searchParams.get('regId'); const guestRegId = useAuthStore((state) => id ? state.guestRegistrations[id] : null @@ -29,165 +76,156 @@ export default function useEventDetail(id?: string) { const setGuestRegistration = useAuthStore( (state) => state.setGuestRegistration ); - const removeGuestRegistration = useAuthStore( - (state) => state.removeGuestRegistration - ); - // 2. 모임 상세 정보 로드 핸들러 - const handleFetchDetail = useCallback( - async (eventId: string) => { - setLoading(true); - setIsDeleted(false); - try { - // (1) 기본 모임 정보 가져오기 - const eventRes = await getEventDetail(eventId); - let mergedData = eventRes.data; - - // (2) 모임 이름을 페이지 제목으로 설정 - if (mergedData.event.title) { - document.title = `${mergedData.event.title} - 모이밍`; - } else { - document.title = '모임 상세 - 모이밍'; - } - - if (mergedData.viewer.status === 'NONE' && effectiveRegId) { - // (3) 비로그인 유저(NONE)인데 식별 ID가 있는 경우 유저 정보 추가 로드 - try { - const regRes = await getRegistrationDetail(effectiveRegId); - - // 데이터 병합: 기존 event 정보는 유지하고 viewer와 capabilities를 유저 정보로 갱신 - mergedData = { - ...mergedData, - viewer: { - ...mergedData.viewer, - status: regRes.status, - name: regRes.guestName, - waitlistPosition: regRes.waitlistPosition, - registrationPublicId: regRes.registrationPublicId, - reservationEmail: regRes.reservationEmail, - }, - capabilities: { - ...mergedData.capabilities, - cancel: true, // 내 정보를 조회했다는 것은 취소 권한이 있다는 의미 - apply: false, // 이미 신청했으므로 신청 버튼은 비활성화 - wait: false, - }, - }; - } catch (regError) { - // ID가 만료되었거나 잘못된 경우 조용히 기본 정보를 보여줌 (NONE 상태 유지) - console.error('Failed to fetch guest registration info:', regError); - } - } - - setData(mergedData); - return mergedData.viewer.status; - } catch (error: unknown) { - if (isAxiosError(error) && error.response?.status === 404) { - setIsDeleted(true); - } else { - console.error('Fetch event detail failed:', error); - } - return 'ERROR'; - } finally { - setLoading(false); + // 1. 이벤트 상세는 regId에 따라 viewer 상태가 달라질 수 있어 key에 함께 포함합니다. + const eventDetailQuery = useQuery({ + queryKey: id + ? queryKeys.events.detail(id, effectiveRegId) + : queryKeys.events.detail('__missing_event__'), + queryFn: () => { + if (!id) { + throw new Error('EVENT_ID_REQUIRED'); } + return getMergedEventDetail(id, effectiveRegId); }, - [effectiveRegId] - ); + enabled: Boolean(id), + retry: (failureCount, error) => { + if (isAxiosError(error) && error.response?.status === 404) return false; + if (isAxiosError(error) && error.response?.status === 401) return false; + if ( + error.message === 'TOKEN_EXPIRED_LOCAL' || + error.message === 'INVALID_TOKEN_FORMAT' + ) { + return false; + } + return failureCount < 3; + }, + }); - // 3. 참여자 전체 명단 로드 핸들러 - const handleFetchRegistrations = useCallback(async (eventId: string) => { - setLoading(true); - try { - const data = await getGuests(eventId); - setGuests(data.data.participants); - } catch (error: unknown) { - console.error('Fetch registrations error:', error); - } finally { - setLoading(false); - } - }, []); + useEffect(() => { + const title = eventDetailQuery.data?.event.title; + document.title = title ? `${title} - 모이밍` : '모임 상세 - 모이밍'; + }, [eventDetailQuery.data?.event.title]); + + // 2. 신청 성공 후 저장된 regId가 다음 상세 조회의 viewer 병합 기준이 됩니다. + const joinMutation = useMutation({ + mutationFn: ({ eventId, data }: JoinEventVariables) => + joinEvent(eventId, data), + onSuccess: async (response, { eventId }) => { + const regId = response.data.registrationPublicId; + + if (regId) { + setGuestRegistration(eventId, regId); + } + + await invalidateEventRegistration(eventId); + }, + }); + + const cancelMutation = useMutation({ + mutationFn: (registrationId: string) => deleteRegistration(registrationId), + onSuccess: async () => { + if (id) { + // 비로그인 사용자의 regId는 유지하고, 서버의 CANCELED 상태를 다시 조회합니다. + await invalidateEventRegistration(id); + } + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (eventId: string) => deleteEvent(eventId), + onSuccess: async (_response, eventId) => { + // 삭제 후 홈 목록과 상세/참여자 캐시가 서로 다른 상태를 보지 않게 함께 무효화합니다. + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: queryKeys.events.detailBase(eventId), + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.events.guests.all(eventId), + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.legacy.myEvents, + }), + queryClient.invalidateQueries({ + queryKey: queryKeys.legacy.myRegistrations, + }), + ]); + }, + }); + + const banMutation = useMutation({ + mutationFn: (registrationId: string) => + patchRegistration(registrationId, { + status: 'BANNED', + }), + onSuccess: async () => { + if (id) { + await invalidateEventRegistration(id); + } + }, + }); - // 4. 참여 신청 로직 핸들러 const handleJoinEvent = async ( - id: string, + eventId: string, data: JoinEventRequest ): Promise => { - setLoading(true); try { - const response = await joinEvent(id, data); - if (response.status === 200 || response.status === 201) { - const regId = response.data.registrationPublicId; - - if (regId) { - setGuestRegistration(id, regId); - } - return true; - } - return false; + await joinMutation.mutateAsync({ eventId, data }); + return true; } catch (error: unknown) { console.error('Join event error:', error); return false; - } finally { - setLoading(false); } }; - // 5. 참여 취소 로직 핸들러 - const handleCancelEvent = async (registrationId: string) => { - setLoading(true); + const handleCancelEvent = async ( + registrationId: string + ): Promise => { try { - await deleteRegistration(registrationId); - if (id) { - removeGuestRegistration(id); - } + await cancelMutation.mutateAsync(registrationId); return true; - } catch (error) { + } catch (error: unknown) { console.error('Cancel event error:', error); return false; - } finally { - setLoading(false); } }; - // 6. 모임 삭제 로직 핸들러 - const handleDeleteEvent = async (eventId: string) => { - setLoading(true); + const handleDeleteEvent = async (eventId: string): Promise => { try { - await deleteEvent(eventId); + await deleteMutation.mutateAsync(eventId); return true; } catch (error: unknown) { console.error('Delete event error:', error); return false; - } finally { - setLoading(false); } }; - // 7. 참여 강제 취소 로직 핸들러 - const handleBanEvent = async (registrationId: string) => { - setLoading(true); + const handleBanEvent = async (registrationId: string): Promise => { try { - await patchRegistration(registrationId, { - status: 'BANNED', - }); - if (id) await handleFetchDetail(id); + await banMutation.mutateAsync(registrationId); return true; - } catch (error) { + } catch (error: unknown) { console.error('Ban event error:', error); return false; - } finally { - setLoading(false); } }; + const isDeleted = + isAxiosError(eventDetailQuery.error) && + eventDetailQuery.error.response?.status === 404; + const hasFetchError = eventDetailQuery.isError && !isDeleted; + return { - loading, - data, + loading: + eventDetailQuery.isLoading || + joinMutation.isPending || + cancelMutation.isPending || + deleteMutation.isPending || + banMutation.isPending, + data: eventDetailQuery.data ?? null, isDeleted, - guests, - handleFetchDetail, - handleFetchRegistrations, + hasFetchError, + isBanning: banMutation.isPending, handleJoinEvent, handleCancelEvent, handleDeleteEvent, diff --git a/src/hooks/useInfiniteGuests.ts b/src/hooks/useInfiniteGuests.ts index 0005aeb..334dd2e 100644 --- a/src/hooks/useInfiniteGuests.ts +++ b/src/hooks/useInfiniteGuests.ts @@ -1,4 +1,5 @@ import { getGuests } from '@/api/events/registrations'; +import { queryKeys } from '@/constants/queryKeys'; import type { GuestsParams, GuestsResponse } from '@/types/events'; import type { InfiniteData } from '@tanstack/react-query'; import { useInfiniteQuery } from '@tanstack/react-query'; @@ -16,11 +17,11 @@ export default function useInfiniteGuests({ GuestsResponse, Error, InfiniteData, - (string | object)[], + ReturnType, number | undefined >({ - // 필터 조건이 바뀔 때마다 새로운 쿼리로 인식하도록 설정 - queryKey: ['guests', eventId, filters], + // 이벤트 단위로 한 번에 invalidate할 수 있게 key 계층을 맞춥니다. + queryKey: queryKeys.events.guests.list(eventId, filters), queryFn: async ({ pageParam }) => { const response = await getGuests(eventId, { From 8eb65006fd9804c0a0e9691c0f83b8bfb7180300 Mon Sep 17 00:00:00 2001 From: jun-0411 Date: Sat, 13 Jun 2026 11:49:12 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20event=20reg?= =?UTF-8?q?istration=20and=20detail=20handling=20with=20TanStack=20Query?= =?UTF-8?q?=20to=20update=20details=20without=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/EventEdit.tsx | 16 ++++++++++------ src/routes/EventMain.tsx | 33 +++++---------------------------- src/routes/EventRegister.tsx | 15 +++++---------- src/routes/Guests.tsx | 27 +++++++++++---------------- 4 files changed, 31 insertions(+), 60 deletions(-) diff --git a/src/routes/EventEdit.tsx b/src/routes/EventEdit.tsx index d64ea3a..c4e039d 100644 --- a/src/routes/EventEdit.tsx +++ b/src/routes/EventEdit.tsx @@ -3,8 +3,10 @@ import { EventForm } from '@/components/EventForm'; import type { FormValues } from '@/components/EventForm'; import LoadingSkeleton from '@/components/LoadingSkeleton'; import { Button } from '@/components/ui/button'; +import { queryKeys } from '@/constants/queryKeys'; import useAuthStore from '@/hooks/useAuthStore'; import useEventDetail from '@/hooks/useEventDetail'; +import { useQueryClient } from '@tanstack/react-query'; import { AlertCircle } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; @@ -14,12 +16,13 @@ export default function EventEdit() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); + const queryClient = useQueryClient(); const { loading: fetchLoading, data, isDeleted, - handleFetchDetail, + hasFetchError, } = useEventDetail(id); const [isSubmitting, setIsSubmitting] = useState(false); @@ -27,12 +30,10 @@ export default function EventEdit() { const user = useAuthStore((state) => state.user); useEffect(() => { - if (id) { - handleFetchDetail(id).then((status) => { - if (status === 'ERROR') navigate(`/event/${id}`); - }); + if (hasFetchError && id) { + navigate(`/event/${id}`); } - }, [id, handleFetchDetail, navigate]); + }, [hasFetchError, id, navigate]); const event = data?.event; @@ -116,6 +117,9 @@ export default function EventEdit() { const response = await updateEvent(id, payload); if (response.status === 201 || response.status === 200) { + await queryClient.invalidateQueries({ + queryKey: queryKeys.events.detailBase(id), + }); toast.success('모임이 수정되었습니다.'); navigate(`/event/${id}`); } diff --git a/src/routes/EventMain.tsx b/src/routes/EventMain.tsx index 0add14f..6a1209f 100644 --- a/src/routes/EventMain.tsx +++ b/src/routes/EventMain.tsx @@ -1,13 +1,5 @@ -import { useQueryClient } from '@tanstack/react-query'; import { motion } from 'framer-motion'; -import { - AlertCircle, - Check, - // Dot, - Link as LinkIcon, - Loader, - X, -} from 'lucide-react'; +import { AlertCircle, Check, Link as LinkIcon, Loader, X } from 'lucide-react'; import { type ComponentProps, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { toast } from 'sonner'; @@ -44,30 +36,25 @@ import { formatEventDate, getRemainingTime } from '@/utils/date'; export default function EventMain() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const queryClient = useQueryClient(); const { user, isLoggedIn } = useAuth(); - const { removeGuestRegistration } = useAuthStore(); // 1. 데이터 가져오기 및 권한 확인 const { loading, data, isDeleted, - handleFetchDetail, + hasFetchError, handleCancelEvent, handleJoinEvent, handleDeleteEvent, } = useEventDetail(id); const view = useEventView(data); - // 2. 초기 데이터 로드 useEffect(() => { - if (id) { - handleFetchDetail(id).then((status) => { - if (status === 'ERROR') navigate('/'); - }); + if (hasFetchError) { + navigate('/'); } - }, [id, handleFetchDetail, navigate]); + }, [hasFetchError, navigate]); if (isDeleted || !id) { return ( @@ -112,9 +99,7 @@ export default function EventMain() { guestEmail: user?.email, }); if (success) { - queryClient.resetQueries({ queryKey: ['myRegistrations'] }); toast.success('신청이 완료되었습니다.'); - navigate(0); } }; @@ -127,8 +112,6 @@ export default function EventMain() { const success = await handleCancelEvent(regId); if (success) { - queryClient.resetQueries({ queryKey: ['myRegistrations'] }); - removeGuestRegistration(id); toast.success('신청이 취소되었습니다.'); } }; @@ -136,8 +119,6 @@ export default function EventMain() { const onDeleteClick = async () => { const success = await handleDeleteEvent(id); if (success) { - queryClient.resetQueries({ queryKey: ['myEvents'] }); - queryClient.resetQueries({ queryKey: ['myRegistrations'] }); toast.success('모임이 삭제되었습니다.'); navigate('/'); } @@ -200,10 +181,6 @@ export default function EventMain() { {creator.email}
- {/* TODO: 모임 생성일시도 요청 - 15분 전 작성 - - */} {getRegistrationStatus(event)} diff --git a/src/routes/EventRegister.tsx b/src/routes/EventRegister.tsx index 274e2a1..3215eb0 100644 --- a/src/routes/EventRegister.tsx +++ b/src/routes/EventRegister.tsx @@ -5,7 +5,6 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import useAuthStore from '@/hooks/useAuthStore'; import type { JoinEventRequest } from '@/types/events'; -import { useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router'; import { ShortEventDetailContent } from '../components/EventDetailContent'; @@ -15,10 +14,9 @@ import useEventDetail from '../hooks/useEventDetail'; export default function EventRegister() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const queryClient = useQueryClient(); const setRedirectUrl = useAuthStore((state) => state.setRedirectUrl); - const { loading, data, handleFetchDetail, handleJoinEvent } = - useEventDetail(); + const { loading, data, isDeleted, hasFetchError, handleJoinEvent } = + useEventDetail(id); // 폼 상태 관리 const [formData, setFormData] = useState({ @@ -27,12 +25,10 @@ export default function EventRegister() { }); useEffect(() => { - if (id) { - handleFetchDetail(id).then((status) => { - if (status === 'ERROR') navigate('/'); - }); + if (!id || isDeleted || hasFetchError) { + navigate('/'); } - }, [id, handleFetchDetail, navigate]); + }, [id, isDeleted, hasFetchError, navigate]); if (loading || !data) { return ( @@ -62,7 +58,6 @@ export default function EventRegister() { const success = await handleJoinEvent(id, requestData); if (success) { - queryClient.resetQueries({ queryKey: ['myRegistrations'] }); // 신청 성공 시 성공 페이지로 이동 navigate(`/event/${id}`); } diff --git a/src/routes/Guests.tsx b/src/routes/Guests.tsx index d037324..2667381 100644 --- a/src/routes/Guests.tsx +++ b/src/routes/Guests.tsx @@ -28,23 +28,17 @@ export default function Guests() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [activeTab, setActiveTab] = useState('CONFIRMED'); - const { handleBanEvent } = useEventDetail(id); + const { handleBanEvent, isBanning } = useEventDetail(id); // 1. 무한 스크롤 훅 연결 - const { - data, - isLoading, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - refetch, - } = useInfiniteGuests({ - eventId: id!, - filters: { - status: activeTab, - orderBy: 'registeredAt', - }, - }); + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteGuests({ + eventId: id!, + filters: { + status: activeTab, + orderBy: 'registeredAt', + }, + }); const totalCount = data?.pages[0]?.totalCount ?? 0; @@ -61,7 +55,6 @@ export default function Guests() { const success = await handleBanEvent(regId); if (success) { toast.success(`${name} 님의 참여가 취소되었습니다.`); - refetch(); } }; @@ -136,6 +129,7 @@ export default function Guests() {