22
33import React , { useCallback , useRef , useEffect , useState } from 'react' ;
44import { useRouter } from 'next/navigation' ;
5- import {
6- Table ,
7- TableHeader ,
8- TableColumn ,
9- TableBody ,
10- TableRow ,
11- TableCell ,
12- User ,
13- Chip ,
14- Pagination ,
15- Input ,
16- Spinner ,
17- } from '@nextui-org/react' ;
18- import { IoSearch } from 'react-icons/io5' ;
5+ import { Table , TableHeader , TableColumn , TableBody , TableRow , TableCell } from '@nextui-org/react' ;
196
207import UserDetailsModal from '@/components/admin/UserDetailsModal' ;
8+ import AdminTableCell from '@/components/admin/AdminTableCell' ;
9+ import AdminTableTopContent from '@/components/admin/AdminTableTopContent' ;
10+ import AdminTableBottomContent from '@/components/admin/AdminTableBottomContent' ;
2111
22- import { users } from '@/mock/users ' ;
12+ import { useAuthenticatedApi } from '@/hooks/useAuthenticatedApi ' ;
2313
2414const columns = [
2515 { name : 'NAME' , uid : 'name' } ,
2616 { name : 'MAJOR / ID' , uid : 'major' } ,
27- { name : 'PAYMENT' , uid : 'status' } ,
17+ { name : 'PAYMENT' , uid : 'isPayed' } ,
18+ { name : 'TOGGLE' , uid : 'togglePay' } ,
2819] ;
2920
3021const statusColorMap = {
@@ -33,80 +24,122 @@ const statusColorMap = {
3324} ;
3425
3526export default function Page ( ) {
36- const [ isLoading , setIsLoading ] = useState ( false ) ;
3727 const router = useRouter ( ) ;
28+ const { apiClient } = useAuthenticatedApi ( ) ;
3829
39- // useEffect(() => {
40- // if(!document.referrer) {
41- // router.push('/')
42- // } else {
43- // setIsLoading(false);
44- // }
45- // }, []);
46- // 추후 users 를 사용해 데이터를 받아오고, totalUsers를 사용해 페이지네이션 만들 예정
47- const [ page , setPage ] = React . useState ( 1 ) ; // 현재 페이지 상태 (추후 페이지 상태에 따라 api 통신으로 데이터 불러오기)
30+ const [ page , setPage ] = React . useState ( 1 ) ;
4831
4932 const [ modalOpen , setModalOpen ] = React . useState ( false ) ; // 모달 열림 상태
5033 const modalClosing = useRef ( false ) ; // 모달이 닫히는 상태를 추적
5134
5235 const [ selectedUser , setSelectedUser ] = React . useState ( null ) ; // 선택된 사용자 데이터
5336 const [ searchValue , setSearchValue ] = React . useState ( '' ) ; // 검색 입력 상태
37+ const [ query , setQuery ] = React . useState ( '' ) ; // API 호출시 검색 내용
38+ const [ loading , setLoading ] = React . useState ( false ) ;
39+ const [ error , setError ] = React . useState ( '' ) ;
40+ const [ currentUsers , setCurrentUsers ] = React . useState ( [ ] ) ;
41+ const [ totalUsers , setTotalUsers ] = React . useState ( 0 ) ;
42+ const [ totalPages , setTotalPages ] = React . useState ( 0 ) ;
5443
5544 const rowsPerPage = 10 ; //한 페이지당 표시될 유저 수
56- const totalUsers = 110 ; //총 유저 수 (총 페이지 표시를 위함)
57-
58- // 현재 페이지 데이터 계산 (임시)
59- const currentUsers = React . useMemo ( ( ) => {
60- const startIndex = ( page - 1 ) * rowsPerPage ;
61- const endIndex = startIndex + rowsPerPage ;
62- return users . slice ( startIndex , endIndex ) ;
63- } , [ page , rowsPerPage ] ) ;
64-
65- const totalPages = React . useMemo ( ( ) => Math . ceil ( totalUsers / rowsPerPage ) , [ totalUsers , rowsPerPage ] ) ;
66-
67- const renderCell = useCallback ( ( user , columnKey ) => {
68- const cellValue = user . member [ columnKey ] ;
69- switch ( columnKey ) {
70- case 'name' :
71- return (
72- < User
73- className = 'text-white'
74- avatarProps = { {
75- className : 'w-0 h-0 overflow-hidden' ,
76- } }
77- description = { user . member . email }
78- name = { cellValue }
79- >
80- { user . member . email }
81- </ User >
82- ) ;
83- case 'major' :
84- return (
85- < div className = 'flex flex-col' >
86- < p className = 'text-white text-bold text-sm capitalize' > { user . member . majors . main } </ p >
87- < p className = 'text-bold text-sm capitalize text-default-400' > { user . member . studentId } </ p >
88- </ div >
89- ) ;
90- case 'status' :
91- return (
92- < Chip className = 'capitalize' color = { statusColorMap [ user . member . isPayed ] } size = 'sm' variant = 'flat' >
93- { user . member . isPayed ? '입금' : '미입금' }
94- </ Chip >
95- ) ;
96- default :
97- return cellValue ;
45+
46+ //유저 데이터 조회
47+ const fetchUsers = useCallback ( async ( ) => {
48+ setLoading ( true ) ;
49+ setError ( '' ) ;
50+ try {
51+ const params = {
52+ page : page - 1 ,
53+ size : rowsPerPage ,
54+ sort : 'createdAt' ,
55+ dir : 'DESC' ,
56+ question : query || undefined ,
57+ } ;
58+ const res = await apiClient . get ( '/recruit/members' , { params } ) ;
59+ const list = Array . isArray ( res ?. data ?. data ) ? res . data . data : [ ] ;
60+ const total = res ?. data ?. meta ?. totalElements ?? list . length ;
61+ const computedTotalPages = Math . max ( 1 , Math . ceil ( total / rowsPerPage ) ) ;
62+
63+ setCurrentUsers ( list ) ;
64+ setTotalUsers ( total ) ;
65+ setTotalPages ( computedTotalPages ) ;
66+ } catch ( err ) {
67+ setError ( String ( err ?. message || 'failed to load users' ) ) ;
68+ setCurrentUsers ( [ ] ) ;
69+ setTotalUsers ( 0 ) ;
70+ } finally {
71+ setLoading ( false ) ;
9872 }
99- } , [ ] ) ;
73+ } , [ apiClient , page , rowsPerPage , query ] ) ;
74+
75+ //회비 지불여부 체크박스
76+ const handleTogglePay = useCallback (
77+ async ( userId , nextValue ) => {
78+ const getItemId = ( user ) => user ?. id ;
79+ const prevUsers = currentUsers ;
80+
81+ setCurrentUsers ( ( prev ) =>
82+ prev . map ( ( u ) => {
83+ if ( getItemId ( u ) === userId ) {
84+ const updated = { ...u , isPayed : nextValue } ;
85+ return updated ;
86+ }
87+ return u ;
88+ } )
89+ ) ;
90+
91+ try {
92+ await apiClient . patch ( `/recruit/members/${ userId } /payment` , { isPayed : nextValue } ) ;
93+ } catch ( err ) {
94+ // rollback
95+ setCurrentUsers ( prevUsers ) ;
96+ alert ( '결제 상태 변경에 실패했습니다. 다시 시도해주세요.' ) ;
97+ }
98+ } ,
99+ [ apiClient , currentUsers ]
100+ ) ;
100101
101- const handleRowClick = ( user ) => {
102+ //테이블 요소
103+ const renderCell = useCallback (
104+ ( user , columnKey ) => {
105+ const normalizedUser = {
106+ ...user ,
107+ name : user ?. name ?? '' ,
108+ major : user ?. major ?? '' ,
109+ studentId : user ?. studentId ?? '' ,
110+ isPayed : typeof user ?. isPayed === 'boolean' ? user . isPayed : '' ,
111+ phoneNumber : user ?. phoneNumber ?? '' ,
112+ id : user ?. id ?? user ?. member ?. id ,
113+ memberId : user ?. member ?. id ?? user ?. id ,
114+ } ;
115+ return < AdminTableCell user = { normalizedUser } columnKey = { columnKey } onTogglePay = { handleTogglePay } /> ;
116+ } ,
117+ [ handleTogglePay ]
118+ ) ;
119+
120+ //유저 상세 정보
121+ const handleRowClick = async ( user ) => {
102122 if ( modalClosing . current ) return ; // 모달이 닫히는 중에는 클릭 무시
103- setSelectedUser ( user ) ;
104- setModalOpen ( true ) ;
123+ try {
124+ const memberId = user ?. id ;
125+ if ( ! memberId ) {
126+ throw new Error ( '멤버 ID를 확인할 수 없습니다.' ) ;
127+ }
128+ const res = await apiClient . get ( `/recruit/members/${ memberId } ` ) ;
129+ const detail = res ?. data ?. data ?? null ;
130+ if ( ! detail ) {
131+ throw new Error ( '상세 정보를 불러오지 못했습니다.' ) ;
132+ }
133+ setSelectedUser ( detail ) ;
134+ setModalOpen ( true ) ;
135+ } catch ( e ) {
136+ alert ( '상세 정보를 불러오는 중 오류가 발생했습니다.' ) ;
137+ }
105138 } ;
106139
107140 const handleSearch = ( ) => {
108- //추후 api 연결 함수로 변경 예정
109- console . log ( searchValue ) ; //임시
141+ setPage ( 1 ) ;
142+ setQuery ( ( searchValue || '' ) . trim ( ) ) ;
110143 } ;
111144
112145 const handleCloseModal = ( ) => {
@@ -117,101 +150,64 @@ export default function Page() {
117150 } , 300 ) ;
118151 } ;
119152
120- // isloading 이 false 일때만 렌더링
153+ useEffect ( ( ) => {
154+ if ( searchValue === '' && query !== '' ) {
155+ setPage ( 1 ) ;
156+ setQuery ( '' ) ;
157+ }
158+ } , [ searchValue , query ] ) ;
159+
160+ useEffect ( ( ) => {
161+ fetchUsers ( ) ;
162+ } , [ fetchUsers ] ) ;
163+
121164 return (
122165 < >
123- { isLoading ? (
124- < div className = 'flex justify-center items-center h-screen' >
125- < Spinner />
126- </ div >
127- ) : (
128- < div >
129- < Table
166+ < div >
167+ < Table
130168 className = 'dark py-[30px] px-[96px] mobile:px-[10px]'
131169 aria-label = 'Example table with custom cells'
132170 bottomContent = {
133- totalPages > 0 ? (
134- < div className = 'flex w-full justify-center' >
135- < Pagination
136- isCompact
137- showControls
138- showShadow
139- color = 'primary'
140- page = { page }
141- total = { totalPages }
142- onChange = { ( newPage ) => setPage ( newPage ) }
143- />
144- </ div >
145- ) : null
171+ < AdminTableBottomContent page = { page } totalPages = { totalPages } onChangePage = { ( newPage ) => setPage ( newPage ) } />
146172 }
147173 topContent = {
148- < Input
149- isClearable
150- classNames = { {
151- label : 'text-black/50 dark:text-white/90' ,
152- input : [
153- 'bg-transparent' ,
154- 'text-black/90 dark:text-white/90' ,
155- 'placeholder:text-default-700/50 dark:placeholder:text-white/60' ,
156- ] ,
157- innerWrapper : 'bg-transparent' ,
158- inputWrapper : [
159- 'shadow-xl' ,
160- 'bg-default-200/50' ,
161- 'dark:bg-default/60' ,
162- 'backdrop-blur-xl' ,
163- 'backdrop-saturate-200' ,
164- 'hover:bg-default-200/70' ,
165- 'dark:hover:bg-default/70' ,
166- 'group-data-[focus=true]:bg-default-200/50' ,
167- 'dark:group-data-[focus=true]:bg-default/60' ,
168- '!cursor-text' ,
169- ] ,
170- } }
171- placeholder = 'Type to search...'
172- radius = 'lg'
173- startContent = {
174- < IoSearch
175- className = 'text-white cursor-pointer'
176- onClick = { handleSearch } // 클릭시 이벤트 발생 (추후 api 연결로 대체)
177- />
178- }
179- value = { searchValue }
180- onChange = { ( e ) => setSearchValue ( e . target . value ) }
181- onKeyDown = { ( e ) => {
182- if ( e . key === 'Enter' && ! e . nativeEvent . isComposing ) {
183- e . preventDefault ( ) ;
184- handleSearch ( ) ; // 클릭시 이벤트 발생 (추후 api 연결로 대체)
185- }
186- } }
187- onClear = { ( ) => setSearchValue ( '' ) }
188- />
174+ < AdminTableTopContent searchValue = { searchValue } setSearchValue = { setSearchValue } onSearch = { handleSearch } />
189175 }
190176 >
191177 < TableHeader columns = { columns } >
192178 { ( column ) => (
193- < TableColumn key = { column . uid } align = { column . uid === 'actions' ? 'center' : 'start' } >
179+ < TableColumn
180+ key = { column . uid }
181+ align = { [ 'actions' , 'togglePay' ] . includes ( column . uid ) ? 'center' : 'start' }
182+ className = { column . uid === 'togglePay' ? 'text-center' : '' }
183+ >
194184 { column . name }
195185 </ TableColumn >
196186 ) }
197187 </ TableHeader >
198- { /* 나중에 여기 users 로 변경할 것 */ }
199- < TableBody items = { currentUsers } >
188+ < TableBody
189+ items = { currentUsers }
190+ isLoading = { loading }
191+ emptyContent = { loading ? '불러오는 중...' : '데이터가 없습니다.' }
192+ >
200193 { ( item ) => (
201194 < TableRow
202195 className = 'hover:bg-[#35353b99] cursor-pointer'
203- key = { item . member . id }
196+ key = { item . member ?. id ?? item . id }
204197 onClick = { ( ) => handleRowClick ( item ) }
205198 >
206- { ( columnKey ) => < TableCell > { renderCell ( item , columnKey ) } </ TableCell > }
199+ { ( columnKey ) => (
200+ < TableCell className = { columnKey === 'togglePay' ? 'text-center' : '' } >
201+ { renderCell ( item , columnKey ) }
202+ </ TableCell >
203+ ) }
207204 </ TableRow >
208205 ) }
209206 </ TableBody >
210207 </ Table >
211208
212209 < UserDetailsModal user = { selectedUser } isOpen = { modalOpen } onClose = { handleCloseModal } preventClose />
213- </ div > ) }
210+ </ div >
214211 </ >
215212 ) ;
216-
217213}
0 commit comments