diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx index 59cbb3596..3b22a05c8 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -68,7 +68,8 @@ export default function AppLayout({ children }: AppLayoutProps) { {presentPath !== '/match' && presentPath !== '/manual' && - presentPath !== '/store' && ( + presentPath !== '/store' && + !presentPath.startsWith('/party') && (
diff --git a/components/admin/SideNav.tsx b/components/admin/SideNav.tsx index 3d01b48fb..1628c994d 100644 --- a/components/admin/SideNav.tsx +++ b/components/admin/SideNav.tsx @@ -9,7 +9,13 @@ import { } from 'react-icons/gr'; import { IoGameControllerOutline, IoReceiptOutline } from 'react-icons/io5'; import { MdOutlineMessage } from 'react-icons/md'; -import { TbCalendarTime, TbCoin, TbPaperBag, TbTrophy } from 'react-icons/tb'; +import { + TbCalendarTime, + TbCoin, + TbPaperBag, + TbTrophy, + TbNote, +} from 'react-icons/tb'; import SideNavContent from 'components/admin/SideNavContent'; import styles from 'styles/admin/SideNav.module.scss'; @@ -121,6 +127,14 @@ export default function SideNav() { > + + + +
); } diff --git a/components/admin/party/AdminCommentReport.tsx b/components/admin/party/AdminCommentReport.tsx new file mode 100644 index 000000000..797af5e55 --- /dev/null +++ b/components/admin/party/AdminCommentReport.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { PartyCommentReport, PartyCommentReportTable } from 'types/partyTypes'; +import { instanceInPartyManage } from 'utils/axios'; +import { toastState } from 'utils/recoil/toast'; +import { tableFormat } from 'constants/admin/table'; +import { + AdminEmptyItem, + AdminTableHead, +} from 'components/admin/common/AdminTable'; +import PageNation from 'components/Pagination'; +import styles from 'styles/admin/Party/AdminPartyCommon.module.scss'; + +const tableTitle: { [key: string]: string } = { + id: '번호', + reporterIntraId: '신고자 이름', + commentsId: '댓글 번호', + roomId: '방', + message: '메세지', + createdAt: '시간', +}; + +export default function AdminCommentReport() { + const [commentInfo, setCommentInfo] = useState({ + commentReportList: [], + totalPage: 0, + currentPage: 0, + }); + const [currentPage, setCurrentPage] = useState(1); + const setSnackBar = useSetRecoilState(toastState); + + useEffect(() => { + instanceInPartyManage + .get(`/reports/comments?page=${currentPage}&size=10`) + .then((res) => { + setCommentInfo({ + commentReportList: res.data.commentReportList, + totalPage: res.data.totalPage, + currentPage: currentPage, + }); + }) + .catch((error) => { + setSnackBar({ + toastName: 'GET request', + message: '댓글신고를 가져오는데 실패했습니다.', + severity: 'error', + clicked: true, + }); + }); + }, [currentPage]); + + return ( +
+
+ 댓글 신고리스트 +
+ + + + + {commentInfo.commentReportList && + commentInfo.commentReportList.length > 0 ? ( + commentInfo.commentReportList.map( + (report: PartyCommentReport, index: number) => ( + + {tableFormat['partyCommentReport'].columns.map( + (columnName) => { + return ( + + {report[ + columnName as keyof PartyCommentReport + ]?.toString()} + + ); + } + )} + + ) + ) + ) : ( + + )} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ ); +} diff --git a/components/admin/party/AdminPartyNoShow.tsx b/components/admin/party/AdminPartyNoShow.tsx new file mode 100644 index 000000000..474a2a940 --- /dev/null +++ b/components/admin/party/AdminPartyNoShow.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { PartyNoshowReport, PartyNoshowReportTable } from 'types/partyTypes'; +import { instanceInPartyManage } from 'utils/axios'; +import { toastState } from 'utils/recoil/toast'; +import { tableFormat } from 'constants/admin/table'; +import { + AdminEmptyItem, + AdminTableHead, +} from 'components/admin/common/AdminTable'; +import PageNation from 'components/Pagination'; +import styles from 'styles/admin/Party/AdminPartyCommon.module.scss'; + +const tableTitle: { [key: string]: string } = { + id: '번호', + reporterIntraId: '신고자 이름', + reporteeIntraId: '피신고자 이름', + roomId: '방', + message: '메세지', + createdAt: '시간', +}; + +export default function AdminPartyNoShow() { + const [noShowInfo, setNoShowInfo] = useState({ + noShowReportList: [], + totalPages: 0, + currentPage: 0, + }); + const [currentPage, setCurrentPage] = useState(1); + const setSnackBar = useSetRecoilState(toastState); + + useEffect(() => { + instanceInPartyManage + .get(`/reports/users?page=${currentPage}&size=10`) + .then((res) => { + setNoShowInfo({ + noShowReportList: res.data.noShowReportList, + totalPages: res.data.totalPage, + currentPage: currentPage, + }); + }) + .catch((error) => { + setSnackBar({ + toastName: 'GET request', + message: '댓글신고를 가져오는데 실패했습니다.', + severity: 'error', + clicked: true, + }); + }); + }, [currentPage]); + + return ( +
+
+ 노쇼 신고리스트 +
+ + + + + {noShowInfo.noShowReportList && + noShowInfo.noShowReportList.length > 0 ? ( + noShowInfo.noShowReportList.map( + (report: PartyNoshowReport, index: number) => ( + + {tableFormat['partyNoshowReport'].columns.map( + (columnName) => { + return ( + + {report[ + columnName as keyof PartyNoshowReport + ]?.toString()} + + ); + } + )} + + ) + ) + ) : ( + + )} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ ); +} diff --git a/components/admin/party/AdminPartyPenalty.tsx b/components/admin/party/AdminPartyPenalty.tsx new file mode 100644 index 000000000..e19fb588b --- /dev/null +++ b/components/admin/party/AdminPartyPenalty.tsx @@ -0,0 +1,133 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { PartyPenaltyAdmin, PartyPenaltyTable } from 'types/partyTypes'; +import { instanceInPartyManage } from 'utils/axios'; +import { modalState } from 'utils/recoil/modal'; +import { toastState } from 'utils/recoil/toast'; +import { tableFormat } from 'constants/admin/table'; +import { + AdminEmptyItem, + AdminTableHead, +} from 'components/admin/common/AdminTable'; +import PageNation from 'components/Pagination'; +import styles from 'styles/admin/Party/AdminPartyCommon.module.scss'; + +const tableTitle: { [key: string]: string } = { + id: '번호', + userIntraId: '유저', + penaltyType: '패널티 타입', + message: '내용', + startTime: '시작 시간', + penaltyTime: '패널티 시간', + edit: '수정', +}; + +export default function AdminCommentReport() { + const [penaltyInfo, setPenaltyInfo] = useState({ + penaltyList: [], + totalPage: 0, + currentPage: 0, + }); + const [currentPage, setCurrentPage] = useState(1); + const setModal = useSetRecoilState(modalState); + const setSnackBar = useSetRecoilState(toastState); + + useEffect(() => { + instanceInPartyManage + .get(`/penalties?page=${currentPage}&size=10`) + .then((res) => { + setPenaltyInfo({ + penaltyList: res.data.penaltyList, + totalPage: res.data.totalPage, + currentPage: currentPage, + }); + }) + .catch((error) => { + setSnackBar({ + toastName: 'GET request', + message: '댓글신고를 가져오는데 실패했습니다.', + severity: 'error', + clicked: true, + }); + }); + }, [currentPage]); + + const handleAddpenalty = () => { + setModal({ modalName: 'ADMIN-PARTY_ADMIN_PENALTY' }); + }; + + const handleEditpenalty = (partyPenalty?: PartyPenaltyAdmin) => { + setModal({ modalName: 'ADMIN-PARTY_ADMIN_PENALTY', partyPenalty }); + }; + + return ( +
+
+ 패널티 리스트 +
+ + + + + + {penaltyInfo.penaltyList && penaltyInfo.penaltyList.length > 0 ? ( + penaltyInfo.penaltyList.map( + (penalty: PartyPenaltyAdmin, index: number) => ( + + {tableFormat['partyPenaltyAdmin'].columns.map( + (columnName) => { + return ( + + {columnName === 'edit' ? ( + + ) : ( + penalty[ + columnName as keyof PartyPenaltyAdmin + ]?.toString() + )} + + ); + } + )} + + ) + ) + ) : ( + + )} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ ); +} diff --git a/components/admin/party/AdminPartyRoomReport.tsx b/components/admin/party/AdminPartyRoomReport.tsx new file mode 100644 index 000000000..067b6281b --- /dev/null +++ b/components/admin/party/AdminPartyRoomReport.tsx @@ -0,0 +1,103 @@ +import { useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { PartyRoomReport, PartyRoomReportTable } from 'types/partyTypes'; +import { instanceInPartyManage } from 'utils/axios'; +import { toastState } from 'utils/recoil/toast'; +import { tableFormat } from 'constants/admin/table'; +import { + AdminEmptyItem, + AdminTableHead, +} from 'components/admin/common/AdminTable'; +import PageNation from 'components/Pagination'; +import styles from 'styles/admin/Party/AdminPartyCommon.module.scss'; + +const tableTitle: { [key: string]: string } = { + id: '번호', + reporterIntraId: '신고자 이름', + reporteeIntraId: '피신고자 이름', + roomId: '방', + message: '메세지', + createdAt: '시간', +}; + +export default function AdminPartyRoomReport() { + const [roomInfo, setRoomInfo] = useState({ + roomReportList: [], + totalPage: 0, + currentPage: 0, + }); + const [currentPage, setCurrentPage] = useState(1); + + const setSnackBar = useSetRecoilState(toastState); + + useEffect(() => { + instanceInPartyManage + .get(`/reports/rooms?page=${currentPage}&size=10`) + .then((res) => { + setRoomInfo({ + roomReportList: res.data.roomReportList, + totalPage: res.data.totalPage, + currentPage: currentPage, + }); + }) + .catch((error) => { + setSnackBar({ + toastName: 'GET request', + message: '댓글신고를 가져오는데 실패했습니다.', + severity: 'error', + clicked: true, + }); + }); + }, [currentPage]); + + return ( +
+
+ 방 신고리스트 +
+ + + + + {roomInfo.roomReportList && roomInfo.roomReportList.length > 0 ? ( + roomInfo.roomReportList.map( + (report: PartyRoomReport, index: number) => ( + + {tableFormat['partyRoomReport'].columns.map( + (columnName) => ( + + {report[ + columnName as keyof PartyRoomReport + ]?.toString()} + + ) + )} + + ) + ) + ) : ( + + )} + +
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ ); +} diff --git a/components/admin/party/PartyCategory.tsx b/components/admin/party/PartyCategory.tsx new file mode 100644 index 000000000..f11bd9cd7 --- /dev/null +++ b/components/admin/party/PartyCategory.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { PartyCategory } from 'types/partyTypes'; +import { tableFormat } from 'constants/admin/table'; +import { AdminTableHead } from 'components/admin/common/AdminTable'; +import usePartyCategory from 'hooks/party/usePartyCategory'; +import styles from 'styles/admin/Party/AdminPartyCommon.module.scss'; + +const tableTitle: { [key: string]: string } = { + categoryId: '카테고리번호', + categoryName: '카테고리', + delete: '삭제', +}; + +export default function PartyCategories() { + const { categories, createCategory, deleteCategory } = usePartyCategory(); + const [newCategoryName, setNewCategoryName] = useState(''); + + const handleConfirm = () => { + if (newCategoryName.trim() !== '') { + createCategory(newCategoryName); + setNewCategoryName(''); + } + }; + + return ( +
+
+ 카테고리 관리 +
+ setNewCategoryName(e.target.value)} + /> + +
+
+ + + + + {categories.map((c) => ( + + {tableFormat['partyCategory'].columns.map((columnName) => { + return ( + + {columnName === 'delete' ? ( + + ) : ( + c[columnName as keyof PartyCategory]?.toString() + )} + + ); + })} + + ))} + +
+
+
+ ); +} diff --git a/components/admin/party/PartyNav.tsx b/components/admin/party/PartyNav.tsx new file mode 100644 index 000000000..3ed0a36b8 --- /dev/null +++ b/components/admin/party/PartyNav.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; +import styles from 'styles/party/PartyNav.module.scss'; +import PartyPenalty from './AdminPartyPenalty'; +import PartyCategory from './PartyCategory'; +import PartyReportNav from './PartyReportNav'; +import PartyTemplate from './PartyTemplate'; + +export default function PartyNav() { + const [navValue, setNavValue] = useState('penalty'); + return ( + <> +
+
    +
  • setNavValue('penalty')}> + 패널티 +
  • +
  • setNavValue('report')}> + 신고 관리 +
  • +
  • setNavValue('room')}> + 방 관리 +
  • +
  • setNavValue('template')}> + 템플릿 관리 +
  • +
  • setNavValue('category')}> + 카테고리 관리 +
  • +
+ + {navValue === 'report' && } + {navValue === 'template' && } + {navValue === 'category' && } + {navValue === 'penalty' && } +
+ + ); +} diff --git a/components/admin/party/PartyReportNav.tsx b/components/admin/party/PartyReportNav.tsx new file mode 100644 index 000000000..de6392674 --- /dev/null +++ b/components/admin/party/PartyReportNav.tsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import styles from 'styles/party/PartyNav.module.scss'; +import AdminCommentReport from './AdminCommentReport'; +import AdminPartyNoShowReport from './AdminPartyNoShow'; +import AdminPartyRoomReport from './AdminPartyRoomReport'; + +export default function PartyReportNav() { + const [navValue, setNavValue] = useState('noshow'); + return ( + <> +
+
    +
  • setNavValue('noshow')}> + 노쇼 관리 +
  • +
  • setNavValue('comment')}> + 댓글 관리 +
  • +
  • setNavValue('room')}> + 방 관리 +
  • +
+ {navValue === 'noshow' && } + {navValue === 'comment' && } + {navValue === 'room' && } +
+ + ); +} diff --git a/components/admin/party/PartyTemplate.tsx b/components/admin/party/PartyTemplate.tsx new file mode 100644 index 000000000..86f791db2 --- /dev/null +++ b/components/admin/party/PartyTemplate.tsx @@ -0,0 +1,106 @@ +import React, { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { PartyGameTemplate } from 'types/partyTypes'; +import { modalState } from 'utils/recoil/modal'; +import { tableFormat } from 'constants/admin/table'; +import { AdminTableHead } from 'components/admin/common/AdminTable'; +import { usePartyTemplate } from 'hooks/party/usePartyTemplate'; +import styles from 'styles/admin/Party/AdminPartyCommon.module.scss'; + +const tableTitle: { [key: string]: string } = { + gameTemplateId: '템플릿번호', + categoryId: '카테고리', + gameName: '템플릿이름', + maxGamePeople: '최대인원', + minGamePeople: '최소인원', + maxGameTime: '최대게임시간', + minGameTime: '최소게임시간', + genre: '장르', + difficulty: '난이도', + summary: '요약', + change: '수정', + delete: '삭제', +}; + +export default function PartyTemplate() { + const { templates, deleteTemplate } = usePartyTemplate(); + const setModal = useSetRecoilState(modalState); + + useEffect(() => { + console.log('Render'); + }, [templates]); + + const handleEditTemplate = (template?: PartyGameTemplate) => { + setModal({ modalName: 'ADMIN-PARTY_TEMPLATE', template }); + }; + + const handleAddTemplate = () => { + setModal({ modalName: 'ADMIN-PARTY_TEMPLATE' }); + }; + + const deleteHandler = (gameTemplateId: number) => { + deleteTemplate({ gameTemplateId }); + }; + + return ( +
+
+
+ 템플릿 관리 +
+ + + + + + {templates.map((t) => ( + + {tableFormat['partyTemplate'].columns.map((columnName) => { + return ( + + {columnName === 'change' && ( + + )} + {columnName === 'delete' ? ( + + ) : ( + t[columnName as keyof PartyGameTemplate]?.toString() + )} + + ); + })} + + ))} + +
+
+
+
+ ); +} diff --git a/components/main/PartyPreview.tsx b/components/main/PartyPreview.tsx new file mode 100644 index 000000000..d9f8beb17 --- /dev/null +++ b/components/main/PartyPreview.tsx @@ -0,0 +1,22 @@ +import usePartyRoomList from 'hooks/party/usePartyRoomList'; +import styles from 'styles/main/PartyPreview.module.scss'; +export default function PartyPreview() { + const { partyRooms } = usePartyRoomList(); + const limitedRooms = partyRooms.slice(0, 3); + return ( +
+
    + {limitedRooms.map((room) => ( +
  • +
    + {room.title} + {`${room.currentPeople}/${room.maxPeople}`} +
    +
  • + ))} +
+
+ ); +} diff --git a/components/main/Section.tsx b/components/main/Section.tsx index 1c4019eca..67e561f73 100644 --- a/components/main/Section.tsx +++ b/components/main/Section.tsx @@ -5,6 +5,7 @@ import GameResult from 'components/game/GameResult'; import TournamentPreview from 'components/main/TournamentPreview'; import RankListMain from 'components/rank/topRank/RankListMain'; import styles from 'styles/main/Section.module.scss'; +import PartyPreview from './PartyPreview'; type SectionProps = { sectionTitle: string; @@ -18,6 +19,7 @@ export default function Section({ sectionTitle, path }: SectionProps) { const pathCheck: pathType = { game: , rank: , + party: , tournament: , }; diff --git a/components/modal/ModalProvider.tsx b/components/modal/ModalProvider.tsx index abbf1aa7f..cd1f462ae 100644 --- a/components/modal/ModalProvider.tsx +++ b/components/modal/ModalProvider.tsx @@ -6,6 +6,7 @@ import AdminModal from 'components/modal/modalType/AdminModal'; import NormalModal from 'components/modal/modalType/NormalModal'; import StoreModal from 'components/modal/modalType/StoreModal'; import styles from 'styles/modal/Modal.module.scss'; +import PartyModal from './modalType/PartyModal'; import TournamentModal from './modalType/TournamentModal'; export default function ModalProvider() { @@ -46,6 +47,8 @@ export default function ModalProvider() { ) : modalType === 'TOURNAMENT' ? ( + ) : modalType === 'PARTY' ? ( + ) : null}
) diff --git a/components/modal/Party/PartyReportModal.tsx b/components/modal/Party/PartyReportModal.tsx new file mode 100644 index 000000000..228fc3e29 --- /dev/null +++ b/components/modal/Party/PartyReportModal.tsx @@ -0,0 +1,83 @@ +import { useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { Modal, PartyReportModalData } from 'types/modalTypes'; +import { instance } from 'utils/axios'; +import { modalState } from 'utils/recoil/modal'; +import styles from 'styles/modal/menu/ReportModal.module.scss'; +import { ModalButton, ModalButtonContainer } from '../ModalButton'; + +export function PartyReportModal({ report }: { report: PartyReportModalData }) { + const [isLoading, setIsLoading] = useState(false); + const [content, setContent] = useState(''); + const setModal = useSetRecoilState(modalState); + + const reportHandler = async () => { + const reportResponse: { [key: string]: string } = { + SUCCESS: '신고해주셔서 감사합니다 ❤️', + REJECT: '내용을 적어주세요 ❤️', + }; + + try { + if (!content.replace(/^\s+|\s+$/g, '')) { + throw new Error('칸을 입력하지 않았습니다.'); + } + switch (report.name) { + case 'COMMENT': + await instance.post(`/party/reports/comments/${report.commentId}`); + break; + case 'ROOM': + await instance.post(`/party/reports/rooms/${report.roomId}`); + break; + case 'NOSHOW': + await instance.post( + `/party/reports/rooms/${report.roomId}/users/${report.userIntraId}` + ); + break; + } + setModal({ modalName: null }); + alert(reportResponse.SUCCESS); + } catch (e) { + alert(reportResponse.REJECT); + throw new Error('REJECT'); + } + }; + + const handleReport = () => { + setIsLoading(true); + reportHandler().catch(() => setIsLoading(false)); + }; + + return ( +
+
+
42GG
+
{report.name}
+
+
+
+
+