diff --git a/components/admin/SideNav.tsx b/components/admin/SideNav.tsx index e4d91804f..3d01b48fb 100644 --- a/components/admin/SideNav.tsx +++ b/components/admin/SideNav.tsx @@ -9,7 +9,7 @@ import { } from 'react-icons/gr'; import { IoGameControllerOutline, IoReceiptOutline } from 'react-icons/io5'; import { MdOutlineMessage } from 'react-icons/md'; -import { TbCalendarTime, TbCoin, TbPaperBag } from 'react-icons/tb'; +import { TbCalendarTime, TbCoin, TbPaperBag, TbTrophy } from 'react-icons/tb'; import SideNavContent from 'components/admin/SideNavContent'; import styles from 'styles/admin/SideNav.module.scss'; @@ -113,6 +113,14 @@ export default function SideNav() { > + + + + ); } diff --git a/components/admin/tournament/TournamentEdit.tsx b/components/admin/tournament/TournamentEdit.tsx new file mode 100644 index 000000000..136ee855b --- /dev/null +++ b/components/admin/tournament/TournamentEdit.tsx @@ -0,0 +1,193 @@ +import dynamic from 'next/dynamic'; +import { useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { QUILL_EDIT_MODULES, QUILL_FORMATS } from 'types/quillTypes'; +import { toastState } from 'utils/recoil/toast'; +import { useUser } from 'hooks/Layout/useUser'; +import styles from 'styles/admin/announcement/AnnounceEdit.module.scss'; +import 'react-quill/dist/quill.snow.css'; +import 'react-quill/dist/quill.bubble.css'; +import { AdminTableHead } from '../common/AdminTable'; + +const Quill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

, +}); + +const tableTitle: { [key: string]: string } = { + tournamentName: '토너먼트 이름', + startTime: '시작 시간', + endTime: '종료 시간', + tournamentType: '토너먼트 유형', +}; + +export default function TournamentEdit() { + const user = useUser(); + const setSnackbar = useSetRecoilState(toastState); + const [content, setContent] = useState(''); + const announceCreateResponse: { [key: string]: string } = { + SUCCESS: '공지사항이 성공적으로 등록되었습니다.', + AN300: + '이미 활성화된 공지사항이 있습니다. 새로운 공지사항을 등록하시려면 활성화된 공지사항을 삭제해 주세요.', + }; + const announceDeleteResponse: { [key: string]: string } = { + SUCCESS: '공지사항이 성공적으로 삭제되었습니다.', + AN100: '삭제 할 활성화된 공지사항이 없습니다.', + }; + + useEffect(() => { + resetHandler(); + }, []); + + const resetHandler = async () => { + try { + //const res = await instance.get('/pingpong/announcement'); + setContent('가장 최근 토너먼트 내용'); //setContent(res?.data.content); + } catch (e) { + alert(e); + } + }; + + if (!user) return null; + + const currentUserId = user.intraId; + + const postHandler = async () => { + // try { + // await instanceInManage.post(`/announcement`, { + // content, + // creatorIntraId: currentUserId, + // }); + // setSnackbar({ + // toastName: `post request`, + // severity: 'success', + // message: `🔥 ${announceCreateResponse.SUCCESS} 🔥`, + // clicked: true, + // }); + // } catch (e: any) { + // setSnackbar({ + // toastName: `bad request`, + // severity: 'error', + // message: `🔥 ${announceCreateResponse[e.response.data.code]} 🔥`, + // clicked: true, + // }); + // } + }; + + const deleteHandler = async () => { + // try { + // await instanceInManage.delete(`/announcement/${currentUserId}`); + // setSnackbar({ + // toastName: `delete request`, + // severity: 'success', + // message: `🔥 ${announceDeleteResponse.SUCCESS} 🔥`, + // clicked: true, + // }); + // } catch (e: any) { + // setSnackbar({ + // toastName: `bad request`, + // severity: 'error', + // message: `🔥 ${announceDeleteResponse[e.response.data.code]} 🔥`, + // clicked: true, + // }); + // } + }; + + return ( +
+
+

추후 토너먼트 페이지 모달 완성시 추가 예정

+ {/* {content ? ( +
+
Notice!
+ +
+ + +
+
+
+ +
+
+
+ ) : ( +
활성화된 공지사항이 없습니다 !
+ )} */} +
+
+ + + + + + + + + + + + + + + + + + + +
+
+ setContent(content)} + /> +
+ + + +
+
+
+ ); +} diff --git a/components/admin/tournament/TournamentList.tsx b/components/admin/tournament/TournamentList.tsx new file mode 100644 index 000000000..cabf147db --- /dev/null +++ b/components/admin/tournament/TournamentList.tsx @@ -0,0 +1,137 @@ +import dynamic from 'next/dynamic'; +import { useCallback, useEffect, useState } from 'react'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { + ITournament, + ITournamentTable, +} from 'types/admin/adminTournamentTypes'; +import { tableFormat } from 'constants/admin/table'; +import { + AdminEmptyItem, + AdminTableHead, +} from 'components/admin/common/AdminTable'; +import PageNation from 'components/Pagination'; +import styles from 'styles/admin/announcement/AnnounceList.module.scss'; +import 'react-quill/dist/quill.snow.css'; +import 'react-quill/dist/quill.bubble.css'; + +const Quill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

, +}); + +const tableTitle: { [key: string]: string } = { + tournamentName: '토너먼트 이름', + startTime: '시작 시간', + endTime: '종료 시간', + tournamentType: '토너먼트 타입', + edit: '수정하기', +}; + +const smapleTournamentList: ITournament[] = Array.from({ length: 10 }, () => ({ + title: '샘플토너먼트제목', + content: '샘플내용', + startTime: new Date(), + endTime: new Date(), + tournamentType: 'CUSTOM', + count: 7, +})); + +export default function TournamentList() { + const [tournamentInfo, setTournamentInfo] = useState({ + tournamentList: [], + totalPage: 0, + currentPage: 0, + }); + + const [currentPage, setCurrentPage] = useState(1); + + const getTournaments = useCallback(async () => { + try { + // const res = await instanceInManage.get( + // `/announcement?page=${currentPage}&size=3` + // ); + setTournamentInfo({ + tournamentList: smapleTournamentList, //res.data.tournamentList, + totalPage: 10, //res.data.totalPage, + currentPage: currentPage, + }); + } catch (e) { + console.error('MS01'); + } + }, [currentPage]); + + useEffect(() => { + getTournaments(); + }, [getTournaments]); + + return ( +
+
+ 토너먼트 리스트 +
+ + + + + {tournamentInfo.tournamentList.length > 0 ? ( + tournamentInfo.tournamentList.map( + (tournament: ITournament, index: number) => ( + + {tableFormat['tournament'].columns.map( + (columnName: string, index: number) => { + return ( + + {columnName === 'startTime' || + columnName === 'endTime' + ? tournament[ + columnName as keyof ITournament + ]?.toLocaleString() + : tournament[ + columnName as keyof ITournament + ]?.toString()} + + ); + } + )} + +
+ +
+
+
+ ) + ) + ) : ( + + )} +
+
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ ); +} diff --git a/components/main/Section.tsx b/components/main/Section.tsx index e4d0e1c83..e00303f4a 100644 --- a/components/main/Section.tsx +++ b/components/main/Section.tsx @@ -17,6 +17,7 @@ export default function Section({ sectionTitle, path }: SectionProps) { const pathCheck: pathType = { game: , rank: , + tournament: <>, }; return ( diff --git a/components/modal/ModalProvider.tsx b/components/modal/ModalProvider.tsx index 1b6f6d4d6..abbf1aa7f 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 TournamentModal from './modalType/TournamentModal'; export default function ModalProvider() { const [{ modalName }, setModal] = useRecoilState(modalState); @@ -43,6 +44,8 @@ export default function ModalProvider() { ) : modalType === 'ADMIN' ? ( + ) : modalType === 'TOURNAMENT' ? ( + ) : null} ) diff --git a/components/modal/modalType/TournamentModal.tsx b/components/modal/modalType/TournamentModal.tsx new file mode 100644 index 000000000..aea7d3053 --- /dev/null +++ b/components/modal/modalType/TournamentModal.tsx @@ -0,0 +1,16 @@ +import { useRecoilValue } from 'recoil'; +import { modalState } from 'utils/recoil/modal'; +import TournamentRegistryModal from '../tournament/TournamentRegistryModal'; + +export default function TournamentModal() { + const { modalName, tournamentInfo } = useRecoilValue(modalState); + + const content: { [key: string]: JSX.Element | null } = { + 'TOURNAMENT-REGISTRY': tournamentInfo ? ( + + ) : null, + }; + + if (!modalName) return null; + return content[modalName]; +} diff --git a/components/modal/tournament/TournamentRegistryModal.tsx b/components/modal/tournament/TournamentRegistryModal.tsx new file mode 100644 index 000000000..6485a6c45 --- /dev/null +++ b/components/modal/tournament/TournamentRegistryModal.tsx @@ -0,0 +1,65 @@ +import dynamic from 'next/dynamic'; +import { useSetRecoilState } from 'recoil'; +import { QUILL_FORMATS } from 'types/quillTypes'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { modalState } from 'utils/recoil/modal'; +import { + ModalButtonContainer, + ModalButton, +} from 'components/modal/ModalButton'; +import styles from 'styles/modal/event/AnnouncementModal.module.scss'; +import 'react-quill/dist/quill.bubble.css'; + +const Quill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

, +}); + +export default function TournamentRegistryModal({ + title, + contents, + startTime, + status, + type, + endTime, +}: TournamentInfo) { + const setModal = useSetRecoilState(modalState); + + const registTournament = () => { + console.log('토너먼트에 등록하시겠습니까.'); + }; + + const closeModalButtonHandler = () => { + setModal({ modalName: null }); + }; + + return ( +
+
{title}
+
{startTime.toString()}
+ +
+ + + + + + +
+
+ ); +} diff --git a/components/rank/topRank/RankListMain.tsx b/components/rank/topRank/RankListMain.tsx index cecf97d2a..4b3160d7d 100644 --- a/components/rank/topRank/RankListMain.tsx +++ b/components/rank/topRank/RankListMain.tsx @@ -53,8 +53,10 @@ export default function RankListMain({ isMain, season }: RankListMainProps) { )); return ( -
-
{bangElements}
+ <> + {!isMain && ( +
{bangElements}
+ )}
{rank !== undefined && rank.map((item: userImages, index: number) => ( @@ -65,7 +67,7 @@ export default function RankListMain({ isMain, season }: RankListMainProps) { /> ))}
-
+ ); } diff --git a/components/tournament/TournamentCard.tsx b/components/tournament/TournamentCard.tsx new file mode 100644 index 000000000..191475fe7 --- /dev/null +++ b/components/tournament/TournamentCard.tsx @@ -0,0 +1,19 @@ +import { TournamentInfo } from 'types/tournamentTypes'; +import styles from 'styles/tournament/TournamentCard.module.scss'; + +export default function TournamentCard({ + tournamentId, + title, + contents, + status, + type, + winnerUser, + startTime, + endTime, +}: TournamentInfo) { + return ( +
+
{title}
+
+ ); +} diff --git a/constants/admin/table.ts b/constants/admin/table.ts index c95b6ff5d..9b0330b4f 100644 --- a/constants/admin/table.ts +++ b/constants/admin/table.ts @@ -150,4 +150,19 @@ export const tableFormat: TableFormat = { 'rankLose', ], }, + tournament: { + name: '토너먼트', + columns: [ + 'tournamentName', + 'content', + 'startTime', + 'endTime', + 'tournamentType', + 'edit', + ], + }, + tournamentCreate: { + name: '토너먼트 생성', + columns: ['tournamentName', 'startTime', 'endTime', 'tournamentType'], + }, }; diff --git a/pages/admin/tournament.tsx b/pages/admin/tournament.tsx new file mode 100644 index 000000000..9bc472b01 --- /dev/null +++ b/pages/admin/tournament.tsx @@ -0,0 +1,12 @@ +import TournamentEdit from 'components/admin/tournament/TournamentEdit'; +import TournamentList from 'components/admin/tournament/TournamentList'; +import styles from 'styles/admin/announcement/Announcement.module.scss'; + +export default function Tournament() { + return ( +
+ + +
+ ); +} diff --git a/pages/index.tsx b/pages/index.tsx index 14a26465b..9ceb0d2c3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -9,6 +9,9 @@ const Home: NextPage = () => {
+
+
+
diff --git a/pages/tournament.tsx b/pages/tournament.tsx new file mode 100644 index 000000000..1cecb53b2 --- /dev/null +++ b/pages/tournament.tsx @@ -0,0 +1,128 @@ +import { useQuery } from 'react-query'; +import { useSetRecoilState } from 'recoil'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { instance } from 'utils/axios'; +import { errorState } from 'utils/recoil/error'; +import TournamentCard from 'components/tournament/TournamentCard'; +import styles from 'styles/tournament/TournamentContainer.module.scss'; + +//나중에 삭제 필요 +interface TempData { + tournaments: TournamentInfo[]; + totalPage: number; +} + +// 내부의 토너먼트 보여주는 부분만 Component화 하면될까? 다른곳에서도 쓰일까? +// 진행중인 토너먼트의 Bracket을 보여주는 부분도 다른곳에서도 쓸수있지않나? +const tempData: TempData = { + tournaments: [ + { + tournamentId: 5, + title: '5회 루키 토너먼트', + contents: '블라블라', + startTime: new Date(), + status: '종료', + type: 'ROOKIE', + endTime: new Date(), + }, + { + tournamentId: 6, + title: '7회 마스터 토너먼트', + contents: '블라블라 하이하이', + startTime: new Date(), + status: '진행중', + type: 'MASTER', + endTime: new Date(), + }, + { + tournamentId: 6, + title: '8회 마스터 토너먼트', + contents: '블라블라 하이하이', + startTime: new Date(), + status: '진행중', + type: 'MASTER', + endTime: new Date(), + }, + { + tournamentId: 6, + title: '9회 마스터 토너먼트', + contents: '블라블라 하이하이', + startTime: new Date(), + status: '진행중', + type: 'MASTER', + endTime: new Date(), + }, + { + tournamentId: 6, + title: '10회 마스터 토너먼트', + contents: '블라블라 하이하이', + startTime: new Date(), + status: '진행중', + type: 'MASTER', + endTime: new Date(), + }, + { + tournamentId: 6, + title: '11회 마스터 토너먼트', + contents: '블라블라 하이하이', + startTime: new Date(), + status: '진행중', + type: 'MASTER', + endTime: new Date(), + }, + { + tournamentId: 6, + title: '12회 마스터 토너먼트', + contents: '블라블라 하이하이', + startTime: new Date(), + status: '진행중', + type: 'MASTER', + endTime: new Date(), + }, + ], + totalPage: 100, +}; + +export default function Tournament() { + const setError = useSetRecoilState(errorState); + + const openTorunamentInfo = useQuery( + 'openTorunamentInfo', + () => + instance + .get('pingpong/tournaments?page=*&type=*&status=진행중') + .then((res) => res.data), + { retry: 1, staleTime: 60000 /* 60초 */ } + ); + + const waitTournamentInfo = useQuery( + 'waitTournamentInfo', + () => + instance + .get('pingpong/tournaments?page=*&type=*&status=예정') + .then((res) => res.data), + { retry: 1, staleTime: 60000 /* 60초 */ } + ); + + if (openTorunamentInfo.isError || waitTournamentInfo.isError) { + setError('junhjeon'); + } + + return ( +
+

Tournament

+
+
대기중인 토너먼트
+
+ {tempData.tournaments.map((tournament, index) => ( + + ))} +
+
진행중인 토너먼트
+
+
무언가 토너먼트의 사진
+
+
+
+ ); +} diff --git a/styles/main/Section.module.scss b/styles/main/Section.module.scss index a220211e3..eecec2c95 100644 --- a/styles/main/Section.module.scss +++ b/styles/main/Section.module.scss @@ -2,7 +2,6 @@ .sectionWrap { position: relative; - z-index: 1; margin-top: 2.5rem; &.mainRank { margin-top: 1rem; @@ -23,6 +22,7 @@ align-items: center; } > button { + z-index: 10; width: 1.563rem; height: 1.563rem; color: #ffffff; diff --git a/styles/rank/RankListMain.module.scss b/styles/rank/RankListMain.module.scss index a0b1f3b78..161f037ed 100644 --- a/styles/rank/RankListMain.module.scss +++ b/styles/rank/RankListMain.module.scss @@ -1,5 +1,9 @@ @import 'styles/common.scss'; +.RankContainer { + z-index: 0; +} + .mainContainer { position: relative; top: 1.5rem; @@ -11,7 +15,6 @@ align-items: end; &.isMain { height: 15.5rem; - margin-top: 7.5rem; } } @@ -82,8 +85,9 @@ .bangContainer { display: flex; - justify-content: space-around; margin-top: -7.5rem; + pointer-events: none; + justify-content: space-around; } .bang { diff --git a/styles/tournament/TournamentCard.module.scss b/styles/tournament/TournamentCard.module.scss new file mode 100644 index 000000000..c175a9325 --- /dev/null +++ b/styles/tournament/TournamentCard.module.scss @@ -0,0 +1,16 @@ +.tournamentCardContainer { + display: flex; + width: 100%; + padding: 1rem; + margin-bottom: 1rem; + background-color: black; + border: 2px solid black; + border-radius: 0.3rem; + + .text { + overflow: hidden; + color: white; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/styles/tournament/TournamentContainer.module.scss b/styles/tournament/TournamentContainer.module.scss new file mode 100644 index 000000000..6adb9e781 --- /dev/null +++ b/styles/tournament/TournamentContainer.module.scss @@ -0,0 +1,67 @@ +@import 'styles/common.scss'; + +.pageWrap { + @include pageWrap; +} + +.title { + @include pageTitle; + width: fit-content; + cursor: pointer; +} + +.tournamentContainer { + display: flex; + height: 60vh; + flex-direction: column; + padding: 1rem 1rem 0rem; + overflow-y: scroll; + background: #d4b8f2; + border-radius: $small-radius; +} + +.waitTournamentBox { + display: flex; + width: 100%; + flex: 5; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 3rem 1rem 0; + overflow-x: hidden; + overflow-y: scroll; + background: rgba(112, 0, 255, 0.17); + border: 2px solid #c67dff; + border-radius: 0.3rem; +} + +.openTournamentBox { + display: flex; + width: 100%; + flex: 15; + flex-direction: column; + padding: 0.2rem 0 0.2rem 0.2rem; + margin-bottom: 1rem; + overflow-x: scroll; + overflow-y: scroll; + background: rgba(112, 0, 255, 0.17); + border: 2px solid #c67dff; + border-radius: 0.3rem; +} + +.tournamentTextWait { + display: flex; + flex: 1; + font-weight: 700; + color: black; + text-align: center; /* 텍스트를 가로로 중앙에 정렬 */ +} + +.tournamentTextOpen { + display: flex; + flex: 1; + padding-top: 1rem; + font-weight: 700; + color: black; + text-align: center; /* 텍스트를 가로로 중앙에 정렬 */ +} diff --git a/types/admin/adminTournamentTypes.ts b/types/admin/adminTournamentTypes.ts new file mode 100644 index 000000000..d7209559b --- /dev/null +++ b/types/admin/adminTournamentTypes.ts @@ -0,0 +1,14 @@ +export interface ITournament { + title: string; //제목 + content: string; //내용 + startTime: Date; //시작시간 + endTime: Date; // 종료시간 + tournamentType: 'CUSTOM' | 'ROOKIE' | 'MASTER'; //토너먼트 타입 + count?: number; //참여인원 (최대참여인원 8명 fix) +} + +export interface ITournamentTable { + tournamentList: ITournament[]; + totalPage: number; + currentPage: number; +} diff --git a/types/admin/tableTypes.ts b/types/admin/tableTypes.ts index 056ba85e8..333b85d68 100644 --- a/types/admin/tableTypes.ts +++ b/types/admin/tableTypes.ts @@ -15,7 +15,9 @@ export type TableName = | 'itemList' | 'itemHistory' | 'coinPolicy' - | 'coinPolicyHistory'; + | 'coinPolicyHistory' + | 'tournament' + | 'tournamentCreate'; export type EtcType = 'button' | 'toggle'; diff --git a/types/modalTypes.ts b/types/modalTypes.ts index 27906a532..1cc1b9dc7 100644 --- a/types/modalTypes.ts +++ b/types/modalTypes.ts @@ -11,6 +11,7 @@ import { MatchMode } from 'types/mainType'; import { ISeason } from 'types/seasonTypes'; import { StoreManualMode } from 'types/storeTypes'; import { ICoin } from 'types/userTypes'; +import { TournamentInfo } from './tournamentTypes'; type EventModal = 'WELCOME' | 'ANNOUNCEMENT'; @@ -27,8 +28,11 @@ type PurchaseModal = 'BUY' | 'GIFT' | 'NO_COIN'; type UseItemModal = ItemType | 'GACHA'; type EditItemModal = 'MEGAPHONE'; + type StoreModal = 'MANUAL' | 'COIN_HISTORY'; +type TournamentModal = 'REGISTRY'; + type AdminModal = | 'PROFILE' | 'USER-COIN' @@ -59,7 +63,8 @@ type ModalName = | `USE-ITEM-${UseItemModal}` | `EDIT-ITEM-${EditItemModal}` | `STORE-${StoreModal}` - | `PURCHASE-${PurchaseModal}`; + | `PURCHASE-${PurchaseModal}` + | `TOURNAMENT-${TournamentModal}`; export interface Cancel { startTime: string; @@ -129,4 +134,5 @@ export interface Modal { isAttended?: boolean; totalCoin?: ICoin; randomItem?: IRandomItem; + tournamentInfo?: TournamentInfo; } diff --git a/types/tournamentTypes.ts b/types/tournamentTypes.ts new file mode 100644 index 000000000..6665330b7 --- /dev/null +++ b/types/tournamentTypes.ts @@ -0,0 +1,12 @@ +import { IUser } from './admin/adminUserTypes'; + +export interface TournamentInfo { + tournamentId: number; + title: string; + contents: string; + status: string; // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' + type: 'CUSTOM' | 'MASTER' | 'ROOKIE'; + winnerUser?: IUser; + startTime: Date; + endTime: Date; +} diff --git a/utils/recoil/modal.ts b/utils/recoil/modal.ts index 5821a6ba7..f0f1911c8 100644 --- a/utils/recoil/modal.ts +++ b/utils/recoil/modal.ts @@ -13,12 +13,15 @@ export const modalTypeState = selector({ let modalType = ''; const normalPrefixes = ['EVENT', 'MENU', 'MATCH', 'USER', 'FIXED']; const storePrefixes = ['COIN', 'STORE', 'PURCHASE', 'USE', 'EDIT']; + const tournamentPrefixes = ['TOURNAMENT']; const prefix = get(modalState).modalName?.split('-')[0] || ''; if (normalPrefixes.includes(prefix)) { modalType = 'NORMAL'; } else if (storePrefixes.includes(prefix)) { modalType = 'STORE'; + } else if (tournamentPrefixes.includes(prefix)) { + modalType = 'TOURNAMENT'; } else if (prefix === 'ADMIN') { modalType = 'ADMIN'; }