Skip to content

Commit ab68b7d

Browse files
authored
Merge pull request #330 from devpalsPlus/feat/#324
관리자 "회원 전체 조회" 페이지 구현 ( issue #330 )
2 parents 7c75e65 + 7d6e27d commit ab68b7d

File tree

24 files changed

+1033
-79
lines changed

24 files changed

+1033
-79
lines changed

src/api/auth.api.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ApiGetAllUsers,
3+
ApiGetAllUsersPreview,
34
type ApiOauth,
45
type ApiVerifyNickname,
56
type VerifyEmail,
@@ -8,6 +9,7 @@ import { httpClient } from './http.api';
89
import { loginFormValues } from '../pages/login/Login';
910
import { registerFormValues } from '../pages/user/register/Register';
1011
import { changePasswordFormValues } from '../pages/user/changePassword/ChangePassword';
12+
import { type SearchType } from '../models/search';
1113

1214
export const postVerificationEmail = async (email: string) => {
1315
try {
@@ -106,9 +108,21 @@ export const getOauthLogin = async (oauthAccessToken: string) => {
106108
}
107109
};
108110

109-
export const getAllUsers = async () => {
111+
export const getAllUsersPreview = async () => {
110112
try {
111-
const response = await httpClient.get<ApiGetAllUsers>(`/users`);
113+
const response = await httpClient.get<ApiGetAllUsersPreview>(
114+
`/users/preview`
115+
);
116+
return response.data.data;
117+
} catch (e) {
118+
console.error(e);
119+
throw e;
120+
}
121+
};
122+
123+
export const getAllUsers = async (params: SearchType) => {
124+
try {
125+
const response = await httpClient.get<ApiGetAllUsers>(`/users`, { params });
112126
return response.data.data;
113127
} catch (e) {
114128
console.error(e);

src/components/admin/adminNotice/AdminNoticeList.tsx

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,15 @@
1-
import { useEffect, useState } from 'react';
21
import SearchBar from '../../../components/common/admin/searchBar/SearchBar';
32
import * as S from './AdminNoticeList.styled';
4-
import type { NoticeSearch } from '../../../models/customerService';
53
import { useGetNotice } from '../../../hooks/user/useGetNotice';
6-
import { useSearchParams } from 'react-router-dom';
74
import Pagination from '../../../components/common/pagination/Pagination';
85
import Spinner from '../../../components/user/mypage/Spinner';
96
import NoticeItem from '../../../pages/user/customerService/notice/noticeItem/NoticeItem';
7+
import useSearchBar from '../../../hooks/admin/useSearchBar';
108

119
export default function AdminNoticeList() {
12-
const [noticeSearch, setNoticeSearch] = useState<NoticeSearch>({
13-
keyword: '',
14-
page: 1,
15-
});
16-
const [value, setValue] = useState<string>('');
17-
const { noticeData, isLoading } = useGetNotice(noticeSearch);
18-
const [searchParams] = useSearchParams();
19-
20-
useEffect(() => {
21-
const searchKeyword = searchParams.get('keyword');
22-
23-
if (searchKeyword) {
24-
setNoticeSearch((prev) => ({ ...prev, keyword: searchKeyword }));
25-
setValue((prev) => (searchKeyword ? searchKeyword : prev));
26-
}
27-
}, [searchParams]);
28-
29-
const handleGetKeyword = (keyword: string) => {
30-
setNoticeSearch((prev) => ({ ...prev, keyword }));
31-
setValue(keyword);
32-
};
33-
const handleChangePagination = (page: number) => {
34-
setNoticeSearch((prev) => ({ ...prev, page }));
35-
};
10+
const { searchUnit, value, handleGetKeyword, handleChangePagination } =
11+
useSearchBar();
12+
const { noticeData, isLoading } = useGetNotice(searchUnit);
3613

3714
if (isLoading) {
3815
return (
@@ -48,7 +25,11 @@ export default function AdminNoticeList() {
4825

4926
return (
5027
<>
51-
<SearchBar onGetKeyword={handleGetKeyword} value={value} />
28+
<SearchBar
29+
onGetKeyword={handleGetKeyword}
30+
value={value}
31+
isNotice={true}
32+
/>
5233
<S.NoticeItemWrapper>
5334
<NoticeItem
5435
noticeData={noticeData.notices}
@@ -57,7 +38,7 @@ export default function AdminNoticeList() {
5738
/>
5839
</S.NoticeItemWrapper>
5940
<Pagination
60-
page={noticeSearch.page}
41+
page={searchUnit.page}
6142
getLastPage={lastPage}
6243
onChangePagination={handleChangePagination}
6344
/>

src/components/admin/mainCard/MainCard.styled.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import styled from 'styled-components';
44
export const Container = styled.div`
55
display: flex;
66
flex-direction: column;
7-
border: 1px solid #ccc;
7+
border: 1px solid ${({ theme }) => theme.color.grey};
88
border-radius: ${({ theme }) => theme.borderRadius.primary};
99
`;
1010

src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from 'react';
22
import * as S from './AllUserPreview.styled';
3-
import { useGetAllUsers } from '../../../../hooks/admin/useGetAllUsers';
43
import Avatar from '../../../common/avatar/Avatar';
54
import { ADMIN_ROUTE } from '../../../../constants/routes';
65
import arrow_right from '../../../../assets/ArrowRight.svg';
76
import LoadingSpinner from '../../../common/loadingSpinner/LoadingSpinner';
7+
import { useGetAllUsersPreview } from '../../../../hooks/admin/useGetAllUsersPreview';
88

99
const AllUserPreview = () => {
10-
const { allUserData, isLoading, isFetching } = useGetAllUsers();
10+
const { allUserData, isLoading, isFetching } = useGetAllUsersPreview();
1111

1212
if (isLoading || isFetching) {
1313
return <LoadingSpinner />;
@@ -17,15 +17,9 @@ const AllUserPreview = () => {
1717
return <S.Container>가입된 회원이 없습니다.</S.Container>;
1818
}
1919

20-
const previewList = allUserData
21-
? allUserData.length > 6
22-
? allUserData.slice(0, 4)
23-
: allUserData
24-
: [];
25-
2620
return (
2721
<S.Container>
28-
{previewList?.map((user) => (
22+
{allUserData?.map((user) => (
2923
<S.Wrapper key={user.id}>
3024
<S.UserArea>
3125
<Avatar image={user.user.img} size='40px' />

src/components/admin/previewComponent/inquiresPreview/InquiresPreview.styled.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ export const Divider = styled.p`
5151

5252
export const InquiryState = styled.p<{ $isCompleted: boolean }>`
5353
font-size: 9px;
54-
color: ${({ $isCompleted }) => ($isCompleted ? `#07DE00` : `#DE1A00`)};
54+
color: ${({ theme, $isCompleted }) =>
55+
$isCompleted ? theme.color.green : theme.color.red};
5556
`;
5657

5758
export const MoveToInquiryArea = styled(Link)`

src/components/admin/previewComponent/reportsPreview/ReportsPreview.styled.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const ContentArea = styled.div`
2121
margin-left: 16px;
2222
`;
2323

24-
export const ImposedCount = styled.div`
24+
export const ReportedCount = styled.div`
2525
font-size: 9px;
2626
opacity: 0.5;
2727
`;
@@ -49,9 +49,10 @@ export const Divider = styled.p`
4949
margin-right: 3px;
5050
`;
5151

52-
export const IsImposed = styled.p<{ $isImposed: boolean }>`
52+
export const IsDone = styled.p<{ $isDone: boolean }>`
5353
font-size: 9px;
54-
color: ${({ $isImposed }) => ($isImposed ? `#07DE00` : `#DE1A00`)};
54+
color: ${({ theme, $isDone }) =>
55+
$isDone ? theme.color.green : theme.color.red};
5556
`;
5657

5758
export const MoveToReportsArea = styled(Link)`

src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ const ReportsPreview = () => {
2020
<S.ReportArea to={`${ADMIN_ROUTE.reports}/${report.id}`}>
2121
<Avatar image={report.user.img} size='40px' />
2222
<S.ContentArea>
23-
<S.ImposedCount>{report.imposedCount}</S.ImposedCount>
23+
<S.ReportedCount>{report.reportedCount}</S.ReportedCount>
2424
<S.Category>{report.category}</S.Category>
2525
<S.StateArea>
2626
<S.ReportDate>{report.createdAt}</S.ReportDate>
2727
<S.Divider>|</S.Divider>
28-
<S.IsImposed $isImposed={report.isImposed}>
29-
{report.isImposed ? '검토 완료' : '검토 미완료'}
30-
</S.IsImposed>
28+
<S.IsDone $isDone={report.isDone}>
29+
{report.isDone ? '검토 완료' : '검토 미완료'}
30+
</S.IsDone>
3131
</S.StateArea>
3232
</S.ContentArea>
3333
</S.ReportArea>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import styled from 'styled-components';
2+
import { UserState } from '../../../models/auth';
3+
4+
export const Container = styled.div`
5+
width: 240px;
6+
display: flex;
7+
flex-direction: column;
8+
border: 1px solid ${({ theme }) => theme.color.grey};
9+
border-radius: ${({ theme }) => theme.borderRadius.primary};
10+
padding: 10px;
11+
`;
12+
13+
export const ProfileHeader = styled.div`
14+
display: flex;
15+
flex-direction: column;
16+
align-items: center;
17+
`;
18+
19+
export const NickName = styled.p`
20+
margin-top: 5px;
21+
`;
22+
23+
export const MainContentArea = styled.div`
24+
padding: 15px;
25+
`;
26+
27+
export const TextLabel = styled.label`
28+
display: inline-block;
29+
font-size: 14px;
30+
opacity: 0.3;
31+
word-break: break-word;
32+
white-space: pre-wrap;
33+
`;
34+
35+
export const TextContent = styled.p<{
36+
$userState?: UserState;
37+
}>`
38+
font-size: 14px;
39+
color: ${({ theme, $userState }) =>
40+
$userState === UserState.ONLINE
41+
? theme.color.green
42+
: $userState === UserState.OFFLINE
43+
? theme.color.blue
44+
: $userState === UserState.SUSPENDED
45+
? theme.color.red
46+
: theme.color.white};
47+
margin-left: 15px;
48+
`;
49+
50+
export const SkillTagArea = styled.div`
51+
display: flex;
52+
gap: 4px;
53+
margin-left: 15px;
54+
`;
55+
56+
export const SkillTag = styled.img`
57+
width: 29px;
58+
height: 29px;
59+
border: 1px solid ${({ theme }) => theme.color.grey};
60+
border-radius: 50%;
61+
`;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React from 'react';
2+
import * as S from './UserCard.styled';
3+
import Avatar from '../../common/avatar/Avatar';
4+
import { type AllUser } from '../../../models/auth';
5+
6+
interface UserCardProps {
7+
userData: AllUser;
8+
}
9+
10+
const UserCard = ({ userData }: UserCardProps) => {
11+
return (
12+
<S.Container>
13+
<S.ProfileHeader>
14+
<Avatar image={userData.user.img} size='46px' />
15+
<S.NickName>{userData.user.nickname}</S.NickName>
16+
</S.ProfileHeader>
17+
<S.MainContentArea>
18+
<S.TextLabel>이메일</S.TextLabel>
19+
<S.TextContent>{userData.email}</S.TextContent>
20+
<S.TextLabel>회원 상태</S.TextLabel>
21+
<S.TextContent $userState={userData.userState}>
22+
{userData.userState}
23+
</S.TextContent>
24+
<S.TextLabel>경고 횟수</S.TextLabel>
25+
<S.TextContent>
26+
{userData.reportedCount === 0
27+
? '없음'
28+
: `${userData.reportedCount}번`}
29+
</S.TextContent>
30+
<S.TextLabel>포지션</S.TextLabel>
31+
<S.TextContent>
32+
{userData.position.map((position) => position.name).join(', ')}
33+
</S.TextContent>
34+
<S.TextLabel>대표 스킬</S.TextLabel>
35+
<S.SkillTagArea>
36+
{userData.skill.map((skillTag) => (
37+
<S.SkillTag key={skillTag.id} src={skillTag.img} />
38+
))}
39+
</S.SkillTagArea>
40+
<S.TextLabel>계정 생성 날짜</S.TextLabel>
41+
<S.TextContent>{userData.createdAt}</S.TextContent>
42+
</S.MainContentArea>
43+
</S.Container>
44+
);
45+
};
46+
47+
export default UserCard;

src/components/common/admin/searchBar/SearchBar.tsx

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
11
import { XMarkIcon } from '@heroicons/react/24/outline';
2-
import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService';
32
import * as S from './SearchBar.styled';
43
import { useState } from 'react';
5-
import { useLocation, useSearchParams } from 'react-router-dom';
4+
import { useSearchParams } from 'react-router-dom';
65
import { useModal } from '../../../../hooks/useModal';
6+
import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService';
77
import Modal from '../../modal/Modal';
88
import { ADMIN_ROUTE } from '../../../../constants/routes';
99

1010
interface SearchBarProps {
11-
onGetKeyword: (keyword: string) => void;
11+
onGetKeyword: (value: string) => void;
1212
value: string;
13+
isNotice?: boolean;
1314
}
1415

15-
export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
16-
const [inputValue, setInputValue] = useState<string>('');
16+
export default function SearchBar({
17+
onGetKeyword,
18+
value,
19+
isNotice,
20+
}: SearchBarProps) {
21+
const [keyword, setKeyword] = useState<string>(value);
1722
const { isOpen, message, handleModalOpen, handleModalClose } = useModal();
1823
const [searchParams, setSearchParams] = useSearchParams();
19-
const location = useLocation();
20-
const keyword = inputValue ? inputValue : value;
2124

2225
const handleKeyword = (inputValue: string) => {
2326
const newSearchParams = new URLSearchParams(searchParams);
@@ -32,22 +35,21 @@ export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
3235
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
3336
e.preventDefault();
3437

35-
if (inputValue.trim() === '') {
38+
if (keyword.trim() === '') {
3639
return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword);
3740
} else {
38-
onGetKeyword(inputValue);
39-
handleKeyword(inputValue);
41+
onGetKeyword(keyword);
42+
handleKeyword(keyword);
4043
return;
4144
}
4245
};
4346

4447
const handleChangeKeyword = (e: React.ChangeEvent<HTMLInputElement>) => {
45-
const value = e.target.value;
46-
setInputValue(value);
48+
setKeyword(e.target.value);
4749
};
4850

4951
const handleClickSearchDefault = () => {
50-
setInputValue('');
52+
setKeyword('');
5153
onGetKeyword('');
5254
handleKeyword('');
5355
};
@@ -73,9 +75,11 @@ export default function SearchBar({ onGetKeyword, value }: SearchBarProps) {
7375
</S.AdminSearchBarInputWrapper>
7476
<S.AdminSearchBarButton>검색</S.AdminSearchBarButton>
7577
</S.AdminSearchBarWrapper>
76-
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
77-
작성하기
78-
</S.WriteLink>
78+
{isNotice && (
79+
<S.WriteLink to={ADMIN_ROUTE.write} state={{ form: location.pathname }}>
80+
작성하기
81+
</S.WriteLink>
82+
)}
7983
<Modal isOpen={isOpen} onClose={handleModalClose}>
8084
{message}
8185
</Modal>

0 commit comments

Comments
 (0)