Skip to content

Commit 3b743f9

Browse files
authored
Merge pull request #151 from GDGoCINHA/Feat(#6)/admin
Refactor, Feat(#6)/ Admin 페이지 리팩토링 및 결제 상태 토글 기능 구현
2 parents 8aaeebc + d1d1e81 commit 3b743f9

File tree

7 files changed

+310
-188
lines changed

7 files changed

+310
-188
lines changed

src/app/admin/page.jsx

Lines changed: 139 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,20 @@
22

33
import React, { useCallback, useRef, useEffect, useState } from 'react';
44
import { 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

207
import 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

2414
const 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

3021
const statusColorMap = {
@@ -33,80 +24,122 @@ const statusColorMap = {
3324
};
3425

3526
export 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
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"use client";
2+
3+
import React from 'react';
4+
import { Pagination } from '@nextui-org/react';
5+
6+
export default function AdminTableBottomContent({ page, totalPages, onChangePage }) {
7+
if (!totalPages || totalPages <= 0) return null;
8+
9+
return (
10+
<div className='flex w-full justify-center'>
11+
<Pagination
12+
isCompact
13+
showControls
14+
showShadow
15+
color='primary'
16+
page={page}
17+
total={totalPages}
18+
onChange={onChangePage}
19+
/>
20+
</div>
21+
);
22+
}
23+
24+

0 commit comments

Comments
 (0)