1- import { useEffect } from 'react' ;
1+ import { useEffect , useState } from 'react' ;
2+ import { useInView } from 'react-intersection-observer' ;
23import { useNavigate , useParams } from 'react-router' ;
34import { toast } from 'sonner' ;
4- import useEventDetail from '../hooks/useEventDetail ' ;
5+ import useInfiniteGuests from '../hooks/useInfiniteGuests ' ;
56
67// shadcn UI μ»΄ν¬λνΈ
78import {
@@ -17,26 +18,42 @@ import {
1718} from '@/components/ui/alert-dialog' ;
1819import { Avatar , AvatarFallback , AvatarImage } from '@/components/ui/avatar' ;
1920import { Button } from '@/components/ui/button' ;
20- import { ChevronLeftIcon } from 'lucide-react' ;
21+ import type { GuestStatus } from '@/types/schemas' ;
22+ import { ChevronLeftIcon , Loader2 } from 'lucide-react' ;
2123
2224export default function Guests ( ) {
2325 const { id } = useParams < { id : string } > ( ) ;
2426 const navigate = useNavigate ( ) ;
25- const { loading, guests, handleFetchRegistrations } = useEventDetail ( ) ;
27+ const [ activeTab , setActiveTab ] = useState < GuestStatus > ( 'CONFIRMED' ) ;
28+
29+ // 1. 무ν μ€ν¬λ‘€ ν
μ°κ²°
30+ const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
31+ useInfiniteGuests ( {
32+ eventId : id ! ,
33+ filters : {
34+ status : activeTab ,
35+ orderBy : 'registeredAt' ,
36+ } ,
37+ } ) ;
38+
39+ const totalCount = data ?. pages [ 0 ] ?. totalCount ?? 0 ;
40+
41+ // 2. λ°λ₯ κ°μ§ ν
(λ§μ§λ§ μμκ° λ³΄μ΄λ©΄ λ€μ νμ΄μ§ λ‘λ)
42+ const { ref, inView } = useInView ( ) ;
2643
2744 useEffect ( ( ) => {
28- if ( id ) {
29- handleFetchRegistrations ( id ) ;
45+ if ( inView && hasNextPage && ! isFetchingNextPage ) {
46+ fetchNextPage ( ) ;
3047 }
31- } , [ id , handleFetchRegistrations ] ) ;
48+ } , [ inView , hasNextPage , isFetchingNextPage , fetchNextPage ] ) ;
3249
3350 const handleCancelGuest = ( name : string | null ) => {
34- // μμ API νμ
51+ // TODO: patchRegistration API νΈμΆ λ‘μ§ μΆκ° νμ
3552 toast . success ( `${ name } λμ μ°Έμ¬κ° μ·¨μλμμ΅λλ€.` ) ;
3653 } ;
3754
3855 // λ‘λ© μ€μ΄κ±°λ λ°μ΄ν°κ° μμ§ μμ λ (리λ€μ΄λ νΈ νλ¨ μ ) μ€νΌλλ λΉ νλ©΄ νμ
39- if ( loading || ! guests ) {
56+ if ( isLoading || ! data ) {
4057 return (
4158 < div className = "min-h-screen flex items-center justify-center" >
4259 < div className = "animate-spin rounded-full h-12 w-12 border-b-2 border-black" />
@@ -58,68 +75,105 @@ export default function Guests() {
5875 < ChevronLeftIcon />
5976 </ Button >
6077 < h1 className = "text-2xl sm:text-3xl font-bold ml-4 text-black" >
61- μ°Έμ¬μ λͺ
λ¨({ guests . length } )
78+ μ°Έμ¬μ λͺ
λ¨({ totalCount } )
6279 </ h1 >
6380 </ div >
6481 </ div >
6582
66- { /* 2. μ°Έμ¬μ 리μ€νΈ */ }
83+ { /* 2. ν UI μμ */ }
84+ < div className = "flex border-b" >
85+ { [ 'μ 체' , 'μ°Έμ¬μ' , 'λκΈ°μ' ] . map ( ( label , idx ) => {
86+ const tabValue = [ 'ALL' , 'CONFIRMED' , 'WAITING' ] [ idx ] as GuestStatus ;
87+ return (
88+ < button
89+ key = { tabValue }
90+ onClick = { ( ) => setActiveTab ( tabValue ) }
91+ className = { `flex-1 py-4 font-bold ${ activeTab === tabValue ? 'border-b-2 border-black text-black' : 'text-gray-400' } ` }
92+ >
93+ { label }
94+ </ button >
95+ ) ;
96+ } ) }
97+ </ div >
98+
99+ { /* 3. μ°Έμ¬μ 리μ€νΈ */ }
67100 < div className = "max-w-2xl min-w-[320px] mx-auto w-[90%] px-6 flex flex-col gap-8 mt-4" >
68- { guests . map ( ( guest ) => (
69- < div
70- key = { guest . registrationId }
71- className = "flex items-center justify-between w-full"
72- >
73- < div className = "flex items-center gap-4" >
74- < Avatar className = "w-16 h-16 border-none shadow-sm" >
75- < AvatarImage src = { guest . email || undefined } />
76- < AvatarFallback className = "bg-black text-white text-xs" >
77- { guest . name ?. slice ( 0 , 2 ) }
78- </ AvatarFallback >
79- </ Avatar >
80- < div className = "flex flex-col" >
81- < span className = "text-xl font-bold text-black" >
82- { guest . name }
83- </ span >
84- { guest . email ? (
85- < span className = "text-gray-400 text-lg" > { guest . email } </ span >
86- ) : null }
101+ { data ?. pages . map ( ( page ) =>
102+ page . participants . map ( ( guest ) => (
103+ < div
104+ key = { guest . registrationId }
105+ className = "flex items-center justify-between w-full"
106+ >
107+ < div className = "flex items-center gap-4" >
108+ < Avatar className = "w-16 h-16 border-none shadow-sm" >
109+ < AvatarImage src = { guest . profileImage || undefined } />
110+ < AvatarFallback className = "bg-black text-white text-xs" >
111+ { guest . name ?. slice ( 0 , 2 ) }
112+ </ AvatarFallback >
113+ </ Avatar >
114+ < div className = "flex flex-col" >
115+ < span className = "text-xl font-bold text-black" >
116+ { guest . name }
117+ </ span >
118+ { guest . email ? (
119+ < span className = "text-gray-400 text-lg" > { guest . email } </ span >
120+ ) : null }
121+ { guest . status === 'WAITLISTED' && (
122+ < span className = "text-amber-600 text-sm font-semibold" >
123+ λκΈ° { guest . waitlistPosition } λ²
124+ </ span >
125+ ) }
126+ </ div >
87127 </ div >
88- </ div >
89128
90- { /* κ°μ μ·¨μ λ²νΌ */ }
91- < AlertDialog >
92- < AlertDialogTrigger asChild >
93- < Button
94- variant = "secondary"
95- className = "bg-[#333333] hover:bg-black text-white rounded-lg px-4 py-6 text-base font-bold"
96- >
97- κ°μ μ·¨μ
98- </ Button >
99- </ AlertDialogTrigger >
100- < AlertDialogContent >
101- < AlertDialogHeader >
102- < AlertDialogTitle >
103- < strong > { guest . name } </ strong > λμ μ μ²μ μ·¨μνμκ² μ΅λκΉ?
104- </ AlertDialogTitle >
105- < AlertDialogDescription >
106- μ·¨μ ν μλ³΅μ΄ μ΄λ ΅μ΅λλ€. μ·¨μ λ©μΌμ΄ μ°Έμ¬μμκ²
107- μ μ‘λ©λλ€.
108- </ AlertDialogDescription >
109- </ AlertDialogHeader >
110- < AlertDialogFooter >
111- < AlertDialogCancel > μ μ² μ μ§νκΈ°</ AlertDialogCancel >
112- < AlertDialogAction
113- onClick = { ( ) => handleCancelGuest ( guest . name ) }
114- className = "bg-red-600 hover:bg-red-700"
129+ { /* κ°μ μ·¨μ λ²νΌ */ }
130+ < AlertDialog >
131+ < AlertDialogTrigger asChild >
132+ < Button
133+ variant = "secondary"
134+ className = "bg-[#333333] hover:bg-black text-white rounded-lg px-4 py-6 text-base font-bold"
115135 >
116- μ·¨μνκΈ°
117- </ AlertDialogAction >
118- </ AlertDialogFooter >
119- </ AlertDialogContent >
120- </ AlertDialog >
121- </ div >
122- ) ) }
136+ κ°μ μ·¨μ
137+ </ Button >
138+ </ AlertDialogTrigger >
139+ < AlertDialogContent >
140+ < AlertDialogHeader >
141+ < AlertDialogTitle >
142+ < strong > { guest . name } </ strong > λμ μ μ²μ
143+ μ·¨μνμκ² μ΅λκΉ?
144+ </ AlertDialogTitle >
145+ < AlertDialogDescription >
146+ μ·¨μ ν μλ³΅μ΄ μ΄λ ΅μ΅λλ€. μ·¨μ λ©μΌμ΄ μ°Έμ¬μμκ²
147+ μ μ‘λ©λλ€.
148+ </ AlertDialogDescription >
149+ </ AlertDialogHeader >
150+ < AlertDialogFooter >
151+ < AlertDialogCancel > μ μ² μ μ§νκΈ°</ AlertDialogCancel >
152+ < AlertDialogAction
153+ onClick = { ( ) => handleCancelGuest ( guest . name ) }
154+ className = "bg-red-600 hover:bg-red-700"
155+ >
156+ μ·¨μνκΈ°
157+ </ AlertDialogAction >
158+ </ AlertDialogFooter >
159+ </ AlertDialogContent >
160+ </ AlertDialog >
161+ </ div >
162+ ) )
163+ ) }
164+
165+ { /* 4. νλ¨ λ¬΄ν μ€ν¬λ‘€ νΈλ¦¬κ±° & λ‘λ© μΈλμΌμ΄ν° */ }
166+ < div ref = { ref } className = "py-10 flex justify-center" >
167+ { isFetchingNextPage ? (
168+ < Loader2 className = "animate-spin h-8 w-8 text-gray-400" />
169+ ) : hasNextPage ? (
170+ < p className = "text-gray-400 text-sm" >
171+ λͺ©λ‘μ λ λΆλ¬μ€κ³ μμ΅λλ€...
172+ </ p >
173+ ) : (
174+ < p className = "text-gray-400 text-sm" > λ§μ§λ§ μ°Έμ¬μμ
λλ€.</ p >
175+ ) }
176+ </ div >
123177 </ div >
124178 </ div >
125179 ) ;
0 commit comments