Skip to content

Commit a4cdd93

Browse files
authored
[feat] 관리자 메인 페이지, 지원자 관리 페이지 구현
* feat: adminMain 페이지 추가 * feat: 관리자메인 페이지, 지원자 관리 페이지 구현
1 parent 52cb890 commit a4cdd93

File tree

6 files changed

+299
-2
lines changed

6 files changed

+299
-2
lines changed

package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"react-dom": "^18.0.0",
2222
"react-router-dom": "^7.1.5",
2323
"react-scripts": "5.0.1",
24+
"react-toastify": "^11.0.3",
2425
"tailwind-merge": "^3.0.1",
2526
"tailwindcss-animate": "^1.0.7",
2627
"typescript": "^4.9.5",

src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import React from 'react'
22
import './assets/styles/index.css'
33
import { BrowserRouter as Router, Routes, Route, useLocation } from 'react-router-dom'
44
import Main from './pages/Main'
5+
import AdminMain from './pages/admin/AdminMain'
56
import Login from './pages/Login'
67
import RecruitMeeting from './pages/RecruitMeeting'
78
import RecruitSubmitMeeting from './pages/RecruitSubmitMeeting'
89
import News from './pages/News'
910
import NewsInfo from './pages/NewsInfo'
11+
import ManApplicants from './pages/admin/ManApplicants'
1012
import UserMain from './pages/UserMain'
1113
import Recruit from './pages/Recruit'
1214
import CoreMembers from './pages/CoreMembers'
@@ -18,6 +20,8 @@ function App() {
1820
<Router>
1921
<Routes>
2022
<Route path='/' element={<Main />} />
23+
<Route path='/admin' element={<AdminMain />} />
24+
<Route path='/admin/applicants' element={<ManApplicants />} />
2125
<Route path='/recruit-meeting' element={<RecruitMeeting />} />
2226
<Route path='/recruit-meeting/submit' element={<RecruitSubmitMeeting />} />
2327
<Route path='/news' element={<News />} />

src/pages/admin/AdminMain.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react'
2+
import { Header } from '../../components/UI/Header'
3+
import { useNavigate } from 'react-router-dom'
4+
5+
// 메뉴 버튼 컴퍼넌트
6+
const AdminMenuBtn = ({ text, link } : { text: string, link: string }) => {
7+
const navigate = useNavigate()
8+
9+
return (
10+
<div
11+
className='cursor-pointer w-[420px] bg-mainColor text-center py-[9px] rounded-[6px] hover:bg-[#278573]'
12+
onClick={() => navigate(link)}
13+
>
14+
{text}
15+
</div>
16+
)
17+
}
18+
19+
const AdminMain: React.FC = () => {
20+
return (
21+
<div className='h-[100vh] w-[100vw] bg-mainBlack'>
22+
<div className='h-[100%] flex flex-col items-center justify-center space-y-4 text-[16px] text-white font-pretendardSemiBold'>
23+
<AdminMenuBtn text='회원 관리' link='/admin/members' />
24+
<AdminMenuBtn text='지원자 관리' link='/admin/applicants' />
25+
<AdminMenuBtn text='일정 관리' link='/admin/date' />
26+
<AdminMenuBtn text='공지사항 관리' link='/admin/news' />
27+
<div
28+
className='text-[14px] font-pretendardRegular text-white'
29+
//onClick 로그아웃 기능
30+
>
31+
로그아웃
32+
</div>
33+
</div>
34+
</div>
35+
)
36+
}
37+
38+
export default AdminMain

src/pages/admin/ManApplicants.tsx

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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`, { 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

yarn.lock

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8636,7 +8636,7 @@ react-dev-utils@^12.0.1:
86368636
strip-ansi "^6.0.1"
86378637
text-table "^0.2.0"
86388638

8639-
react-dom@^18.0.0, "react-dom@^18.0.0 || ^19.0.0", react-dom@>=16.8.0, react-dom@>=18:
8639+
"react-dom@^18 || ^19", react-dom@^18.0.0, "react-dom@^18.0.0 || ^19.0.0", react-dom@>=16.8.0, react-dom@>=18:
86408640
version "18.0.0"
86418641
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.0.0.tgz"
86428642
integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==
@@ -8741,7 +8741,14 @@ [email protected]:
87418741
optionalDependencies:
87428742
fsevents "^2.3.2"
87438743

8744-
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", react@^18.0.0, "react@^18.0.0 || ^19.0.0", react@^18.3.1, "react@>= 16", react@>=16.8.0, react@>=18:
8744+
react-toastify@^11.0.3:
8745+
version "11.0.3"
8746+
resolved "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz"
8747+
integrity sha512-cbPtHJPfc0sGqVwozBwaTrTu1ogB9+BLLjd4dDXd863qYLj7DGrQ2sg5RAChjFUB4yc3w8iXOtWcJqPK/6xqRQ==
8748+
dependencies:
8749+
clsx "^2.1.1"
8750+
8751+
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19", react@^18.0.0, "react@^18.0.0 || ^19.0.0", react@^18.3.1, "react@>= 16", react@>=16.8.0, react@>=18:
87458752
version "18.3.1"
87468753
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
87478754
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==

0 commit comments

Comments
 (0)