diff --git a/src/api/admin/report.api.ts b/src/api/admin/report.api.ts new file mode 100644 index 00000000..682c7658 --- /dev/null +++ b/src/api/admin/report.api.ts @@ -0,0 +1,45 @@ +import type { ApiReportDetail } from '../../models/admin/userDetail/reportDetail'; +import type { ApiAllReports } from '../../models/report'; +import type { SearchType } from '../../models/search'; +import { httpClient } from '../http.api'; + +export const postDeleteReport = async (id: number) => { + try { + await httpClient.delete(`/report/${id}`); + } catch (e) { + console.error(e); + throw e; + } +}; + +export const getReportDetail = async (reportId: number) => { + try { + const response = await httpClient.get( + `/report/${reportId}` + ); + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const getAllReports = async (params: SearchType) => { + try { + const response = await httpClient.get(`/report`, { params }); + return response.data; + } catch (e) { + console.error(e); + throw e; + } +}; + +export const getAllReportsPreview = async () => { + try { + const response = await httpClient.get(`/report`); + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; diff --git a/src/api/admin/user.api.ts b/src/api/admin/user.api.ts index 20bbb081..10cb2db3 100644 --- a/src/api/admin/user.api.ts +++ b/src/api/admin/user.api.ts @@ -2,7 +2,7 @@ import type { ApiUserApplicantsData } from '../../models/admin/userDetail/userDe import type { ApiUserProjectDataResponse } from '../../models/admin/userDetail/userProjectData'; import { httpClient } from '../http.api'; -export const postBanUser = async (id: string) => { +export const postBanUser = async (id: number) => { try { await httpClient.post(`/ban/${id}`); } catch (e) { @@ -11,7 +11,7 @@ export const postBanUser = async (id: string) => { } }; -export const postWarningUser = async (id: string) => { +export const postWarningUser = async (id: number) => { try { await httpClient.post(`/warning/${id}`); } catch (e) { diff --git a/src/api/customerService.api.ts b/src/api/customerService.api.ts index 19483fb9..56b1ac1f 100644 --- a/src/api/customerService.api.ts +++ b/src/api/customerService.api.ts @@ -29,6 +29,17 @@ export const getNotice = async (params: NoticeSearch) => { } }; +export const getNoticePreview = async () => { + try { + const response = await httpClient.get(`/notice`); + + return response.data.data; + } catch (e) { + console.error(e); + throw e; + } +}; + export const getNoticeDetail = async (id: string) => { try { const response = await httpClient.get(`/notice/${id}`); diff --git a/src/api/report.api.ts b/src/api/report.api.ts index 0b0d24dd..111ef186 100644 --- a/src/api/report.api.ts +++ b/src/api/report.api.ts @@ -1,4 +1,4 @@ -import { type ApiAllReports, type ApiPostContent } from '../models/report'; +import type { ApiPostContent } from '../models/report'; import { httpClient } from './http.api'; export const postReport = async (formData: ApiPostContent) => { @@ -13,13 +13,3 @@ export const postReport = async (formData: ApiPostContent) => { throw error; } }; - -export const getAllReports = async () => { - try { - const response = await httpClient.get(`/reports`); - return response.data.data; - } catch (e) { - console.error(e); - throw e; - } -}; diff --git a/src/components/admin/adminUserReport/AdminReportDetail.styled.ts b/src/components/admin/adminUserReport/AdminReportDetail.styled.ts new file mode 100644 index 00000000..09bb7998 --- /dev/null +++ b/src/components/admin/adminUserReport/AdminReportDetail.styled.ts @@ -0,0 +1,144 @@ +import styled from 'styled-components'; +import { ScrollArea } from '../../common/header/Notification/Notification.styled'; +import { SpinnerContainer } from '../../user/mypage/Spinner.styled'; +import { Link } from 'react-router-dom'; +import Button from '../../common/Button/Button'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + margin: 8rem 0 auto; +`; + +export const Spinner = styled(SpinnerContainer)``; + +export const Wrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +export const HeaderContainer = styled.div` + width: 80%; + height: 100px; + display: flex; + justify-content: space-around; + align-items: center; + border: 1px solid ${({ theme }) => theme.color.border}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; +`; + +export const UserContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; +`; + +export const NickName = styled.p` + font-size: 0.8rem; + opacity: 40%; +`; + +export const Category = styled.p``; + +export const Arrow = styled.div` + width: 50px; + height: 50px; + position: relative; + margin-bottom: 30px; + + &::before, + &::after { + content: ''; + position: absolute; + } + + &::before { + width: 45%; + height: 45%; + top: 59%; + right: -10rem; + border: 1.5px solid ${({ theme }) => theme.color.primary}; + border-right: 0; + border-bottom: 0; + transform: rotate(130deg); + } + + &::after { + width: 24.3rem; + height: 1px; + top: 78%; + left: -11rem; + background-color: ${({ theme }) => theme.color.primary}; + transform-origin: 0 100%; + transform: rotate(0deg); + + } + } +`; + +export const Date = styled.p` + opacity: 60%; +`; + +export const ContentContainer = styled.div` + width: 80%; + height: 550px; + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid ${({ theme }) => theme.color.border}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + margin-top: 30px; + padding: 40px; +`; + +export const ContentHeader = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +`; + +export const Divider = styled.hr` + width: 400px; + height: 1px; + margin: 20px 0 20px 0; + opacity: 40%; +`; + +export const Scroll = styled(ScrollArea)` + padding: 20px; +`; + +export const Content = styled.p` + white-space: pre-wrap; +`; + +export const ConfirmContainer = styled.div` + width: 80%; + display: flex; + justify-content: space-between; + margin-top: 30px; +`; + +export const ConfirmArea = styled(Link)` + display: flex; + justify-content: flex-end; + align-items: center; +`; + +export const ButtonArea = styled.div` + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +`; + +export const WarningButton = styled(Button)` + background-color: ${({ theme }) => theme.color.red}; +`; + +export const BanButton = styled(Button)``; + +export const ConfirmButton = styled(Button)``; diff --git a/src/components/admin/adminUserReport/AdminReportDetail.tsx b/src/components/admin/adminUserReport/AdminReportDetail.tsx new file mode 100644 index 00000000..b20158ef --- /dev/null +++ b/src/components/admin/adminUserReport/AdminReportDetail.tsx @@ -0,0 +1,164 @@ +import React from 'react'; +import * as S from './AdminReportDetail.styled'; +import AdminTitle from '../../common/admin/title/AdminTitle'; +import Avatar from '../../common/avatar/Avatar'; +import defaultImg from '../../../assets/defaultImg.png'; +import ReportCheckBox from '../../common/reportCheckBox/ReportCheckBox'; +import ScrollPreventor from '../../common/modal/ScrollPreventor'; +import { Link, useParams } from 'react-router-dom'; +import { useGetReportDetail } from '../../../hooks/admin/useAdminReportDetail'; +import { Spinner } from '../../common/loadingSpinner/LoadingSpinner.styled'; +import { formatDate } from '../../../util/formatDate'; +import { useHandleUser } from '../../../hooks/admin/useHandleUser'; +import { useModal } from '../../../hooks/useModal'; +import Modal from '../../common/modal/Modal'; +import { REPORT_CATEGORY_LIST } from '../../../constants/admin/adminReportCategoryList'; + +export default function AdminReportDetail() { + const { id: reportId } = useParams(); + + const { reportDetailData, isLoading, isFetching } = useGetReportDetail( + Number(reportId) + ); + + const { isOpen, message, handleModalOpen, handleModalClose, handleConfirm } = + useModal(); + + const { handleClickBanButton, handleClickWarningButton } = useHandleUser({ + handleModalOpen, + handleConfirm, + }); + + if (!reportDetailData) { + return 신고 상세 정보를 찾을 수 없습니다.; + } + + if (isLoading || isFetching) { + return ( + + + + ); + } + + const linkUrl = ( + id: number, + location: 'USER' | 'PROJECT' | 'COMMENT' | 'RECOMMENT' + ) => { + if ( + location === 'PROJECT' || + location === 'COMMENT' || + location === 'RECOMMENT' + ) { + return `/project-detail/${id}`; + } else { + return `/user/${id}`; + } + }; + + return ( + + + + + + + + + {reportDetailData.reporter.nickname} + + + + 신고 + + + + + + {reportDetailData.reportedUser.nickname} + + + + + + + REPORT_CATEGORY_LIST[num - 1] + )} + /> + {formatDate(reportDetailData.reportedAt)} + + + + {reportDetailData.reason} + + + + + + 검토 하기 + + + + + handleClickWarningButton( + e, + reportDetailData.reportedUser.userId + ) + } + > + 경고 + + + handleClickBanButton(e, reportDetailData.reportedUser.userId) + } + > + 강퇴 + + + + + + + {message} + + + ); +} diff --git a/src/components/admin/previewComponent/noticePreview/NoticePreview.tsx b/src/components/admin/previewComponent/noticePreview/NoticePreview.tsx index e83fe5fe..fc6ca315 100644 --- a/src/components/admin/previewComponent/noticePreview/NoticePreview.tsx +++ b/src/components/admin/previewComponent/noticePreview/NoticePreview.tsx @@ -1,13 +1,10 @@ import * as S from './NoticePreview.styled'; -import { useGetNotice } from '../../../../hooks/user/useGetNotice'; import line from '../../../../assets/line.svg'; import { Spinner } from '../../../common/loadingSpinner/LoadingSpinner.styled'; +import { useGetNoticePreview } from '../../../../hooks/admin/useGetAllNoticePreview'; const NoticePreview = () => { - const { noticeData, isLoading, isFetching } = useGetNotice({ - keyword: '', - page: 1, - }); + const { noticeData, isLoading, isFetching } = useGetNoticePreview(); if (isLoading || isFetching) { return ( @@ -17,13 +14,13 @@ const NoticePreview = () => { ); } - if (!noticeData?.notices || noticeData.notices.length === 0) { + if (!noticeData) { return 공지사힝이 없습니다.; } return ( - {noticeData.notices.map((notice) => ( + {noticeData.map((notice) => ( {notice.title} diff --git a/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx b/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx index eda68d8d..05cf9466 100644 --- a/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx +++ b/src/components/admin/previewComponent/reportsPreview/ReportsPreview.tsx @@ -1,12 +1,12 @@ import * as S from './ReportsPreview.styled'; -import { useGetAllReports } from '../../../../hooks/admin/useGetAllReports'; import Avatar from '../../../common/avatar/Avatar'; import arrow_right from '../../../../assets/ArrowRight.svg'; import { ADMIN_ROUTE } from '../../../../constants/routes'; import { Spinner } from '../../../common/loadingSpinner/LoadingSpinner.styled'; +import { useGetAllReportsPreview } from '../../../../hooks/admin/useGetAllReportsPreview'; const ReportsPreview = () => { - const { allReportsData, isLoading, isFetching } = useGetAllReports(); + const { allReportsData, isLoading, isFetching } = useGetAllReportsPreview(); if (isLoading || isFetching) { return ( @@ -19,22 +19,22 @@ const ReportsPreview = () => { return ( {allReportsData?.map((report) => ( - - - + + + - {report.reportedCount} 번 + {report.warning} 번 {report.category} - {report.createdAt} + {report.reportedAt} | - - {report.isDone ? '검토 완료' : '검토 미완료'} + + {report.imposed ? '검토 완료' : '검토 미완료'} - + 신고 상세 보기 diff --git a/src/components/common/reportCheckBox/ReportCheckBox.styled.ts b/src/components/common/reportCheckBox/ReportCheckBox.styled.ts new file mode 100644 index 00000000..46d676f8 --- /dev/null +++ b/src/components/common/reportCheckBox/ReportCheckBox.styled.ts @@ -0,0 +1,31 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; +`; + +export const CheckRow = styled.div` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; +`; + +export const CheckItem = styled.div``; + +export const CheckInput = styled.input` + accent-color: red; + margin-right: 0.5rem; + cursor: pointer; +`; + +export const CheckContent = styled.label` + font-size: 0.9rem; +`; + +export const ErrorMessage = styled.p` + font-size: 11px; + color: ${({ theme }) => theme.color.red}; +`; diff --git a/src/components/common/reportCheckBox/ReportCheckBox.tsx b/src/components/common/reportCheckBox/ReportCheckBox.tsx new file mode 100644 index 00000000..626f5b46 --- /dev/null +++ b/src/components/common/reportCheckBox/ReportCheckBox.tsx @@ -0,0 +1,37 @@ +import * as S from './ReportCheckBox.styled'; +import { REASON_LIST } from '../../../constants/user/reportConstants'; + +interface ReportCheckBoxProps { + isNotExist?: boolean; + isAdmin?: boolean; + selectedCheckbox?: string[]; +} + +const ReportCheckBox = ({ + isNotExist = false, + isAdmin = false, + selectedCheckbox, +}: ReportCheckBoxProps) => { + return ( + + {REASON_LIST.map((reason, index) => ( + + + {reason} + + ))} + + {isNotExist && ( + 신고 사유를 하나 이상 선택해주세요. + )} + + ); +}; + +export default ReportCheckBox; diff --git a/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx b/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx index dd8ba511..a9c573e8 100644 --- a/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx +++ b/src/components/user/mypage/activityLog/inquiries/Inquiries.tsx @@ -29,7 +29,9 @@ export default function Inquiries() { ); - const myInquiriesData = userActivityData as MyInquiries[]; + const myInquiriesData = Array.isArray(userActivityData) + ? (userActivityData as MyInquiries[]) + : []; return ( diff --git a/src/components/user/reportComponent/ReportModal.tsx b/src/components/user/reportComponent/ReportModal.tsx index 537714d8..b91c222e 100644 --- a/src/components/user/reportComponent/ReportModal.tsx +++ b/src/components/user/reportComponent/ReportModal.tsx @@ -1,8 +1,8 @@ import { postReport } from '../../../api/report.api'; -import { reasons } from '../../../constants/user/reportConstants'; import Avatar from '../../common/avatar/Avatar'; import Button from '../../common/Button/Button'; import ScrollPreventor from '../../common/modal/ScrollPreventor'; +import ReportCheckBox from '../../common/reportCheckBox/ReportCheckBox'; import * as S from './ReportModal.styled'; import { useRef, useState } from 'react'; @@ -80,22 +80,7 @@ const ReportModal = ({ 신고 사유 - - {reasons.map((reason) => ( - - - - {reason} - - - ))} - - {isNotExist && ( - - 신고 사유를 하나 이상 선택해주세요. - - )} - + 상세 작성(생략 가능) { + const { + data: reportDetailData, + isLoading, + isFetching, + } = useQuery({ + queryKey: [ReportData.reportDetail, reportId], + queryFn: () => getReportDetail(reportId), + }); + + return { reportDetailData, isLoading, isFetching }; +}; diff --git a/src/hooks/admin/useAdminReports.ts b/src/hooks/admin/useAdminReports.ts new file mode 100644 index 00000000..9fac5622 --- /dev/null +++ b/src/hooks/admin/useAdminReports.ts @@ -0,0 +1,28 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; +import { ReportData } from '../queries/keys'; +import { ADMIN_MODAL_MESSAGE } from '../../constants/admin/adminModal'; +import { postDeleteReport } from '../../api/admin/report.api'; + +interface useAdminReportsProps { + handleModalOpen: (message: string) => void; +} + +export const useAdminReports = ({ handleModalOpen }: useAdminReportsProps) => { + const queryClient = useQueryClient(); + + const postDelete = useMutation({ + mutationFn: (id: number) => postDeleteReport(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [ReportData.allReports], + }); + queryClient.invalidateQueries({ + queryKey: [ReportData.allReportsPreview], + }); + handleModalOpen(ADMIN_MODAL_MESSAGE.banSuccess); + }, + }); + + return { postDelete }; +}; diff --git a/src/hooks/admin/useGetAllNoticePreview.ts b/src/hooks/admin/useGetAllNoticePreview.ts new file mode 100644 index 00000000..b4228ea1 --- /dev/null +++ b/src/hooks/admin/useGetAllNoticePreview.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; +import { CustomerService } from '../queries/keys'; +import { getNoticePreview } from '../../api/customerService.api'; + +export const useGetNoticePreview = () => { + const { + data: noticeData, + isLoading, + isFetching, + } = useQuery({ + queryKey: [CustomerService.noticePreview], + queryFn: () => getNoticePreview(), + select: (notice) => notice.notices.slice(0, 5), + staleTime: Infinity, + gcTime: Infinity, + }); + + return { noticeData, isLoading, isFetching }; +}; diff --git a/src/hooks/admin/useGetAllReports.ts b/src/hooks/admin/useGetAllReports.ts index 951f30b9..8e8c0a0d 100644 --- a/src/hooks/admin/useGetAllReports.ts +++ b/src/hooks/admin/useGetAllReports.ts @@ -1,16 +1,16 @@ import { useQuery } from '@tanstack/react-query'; import { ReportData } from '../queries/keys'; -import { getAllReports } from '../../api/report.api'; +import { getAllReports } from '../../api/admin/report.api'; +import type { SearchType } from '../../models/search'; -export const useGetAllReports = () => { +export const useGetAllReports = (searchUnit: SearchType) => { const { data: allReportsData, isLoading, isFetching, } = useQuery({ - queryKey: [ReportData.allReports], - queryFn: () => getAllReports(), - select: (allReports) => allReports.slice(0, 5), + queryKey: [ReportData.allReports, searchUnit.keyword, searchUnit.page], + queryFn: () => getAllReports(searchUnit), }); return { allReportsData, isLoading, isFetching }; diff --git a/src/hooks/admin/useGetAllReportsPreview.ts b/src/hooks/admin/useGetAllReportsPreview.ts new file mode 100644 index 00000000..8756a1ab --- /dev/null +++ b/src/hooks/admin/useGetAllReportsPreview.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import { ReportData } from '../queries/keys'; +import { getAllReportsPreview } from '../../api/admin/report.api'; + +export const useGetAllReportsPreview = () => { + const { + data: allReportsData, + isLoading, + isFetching, + } = useQuery({ + queryKey: [ReportData.allReportsPreview], + queryFn: () => getAllReportsPreview(), + select: (allReports) => allReports.slice(0, 5), + }); + + return { allReportsData, isLoading, isFetching }; +}; diff --git a/src/hooks/admin/useHandleUser.ts b/src/hooks/admin/useHandleUser.ts new file mode 100644 index 00000000..ba3e2dce --- /dev/null +++ b/src/hooks/admin/useHandleUser.ts @@ -0,0 +1,56 @@ +import { useAdminReports } from './useAdminReports'; +import { useHandleUserApi } from './useHandleUserApi'; + +interface useBanUserProps { + handleModalOpen: (message: string) => void; + handleConfirm: () => void; +} + +export const useHandleUser = ({ + handleModalOpen, + handleConfirm, +}: useBanUserProps) => { + const { postBan, postWarning } = useHandleUserApi({ + handleModalOpen, + handleConfirm, + }); + + const { postDelete } = useAdminReports({ handleModalOpen }); + + const handleClickBanButton = ( + e: React.MouseEvent, + userId: number + ) => { + e.preventDefault(); + e.stopPropagation(); + if (confirm('정말 강퇴 하시겠습니까?')) { + postBan.mutate(userId); + } + }; + + const handleClickWarningButton = ( + e: React.MouseEvent, + userId: number + ) => { + e.preventDefault(); + e.stopPropagation(); + if (confirm('정말 경고 하시겠습니까?')) { + postWarning.mutate(userId); + } + }; + + const handleClickDeleteButton = ( + e: React.MouseEvent, + id: number + ) => { + e.preventDefault(); + e.stopPropagation(); + postDelete.mutate(id); + }; + + return { + handleClickBanButton, + handleClickWarningButton, + handleClickDeleteButton, + }; +}; diff --git a/src/hooks/admin/useHandleUserApi.ts b/src/hooks/admin/useHandleUserApi.ts new file mode 100644 index 00000000..538c7c3f --- /dev/null +++ b/src/hooks/admin/useHandleUserApi.ts @@ -0,0 +1,56 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { postBanUser, postWarningUser } from '../../api/admin/user.api'; +import { ADMIN_MODAL_MESSAGE } from '../../constants/admin/adminModal'; +import { ReportData, UserData } from '../queries/keys'; +import { AxiosError } from 'axios'; + +interface useHandleUserApiProps { + handleModalOpen: (message: string) => void; + handleConfirm: () => void; +} + +export const useHandleUserApi = ({ + handleModalOpen, +}: useHandleUserApiProps) => { + const queryClient = useQueryClient(); + + const postBan = useMutation({ + mutationFn: (id: number) => postBanUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [UserData.allUser], + }); + queryClient.invalidateQueries({ + queryKey: [UserData.allUserPreview], + }); + queryClient.invalidateQueries({ + queryKey: [ReportData.allReports], + }); + queryClient.invalidateQueries({ + queryKey: [ReportData.allReportsPreview], + }); + handleModalOpen(ADMIN_MODAL_MESSAGE.warningSuccess); + }, + }); + + const postWarning = useMutation({ + mutationFn: (id: number) => postWarningUser(id), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [UserData.allUser], + }); + queryClient.invalidateQueries({ + queryKey: [UserData.allUserPreview], + }); + queryClient.invalidateQueries({ + queryKey: [ReportData.allReports], + }); + queryClient.invalidateQueries({ + queryKey: [ReportData.allReportsPreview], + }); + handleModalOpen(ADMIN_MODAL_MESSAGE.banSuccess); + }, + }); + + return { postBan, postWarning }; +}; diff --git a/src/hooks/queries/keys.ts b/src/hooks/queries/keys.ts index 8ec9d37d..ec42a7aa 100644 --- a/src/hooks/queries/keys.ts +++ b/src/hooks/queries/keys.ts @@ -56,6 +56,7 @@ export const Inquiries = { export const CustomerService = { faq: 'faq', notice: 'notice', + noticePreview: 'noticePreview', noticeDetail: 'noticeDetail', inquiryDetail: 'inquiryDetail', } as const; @@ -63,6 +64,7 @@ export const CustomerService = { export const ReportData = { allReports: ['AllReports'], allReportsPreview: ['AllReportsPreview'], + reportDetail: ['ReportDetail'], } as const; export const UserData = { diff --git a/src/models/admin/userDetail/reportDetail.ts b/src/models/admin/userDetail/reportDetail.ts new file mode 100644 index 00000000..d9ec8ccc --- /dev/null +++ b/src/models/admin/userDetail/reportDetail.ts @@ -0,0 +1,22 @@ +import type { ApiCommonType } from '../../apiCommon'; + +export interface UserData { + userId: number; + nickname: string; + profileImg: string; +} + +export interface ReportDetail { + reportId: number; + reporter: UserData; + reportedUser: UserData; + reportedAt: string; + reason: string; + category: number[]; + location: 'USER' | 'PROJECT' | 'COMMENT' | 'RECOMMENT'; + locationId: number; +} + +export interface ApiReportDetail extends ApiCommonType { + data: ReportDetail; +} diff --git a/src/models/alarm.ts b/src/models/alarm.ts index e8ea2397..58330006 100644 --- a/src/models/alarm.ts +++ b/src/models/alarm.ts @@ -1,4 +1,4 @@ -import { ApiCommonType } from './apiCommon'; +import type { ApiCommonType } from './apiCommon'; export interface ApiAlarmList extends ApiCommonType { data: Alarm[] | null; diff --git a/src/models/applicant.ts b/src/models/applicant.ts index 633c44c3..ce325fa3 100644 --- a/src/models/applicant.ts +++ b/src/models/applicant.ts @@ -1,5 +1,5 @@ -import { ApiCommonType } from './apiCommon'; -import { SkillTag } from './tags'; +import type { ApiCommonType } from './apiCommon'; +import type { SkillTag } from './tags'; export interface ApplicantInfo { userId: number; diff --git a/src/models/comment.ts b/src/models/comment.ts index ffe0aec5..4c57d0ff 100644 --- a/src/models/comment.ts +++ b/src/models/comment.ts @@ -1,4 +1,4 @@ -import { ApiCommonType, User } from './apiCommon'; +import type { ApiCommonType, User } from './apiCommon'; export interface GetCommentType extends ApiCommonType { data: CommentType[]; diff --git a/src/models/customerService.ts b/src/models/customerService.ts index c7ded677..17bda365 100644 --- a/src/models/customerService.ts +++ b/src/models/customerService.ts @@ -1,4 +1,4 @@ -import { ApiCommonType } from './apiCommon'; +import type { ApiCommonType } from './apiCommon'; export interface FAQ { id: number; diff --git a/src/models/manageMyProject.ts b/src/models/manageMyProject.ts index 249eea34..4e40bf78 100644 --- a/src/models/manageMyProject.ts +++ b/src/models/manageMyProject.ts @@ -1,5 +1,5 @@ -import { ApiCommonType } from './apiCommon'; -import { SkillTag } from './tags'; +import type { ApiCommonType } from './apiCommon'; +import type { SkillTag } from './tags'; export interface ManagedProject { id: number; diff --git a/src/models/projectDetail.ts b/src/models/projectDetail.ts index de1a594a..d3cc56c7 100644 --- a/src/models/projectDetail.ts +++ b/src/models/projectDetail.ts @@ -1,4 +1,4 @@ -import { ApiCommonType, User } from './apiCommon'; +import type { ApiCommonType, User } from './apiCommon'; import type { MethodTag, PositionTag, SkillTag } from './tags'; export interface ProjectSkillTagList { diff --git a/src/models/report.ts b/src/models/report.ts index 783b3fbb..df339e4d 100644 --- a/src/models/report.ts +++ b/src/models/report.ts @@ -1,4 +1,4 @@ -import { type ApiCommonType, type User } from './apiCommon'; +import type { ApiCommonType } from './apiCommon'; export interface ApiPostContent { reportTargetId: number; @@ -9,13 +9,16 @@ export interface ApiPostContent { export interface ApiAllReports extends ApiCommonType { data: AllReports[]; + totalPage: number; } export interface AllReports { - id: number; - reportedCount: number; - category: string; - user: User; - isDone: boolean; - createdAt: string; + reportId: number; + userId: number; + nickname: string; + profileImg: string; + warning: number; + category: number[]; + reportedAt: string; + imposed: boolean; } diff --git a/src/pages/admin/adminReports/AdminReports.styled.ts b/src/pages/admin/adminReports/AdminReports.styled.ts new file mode 100644 index 00000000..cb84af9f --- /dev/null +++ b/src/pages/admin/adminReports/AdminReports.styled.ts @@ -0,0 +1,103 @@ +import styled from 'styled-components'; +import Button from '../../../components/common/Button/Button'; +import { Link } from 'react-router-dom'; + +export const Container = styled.div` + display: flex; + flex-direction: column; +`; + +export const SpinnerWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + height: 100%; +`; + +export const SearchBarWrapper = styled.div` + width: 100%; + margin-top: 120px; +`; + +export const List = styled.div` + width: 90%; + height: 600px; + border: 1px solid ${({ theme }) => theme.color.border}; + border-radius: ${({ theme }) => theme.borderRadius.primary}; + padding: 45px; + margin: 30px auto 0; + overflow-y: auto; + padding-bottom: 30px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background-color: #ccc; + border-radius: 4px; + } +`; + +export const Item = styled(Link)` + display: flex; + align-items: center; + margin-bottom: 24px; +`; + +export const ProfileImg = styled.div` + width: 80px; + display: flex; + flex-direction: column; + align-items: center; + margin-right: 16px; + flex-shrink: 0; +`; + +export const NickName = styled.p` + ${({ theme }) => theme.heading.xsSmall}; + margin-top: 3px; + opacity: 50%; +`; + +export const ContentArea = styled.div` + display: flex; + flex: 1; + justify-content: flex-start; + align-items: flex-start; + margin-left: 35px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +export const Category = styled.p` + padding: 5px 10px; + ${({ theme }) => theme.heading.small}; + opacity: 50%; +`; + +export const Content = styled.p` + ${({ theme }) => theme.heading.semiSmall}; + font-weight: 500; +`; + +export const ButtonArea = styled.div` + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +`; + +export const WarningButton = styled(Button)` + background-color: ${({ theme }) => theme.color.red}; +`; + +export const BanButton = styled(Button)``; + +export const DeleteButton = styled.button` + border: 1px solid ${({ theme }) => theme.color.primary}; + border-radius: ${({ theme }) => theme.borderRadius.large}; + margin-left: 10px; +`; diff --git a/src/pages/admin/adminReports/AdminReports.tsx b/src/pages/admin/adminReports/AdminReports.tsx index 2d5490d9..3d8bc99a 100644 --- a/src/pages/admin/adminReports/AdminReports.tsx +++ b/src/pages/admin/adminReports/AdminReports.tsx @@ -1,3 +1,123 @@ +import SearchBar from '../../../components/common/admin/searchBar/SearchBar'; +import AdminTitle from '../../../components/common/admin/title/AdminTitle'; +import Avatar from '../../../components/common/avatar/Avatar'; +import useSearchBar from '../../../hooks/admin/useSearchBar'; +import * as S from './AdminReports.styled'; +import defaultImg from '../../../assets/defaultImg.png'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import Pagination from '../../../components/common/pagination/Pagination'; +import { useModal } from '../../../hooks/useModal'; +import Modal from '../../../components/common/modal/Modal'; +import { useGetAllReports } from '../../../hooks/admin/useGetAllReports'; +import { Spinner } from '../../../components/common/loadingSpinner/LoadingSpinner.styled'; +import { useHandleUser } from '../../../hooks/admin/useHandleUser'; +import { ADMIN_MODAL_MESSAGE } from '../../../constants/admin/adminModal'; +import { REPORT_CATEGORY_LIST } from '../../../constants/admin/adminReportCategoryList'; + export default function AdminReports() { - return
; + const { searchUnit, value, handleChangePagination, handleGetKeyword } = + useSearchBar(); + const { isOpen, message, handleModalOpen, handleModalClose, handleConfirm } = + useModal(); + + const { allReportsData, isLoading, isFetching } = + useGetAllReports(searchUnit); + + const { + handleClickBanButton, + handleClickWarningButton, + handleClickDeleteButton, + } = useHandleUser({ + handleModalOpen, + handleConfirm, + }); + + if (!allReportsData) { + return {ADMIN_MODAL_MESSAGE.NO_RESULT}; + } + + if (isLoading || isFetching) { + return ( + + + + ); + } + + return ( + <> + + + + + + + {allReportsData?.data.map((data) => ( + + + + {data.nickname} + + + {data.category.map((category, index) => ( + + " + {REPORT_CATEGORY_LIST[category - 1] || + '알 수 없는 카테고리'} + " + + ))} + + + handleClickWarningButton(e, data.userId)} + > + 경고 + + handleClickBanButton(e, data.userId)} + > + 강퇴 + + + handleClickDeleteButton(e, data.reportId)} + > + + + + + ))} + + + + + {message} + + + ); }