diff --git a/src/api/auth.api.ts b/src/api/auth.api.ts index b866b37c..6a236128 100644 --- a/src/api/auth.api.ts +++ b/src/api/auth.api.ts @@ -1,5 +1,6 @@ import { ApiGetAllUsers, + ApiGetAllUsersPreview, type ApiOauth, type ApiVerifyNickname, type VerifyEmail, @@ -8,6 +9,7 @@ import { httpClient } from './http.api'; import { loginFormValues } from '../pages/login/Login'; import { registerFormValues } from '../pages/user/register/Register'; import { changePasswordFormValues } from '../pages/user/changePassword/ChangePassword'; +import { type SearchType } from '../models/search'; export const postVerificationEmail = async (email: string) => { try { @@ -106,9 +108,21 @@ export const getOauthLogin = async (oauthAccessToken: string) => { } }; -export const getAllUsers = async () => { +export const getAllUsersPreview = async () => { try { - const response = await httpClient.get(`/users`); + const response = await httpClient.get( + `/users/preview` + ); + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const getAllUsers = async (params: SearchType) => { + try { + const response = await httpClient.get(`/users`, { params }); return response.data.data; } catch (e) { console.error(e); diff --git a/src/components/admin/adminNotice/AdminNoticeList.tsx b/src/components/admin/adminNotice/AdminNoticeList.tsx index 3bb10740..acbf1ea0 100644 --- a/src/components/admin/adminNotice/AdminNoticeList.tsx +++ b/src/components/admin/adminNotice/AdminNoticeList.tsx @@ -1,38 +1,15 @@ -import { useEffect, useState } from 'react'; import SearchBar from '../../../components/common/admin/searchBar/SearchBar'; import * as S from './AdminNoticeList.styled'; -import type { NoticeSearch } from '../../../models/customerService'; import { useGetNotice } from '../../../hooks/user/useGetNotice'; -import { useSearchParams } from 'react-router-dom'; import Pagination from '../../../components/common/pagination/Pagination'; import Spinner from '../../../components/user/mypage/Spinner'; import NoticeItem from '../../../pages/user/customerService/notice/noticeItem/NoticeItem'; +import useSearchBar from '../../../hooks/admin/useSearchBar'; export default function AdminNoticeList() { - const [noticeSearch, setNoticeSearch] = useState({ - keyword: '', - page: 1, - }); - const [value, setValue] = useState(''); - const { noticeData, isLoading } = useGetNotice(noticeSearch); - const [searchParams] = useSearchParams(); - - useEffect(() => { - const searchKeyword = searchParams.get('keyword'); - - if (searchKeyword) { - setNoticeSearch((prev) => ({ ...prev, keyword: searchKeyword })); - setValue((prev) => (searchKeyword ? searchKeyword : prev)); - } - }, [searchParams]); - - const handleGetKeyword = (keyword: string) => { - setNoticeSearch((prev) => ({ ...prev, keyword })); - setValue(keyword); - }; - const handleChangePagination = (page: number) => { - setNoticeSearch((prev) => ({ ...prev, page })); - }; + const { searchUnit, value, handleGetKeyword, handleChangePagination } = + useSearchBar(); + const { noticeData, isLoading } = useGetNotice(searchUnit); if (isLoading) { return ( @@ -48,7 +25,11 @@ export default function AdminNoticeList() { return ( <> - + diff --git a/src/components/admin/mainCard/MainCard.styled.ts b/src/components/admin/mainCard/MainCard.styled.ts index 44e6d803..99bad462 100644 --- a/src/components/admin/mainCard/MainCard.styled.ts +++ b/src/components/admin/mainCard/MainCard.styled.ts @@ -4,7 +4,7 @@ import styled from 'styled-components'; export const Container = styled.div` display: flex; flex-direction: column; - border: 1px solid #ccc; + border: 1px solid ${({ theme }) => theme.color.grey}; border-radius: ${({ theme }) => theme.borderRadius.primary}; `; diff --git a/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx b/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx index 9131fd10..00c91d60 100644 --- a/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx +++ b/src/components/admin/previewComponent/allUserPreview/AllUserPreview.tsx @@ -1,13 +1,13 @@ import React from 'react'; import * as S from './AllUserPreview.styled'; -import { useGetAllUsers } from '../../../../hooks/admin/useGetAllUsers'; import Avatar from '../../../common/avatar/Avatar'; import { ADMIN_ROUTE } from '../../../../constants/routes'; import arrow_right from '../../../../assets/ArrowRight.svg'; import LoadingSpinner from '../../../common/loadingSpinner/LoadingSpinner'; +import { useGetAllUsersPreview } from '../../../../hooks/admin/useGetAllUsersPreview'; const AllUserPreview = () => { - const { allUserData, isLoading, isFetching } = useGetAllUsers(); + const { allUserData, isLoading, isFetching } = useGetAllUsersPreview(); if (isLoading || isFetching) { return ; @@ -17,15 +17,9 @@ const AllUserPreview = () => { return 가입된 회원이 없습니다.; } - const previewList = allUserData - ? allUserData.length > 6 - ? allUserData.slice(0, 4) - : allUserData - : []; - return ( - {previewList?.map((user) => ( + {allUserData?.map((user) => ( diff --git a/src/components/admin/previewComponent/inquiresPreview/InquiresPreview.styled.ts b/src/components/admin/previewComponent/inquiresPreview/InquiresPreview.styled.ts index 0fa5c181..216304c5 100644 --- a/src/components/admin/previewComponent/inquiresPreview/InquiresPreview.styled.ts +++ b/src/components/admin/previewComponent/inquiresPreview/InquiresPreview.styled.ts @@ -51,7 +51,8 @@ export const Divider = styled.p` export const InquiryState = styled.p<{ $isCompleted: boolean }>` font-size: 9px; - color: ${({ $isCompleted }) => ($isCompleted ? `#07DE00` : `#DE1A00`)}; + color: ${({ theme, $isCompleted }) => + $isCompleted ? theme.color.green : theme.color.red}; `; export const MoveToInquiryArea = styled(Link)` diff --git a/src/components/admin/previewComponent/reportsPreview/ReportsPreview.styled.ts b/src/components/admin/previewComponent/reportsPreview/ReportsPreview.styled.ts index 5514ca39..5ae6b91a 100644 --- a/src/components/admin/previewComponent/reportsPreview/ReportsPreview.styled.ts +++ b/src/components/admin/previewComponent/reportsPreview/ReportsPreview.styled.ts @@ -21,7 +21,7 @@ export const ContentArea = styled.div` margin-left: 16px; `; -export const ImposedCount = styled.div` +export const ReportedCount = styled.div` font-size: 9px; opacity: 0.5; `; @@ -49,9 +49,10 @@ export const Divider = styled.p` margin-right: 3px; `; -export const IsImposed = styled.p<{ $isImposed: boolean }>` +export const IsDone = styled.p<{ $isDone: boolean }>` font-size: 9px; - color: ${({ $isImposed }) => ($isImposed ? `#07DE00` : `#DE1A00`)}; + color: ${({ theme, $isDone }) => + $isDone ? theme.color.green : theme.color.red}; `; export const MoveToReportsArea = styled(Link)` diff --git a/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx b/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx index 0f7f4d21..794d952d 100644 --- a/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx +++ b/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx @@ -20,14 +20,14 @@ const ReportsPreview = () => { - {report.imposedCount} 번 + {report.reportedCount} 번 {report.category} {report.createdAt} | - - {report.isImposed ? '검토 완료' : '검토 미완료'} - + + {report.isDone ? '검토 완료' : '검토 미완료'} + diff --git a/src/components/admin/userCard/UserCard.styled.ts b/src/components/admin/userCard/UserCard.styled.ts new file mode 100644 index 00000000..1bc8839e --- /dev/null +++ b/src/components/admin/userCard/UserCard.styled.ts @@ -0,0 +1,61 @@ +import styled from 'styled-components'; +import { UserState } from '../../../models/auth'; + +export const Container = styled.div` + width: 240px; + display: flex; + flex-direction: column; + border: 1px solid ${({ theme }) => theme.color.grey}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + padding: 10px; +`; + +export const ProfileHeader = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +export const NickName = styled.p` + margin-top: 5px; +`; + +export const MainContentArea = styled.div` + padding: 15px; +`; + +export const TextLabel = styled.label` + display: inline-block; + font-size: 14px; + opacity: 0.3; + word-break: break-word; + white-space: pre-wrap; +`; + +export const TextContent = styled.p<{ + $userState?: UserState; +}>` + font-size: 14px; + color: ${({ theme, $userState }) => + $userState === UserState.ONLINE + ? theme.color.green + : $userState === UserState.OFFLINE + ? theme.color.blue + : $userState === UserState.SUSPENDED + ? theme.color.red + : theme.color.white}; + margin-left: 15px; +`; + +export const SkillTagArea = styled.div` + display: flex; + gap: 4px; + margin-left: 15px; +`; + +export const SkillTag = styled.img` + width: 29px; + height: 29px; + border: 1px solid ${({ theme }) => theme.color.grey}; + border-radius: 50%; +`; diff --git a/src/components/admin/userCard/UserCard.tsx b/src/components/admin/userCard/UserCard.tsx new file mode 100644 index 00000000..27bf9b5b --- /dev/null +++ b/src/components/admin/userCard/UserCard.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import * as S from './UserCard.styled'; +import Avatar from '../../common/avatar/Avatar'; +import { type AllUser } from '../../../models/auth'; + +interface UserCardProps { + userData: AllUser; +} + +const UserCard = ({ userData }: UserCardProps) => { + return ( + + + + {userData.user.nickname} + + + 이메일 + {userData.email} + 회원 상태 + + {userData.userState} + + 경고 횟수 + + {userData.reportedCount === 0 + ? '없음' + : `${userData.reportedCount}번`} + + 포지션 + + {userData.position.map((position) => position.name).join(', ')} + + 대표 스킬 + + {userData.skill.map((skillTag) => ( + + ))} + + 계정 생성 날짜 + {userData.createdAt} + + + ); +}; + +export default UserCard; diff --git a/src/components/common/admin/searchBar/SearchBar.tsx b/src/components/common/admin/searchBar/SearchBar.tsx index 3b07c8ce..ecd605a4 100644 --- a/src/components/common/admin/searchBar/SearchBar.tsx +++ b/src/components/common/admin/searchBar/SearchBar.tsx @@ -1,23 +1,26 @@ import { XMarkIcon } from '@heroicons/react/24/outline'; -import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService'; import * as S from './SearchBar.styled'; import { useState } from 'react'; -import { useLocation, useSearchParams } from 'react-router-dom'; +import { useSearchParams } from 'react-router-dom'; import { useModal } from '../../../../hooks/useModal'; +import { MODAL_MESSAGE_CUSTOMER_SERVICE } from '../../../../constants/user/customerService'; import Modal from '../../modal/Modal'; import { ADMIN_ROUTE } from '../../../../constants/routes'; interface SearchBarProps { - onGetKeyword: (keyword: string) => void; + onGetKeyword: (value: string) => void; value: string; + isNotice?: boolean; } -export default function SearchBar({ onGetKeyword, value }: SearchBarProps) { - const [inputValue, setInputValue] = useState(''); +export default function SearchBar({ + onGetKeyword, + value, + isNotice, +}: SearchBarProps) { + const [keyword, setKeyword] = useState(value); const { isOpen, message, handleModalOpen, handleModalClose } = useModal(); const [searchParams, setSearchParams] = useSearchParams(); - const location = useLocation(); - const keyword = inputValue ? inputValue : value; const handleKeyword = (inputValue: string) => { const newSearchParams = new URLSearchParams(searchParams); @@ -32,22 +35,21 @@ export default function SearchBar({ onGetKeyword, value }: SearchBarProps) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (inputValue.trim() === '') { + if (keyword.trim() === '') { return handleModalOpen(MODAL_MESSAGE_CUSTOMER_SERVICE.noKeyword); } else { - onGetKeyword(inputValue); - handleKeyword(inputValue); + onGetKeyword(keyword); + handleKeyword(keyword); return; } }; const handleChangeKeyword = (e: React.ChangeEvent) => { - const value = e.target.value; - setInputValue(value); + setKeyword(e.target.value); }; const handleClickSearchDefault = () => { - setInputValue(''); + setKeyword(''); onGetKeyword(''); handleKeyword(''); }; @@ -73,9 +75,11 @@ export default function SearchBar({ onGetKeyword, value }: SearchBarProps) { 검색 - - 작성하기 - + {isNotice && ( + + 작성하기 + + )} {message} diff --git a/src/constants/admin/adminModal.ts b/src/constants/admin/adminModal.ts index a4f8082f..e1c88bf8 100644 --- a/src/constants/admin/adminModal.ts +++ b/src/constants/admin/adminModal.ts @@ -4,4 +4,5 @@ export const ADMIN_MODAL_MESSAGE = { writeDeleteSuccess: '삭제되었습니다.', writeDeleteFail: '삭제가 실패하였습니다.', writeError: '알수없는 에러가 발생했습니다.', + NO_RESULT: '결과가 존재하지 않습니다.', }; diff --git a/src/hooks/admin/useGetAllUsers.ts b/src/hooks/admin/useGetAllUsers.ts index 769cfea7..243fffea 100644 --- a/src/hooks/admin/useGetAllUsers.ts +++ b/src/hooks/admin/useGetAllUsers.ts @@ -1,15 +1,16 @@ import { useQuery } from '@tanstack/react-query'; import { UserData } from '../queries/user/keys'; import { getAllUsers } from '../../api/auth.api'; +import type { SearchType } from '../../models/search'; -export const useGetAllUsers = () => { +export const useGetAllUsers = (searchUnit: SearchType) => { const { data: allUserData, isLoading, isFetching, } = useQuery({ - queryKey: [UserData.allUser], - queryFn: () => getAllUsers(), + queryKey: [UserData.allUser, searchUnit.keyword, searchUnit.page], + queryFn: () => getAllUsers(searchUnit), }); return { allUserData, isLoading, isFetching }; diff --git a/src/hooks/admin/useGetAllUsersPreview.ts b/src/hooks/admin/useGetAllUsersPreview.ts new file mode 100644 index 00000000..9a90d386 --- /dev/null +++ b/src/hooks/admin/useGetAllUsersPreview.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { UserData } from '../queries/user/keys'; +import { getAllUsersPreview } from '../../api/auth.api'; + +export const useGetAllUsersPreview = () => { + const { + data: allUserData, + isLoading, + isFetching, + } = useQuery({ + queryKey: [UserData.allUserPreview], + queryFn: () => getAllUsersPreview(), + select: (allUsers) => allUsers.slice(0, 5), + }); + + return { allUserData, isLoading, isFetching }; +}; diff --git a/src/hooks/admin/useSearchBar.ts b/src/hooks/admin/useSearchBar.ts new file mode 100644 index 00000000..84ed2f6d --- /dev/null +++ b/src/hooks/admin/useSearchBar.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import type { SearchType } from '../../models/search'; + +const useSearchBar = () => { + const [searchUnit, setSearchUnit] = useState({ + keyword: '', + page: 1, + }); + const [value, setValue] = useState(''); + const [searchParams] = useSearchParams(); + + useEffect(() => { + const searchKeyword = searchParams.get('keyword'); + + if (searchKeyword) { + setSearchUnit((prev) => ({ ...prev, keyword: searchKeyword })); + setValue((prev) => (searchKeyword ? searchKeyword : prev)); + } else { + setSearchUnit((prev) => ({ ...prev, keyword: '' })); + setValue(''); + } + }, [searchParams]); + + const handleGetKeyword = (keyword: string) => { + setSearchUnit((prev) => ({ ...prev, keyword, page: 1 })); + setValue(keyword); + }; + + const handleChangePagination = (page: number) => { + setSearchUnit((prev) => ({ ...prev, page })); + }; + + return { searchUnit, value, handleGetKeyword, handleChangePagination }; +}; + +export default useSearchBar; diff --git a/src/hooks/queries/user/keys.ts b/src/hooks/queries/user/keys.ts index ae2f244a..1c50aa6b 100644 --- a/src/hooks/queries/user/keys.ts +++ b/src/hooks/queries/user/keys.ts @@ -65,4 +65,5 @@ export const ReportData = { export const UserData = { allUser: ['AllUser'], + allUserPreview: ['AllUserPreview'], }; diff --git a/src/mock/browser.ts b/src/mock/browser.ts index 2c4da947..caac0f09 100644 --- a/src/mock/browser.ts +++ b/src/mock/browser.ts @@ -18,6 +18,7 @@ import { } from './mypage'; import { userAll, + userAllPreview, userPageAppliedProjectList, userPageProfile, } from './userpage'; @@ -57,6 +58,7 @@ export const handlers = [ createProject, reportsAll, userAll, + userAllPreview, ]; export const worker = setupWorker(...handlers); diff --git a/src/mock/mockAllUser.json b/src/mock/mockAllUser.json new file mode 100644 index 00000000..0c7a4aa6 --- /dev/null +++ b/src/mock/mockAllUser.json @@ -0,0 +1,669 @@ +{ + "success": true, + "message": "성공", + "data": { + "allUsers": [ + { + "id": 1, + "email": "user1@example.com", + "user": { + "id": 101, + "nickname": "alpha", + "img": "https://example.com/avatars/alpha.png" + }, + "userState": "접속 중", + "createdAt": "2023-12-01", + "skill": [ + { + "id": 11, + "name": "JavaScript", + "img": "https://example.com/skills/javascript.png", + "createdAt": "2023-01-10" + }, + { + "id": 12, + "name": "React", + "img": "https://example.com/skills/react.png", + "createdAt": "2023-02-15" + } + ], + "position": [ + { + "id": 21, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 0 + }, + { + "id": 2, + "email": "user2@example.com", + "user": { + "id": 102, + "nickname": "bravo", + "img": "https://example.com/avatars/bravo.png" + }, + "userState": "오프라인", + "createdAt": "2024-01-15", + "skill": [ + { + "id": 13, + "name": "TypeScript", + "img": "https://example.com/skills/typescript.png", + "createdAt": "2023-04-12" + }, + { + "id": 14, + "name": "HTML", + "img": "https://example.com/skills/html.png", + "createdAt": "2023-05-01" + } + ], + "position": [ + { + "id": 22, + "name": "Fullstack", + "createdAt": "2023-06-20" + } + ], + "reportedCount": 1 + }, + { + "id": 3, + "email": "user3@example.com", + "user": { + "id": 103, + "nickname": "charlie", + "img": "https://example.com/avatars/charlie.png" + }, + "userState": "정지", + "createdAt": "2022-11-20", + "skill": [ + { + "id": 15, + "name": "CSS", + "img": "https://example.com/skills/css.png", + "createdAt": "2023-03-22" + }, + { + "id": 16, + "name": "Node.js", + "img": "https://example.com/skills/nodejs.png", + "createdAt": "2023-04-18" + } + ], + "position": [ + { + "id": 23, + "name": "Backend", + "createdAt": "2023-05-30" + } + ], + "reportedCount": 3 + }, + { + "id": 4, + "email": "user4@example.com", + "user": { + "id": 104, + "nickname": "delta", + "img": "https://example.com/avatars/delta.png" + }, + "userState": "접속 중", + "createdAt": "2024-03-03", + "skill": [ + { + "id": 17, + "name": "Python", + "img": "https://example.com/skills/python.png", + "createdAt": "2023-06-10" + }, + { + "id": 18, + "name": "Docker", + "img": "https://example.com/skills/docker.png", + "createdAt": "2023-07-25" + } + ], + "position": [ + { + "id": 24, + "name": "DevOps", + "createdAt": "2023-08-05" + } + ], + "reportedCount": 0 + }, + { + "id": 5, + "email": "user5@example.com", + "user": { + "id": 105, + "nickname": "echo", + "img": "https://example.com/avatars/echo.png" + }, + "userState": "오프라인", + "createdAt": "2024-02-10", + "skill": [ + { + "id": 19, + "name": "AWS", + "img": "https://example.com/skills/aws.png", + "createdAt": "2023-09-02" + }, + { + "id": 20, + "name": "HTML", + "img": "https://example.com/skills/html.png", + "createdAt": "2023-05-01" + } + ], + "position": [ + { + "id": 25, + "name": "Fullstack", + "createdAt": "2023-10-12" + } + ], + "reportedCount": 2 + }, + { + "id": 6, + "email": "user6@example.com", + "user": { + "id": 106, + "nickname": "foxtrot", + "img": "https://example.com/avatars/foxtrot.png" + }, + "userState": "정지", + "createdAt": "2023-08-15", + "skill": [ + { + "id": 21, + "name": "JavaScript", + "img": "https://example.com/skills/javascript.png", + "createdAt": "2023-01-10" + }, + { + "id": 22, + "name": "React", + "img": "https://example.com/skills/react.png", + "createdAt": "2023-02-15" + } + ], + "position": [ + { + "id": 26, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 0 + }, + { + "id": 7, + "email": "user7@example.com", + "user": { + "id": 107, + "nickname": "golf", + "img": "https://example.com/avatars/golf.png" + }, + "userState": "접속 중", + "createdAt": "2024-05-01", + "skill": [ + { + "id": 23, + "name": "TypeScript", + "img": "https://example.com/skills/typescript.png", + "createdAt": "2023-04-12" + }, + { + "id": 24, + "name": "CSS", + "img": "https://example.com/skills/css.png", + "createdAt": "2023-03-22" + } + ], + "position": [ + { + "id": 27, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 4 + }, + { + "id": 8, + "email": "user8@example.com", + "user": { + "id": 108, + "nickname": "hotel", + "img": "https://example.com/avatars/hotel.png" + }, + "userState": "오프라인", + "createdAt": "2023-09-10", + "skill": [ + { + "id": 25, + "name": "Node.js", + "img": "https://example.com/skills/nodejs.png", + "createdAt": "2023-04-18" + }, + { + "id": 26, + "name": "Docker", + "img": "https://example.com/skills/docker.png", + "createdAt": "2023-07-25" + } + ], + "position": [ + { + "id": 28, + "name": "Backend", + "createdAt": "2023-05-30" + } + ], + "reportedCount": 5 + }, + { + "id": 9, + "email": "user9@example.com", + "user": { + "id": 109, + "nickname": "india", + "img": "https://example.com/avatars/india.png" + }, + "userState": "정지", + "createdAt": "2024-04-20", + "skill": [ + { + "id": 27, + "name": "Python", + "img": "https://example.com/skills/python.png", + "createdAt": "2023-06-10" + }, + { + "id": 28, + "name": "AWS", + "img": "https://example.com/skills/aws.png", + "createdAt": "2023-09-02" + } + ], + "position": [ + { + "id": 29, + "name": "DevOps", + "createdAt": "2023-08-05" + } + ], + "reportedCount": 0 + }, + { + "id": 10, + "email": "user10@example.com", + "user": { + "id": 110, + "nickname": "juliet", + "img": "https://example.com/avatars/juliet.png" + }, + "userState": "접속 중", + "createdAt": "2022-12-25", + "skill": [ + { + "id": 29, + "name": "HTML", + "img": "https://example.com/skills/html.png", + "createdAt": "2023-05-01" + }, + { + "id": 30, + "name": "CSS", + "img": "https://example.com/skills/css.png", + "createdAt": "2023-03-22" + } + ], + "position": [ + { + "id": 30, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 2 + }, + { + "id": 11, + "email": "user11@example.com", + "user": { + "id": 111, + "nickname": "kilo", + "img": "https://example.com/avatars/kilo.png" + }, + "userState": "오프라인", + "createdAt": "2024-06-02", + "skill": [ + { + "id": 31, + "name": "JavaScript", + "img": "https://example.com/skills/javascript.png", + "createdAt": "2023-01-10" + }, + { + "id": 32, + "name": "React", + "img": "https://example.com/skills/react.png", + "createdAt": "2023-02-15" + } + ], + "position": [ + { + "id": 31, + "name": "Fullstack", + "createdAt": "2023-06-20" + } + ], + "reportedCount": 1 + }, + { + "id": 12, + "email": "user12@example.com", + "user": { + "id": 112, + "nickname": "lima", + "img": "https://example.com/avatars/lima.png" + }, + "userState": "정지", + "createdAt": "2023-07-18", + "skill": [ + { + "id": 33, + "name": "TypeScript", + "img": "https://example.com/skills/typescript.png", + "createdAt": "2023-04-12" + }, + { + "id": 34, + "name": "Node.js", + "img": "https://example.com/skills/nodejs.png", + "createdAt": "2023-04-18" + } + ], + "position": [ + { + "id": 32, + "name": "Backend", + "createdAt": "2023-05-30" + } + ], + "reportedCount": 0 + }, + { + "id": 13, + "email": "user13@example.com", + "user": { + "id": 113, + "nickname": "mike", + "img": "https://example.com/avatars/mike.png" + }, + "userState": "접속 중", + "createdAt": "2024-02-28", + "skill": [ + { + "id": 35, + "name": "Docker", + "img": "https://example.com/skills/docker.png", + "createdAt": "2023-07-25" + }, + { + "id": 36, + "name": "AWS", + "img": "https://example.com/skills/aws.png", + "createdAt": "2023-09-02" + } + ], + "position": [ + { + "id": 33, + "name": "DevOps", + "createdAt": "2023-08-05" + } + ], + "reportedCount": 4 + }, + { + "id": 14, + "email": "user14@example.com", + "user": { + "id": 114, + "nickname": "november", + "img": "https://example.com/avatars/november.png" + }, + "userState": "오프라인", + "createdAt": "2023-10-05", + "skill": [ + { + "id": 37, + "name": "HTML", + "img": "https://example.com/skills/html.png", + "createdAt": "2023-05-01" + }, + { + "id": 38, + "name": "CSS", + "img": "https://example.com/skills/css.png", + "createdAt": "2023-03-22" + } + ], + "position": [ + { + "id": 34, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 1 + }, + { + "id": 15, + "email": "user15@example.com", + "user": { + "id": 115, + "nickname": "oscar", + "img": "https://example.com/avatars/oscar.png" + }, + "userState": "정지", + "createdAt": "2024-03-22", + "skill": [ + { + "id": 39, + "name": "JavaScript", + "img": "https://example.com/skills/javascript.png", + "createdAt": "2023-01-10" + }, + { + "id": 40, + "name": "React", + "img": "https://example.com/skills/react.png", + "createdAt": "2023-02-15" + } + ], + "position": [ + { + "id": 35, + "name": "Fullstack", + "createdAt": "2023-06-20" + } + ], + "reportedCount": 2 + }, + { + "id": 16, + "email": "user16@example.com", + "user": { + "id": 116, + "nickname": "papa", + "img": "https://example.com/avatars/papa.png" + }, + "userState": "접속 중", + "createdAt": "2023-11-11", + "skill": [ + { + "id": 41, + "name": "TypeScript", + "img": "https://example.com/skills/typescript.png", + "createdAt": "2023-04-12" + }, + { + "id": 42, + "name": "Node.js", + "img": "https://example.com/skills/nodejs.png", + "createdAt": "2023-04-18" + } + ], + "position": [ + { + "id": 36, + "name": "Backend", + "createdAt": "2023-05-30" + } + ], + "reportedCount": 0 + }, + { + "id": 17, + "email": "user17@example.com", + "user": { + "id": 117, + "nickname": "quebec", + "img": "https://example.com/avatars/quebec.png" + }, + "userState": "오프라인", + "createdAt": "2023-08-30", + "skill": [ + { + "id": 43, + "name": "Docker", + "img": "https://example.com/skills/docker.png", + "createdAt": "2023-07-25" + }, + { + "id": 44, + "name": "AWS", + "img": "https://example.com/skills/aws.png", + "createdAt": "2023-09-02" + } + ], + "position": [ + { + "id": 37, + "name": "DevOps", + "createdAt": "2023-08-05" + } + ], + "reportedCount": 3 + }, + { + "id": 18, + "email": "user18@example.com", + "user": { + "id": 118, + "nickname": "romeo", + "img": "https://example.com/avatars/romeo.png" + }, + "userState": "정지", + "createdAt": "2022-10-10", + "skill": [ + { + "id": 45, + "name": "JavaScript", + "img": "https://example.com/skills/javascript.png", + "createdAt": "2023-01-10" + }, + { + "id": 46, + "name": "React", + "img": "https://example.com/skills/react.png", + "createdAt": "2023-02-15" + } + ], + "position": [ + { + "id": 38, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 5 + }, + { + "id": 19, + "email": "user19@example.com", + "user": { + "id": 119, + "nickname": "sierra", + "img": "https://example.com/avatars/sierra.png" + }, + "userState": "접속 중", + "createdAt": "2023-06-14", + "skill": [ + { + "id": 47, + "name": "Python", + "img": "https://example.com/skills/python.png", + "createdAt": "2023-06-10" + }, + { + "id": 48, + "name": "AWS", + "img": "https://example.com/skills/aws.png", + "createdAt": "2023-09-02" + } + ], + "position": [ + { + "id": 39, + "name": "DevOps", + "createdAt": "2023-08-05" + } + ], + "reportedCount": 0 + }, + { + "id": 20, + "email": "user20@example.com", + "user": { + "id": 120, + "nickname": "tango", + "img": "https://example.com/avatars/tango.png" + }, + "userState": "오프라인", + "createdAt": "2024-05-30", + "skill": [ + { + "id": 49, + "name": "HTML", + "img": "https://example.com/skills/html.png", + "createdAt": "2023-05-01" + }, + { + "id": 50, + "name": "CSS", + "img": "https://example.com/skills/css.png", + "createdAt": "2023-03-22" + } + ], + "position": [ + { + "id": 40, + "name": "Frontend", + "createdAt": "2023-03-05" + } + ], + "reportedCount": 1 + } + ], + "totalPages": 2 + } +} diff --git a/src/mock/userpage.ts b/src/mock/userpage.ts index 21090fff..94b9d8a9 100644 --- a/src/mock/userpage.ts +++ b/src/mock/userpage.ts @@ -2,6 +2,7 @@ import { http, HttpResponse } from 'msw'; import mockUserpageProfileData from './mockUserpageProfileData.json'; import mockUserpageAppliedProjectListData from './mockUserpageAppliedProjectListData.json'; import mockUsers from './mockUsers.json'; +import mockAllUsers from './mockAllUser.json'; export const userPageProfile = http.get( `${import.meta.env.VITE_APP_API_BASE_URL}user/:id`, @@ -21,9 +22,16 @@ export const userPageAppliedProjectList = http.get( } ); +export const userAllPreview = http.get( + `${import.meta.env.VITE_APP_API_BASE_URL}users/preview`, + () => { + return HttpResponse.json(mockUsers, { status: 200 }); + } +); + export const userAll = http.get( `${import.meta.env.VITE_APP_API_BASE_URL}users`, () => { - return HttpResponse.json(mockUsers, { status: 200 }); + return HttpResponse.json(mockAllUsers, { status: 200 }); } ); diff --git a/src/models/auth.ts b/src/models/auth.ts index f663c1aa..6069ffad 100644 --- a/src/models/auth.ts +++ b/src/models/auth.ts @@ -1,5 +1,18 @@ +import { PositionTag, SkillTag } from './tags'; import { type ApiCommonType, type User } from './apiCommon'; +export enum UserState { + ONLINE = 'ONLINE', + OFFLINE = 'OFFLINE', + SUSPENDED = 'SUSPENDED', +} + +export const USER_STATE_LABELS = { + [UserState.ONLINE]: '접속 중', + [UserState.OFFLINE]: '오프라인', + [UserState.SUSPENDED]: '정지', +} as const; + export interface VerifyEmail { email: string; code: string; @@ -27,13 +40,28 @@ export interface ApiOauth extends ApiCommonType { user: UserData; } -export interface ApiGetAllUsers extends ApiCommonType { - data: AllUser[]; +export interface ApiGetAllUsersPreview extends ApiCommonType { + data: AllUserPreview[]; } - -export interface AllUser { +export interface AllUserPreview { id: number; email: string; user: User; + userState: UserState; createdAt: string; } + +export interface ApiGetAllUsers extends ApiCommonType { + data: AllUserList; +} + +export interface AllUser extends AllUserPreview { + skill: SkillTag[]; + position: PositionTag[]; + reportedCount: number; +} + +export interface AllUserList { + allUsers: AllUser[]; + totalPages: number; +} diff --git a/src/models/report.ts b/src/models/report.ts index 0c24868e..783b3fbb 100644 --- a/src/models/report.ts +++ b/src/models/report.ts @@ -13,9 +13,9 @@ export interface ApiAllReports extends ApiCommonType { export interface AllReports { id: number; - imposedCount: number; + reportedCount: number; category: string; user: User; - isImposed: boolean; + isDone: boolean; createdAt: string; } diff --git a/src/models/search.ts b/src/models/search.ts new file mode 100644 index 00000000..db97ff2e --- /dev/null +++ b/src/models/search.ts @@ -0,0 +1,4 @@ +export interface SearchType { + keyword: string; + page: number; +} diff --git a/src/pages/admin/adminAllUser/AdminAllUser.styled.ts b/src/pages/admin/adminAllUser/AdminAllUser.styled.ts new file mode 100644 index 00000000..74b4443f --- /dev/null +++ b/src/pages/admin/adminAllUser/AdminAllUser.styled.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +export const Container = styled.div``; + +export const SearchBar = styled.div` + margin-top: 20px; +`; + +export const ScrollArea = styled.div` + height: calc(100vh - 200px); + overflow-y: auto; + padding-bottom: 100px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background-color: #ccc; + border-radius: 4px; + } +`; + +export const UserContainer = styled.div` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 10px; + padding: 20px; +`; diff --git a/src/pages/admin/adminAllUser/AdminAllUser.tsx b/src/pages/admin/adminAllUser/AdminAllUser.tsx index c2e5ee24..259a433a 100644 --- a/src/pages/admin/adminAllUser/AdminAllUser.tsx +++ b/src/pages/admin/adminAllUser/AdminAllUser.tsx @@ -1,3 +1,55 @@ -export default function AdminAllUser() { - return
; -} +import * as S from './AdminAllUser.styled'; +import AdminTitle from '../../../components/common/admin/title/AdminTitle'; +import { useGetAllUsers } from '../../../hooks/admin/useGetAllUsers'; +import LoadingSpinner from '../../../components/common/loadingSpinner/LoadingSpinner'; +import UserCard from '../../../components/admin/userCard/UserCard'; +import ScrollPreventor from '../../../components/common/modal/ScrollPreventor'; +import SearchBar from '../../../components/common/admin/searchBar/SearchBar'; +import Pagination from '../../../components/common/pagination/Pagination'; +import useSearchBar from '../../../hooks/admin/useSearchBar'; +import { ADMIN_MODAL_MESSAGE } from '../../../constants/admin/adminModal'; + +const AdminAllUser = () => { + const { searchUnit, value, handleGetKeyword, handleChangePagination } = + useSearchBar(); + const { allUserData, isLoading, isFetching } = useGetAllUsers(searchUnit); + + if (isLoading || isFetching) { + return ; + } + + if (!allUserData || allUserData.allUsers.length === 0) { + return {ADMIN_MODAL_MESSAGE.NO_RESULT}; + } + + return ( + + + + + + + + + + + {allUserData.allUsers?.map((userData) => ( + + ))} + + + + + + ); +}; + +export default AdminAllUser; diff --git a/src/style/theme.ts b/src/style/theme.ts index 32ac3e58..73c0e363 100644 --- a/src/style/theme.ts +++ b/src/style/theme.ts @@ -10,7 +10,8 @@ export type ColorKey = | 'green' | 'navy' | 'lightnavy' - | 'warn'; + | 'warn' + | 'blue'; export type HeadingSize = | 'large' @@ -78,6 +79,7 @@ export const defaultTheme: Theme = { navy: '#213555', lightnavy: '#92bbf0', warn: '#EC1E4F', + blue: '#2560E8', }, heading: { large: { fontSize: '1.75rem', tabletFontSize: '1.5rem' },