1+ import  React ,  {  useState ,  useEffect  }  from  'react' 
2+ import  axios  from  'axios' 
3+ import  {  Header  }  from  '../../components/UI/Header' 
4+ import  {  toast ,  ToastContainer  }  from  'react-toastify' 
5+ import  'react-toastify/dist/ReactToastify.css' 
6+ 
7+ const  ManApplicants : React . FC  =  ( )  =>  { 
8+     const  [ applicants ,  setApplicants ]  =  useState < any [ ] > ( [ ] )              // 전체 조회 시 지원자 정보 
9+     const  [ count ,  setCount ]  =  useState < number > ( 0 )                        // 지원자 수 
10+     const  [ detailInfo ,  setDetailInfo ]  =  useState < any > ( null )              // 특정 지원자 상세정보 
11+     const  [ selectedId ,  setSelectedId ]  =  useState < number  |  null > ( null )    // 상태 변경 시 선택된 지원자id 
12+ 
13+     const  accessToken  =  localStorage . getItem ( 'accessToken' ) 
14+     
15+     // 합격 불합격 상태 
16+     const  statusMap : Record < string ,  string >  =  { 
17+         PENDING : '지원 완료' , 
18+         DOCUMENT_PASSED : '서류 합격' , 
19+         DOCUMENT_FAILED : '서류 불합격' , 
20+         INTERVIEW_PASSED : '면접 합격' , 
21+         INTERVIEW_FAILED : '면접 불합격' 
22+     } 
23+     const  reverseStatusMap  =  Object . fromEntries ( 
24+         Object . entries ( statusMap ) . map ( ( [ key ,  value ] )  =>  [ value ,  key ] ) 
25+     ) 
26+     const  statusOptions  =  Object . values ( statusMap ) 
27+     const  [ openDropdownId ,  setOpenDropdownId ]  =  useState < number  |  null > ( null ) 
28+ 
29+     // 페이지네이션 
30+     const  [ currentPage ,  setCurrentPage ]  =  useState < number > ( 1 )               // 현재 페이지 
31+     const  applicantsPerPage  =  20                                            // 페이지 당 지원자 수 
32+     const  LastApplicant  =  currentPage  *  applicantsPerPage                   // 페이지에서 마지막 지원자 
33+     const  FirstApplicant  =  LastApplicant  -  applicantsPerPage                // 페이지에서 첫번째 지원자 
34+     const  currentApplicants  =  applicants . slice ( FirstApplicant ,  LastApplicant )  // 현재 페이지에서 지원자 
35+     const  totalPages  =  Math . ceil ( applicants . length  /  applicantsPerPage )        // 총 페이지 수수 
36+ 
37+ 
38+ 
39+     // 지원자 전체 조회 
40+     useEffect ( ( )  =>  { 
41+         const  fetchData  =  async  ( )  =>  { 
42+             try  { 
43+                 const  response  =  await  axios . get ( 'https://dmu-dasom.or.kr/api/admin/applicants' ,  { 
44+                     headers : { 
45+                         Authorization : `Bearer ${ accessToken }  
46+                     } 
47+                 } ) 
48+                 console . log ( response . data ) 
49+                 setApplicants ( response . data . content ) 
50+                 setCount ( response . data . totalElements ) 
51+             }  catch  ( err : any )  { 
52+                 console . error ( err ) 
53+                 const  errorCode  =  err . response ?. data ?. code 
54+                 if  ( errorCode  ===  'C012' )  { 
55+                     alert ( '조회된 데이터가 없습니다.' ) 
56+                 }  else  { 
57+                     alert ( '데이터 불러오기에 실패하였습니다.' ) 
58+                 } 
59+             } 
60+         } 
61+ 
62+         fetchData ( ) 
63+     } ,  [ ] ) 
64+ 
65+     // 상세정보 조회 및 토글 
66+     const  toggleDetail  =  async  ( id : number )  =>  { 
67+         if  ( selectedId  ===  id )  { 
68+             setSelectedId ( null ) 
69+             setDetailInfo ( null ) 
70+             return 
71+         } 
72+     
73+         try  { 
74+             const  response  =  await  axios . get ( `https://dmu-dasom.or.kr/api/admin/applicants/${ id }  ,  { 
75+                 headers : { 
76+                     Authorization : `Bearer ${ accessToken }  
77+                 } 
78+             } ) 
79+             setDetailInfo ( response . data ) 
80+             setSelectedId ( id ) 
81+         }  catch  ( err : any )  { 
82+             console . error ( '지원자 상세 조회 실패:' ,  err ) 
83+             const  errorCode  =  err . response ?. data ?. code 
84+             if  ( errorCode  ===  'C012' )  { 
85+                 alert ( '해당 지원자의 상세 정보가 없습니다.' ) 
86+             }  else  { 
87+                 alert ( '상세 정보를 불러오는 데 실패하였습니다.' ) 
88+             } 
89+         } 
90+     } 
91+ 
92+     // 지원자 상태 변경 
93+     const  handleStatusChange  =  ( id : number ,  newStatus : string )  =>  { 
94+         const  statusValue  =  reverseStatusMap [ newStatus ]  ||  'PENDING' 
95+         
96+         setApplicants ( ( prev )  => 
97+             prev . map ( ( applicant )  => 
98+                 applicant . id  ===  id  ? {  ...applicant ,  status : statusValue  }  : applicant 
99+             ) 
100+         ) 
101+         setOpenDropdownId ( null )  // 드롭다운 닫기 
102+ 
103+         axios . patch ( `https://dmu-dasom.or.kr/api/admin/applicants/${ id }  ,  {  status : statusValue  } ,  { 
104+             headers : {  Authorization : `Bearer ${ accessToken }   } 
105+         } ) 
106+         . then ( ( )  =>  { 
107+             console . log ( '상태 변경 성공:' ,  newStatus ) 
108+             toast . success ( '상태 변경이 완료되었습니다!' ) 
109+         } ) 
110+         . catch ( ( err )  =>  console . error ( '상태 변경 실패:' ,  err ) ) 
111+     } 
112+ 
113+ 
114+     // 지원자 리스트 항목 컴퍼넌트 
115+     const  ApplicantInfo  =  ( { applicant} :{ applicant :any } )  =>  { 
116+         return  ( 
117+             < tr  className = 'text-center' > 
118+                 < td  className = 'border border-gray-500 py-[4px]' > { applicant . id } </ td > 
119+                 < td  className = 'border border-gray-500 py-[4px]' > { applicant . name } </ td > 
120+                 < td  className = 'border border-gray-500 py-[4px]' > { applicant . studentNo } </ td > 
121+                 < td  className = "border border-gray-500 py-[4px] relative" > 
122+                     < div  
123+                         className = "w-[120px] m-auto p-[4px] rounded-[6px] cursor-pointer bg-gray-700 text-white" 
124+                         onClick = { ( )  =>  setOpenDropdownId ( openDropdownId  ===  applicant . id  ? null  : applicant . id ) } 
125+                     > 
126+                         { statusMap [ applicant . status ] } 
127+                     </ div > 
128+                     { /* 드롭다운 */ } 
129+                     { openDropdownId  ===  applicant . id  &&  ( 
130+                         < div  className = "absolute top-[40px] left-1/2 transform -translate-x-1/2 w-[130px] bg-gray-700 rounded-[6px] z-10 text-white" > 
131+                             { statusOptions . map ( ( option )  =>  ( 
132+                                 < div  
133+                                     key = { option }  
134+                                     className = "px-4 py-2 hover:bg-gray-800 rounded-[6px] cursor-pointer" 
135+                                     onClick = { ( )  =>  handleStatusChange ( applicant . id ,  option ) } 
136+                                 > 
137+                                     { option } 
138+                                 </ div > 
139+                             ) ) } 
140+                         </ div > 
141+                     ) } 
142+                 </ td > 
143+                 < td  className = 'border border-gray-500 py-[4px] text-left' > 
144+                 < div  className = 'ml-[4px]' > 
145+                     < button  
146+                         className = 'bg-gray-700 text-white px-2 py-1 rounded' 
147+                         onClick = { ( )  =>  toggleDetail ( applicant . id ) } 
148+                     > 
149+                         { selectedId  ===  applicant . id  ? '닫기'  : '보기' } 
150+                     </ button > 
151+                     { selectedId  ===  applicant . id  &&  ( 
152+                         < div  className = 'mt-1 p-2' > 
153+                             < ApplicantDetailInfo  applicant = { detailInfo }  /> 
154+                         </ div > 
155+                     ) } 
156+                 </ div > 
157+                 </ td > 
158+             </ tr > 
159+         ) 
160+     } 
161+ 
162+     // 상세 정보 아이템 컴포넌트 
163+     const  DetailItem  =  ( {  label,  value } : {  label : string ,  value : string  } )  =>  { 
164+         return  ( 
165+             < div  className = 'flex' > 
166+                 < div  className = 'w-[110px]' > { label } </ div > 
167+                 < div  className = 'w-[576px]' > { value } </ div > 
168+             </ div > 
169+         ) 
170+     } 
171+     // 지원자 상세 정보 컴포넌트 
172+     const  ApplicantDetailInfo  =  ( {  applicant } : {  applicant : any  } )  =>  { 
173+         return  ( 
174+             < div  className = 'flex flex-col space-y-[4px]' > 
175+                 < DetailItem  label = "연락처"  value = { applicant . contact }  /> 
176+                 < DetailItem  label = "이메일"  value = { applicant . email }  /> 
177+                 < DetailItem  label = "지원 동기"  value = { applicant . reasonForApply }  /> 
178+                 < DetailItem  label = "희망 활동"  value = { applicant . activityWish }  /> 
179+                 < DetailItem  label = "개인정보 동의"  value = { applicant . isPrivacyPolicyAgreed  ? 'O'  : 'X' }  /> 
180+                 < DetailItem  label = "지원 일시"  value = { applicant . createdAt }  /> 
181+                 < DetailItem  label = "최종수정 일시"  value = { applicant . updatedAt }  /> 
182+             </ div > 
183+         ) 
184+     } 
185+     
186+ 
187+     return  ( 
188+         < div  className = 'h-[100vh] w-[100vw] bg-mainBlack font-pretendardRegular text-white flex flex-col items-center' > 
189+             < ToastContainer  /> 
190+             < div  className = 'mb-[4px] mt-[155px] justify-start w-[1220px]' > 
191+                 < span  className = 'font-pretendardBold text-mainColor' > { count } </ span > 명의 지원자가 있습니다.
192+             </ div > 
193+ 
194+             { /* 지원자 목록 테이블 */ } 
195+             < table  className = 'w-[1220px]' > 
196+                 < thead > 
197+                     < tr  className = 'border border-gray-500 py-[4px] font-pretendardBold' > 
198+                         < th  className = 'w-[60px]' > ID</ th > 
199+                         < th  className = 'border border-gray-500 py-[4px] w-[150px]' > 이름</ th > 
200+                         < th  className = 'border border-gray-500 py-[4px] w-[150px]' > 학번</ th > 
201+                         < th  className = 'border border-gray-500 py-[4px] w-[150px]' > 상태</ th > 
202+                         < th  className = 'border border-gray-500 py-[4px]' > 상세정보</ th > 
203+                     </ tr > 
204+                 </ thead > 
205+                 < tbody > 
206+                     { currentApplicants . map ( ( applicant )  =>  ( 
207+                         < ApplicantInfo  key = { applicant . id }  applicant = { applicant }  /> 
208+                     ) ) } 
209+                 </ tbody > 
210+             </ table > 
211+             { /* 페이지네이션 */ } 
212+             < div  className = 'mt-6 space-x-4' > 
213+                 < button  
214+                     className = 'px-4 py-2 bg-gray-700 text-white rounded-lg disabled:opacity-50'  
215+                     onClick = { ( )  =>  setCurrentPage ( ( prev )  =>  Math . max ( prev  -  1 ,  1 ) ) }  
216+                     disabled = { currentPage  ===  1 } 
217+                 > 
218+                     이전
219+                 </ button > 
220+                 < span  className = 'text-lg font-bold' > { currentPage }  / { totalPages } </ span > 
221+                 < button  
222+                     className = 'px-4 py-2 bg-gray-700 text-white rounded-lg disabled:opacity-50'  
223+                     onClick = { ( )  =>  setCurrentPage ( ( prev )  =>  Math . min ( prev  +  1 ,  totalPages ) ) }  
224+                     disabled = { currentPage  ===  totalPages } 
225+                 > 
226+                     다음
227+                 </ button > 
228+             </ div > 
229+         </ div > 
230+     ) 
231+ } 
232+ 
233+ export  default  ManApplicants 
0 commit comments