diff --git a/.eslintrc.json b/.eslintrc.json index f39bf35fd..966de64c6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,10 +9,10 @@ "next/core-web-vitals", "eslint:recommended", "plugin:@typescript-eslint/recommended", - "prettier" - ], - "overrides": [ + "prettier", + "plugin:storybook/recommended" ], + "overrides": [], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", @@ -22,6 +22,5 @@ "react", "@typescript-eslint" ], - "rules": { - } + "rules": {} } diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 7db26c7c5..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -if [ $(git rev-parse --abbrev-ref HEAD) = 'deploy' ]; then - echo 'You cannot commit directly to the deploy branch' - exit 1 -fi - -npx lint-staged diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg deleted file mode 100755 index ea77f548d..000000000 --- a/.husky/prepare-commit-msg +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -branch_name=$(git symbolic-ref --short HEAD) - -commit_msg_title=$(head -n 1 $1) -commit_msg_body=$(tail -n +2 $1) -issue_number=$(echo $branch_name | sed -n 's/^.*#\([0-9]*\)$/\1/p') -issue_number_in_msg=$(grep -c "\#$issue_number" $1) - -if [[ -n $issue_number ]] && [[ ! $issue_number_in_msg -ge 1 ]]; then - # echo "$commit_msg #$issue_number" > $1 - echo "$commit_msg_title #$issue_number" > $1 - if [[ -n $commit_msg_body ]]; then - echo "$commit_msg_body" >> $1 - fi -else - echo "check issue number in branch name" - exit 1 -fi \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 000000000..9dbdf6fc9 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,38 @@ +import type { StorybookConfig } from '@storybook/nextjs'; +const config: StorybookConfig = { + stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/nextjs', + options: {}, + }, + docs: { + autodocs: 'tag', + }, + webpackFinal: async (config) => { + const imageRule = config.module?.rules?.find((rule) => { + const test = (rule as { test: RegExp }).test; + + if (!test) { + return false; + } + + return test.test('.svg'); + }) as { [key: string]: any }; + + imageRule.exclude = /\.svg$/; + + config.module?.rules?.push({ + test: /\.svg$/, + use: ['@svgr/webpack'], + }); + + return config; + }, +}; + +export default config; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 000000000..263ca1299 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type { Preview } from '@storybook/react'; +import { StoryFn } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + backgrounds: { + default: 'purple', + values: [{ name: 'purple', value: '#301451' }], + }, + }, + decorators: [ + (Story: StoryFn) => ( + + + + ), + ], +}; + +export default preview; diff --git a/components/Layout/CurrentMatch.tsx b/components/Layout/CurrentMatch.tsx index bbbb1b531..930c9bc39 100644 --- a/components/Layout/CurrentMatch.tsx +++ b/components/Layout/CurrentMatch.tsx @@ -1,73 +1,148 @@ import Link from 'next/link'; -import { useRouter } from 'next/router'; -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { instance } from 'utils/axios'; -import { errorState } from 'utils/recoil/error'; +import { useEffect, useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { modalState } from 'utils/recoil/modal'; -import { reloadMatchState, currentMatchState } from 'utils/recoil/match'; -import { gameTimeToString, isBeforeMin } from 'utils/handleTime'; +import { stringToHourMin } from 'utils/handleTime'; +import useGetCurrentMatch from 'hooks/Layout/useGetCurrentMatch'; +import { currentMatchState } from 'utils/recoil/match'; +import { CurrentMatchList, CurrentMatchListElement } from 'types/matchTypes'; +import { Modal } from 'types/modalTypes'; import styles from 'styles/Layout/CurrentMatchInfo.module.scss'; +import { TbMenu } from 'react-icons/tb'; +import LoudSpeaker from './LoudSpeaker'; export default function CurrentMatch() { - const [{ isMatched, enemyTeam, time, slotId, isImminent }, setCurrentMatch] = - useRecoilState(currentMatchState); - const [reloadMatch, setReloadMatch] = useRecoilState(reloadMatchState); - const setModal = useSetRecoilState(modalState); - const setError = useSetRecoilState(errorState); - const matchingMessage = time && makeMessage(time, isMatched); - const blockCancelButton = isImminent && enemyTeam.length; - const presentPath = useRouter().asPath; + const currentMatchList = + useRecoilValue(currentMatchState).match; + const [showDropdown, setShowDropdown] = useState(false); + const [dropdownAnimation, setDropdownAnimation] = useState(false); + + const dropdownStyle = showDropdown + ? styles.visibleDropdown + : styles.hiddenDropdown; + + useGetCurrentMatch(); useEffect(() => { - getCurrentMatchHandler(); - if (reloadMatch) setReloadMatch(false); - }, [presentPath, reloadMatch]); - - const getCurrentMatchHandler = async () => { - try { - const res = await instance.get(`/pingpong/match/current`); - setCurrentMatch(res?.data); - } catch (e) { - setError('JB01'); + if (showDropdown) { + setDropdownAnimation(true); + } else { + setTimeout(() => { + setDropdownAnimation(false); + }, 400); } - }; + }, [showDropdown]); - const onCancel = () => { + const dropButtonStyle = showDropdown ? styles.dropup : styles.dropdown; + const matchCountStyle = + currentMatchList.length === 2 + ? styles.two + : currentMatchList.length === 3 + ? styles.three + : styles.one; + + return ( +
+
+
+ {currentMatchList && } +
+
+ {dropdownAnimation ? ( +
+ {currentMatchList.slice(1).map((currentMatch, index) => ( + + ))} +
+ ) : ( + <> + )} + {currentMatchList.length > 1 ? ( + + ) : ( + <> + )} +
+
+
+ ); +} + +interface CurrentMatchContentProp { + currentMatch: CurrentMatchListElement; + index: number; +} + +export function CurrentMatchContent(prop: CurrentMatchContentProp) { + const { currentMatch, index } = prop; + const { startTime, isMatched, enemyTeam, isImminent } = currentMatch; + + const currentMatchList = + useRecoilValue(currentMatchState).match; + + const setModal = useSetRecoilState(modalState); + const cancelButtonStyle = + isImminent && enemyTeam.length ? styles.block : styles.nonBlock; + + const onCancel = (startTime: string) => { setModal({ modalName: 'MATCH-CANCEL', - cancel: { isMatched, slotId, time }, + cancel: { startTime: startTime }, }); }; + const currentMatchContentStyle = index + ? styles.middle + : currentMatchList.length > 1 + ? styles.mainMore + : styles.mainOne; + return ( <> -
-
-
-
- {matchingMessage} - -
+
+ +
+ +
-
{ + event.stopPropagation(); + onCancel(startTime); + }} > - -
+ {cancelButtonStyle === styles.block + ? '취소 불가' + : '예약 취소'} +
); } -function makeMessage(time: string, isMatched: boolean) { - const formattedTime = gameTimeToString(time); +interface MakeMessageProps { + time: string; + isMatched: boolean; +} + +function MakeMessage({ time, isMatched }: MakeMessageProps) { + const formattedTime = `${stringToHourMin(time).sHour}시 ${ + stringToHourMin(time).sMin + }분`; return (
{formattedTime} @@ -76,7 +151,7 @@ function makeMessage(time: string, isMatched: boolean) { '에 경기가 시작됩니다!' ) : ( <> - 참가자 기다리는 중 +  참가자 기다리는 중 . . diff --git a/components/Layout/Header.tsx b/components/Layout/Header.tsx index 92c7c4f93..8477503f6 100644 --- a/components/Layout/Header.tsx +++ b/components/Layout/Header.tsx @@ -1,85 +1,96 @@ import Link from 'next/link'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { liveState } from 'utils/recoil/layout'; import { useEffect } from 'react'; -import { useRecoilValue, useRecoilState } from 'recoil'; -import { - openMenuBarState, - openNotiBarState, - userState, - liveState, -} from 'utils/recoil/layout'; -import MenuBar from './MenuBar'; -import NotiBar from './NotiBar'; -import PlayerImage from 'components/PlayerImage'; +import MenuBar from './MenuBar/MenuBar'; import { FiMenu } from 'react-icons/fi'; import { BsMegaphone } from 'react-icons/bs'; -import { VscBell, VscBellDot } from 'react-icons/vsc'; import styles from 'styles/Layout/Header.module.scss'; +import NotiBar from './NotiBar/NotiBar'; +import { HeaderContextState, HeaderContext } from './HeaderContext'; +import { useContext } from 'react'; +import { Modal } from 'types/modalTypes'; +import { modalState } from 'utils/recoil/modal'; +import useAxiosGet from 'hooks/useAxiosGet'; +import NotiBell from 'public/image/noti_bell.svg'; export default function Header() { - const user = useRecoilValue(userState); const [live, setLive] = useRecoilState(liveState); - const [openMenuBar, setOpenMenuBar] = useRecoilState(openMenuBarState); - const [openNotiBar, setOpenNotiBar] = useRecoilState(openNotiBarState); - + const HeaderState = useContext(HeaderContext); const openMenuBarHandler = () => { - setOpenMenuBar(!openMenuBar); + HeaderState?.setOpenMenuBarState(!HeaderState?.openMenuBarState); }; - const openNotiBarHandler = () => { - setOpenNotiBar(!openNotiBar); + HeaderState?.setOpenNotiBarState(!HeaderState?.openNotiBarState); setLive((prev) => ({ ...prev, notiCount: 0 })); }; useEffect(() => { setMenuOutsideScroll(); - }, [openMenuBar, openNotiBar]); + }, [HeaderState?.openMenuBarState, HeaderState?.openNotiBarState]); const setMenuOutsideScroll = () => (document.body.style.overflow = - openMenuBar || openNotiBar ? 'hidden' : 'unset'); + HeaderState?.openMenuBarState || HeaderState?.openNotiBarState + ? 'hidden' + : 'unset'); + + const setModal = useSetRecoilState(modalState); + + const getAnnouncementHandler = useAxiosGet({ + url: '/pingpong/announcement', + setState: (data) => { + data.content !== '' && + setModal({ + modalName: 'EVENT-ANNOUNCEMENT', + announcement: data, + }); + }, + err: 'RJ01', + type: 'setError', + }); return (
-
- -
-
- -
42GG
- +
+
+ + 42GG +
- window.open( - 'https://far-moonstone-7ff.notion.site/91925f9c945340c6a139f64fb849990d' - ) - } + className={styles.announceIcon} + onClick={() => getAnnouncementHandler()} > - +
-
+
{live.notiCount ? (
- +
+
+
+ {live.notiCount > 9 ? '9+' : live.notiCount} +
+
+ +
) : ( - +
+ +
)}
- - -
- {openMenuBar && } - {openNotiBar && } + {HeaderState?.openMenuBarState && } + {HeaderState?.openNotiBarState && }
); } diff --git a/components/Layout/HeaderContext.tsx b/components/Layout/HeaderContext.tsx new file mode 100644 index 000000000..f14d0c1fa --- /dev/null +++ b/components/Layout/HeaderContext.tsx @@ -0,0 +1,65 @@ +import React, { + Dispatch, + SetStateAction, + useState, + createContext, +} from 'react'; +import { useSetRecoilState } from 'recoil'; +import { instance } from 'utils/axios'; +import { errorState } from 'utils/recoil/error'; + +export interface HeaderContextState { + openMenuBarState: boolean; + openNotiBarState: boolean; + setOpenMenuBarState: Dispatch>; + setOpenNotiBarState: Dispatch>; + resetOpenMenuBarState: () => void; + resetOpenNotiBarState: () => void; + putNotiHandler: () => Promise; +} + +export const HeaderContext = createContext(null); + +interface HeaderStateContextProps { + children: React.ReactNode; +} + +const HeaderStateContext = (props: HeaderStateContextProps) => { + const [menu, setMenu] = useState(false); + const [noti, setNoti] = useState(false); + const setError = useSetRecoilState(errorState); + + const resetMenuHandler = () => { + setMenu(false); + }; + + const putNotiHandler = async () => { + try { + await instance.put(`/pingpong/notifications/check`); + } catch (e) { + setError('JB05'); + } + }; + + const resetNotiHandler = () => { + putNotiHandler(); + setNoti(false); + }; + + const HeaderState: HeaderContextState = { + openMenuBarState: menu, + openNotiBarState: noti, + setOpenMenuBarState: setMenu, + setOpenNotiBarState: setNoti, + resetOpenMenuBarState: resetMenuHandler, + resetOpenNotiBarState: resetNotiHandler, + putNotiHandler: putNotiHandler, + }; + return ( + + {props.children} + + ); +}; + +export default HeaderStateContext; diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx index a5050491e..ef26e4bef 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -1,104 +1,46 @@ -import Link from 'next/link'; import { useRouter } from 'next/router'; -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { userState, liveState } from 'utils/recoil/layout'; -import { reloadMatchState, openCurrentMatchState } from 'utils/recoil/match'; -import { errorState } from 'utils/recoil/error'; -import { modalState } from 'utils/recoil/modal'; -import { seasonListState } from 'utils/recoil/seasons'; -import { instance } from 'utils/axios'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { colorModeState } from 'utils/recoil/colorMode'; +import { loginState } from 'utils/recoil/login'; +import { userState } from 'utils/recoil/layout'; import Statistics from 'pages/statistics'; import Header from './Header'; import Footer from './Footer'; -import CurrentMatch from './CurrentMatch'; import AdminLayout from '../admin/Layout'; import AdminReject from '../admin/AdminReject'; import styles from 'styles/Layout/Layout.module.scss'; +import useAnnouncementCheck from 'hooks/Layout/useAnnouncementCheck'; +import useSetAfterGameModal from 'hooks/Layout/useSetAfterGameModal'; +import useGetUserSeason from 'hooks/Layout/useGetUserSeason'; +import useLiveCheck from 'hooks/Layout/useLiveCheck'; +import HeaderStateContext from './HeaderContext'; +import StyledButton from 'components/StyledButton'; +import MainPageProfile from './MainPageProfile'; +import { openCurrentMatchState } from 'utils/recoil/match'; +import CurrentMatch from './CurrentMatch'; +import useAxiosResponse from 'hooks/useAxiosResponse'; +import { useEffect } from 'react'; +import Cookies from 'js-cookie'; type AppLayoutProps = { children: React.ReactNode; }; export default function AppLayout({ children }: AppLayoutProps) { - const [user, setUser] = useRecoilState(userState); - const [live, setLive] = useRecoilState(liveState); - const [openCurrentMatch, setOpenCurrentMatch] = useRecoilState( - openCurrentMatchState - ); - const [reloadMatch, setReloadMatch] = useRecoilState(reloadMatchState); - const setSeasonList = useSetRecoilState(seasonListState); - const setError = useSetRecoilState(errorState); - const setModal = useSetRecoilState(modalState); + const user = useRecoilValue(userState); + const colorMode = useRecoilValue(colorModeState); const presentPath = useRouter().asPath; - const announcementTime = localStorage.getItem('announcementTime'); - useEffect(() => { - getUserHandler(); - getSeasonListHandler(); - }, []); - - const getAnnouncementHandler = async () => { - try { - const res = await instance.get(`/pingpong/announcement`); - res.data.content !== '' && - setModal({ - modalName: 'EVENT-ANNOUNCEMENT', - announcement: res.data, - }); - } catch (e) { - setError('RJ01'); - } - }; - const getSeasonListHandler = async () => { - try { - const res = await instance.get(`/pingpong/seasonlist`); - setSeasonList({ ...res?.data }); - } catch (e) { - setError('DK02'); - } - }; - - useEffect(() => { - if (presentPath === '/') { - if ( - !announcementTime || - Date.parse(announcementTime) < Date.parse(new Date().toString()) - ) - getAnnouncementHandler(); - } else setModal({ modalName: null }); - }, [presentPath]); - - useEffect(() => { - if (user.intraId) { - getLiveHandler(); - if (reloadMatch) setReloadMatch(false); - } - }, [presentPath, user, reloadMatch]); - - useEffect(() => { - if (live?.event === 'match') setOpenCurrentMatch(true); - else { - if (live?.event === 'game') setModal({ modalName: 'FIXED-AFTER_GAME' }); - setOpenCurrentMatch(false); - } - }, [live]); - - const getUserHandler = async () => { - try { - const res = await instance.get(`/pingpong/users`); - setUser(res?.data); - } catch (e) { - setError('JB02'); - } - }; + const router = useRouter(); + const openCurrentMatch = useRecoilValue(openCurrentMatchState); - const getLiveHandler = async () => { - try { - const res = await instance.get(`/pingpong/users/live`); - setLive({ ...res?.data }); - } catch (e) { - setError('JB03'); - } + useAxiosResponse(); + useGetUserSeason(); + useSetAfterGameModal(); + useLiveCheck(presentPath); + useAnnouncementCheck(presentPath); + const onClickMatch = () => { + router.replace('/'); + router.push(`/match`); }; return presentPath.includes('/admin') ? ( @@ -109,22 +51,31 @@ export default function AppLayout({ children }: AppLayoutProps) { ) ) : (
-
+
{presentPath === '/statistics' && user.isAdmin ? ( ) : ( user.intraId && ( <> -
- {openCurrentMatch && } + +
+ {presentPath !== '/match' && presentPath !== '/manual' && ( - -
-
🏓
+
+
+ + Play +
- +
)} +
+ {openCurrentMatch && } + {presentPath === '/' && } +
{children}
diff --git a/components/Layout/LoudSpeaker.tsx b/components/Layout/LoudSpeaker.tsx new file mode 100644 index 000000000..856af5c80 --- /dev/null +++ b/components/Layout/LoudSpeaker.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import styles from 'styles/Layout/LoudSpeaker.module.scss'; + +const LoudSpeaker = () => { + return ( + + + + ); +}; + +export default LoudSpeaker; diff --git a/components/Layout/MainPageProfile.tsx b/components/Layout/MainPageProfile.tsx new file mode 100644 index 000000000..066884331 --- /dev/null +++ b/components/Layout/MainPageProfile.tsx @@ -0,0 +1,38 @@ +import Link from 'next/link'; +import { useRecoilValue } from 'recoil'; +import { userState } from 'utils/recoil/layout'; +import PlayerImage from 'components/PlayerImage'; +import styles from 'styles/Layout/MainPageProfile.module.scss'; + +const MainPageProfile = () => { + const user = useRecoilValue(userState); + + return ( +
+
+ + + +
+
안녕하세요,
+
+ 탁구왕  + + {user.intraId} + + 님 +
+
+
+
+ ); +}; + +export default MainPageProfile; diff --git a/components/Layout/MenuBar.tsx b/components/Layout/MenuBar.tsx deleted file mode 100644 index 59551c303..000000000 --- a/components/Layout/MenuBar.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import Link from 'next/link'; -import { useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil'; -import { userState } from 'utils/recoil/layout'; -import { seasonListState } from 'utils/recoil/seasons'; -import { openMenuBarState } from 'utils/recoil/layout'; -import { modalState } from 'utils/recoil/modal'; -import styles from 'styles/Layout/MenuBar.module.scss'; - -export default function MenuBar() { - const { intraId, isAdmin } = useRecoilValue(userState); - const { seasonMode } = useRecoilValue(seasonListState); - const resetOpenMenuBar = useResetRecoilState(openMenuBarState); - const setModal = useSetRecoilState(modalState); - const menuList = [ - { - name: `${seasonMode === 'normal' ? 'VIP' : '랭킹'}`, - link: '/rank', - }, - { name: '최근 경기', link: '/game' }, - { name: '내 정보', link: `/users/detail?intraId=${intraId}` }, - ]; - - return ( - <> -
-
e.stopPropagation()}> - - -
-
- - ); -} diff --git a/components/Layout/MenuBar/MenuBar.tsx b/components/Layout/MenuBar/MenuBar.tsx new file mode 100644 index 000000000..bb731d576 --- /dev/null +++ b/components/Layout/MenuBar/MenuBar.tsx @@ -0,0 +1,98 @@ +import React, { useContext, useEffect, useState } from 'react'; +import styles from 'styles/Layout/MenuBar.module.scss'; +import { HeaderContextState, HeaderContext } from '../HeaderContext'; +import { useRecoilValue } from 'recoil'; +import { userState } from 'utils/recoil/layout'; +import { User } from 'types/mainType'; +import Link from 'next/link'; +import PlayerImage from 'components/PlayerImage'; +import { ProfileBasic } from 'types/userTypes'; +import { MainMenu, AdminMenu } from './MenuBarElement'; +import useAxiosGet from 'hooks/useAxiosGet'; + +const MenuTop = () => { + const HeaderState = useContext(HeaderContext); + + return ( +
+
42GG
+ +
+ ); +}; + +const MenuProfile = () => { + const HeaderState = useContext(HeaderContext); + const user = useRecoilValue(userState); + const [profile, setProfile] = useState({ + intraId: '', + userImageUri: '', + racketType: 'shakeHand', + statusMessage: '', + level: 0, + currentExp: 0, + maxExp: 0, + expRate: 0, + snsNotiOpt: 'SLACK', + }); + + const getProfile = useAxiosGet({ + url: `/pingpong/users/${user.intraId}`, + setState: setProfile, + err: 'SJ03', + type: 'setError', + }); + + useEffect(() => { + getProfile(); + }, []); + + return ( +
+ + + +
+
+ 탁구왕
+ + {user.intraId} + + 님 +
+
LV.{profile.level}
+
+
+ ); +}; + +const MenuBar = () => { + const HeaderState = useContext(HeaderContext); + + return ( +
+
e.stopPropagation()}> + + + + +
+
+ ); +}; + +export default MenuBar; diff --git a/components/Layout/MenuBar/MenuBarElement.tsx b/components/Layout/MenuBar/MenuBarElement.tsx new file mode 100644 index 000000000..d9775d483 --- /dev/null +++ b/components/Layout/MenuBar/MenuBarElement.tsx @@ -0,0 +1,152 @@ +import React, { MouseEvent, useContext, MouseEventHandler } from 'react'; +import Link from 'next/link'; +import styles from 'styles/Layout/MenuBar.module.scss'; +import { HeaderContextState, HeaderContext } from '../HeaderContext'; +import useAxiosGet from 'hooks/useAxiosGet'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { Modal } from 'types/modalTypes'; +import { modalState } from 'utils/recoil/modal'; +import { userState } from 'utils/recoil/layout'; +import { User } from 'types/mainType'; +import RankingEmoji from 'public/image/menu_ranking.svg'; +import CurrentMatchEmoji from 'public/image/menu_currentMatch.svg'; +import AnnouncementEmoji from 'public/image/menu_announcement.svg'; +import ManualEmoji from 'public/image/menu_manual.svg'; +import ReportEmoji from 'public/image/menu_report.svg'; +import StatisticsEmoji from 'public/image/menu_statistics.svg'; +import AdminEmoji from 'public/image/menu_admin.svg'; +import SignOutEmoji from 'public/image/menu_signOut.svg'; + +interface MenuLinkProps { + link: string; + onClick?: (event: MouseEvent) => void; + itemName: string; +} +interface menuItemProps { + itemName: string; + onClick?: MouseEventHandler; +} + +const MenuItem = ({ itemName, onClick }: menuItemProps) => { + const menuList: { [key: string]: { [key: string]: string | JSX.Element } } = { + Ranking: { + name: '랭킹', + svg: , + }, + CurrentMatch: { + name: '최근 경기', + svg: , + }, + Announcement: { + name: '공지사항', + svg: , + }, + Manual: { + name: '사용 설명서', + svg: , + }, + Report: { + name: '건의하기', + svg: , + }, + Statistics: { + name: '통계페이지', + svg: , + }, + Admin: { + name: '관리자', + svg: , + }, + }; + return ( +
+
{menuList[itemName].svg}
+
{menuList[itemName].name}
+
+ ); +}; + +const MenuLink = ({ link, onClick, itemName }: MenuLinkProps) => { + return ( + + + + ); +}; + +export const MainMenu = () => { + const HeaderState = useContext(HeaderContext); + const setModal = useSetRecoilState(modalState); + + const getAnnouncementHandler = useAxiosGet({ + url: '/pingpong/announcement', + setState: (data) => { + data.content !== '' && + setModal({ + modalName: 'EVENT-ANNOUNCEMENT', + announcement: data, + }); + }, + err: 'RJ01', + type: 'setError', + }); + + return ( + + ); +}; + +export const AdminMenu = () => { + const HeaderState = useContext(HeaderContext); + const { isAdmin } = useRecoilValue(userState); + const setModal = useSetRecoilState(modalState); + + return ( + + ); +}; diff --git a/components/Layout/NotiBar.tsx b/components/Layout/NotiBar.tsx deleted file mode 100644 index ec4cf8c3c..000000000 --- a/components/Layout/NotiBar.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useEffect, useState } from 'react'; -import { useSetRecoilState, useResetRecoilState } from 'recoil'; -import { Noti } from 'types/notiTypes'; -import { openNotiBarState } from 'utils/recoil/layout'; -import { errorState } from 'utils/recoil/error'; -import { instance } from 'utils/axios'; -import NotiItem from './NotiItem'; -import styles from 'styles/Layout/NotiBar.module.scss'; - -export default function NotiBar() { - const [noti, setNoti] = useState([]); - const [clickReloadNoti, setClickReloadNoti] = useState(false); - const [spinReloadButton, setSpinReloadButton] = useState(false); - const resetOpenNotiBar = useResetRecoilState(openNotiBarState); - const setError = useSetRecoilState(errorState); - - useEffect(() => { - getNotiHandler(); - }, []); - - useEffect(() => { - if (clickReloadNoti) getNotiHandler(); - }, [clickReloadNoti]); - - const getNotiHandler = async () => { - if (clickReloadNoti) { - setSpinReloadButton(true); - setTimeout(() => { - setSpinReloadButton(false); - }, 1000); - } - try { - const res = await instance.get(`/pingpong/notifications`); - setNoti(res?.data.notifications); - setClickReloadNoti(false); - } catch (e) { - setError('JB04'); - } - }; - - return ( -
-
e.stopPropagation()}> - - {noti.length ? ( - <> -
- - -
-
- {noti.map((data: Noti) => ( - - ))} -
- - ) : ( -
- <> - -
💭 새로운 알림이 없습니다!
-
- )} -
-
- ); -} - -interface ReloadNotiButtonProps { - spinReloadButton: boolean; - setClickReloadNoti: React.Dispatch>; -} - -function ReloadNotiButton({ - spinReloadButton, - setClickReloadNoti, -}: ReloadNotiButtonProps) { - return ( - - ); -} - -function DeleteAllButton() { - const resetOpenNotiBar = useResetRecoilState(openNotiBarState); - const setError = useSetRecoilState(errorState); - const allNotiDeleteHandler = async () => { - try { - await instance.delete(`/pingpong/notifications`); - alert('알림이 성공적으로 삭제되었습니다.'); - resetOpenNotiBar(); - } catch (e) { - setError('JB05'); - } - }; - return ( - - ); -} diff --git a/components/Layout/NotiBar/NotiBar.tsx b/components/Layout/NotiBar/NotiBar.tsx new file mode 100644 index 000000000..00fbb6b3c --- /dev/null +++ b/components/Layout/NotiBar/NotiBar.tsx @@ -0,0 +1,141 @@ +import { useContext } from 'react'; +import styles from 'styles/Layout/NotiBar.module.scss'; +import { HeaderContextState, HeaderContext } from '../HeaderContext'; +import NotiStateContext from './NotiContext'; +import { NotiContextState, NotiProvider } from './NotiContext'; +import NotiItem from './NotiItem'; +import { useSetRecoilState } from 'recoil'; +import { errorState } from 'utils/recoil/error'; +import { instance } from 'utils/axios'; +import { Noti } from 'types/notiTypes'; +import NotiEmptyEmoji from 'public/image/noti_empty.svg'; + +export default function NotiBar() { + const HeaderState = useContext(HeaderContext); + + return ( +
HeaderState?.resetOpenNotiBarState()} + > +
e.stopPropagation()}> +
+ +
+ +
+ + +
+ +
+
+
+
+
+
+
+ ); +} + +function NotiMain() { + const NotiContext = useContext(NotiProvider); + + return NotiContext?.noti.length ? : ; +} + +function NotiExist() { + const NotiContext = useContext(NotiProvider); + + return ( +
+ {NotiContext?.noti.map((data: Noti) => + data.message ? ( + + ) : ( + + ) + )} +
+ ); +} + +function NotiEmpty() { + return ( +
+
새로운 알림이 없습니다!
+
+
+
+
+
+
+ +
+
+ ); +} + +function ReloadNotiButton() { + const HeaderState = useContext(HeaderContext); + const NotiContext = useContext(NotiProvider); + const reloadButtonStyle = NotiContext?.spinReloadButton + ? styles.spinReloadButton + : styles.reloadButton; + + const clickReloadNoti = async () => { + await HeaderState?.putNotiHandler(); + NotiContext?.setClickReloadNoti?.(true); + }; + + return ( + + ); +} + +function DeleteAllButton() { + const HeaderState = useContext(HeaderContext); + const setError = useSetRecoilState(errorState); + const allNotiDeleteHandler = async () => { + try { + await instance.delete(`/pingpong/notifications`); + alert('알림이 성공적으로 삭제되었습니다.'); + HeaderState?.resetOpenNotiBarState(); + } catch (e) { + setError('JB05'); + } + }; + return ( + + ); +} + +interface NullNotiProps { + createdAt: string; + isChecked: boolean; +} + +function NullNoti({ createdAt, isChecked }: NullNotiProps) { + const date = createdAt.slice(5).replace('T', ' '); + + return ( +
+ 실패 +
알림을 불러올 수 없습니다!
+
{date}
+
+ ); +} diff --git a/components/Layout/NotiBar/NotiContext.tsx b/components/Layout/NotiBar/NotiContext.tsx new file mode 100644 index 000000000..7daf111cc --- /dev/null +++ b/components/Layout/NotiBar/NotiContext.tsx @@ -0,0 +1,65 @@ +import { Dispatch, SetStateAction, createContext } from 'react'; +import { useState, useEffect } from 'react'; +import { Noti } from 'types/notiTypes'; +import useReloadHandler from 'hooks/useReloadHandler'; +import useAxiosGet from 'hooks/useAxiosGet'; + +export interface NotiContextState { + noti: Noti[]; + spinReloadButton: boolean; + clickReloadNoti: boolean; + setClickReloadNoti: Dispatch>; +} + +export const NotiProvider = createContext(null); + +interface NotiStateContextProps { + children: React.ReactNode; +} + +const NotiStateContext = (props: NotiStateContextProps) => { + const [noti, setNoti] = useState([]); + const [clickReloadNoti, setClickReloadNoti] = useState(false); + const [spinReloadButton, setSpinReloadButton] = useState(false); + + const getNotiHandler = useAxiosGet({ + url: '/pingpong/notifications', + setState: (data) => { + setNoti(data.notifications); + }, + err: 'JB04', + type: 'setError', + }); + + const reloadNotiHandler = useReloadHandler({ + setSpinReloadButton: setSpinReloadButton, + setState: setClickReloadNoti, + state: false, + }); + + useEffect(() => { + getNotiHandler(); + }, []); + + useEffect(() => { + if (clickReloadNoti) { + reloadNotiHandler(); + getNotiHandler(); + } + }, [clickReloadNoti]); + + const NotiState: NotiContextState = { + noti: noti, + spinReloadButton: spinReloadButton, + clickReloadNoti: clickReloadNoti, + setClickReloadNoti: setClickReloadNoti, + }; + + return ( + + {props.children} + + ); +}; + +export default NotiStateContext; diff --git a/components/Layout/NotiBar/NotiItem.tsx b/components/Layout/NotiBar/NotiItem.tsx new file mode 100644 index 000000000..d09b1e739 --- /dev/null +++ b/components/Layout/NotiBar/NotiItem.tsx @@ -0,0 +1,109 @@ +import Link from 'next/link'; +import { Noti } from 'types/notiTypes'; +import styles from 'styles/Layout/NotiItem.module.scss'; +import { HeaderContextState, HeaderContext } from '../HeaderContext'; +import { useContext } from 'react'; +import { BsCheckLg } from 'react-icons/bs'; + +interface NotiItemProps { + data: Noti; +} + +export default function NotiItem({ data }: NotiItemProps) { + const date = data.createdAt.slice(5, -3).replace('T', ' '); + const { type, message, isChecked } = data; + + const noti: { + [key: string]: { [key: string]: string | JSX.Element | undefined }; + } = { + IMMINENT: { + style: styles.imminent, + content: MakeImminentContent(message), + }, + ANNOUNCE: { + style: styles.announcement, + content: MakeAnnounceContent(message), + }, + MATCHED: { + style: styles.matched, + content: message, + }, + CANCELEDBYMAN: { + style: styles.canceldByMan, + content: message, + }, + }; + + const notiWrapperStyle = isChecked ? styles.readWrap : styles.unreadWrap; + const notiContentStyle = + type === 'IMMINENT' + ? styles.imminent + : type === 'MATCHED' + ? styles.matched + : type === 'CANCELEDBYMAN' + ? styles.canceledByMan + : styles.announcement; + + return ( +
+
+ {noti[type].content} +
+
+ {date}  + {isChecked ? : <>} +
+
+ ); +} + +function MakeAnnounceContent(message: string | undefined) { + if (message && !message.includes('https')) return message; + const url = message?.split('https')[1]; + const content = message?.split('https')[0].split('=>')[0]; + const linkedContent = message?.split('https')[0].split('=>')[1]; + return ( + <> + {content} {'=>'} + window.open(`https${url}`)}>{linkedContent} + + ); +} + +function MakeImminentContent(message: string) { + const HeaderState = useContext(HeaderContext); + + const parseEnemyIdMessage = ( + message: string + ): { + enemyId: string[]; + enemyMessage: string; + } => { + const regList = //; + const regId = /^[a-zA-Z0-9]*$/; + const parseList = message.split(regList).filter((str) => str !== ''); + const enemyId = parseList.filter((id) => regId.test(id) !== false); + const enemyMessage = parseList.filter((id) => regId.test(id) === false)[0]; + return { enemyId, enemyMessage }; + }; + + const enemyId = parseEnemyIdMessage(message).enemyId; + const enemyMessage = parseEnemyIdMessage(message).enemyMessage; + + return enemyId.length ? ( +
+ {enemyId.map((intraId: string, i: number) => ( + HeaderState?.resetOpenNotiBarState()} + > + {intraId} + {enemyId && i < enemyId.length - 1 ? ', ' : ''} + + ))} + {enemyMessage} +
+ ) : ( + <> + ); +} diff --git a/components/Layout/NotiItem.tsx b/components/Layout/NotiItem.tsx deleted file mode 100644 index 66256d4ef..000000000 --- a/components/Layout/NotiItem.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import Link from 'next/link'; -import { useSetRecoilState } from 'recoil'; -import { Noti } from 'types/notiTypes'; -import { openNotiBarState } from 'utils/recoil/layout'; -import { gameTimeToString } from 'utils/handleTime'; -import styles from 'styles/Layout/NotiItem.module.scss'; - -interface NotiItemProps { - data: Noti; -} - -export default function NotiItem({ data }: NotiItemProps) { - const date = data.createdAt.slice(5, 16).replace('T', ' '); - const noti: { - [key: string]: { [key: string]: string | JSX.Element | undefined }; - } = { - imminent: { - title: '경기 준비', - content: MakeImminentContent(data.enemyTeam, data.time, data.createdAt), - }, - announce: { title: '공 지', content: MakeAnnounceContent(data.message) }, - matched: { - title: '매칭 성사', - content: makeContent(data.time, '에 신청한 매칭이 성사되었습니다.'), - }, - canceledbyman: { - title: '매칭 취소', - content: makeContent( - data.time, - '에 신청한 매칭이 상대에 의해 취소되었습니다.' - ), - }, - canceledbytime: { - title: '매칭 취소', - content: makeContent( - data.time, - '에 신청한 매칭이 상대 없음으로 취소되었습니다.' - ), - }, - }; - return ( -
- {noti[data.type].title} -
{noti[data.type].content}
-
{date}
-
- ); -} - -function makeContent(time: string | undefined, message: string) { - if (time) return gameTimeToString(time) + message; -} - -function MakeAnnounceContent(message: string | undefined) { - if (message && !message.includes('https')) return message; - const url = message?.split('https')[1]; - const content = message?.split('https')[0].split('=>')[0]; - const linkedContent = message?.split('https')[0].split('=>')[1]; - return ( - <> - {content} {'=>'} - window.open(`https${url}`)}>{linkedContent} - - ); -} - -function MakeImminentContent( - enemyTeam: string[] | undefined, - time: string | undefined, - createdAt: string -) { - const setOpenNotiBar = useSetRecoilState(openNotiBarState); - const makeEnemyUsers = (enemyTeam: string[]) => { - return enemyTeam.map((intraId: string, i: number) => ( - setOpenNotiBar(false)}> - {intraId} - {enemyTeam && i < enemyTeam.length - 1 ? ', ' : ''} - - )); - }; - const makeImminentMinute = (gameTime: string, createdAt: string) => - Math.floor( - (Number(new Date(gameTime)) - Number(new Date(createdAt))) / 60000 - ); - return ( - <> - {enemyTeam && time && ( - <> - {makeEnemyUsers(enemyTeam)}님과 경기{' '} - {makeImminentMinute(time, createdAt)}분 전 입니다. 서두르세요! - - )} - - ); -} diff --git a/components/LoginChecker.tsx b/components/LoginChecker.tsx index 4e4af1e81..3b209dd1d 100644 --- a/components/LoginChecker.tsx +++ b/components/LoginChecker.tsx @@ -8,30 +8,13 @@ import Login from 'pages/login'; import WelcomeModal from './modal/event/WelcomeModal'; import styles from 'styles/Layout/Layout.module.scss'; +import useLoginCheck from 'hooks/Login/useLoginCheck'; interface LoginCheckerProps { children: React.ReactNode; } export default function LoginChecker({ children }: LoginCheckerProps) { - const [isLoading, setIsLoading] = useState(true); - const [loggedIn, setLoggedIn] = useRecoilState(loginState); - const [firstVisited, setFirstVisited] = useRecoilState(firstVisitedState); - - const router = useRouter(); - const presentPath = router.asPath; - const token = presentPath.split('?token=')[1]; - - useEffect(() => { - if (token) { - localStorage.setItem('42gg-token', token); - setFirstVisited(true); - router.replace(`/`); - } - if (localStorage.getItem('42gg-token')) { - setLoggedIn(true); - } - setIsLoading(false); - }, []); + const [isLoading, loggedIn, firstVisited] = useLoginCheck(); return loggedIn ? ( <> @@ -43,10 +26,4 @@ export default function LoginChecker({ children }: LoginCheckerProps) {
{!isLoading && }
); - - // return ( - //
- //
{!isLoading && }
- //
- // ); } diff --git a/components/Pagination.tsx b/components/Pagination.tsx index ee07dcf4b..00f539807 100644 --- a/components/Pagination.tsx +++ b/components/Pagination.tsx @@ -9,7 +9,7 @@ import { interface GreetingProps { curPage: number | undefined; totalPages: number | undefined; - pageChangeHandler: (page: number) => void; + pageChangeHandler: (pageNumber: number) => void; } function PageNation({ @@ -29,7 +29,7 @@ function PageNation({ lastPageText={} prevPageText={} nextPageText={} - onChange={pageChangeHandler} + onChange={(page) => pageChangeHandler(page)} /> )} diff --git a/components/PlayerImage.tsx b/components/PlayerImage.tsx index 73692f01a..fd00f051f 100644 --- a/components/PlayerImage.tsx +++ b/components/PlayerImage.tsx @@ -28,6 +28,7 @@ export default function PlayerImage({ quality={`${size}`} unoptimized={imgError} onError={() => setImgError(true)} + priority={true} />
diff --git a/components/StyledButton.tsx b/components/StyledButton.tsx new file mode 100644 index 000000000..3833338ae --- /dev/null +++ b/components/StyledButton.tsx @@ -0,0 +1,25 @@ +import { MouseEventHandler } from 'react'; +import styles from 'styles/StyledButton.module.scss'; + +type StyledButtonProps = { + onClick: MouseEventHandler; + children: React.ReactNode; + width?: string; +}; + +export default function StyledButton({ + onClick, + children, + width, +}: StyledButtonProps) { + const buttonWidth = { + width: width || 'auto', + }; + return ( + + ); +} diff --git a/components/admin/announcement/AnnounceEdit.tsx b/components/admin/announcement/AnnounceEdit.tsx index c64449622..51b376884 100644 --- a/components/admin/announcement/AnnounceEdit.tsx +++ b/components/admin/announcement/AnnounceEdit.tsx @@ -33,21 +33,11 @@ export default function AnnounceEdit() { resetHandler(); }, []); - const resetHandler = async () => { - try { - const res = await instance.get(`/pingpong/announcement`); - setContent(res?.data.content); - } catch (e) { - alert(e); - } - }; - const postHandler = async () => { try { await instanceInManage.post(`/announcement`, { content, creatorIntraId: currentUserId, - createdTime: new Date(new Date().getTime() + koreaTimeOffset), }); setSnackbar({ toastName: `post request`, @@ -67,10 +57,7 @@ export default function AnnounceEdit() { const deleteHandler = async () => { try { - await instanceInManage.put(`/announcement`, { - deleterIntraId: currentUserId, - deletedTime: new Date(new Date().getTime() + koreaTimeOffset), - }); + await instanceInManage.delete(`/announcement/${currentUserId}`); setSnackbar({ toastName: `delete request`, severity: 'success', @@ -87,6 +74,15 @@ export default function AnnounceEdit() { } }; + const resetHandler = async () => { + try { + const res = await instance.get('/pingpong/announcement'); + setContent(res?.data.content); + } catch (e) { + alert(e); + } + }; + return (
diff --git a/components/admin/announcement/AnnounceList.tsx b/components/admin/announcement/AnnounceList.tsx index 5d28753dd..ee5484fa4 100644 --- a/components/admin/announcement/AnnounceList.tsx +++ b/components/admin/announcement/AnnounceList.tsx @@ -24,20 +24,20 @@ const Quill = dynamic(() => import('react-quill'), { const tableTitle: { [key: string]: string } = { content: '내용', - createdTime: '생성일', + createdAt: '생성일', creatorIntraId: '생성한 사람', - deletedTime: '삭제일', + deletedAt: '삭제일', deleterIntraId: '삭제한 사람', - isDel: '삭제 여부', + modifiedAt: '수정 여부', }; interface IAnnouncement { content: string; creatorIntraId: string; deleterIntraId: string; - deletedTime: Date; - createdTime: Date; - isDel: boolean; + deletedAt: Date; + createdAt: Date; + modifiedAt: Date; } interface IAnnouncementTable { @@ -59,7 +59,7 @@ export default function AnnounceList() { const res = await instanceInManage.get( `/announcement?page=${currentPage}&size=5` ); - setAnnouncementInfo({ ...res.data }); + setAnnouncementInfo({ ...res.data, currentPage: currentPage }); } catch (e) { console.error('MS01'); } @@ -109,8 +109,9 @@ export default function AnnounceList() { className={styles.tableBodyItem} key={index} > - {columnName === 'createdTime' || - columnName === 'deletedTime' + {columnName === 'createdAt' || + columnName === 'deletedAt' || + columnName === 'modifiedAt' ? announcement[columnName as keyof IAnnouncement] ?.toString() .replace('T', ' ') diff --git a/components/admin/common/AdminSearchBar.tsx b/components/admin/common/AdminSearchBar.tsx index f3d7f4884..fa1f75137 100644 --- a/components/admin/common/AdminSearchBar.tsx +++ b/components/admin/common/AdminSearchBar.tsx @@ -1,12 +1,7 @@ -import { useEffect, useState, useRef, useCallback } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { instance } from 'utils/axios'; -import { errorState } from 'utils/recoil/error'; import { GoSearch } from 'react-icons/go'; import { IoIosCloseCircle } from 'react-icons/io'; import styles from 'styles/admin/common/AdminSearchBar.module.scss'; - -let timer: ReturnType; +import useSearchBar from 'hooks/useSearchBar'; const MAX_SEARCH_LENGTH = 15; @@ -15,48 +10,24 @@ export default function AdminSearchBar({ }: { initSearch: (intraId?: string) => void; }) { - const [keyword, setKeyword] = useState(''); - const [showDropDown, setShowDropDown] = useState(false); - const [searchResult, setSearchResult] = useState([]); - const setError = useSetRecoilState(errorState); - const searchBarRef = useRef(null); - - const getSearchResultHandler = useCallback(async () => { - try { - const res = await instance.get(`/pingpong/users/searches?q=${keyword}`); - setSearchResult(res?.data.users); - } catch (e) { - setError('MS02'); - } - }, [keyword, setError]); + const { + keyword, + setKeyword, + keywordHandler, + showDropDown, + setShowDropDown, + searchResult, + searchBarRef, + } = useSearchBar(); - useEffect(() => { - const checkId = /^[a-z|A-Z|0-9|-]+$/; - if (keyword === '' || (keyword.length && !checkId.test(keyword))) { - clearTimeout(timer); - setSearchResult([]); - } else if (checkId.test(keyword)) { - debounce(getSearchResultHandler, 500)(); + const adminhandleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + if (keyword === searchResult[0]) { + setShowDropDown(false); + event.currentTarget.blur(); + initSearch(keyword); + } } - }, [keyword, getSearchResultHandler]); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - const handleClickOutside = (event: MouseEvent) => { - if ( - searchBarRef.current && - !searchBarRef.current.contains(event.target as Node) - ) - setShowDropDown(false); - }; - - const keywordHandler = (event: React.ChangeEvent) => { - setKeyword(event.target.value); }; return ( @@ -64,6 +35,7 @@ export default function AdminSearchBar({ setShowDropDown(true)} placeholder='유저 검색하기' maxLength={MAX_SEARCH_LENGTH} @@ -114,12 +86,3 @@ export default function AdminSearchBar({
); } - -function debounce(callback: () => void, timeout: number) { - return () => { - clearTimeout(timer); - timer = setTimeout(() => { - callback(); - }, timeout); - }; -} diff --git a/components/admin/feedback/FeedbackTable.tsx b/components/admin/feedback/FeedbackTable.tsx index e135097be..11f19700d 100644 --- a/components/admin/feedback/FeedbackTable.tsx +++ b/components/admin/feedback/FeedbackTable.tsx @@ -22,7 +22,7 @@ const tableTitle: { [key: string]: string } = { intraId: 'intra ID', category: '종류', content: '내용', - createdTime: '생성일', + createdAt: '생성일', isSolved: '해결 여부', }; @@ -31,7 +31,7 @@ export interface IFeedback { intraId: string; category: number; // 1: bug, 2: suggestion, 3: question content: string; - createdTime: Date; + createdAt: Date; isSolved: boolean; } @@ -56,21 +56,21 @@ export default function FeedbackTable() { const getUserFeedbacks = useCallback(async () => { try { const res = await instanceInManage.get( - `/feedback/users?q=${intraId}&page=${currentPage}&size=10` + `/feedback?intraId=${intraId}&page=${currentPage}&size=10` ); setIntraId(intraId); setFeedbackInfo({ feedbackList: res.data.feedbackList.map((feedback: IFeedback) => { const { year, month, date, hour, min } = getFormattedDateToString( - new Date(feedback.createdTime) + new Date(feedback.createdAt) ); return { ...feedback, - createdTime: `${year}-${month}-${date} ${hour}:${min}`, + createdAt: `${year}-${month}-${date} ${hour}:${min}`, }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS04'); @@ -85,15 +85,15 @@ export default function FeedbackTable() { setFeedbackInfo({ feedbackList: res.data.feedbackList.map((feedback: IFeedback) => { const { year, month, date, hour, min } = getFormattedDateToString( - new Date(feedback.createdTime) + new Date(feedback.createdAt) ); return { ...feedback, - createdTime: `${year}-${month}-${date} ${hour}:${min}`, + createdAt: `${year}-${month}-${date} ${hour}:${min}`, }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS03'); @@ -165,9 +165,13 @@ export default function FeedbackTable() { - ) : value.toString().length > MAX_CONTENT_LENGTH ? ( + ) : (value?.toString() || '').length > + MAX_CONTENT_LENGTH ? (
- {value.toString().slice(0, MAX_CONTENT_LENGTH)} + {(value?.toString() || '').slice( + 0, + MAX_CONTENT_LENGTH + )} openDetailModal(feedback)} @@ -176,7 +180,7 @@ export default function FeedbackTable() {
) : ( - value.toString() + value?.toString() || '' )} ); diff --git a/components/admin/games/GamesTable.tsx b/components/admin/games/GamesTable.tsx index fa4af7d2b..6044b6607 100644 --- a/components/admin/games/GamesTable.tsx +++ b/components/admin/games/GamesTable.tsx @@ -2,9 +2,10 @@ import { useCallback, useEffect, useState } from 'react'; import PageNation from 'components/Pagination'; import { IGames, IGameLog } from 'types/admin/gameLogTypes'; import { instanceInManage } from 'utils/axios'; -import { getFormattedDateToString } from 'utils/handleTime'; +import { getFormattedDateToString, gameTimeToString } from 'utils/handleTime'; import AdminSearchBar from '../common/AdminSearchBar'; import styles from 'styles/admin/games/GamesTable.module.scss'; +import ModifyScoreForm from './ModifyScoreForm'; export default function GamesTable() { const [currentPage, setCurrentPage] = useState(1); @@ -12,7 +13,6 @@ export default function GamesTable() { const [gameInfo, setGameInfo] = useState({ gameLog: [], totalPage: 1, - currentPage: 1, }); const initSearch = useCallback((intraId?: string) => { @@ -23,7 +23,7 @@ export default function GamesTable() { const getAllGames = useCallback(async () => { try { const res = await instanceInManage.get( - `/games?season=0&page=${currentPage}&size=5` + `/games?page=${currentPage}&size=4` ); setGameInfo({ @@ -37,7 +37,6 @@ export default function GamesTable() { }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, }); } catch (e) { console.error('MS07'); @@ -47,7 +46,7 @@ export default function GamesTable() { const getUserGames = useCallback(async () => { try { const res = await instanceInManage.get( - `/games/users?q=${intraId}&page=${currentPage}&size=5` + `/games/users?intraId=${intraId}&page=${currentPage}&size=4` ); setGameInfo({ gameLog: res.data.gameLogList.map((game: IGameLog) => { @@ -60,7 +59,6 @@ export default function GamesTable() { }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, }); } catch (e) { console.error('MS08'); @@ -80,6 +78,8 @@ export default function GamesTable() {
{gameInfo.gameLog.map((game: IGameLog) => { + const { team1, team2 } = game; + const mode = game.mode.toUpperCase(); return (
{game.gameId}
@@ -87,21 +87,28 @@ export default function GamesTable() {
시작 날짜: {game.startAt.toLocaleString().split(' ')[0]}
-
게임 모드: {game.mode}
+
+ 시작 시간: {gameTimeToString(game.startAt)} +
+
게임 모드: {mode}
슬롯 시간: {game.slotTime}분
- {game.mode === 'Normal' - ? '' - : `Team 1 (${game.team1.score}) : Team 2 (${game.team2.score})`} + {mode === 'RANK' && ( + + )}
team1
team2
+
); })}
diff --git a/components/admin/games/ModifyScoreForm.tsx b/components/admin/games/ModifyScoreForm.tsx new file mode 100644 index 000000000..13b62bd0e --- /dev/null +++ b/components/admin/games/ModifyScoreForm.tsx @@ -0,0 +1,55 @@ +import { useSetRecoilState } from 'recoil'; +import { modalState } from 'utils/recoil/modal'; +import { ModifyScoreType } from 'types/admin/gameLogTypes'; +import styles from 'styles/admin/games/GamesTable.module.scss'; + +export default function ModifyScoreForm({ + gameId, + team1, + team2, +}: ModifyScoreType) { + const setModal = useSetRecoilState(modalState); + + const handleModifyScore = (event: React.FormEvent) => { + event.preventDefault(); + const form = new FormData(event.currentTarget); + const team1Score = Number(form.get('team1Score')); + const team2Score = Number(form.get('team2Score')); + if (team1Score !== null && team2Score !== null) { + setModal({ + modalName: 'ADMIN-MODIFY_SCORE', + ModifyScore: { + gameId: gameId, + team1: { ...team1, score: team1Score, win: team1Score > team2Score }, + team2: { ...team2, score: team2Score, win: team2Score > team1Score }, + }, + }); + } + }; + return ( +
+ 게임 점수: + + : + +
+ ); +} diff --git a/components/admin/notification/CreateNotiButton.tsx b/components/admin/notification/CreateNotiButton.tsx index b09fac595..db95ebf95 100644 --- a/components/admin/notification/CreateNotiButton.tsx +++ b/components/admin/notification/CreateNotiButton.tsx @@ -8,17 +8,11 @@ export default function CreateNotiButton() { return ( <>
-
diff --git a/components/admin/notification/NotificationTable.tsx b/components/admin/notification/NotificationTable.tsx index da969e1ca..374f32a72 100644 --- a/components/admin/notification/NotificationTable.tsx +++ b/components/admin/notification/NotificationTable.tsx @@ -22,20 +22,18 @@ const tableTitle: { [key: string]: string } = { notiId: 'ID', roleType: '권한', intraId: 'Intra ID', - slotId: '슬롯 ID', type: '종류', message: '내용', - createdTime: '생성일', + createdAt: '생성일', isChecked: '확인 여부', }; interface INotification { notiId: number; intraId: string; - slotId: number; message: string; type: string; - createdTime: Date; + createdAt: Date; isChecked: boolean; } @@ -60,21 +58,21 @@ export default function NotificationTable() { const getUserNotifications = useCallback(async () => { try { const res = await instanceInManage.get( - `/notifications?q=${intraId}&page=${currentPage}&size=10` + `/notifications?intraId=${intraId}&page=${currentPage}&size=10` ); setIntraId(intraId); setNotificationInfo({ notiList: res.data.notiList.map((noti: INotification) => { const { year, month, date, hour, min } = getFormattedDateToString( - new Date(noti.createdTime) + new Date(noti.createdAt) ); return { ...noti, - createdTime: `${year}-${month}-${date} ${hour}:${min}`, + createdAt: `${year}-${month}-${date} ${hour}:${min}`, }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS00'); @@ -95,15 +93,15 @@ export default function NotificationTable() { setNotificationInfo({ notiList: res.data.notiList.map((noti: INotification) => { const { year, month, date, hour, min } = getFormattedDateToString( - new Date(noti.createdTime) + new Date(noti.createdAt) ); return { ...noti, - createdTime: `${year}-${month}-${date} ${hour}:${min}`, + createdAt: `${year}-${month}-${date} ${hour}:${min}`, }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS01'); @@ -130,8 +128,10 @@ export default function NotificationTable() {
알림 관리 - - +
+ + +
@@ -154,11 +154,11 @@ export default function NotificationTable() { (columnName: string, index: number) => { return ( - {columnName === 'createdTime' ? ( + {columnName === 'createdAt' ? (
- {notification.createdTime.toString().slice(0, 4)} + {notification.createdAt.toString().slice(0, 4)}
- {notification.createdTime.toString().slice(5, 10)} + {notification.createdAt.toString().slice(5, 10)}
) : notification[ columnName as keyof INotification diff --git a/components/admin/penalty/PenaltyTable.tsx b/components/admin/penalty/PenaltyTable.tsx index 390502d13..2f1fcaaf7 100644 --- a/components/admin/penalty/PenaltyTable.tsx +++ b/components/admin/penalty/PenaltyTable.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilState } from 'recoil'; import { Table, TableBody, @@ -18,6 +18,7 @@ import styles from 'styles/admin/penalty/PenaltyTable.module.scss'; import { getFormattedDateToString } from 'utils/handleTime'; interface IPenalty { + penaltyId: number; intraId: string; reason: string; releaseTime: Date; @@ -44,10 +45,11 @@ export default function PenaltyTable() { }); const [currentPage, setCurrentPage] = useState(1); const [intraId, setIntraId] = useState(''); - const setModal = useSetRecoilState(modalState); + const [current, setCurrent] = useState(true); + const [modal, setModal] = useRecoilState(modalState); - const handleButtonAction = (intraId: string) => - setModal({ modalName: 'ADMIN-PENALTY_DELETE', intraId }); + const handleButtonAction = (intraId: string, penaltyId: number) => + setModal({ modalName: 'ADMIN-PENALTY_DELETE', intraId, penaltyId }); const initSearch = useCallback((intraId?: string) => { setIntraId(intraId || ''); @@ -57,7 +59,7 @@ export default function PenaltyTable() { const getUserPenalty = useCallback(async () => { try { const res = await instanceInManage.get( - `/penalty/users?q=${intraId}&page=${currentPage}&size=10` + `/penalty?intraId=${intraId}&page=${currentPage}&size=10¤t=${current}` ); setIntraId(intraId); setPenaltyInfo({ @@ -71,17 +73,17 @@ export default function PenaltyTable() { }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS07'); } - }, [intraId, currentPage]); + }, [intraId, currentPage, current]); const getAllUserPenalty = useCallback(async () => { try { const res = await instanceInManage.get( - `/penalty/users?page=${currentPage}&size=10` + `/penalty?page=${currentPage}&size=10¤t=${current}` ); setIntraId(''); setPenaltyInfo({ @@ -95,22 +97,30 @@ export default function PenaltyTable() { }; }), totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS08'); } - }, [currentPage]); + }, [currentPage, current]); useEffect(() => { intraId ? getUserPenalty() : getAllUserPenalty(); - }, [intraId, getUserPenalty, getAllUserPenalty]); + }, [intraId, getUserPenalty, getAllUserPenalty, modal]); return ( <>
- 패널티 관리 + +
패널티 관리
+ +
@@ -129,34 +139,44 @@ export default function PenaltyTable() { {penaltyInfo.penaltyList.length > 0 ? ( - penaltyInfo.penaltyList.map((penalty: IPenalty) => ( - - {tableFormat['penalty'].columns.map( - (columnName: string) => ( - - {columnName !== 'etc' - ? penalty[columnName as keyof IPenalty]?.toString() - : tableFormat['penalty'].etc?.value.map( - (buttonName: string) => ( - - ) - )} - - ) - )} - - )) + penaltyInfo.penaltyList.map( + (penalty: IPenalty, index: number) => ( + + {tableFormat['penalty'].columns.map( + (columnName: string) => ( + + {columnName !== 'etc' + ? penalty[ + columnName as keyof IPenalty + ]?.toString() + : tableFormat['penalty'].etc?.value.map( + (buttonName: string) => + current ? ( + + ) : ( + <> + ) + )} + + ) + )} + + ) + ) ) : ( 비어있습니다 diff --git a/components/admin/season/SeasonCreate.tsx b/components/admin/season/SeasonCreate.tsx index 023ed495e..88371008d 100644 --- a/components/admin/season/SeasonCreate.tsx +++ b/components/admin/season/SeasonCreate.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { instanceInManage } from 'utils/axios'; import { toastState } from 'utils/recoil/toast'; +import { ISeasonEditInfo } from 'types/seasonTypes'; import { Paper, Table, @@ -13,21 +14,12 @@ import { } from '@mui/material'; import styles from 'styles/admin/season/SeasonCreate.module.scss'; -interface SeasonCreateInfo { - seasonName: string; - startTime: Date; - startPpp: number; - pppGap: number; - seasonMode: string; -} - export default function SeasonCreate() { - const [seasonInfo, setSeasonInfo] = useState({ + const [seasonInfo, setSeasonInfo] = useState({ seasonName: '', startTime: new Date(), startPpp: 0, pppGap: 0, - seasonMode: 'BOTH', }); const setSnackBar = useSetRecoilState(toastState); @@ -43,7 +35,7 @@ export default function SeasonCreate() { const postHandler = async () => { try { - await instanceInManage.post(`/season`, seasonInfo); + await instanceInManage.post(`/seasons`, seasonInfo); setSnackBar({ toastName: 'Season Create', severity: 'success', @@ -103,13 +95,6 @@ export default function SeasonCreate() { onChange={inputChangeHandler} /> - - -
diff --git a/components/admin/season/SeasonList.tsx b/components/admin/season/SeasonList.tsx index eed2a977d..956e5585e 100644 --- a/components/admin/season/SeasonList.tsx +++ b/components/admin/season/SeasonList.tsx @@ -1,9 +1,10 @@ -import { SyntheticEvent, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { tableFormat } from 'constants/admin/table'; import { instanceInManage } from 'utils/axios'; import { modalState } from 'utils/recoil/modal'; import { toastState } from 'utils/recoil/toast'; +import { ISeason, ISeasonList } from 'types/seasonTypes'; import { Paper, Table, @@ -12,48 +13,21 @@ import { TableContainer, TableHead, TableRow, - Tabs, - Tab, } from '@mui/material'; import styles from 'styles/admin/season/SeasonList.module.scss'; -interface ISeason { - seasonId: number; - seasonMode: string; - seasonName: string; - startTime: Date; - endTime: Date; - startPpp: number; - pppGap: number; - status: number; -} - -interface ISeasonTable { - mode: string; - seasonList: ISeason[]; -} - -const VAL_SEASON_MODE: { [key: number]: string } = { - 0: 'both', - 1: 'rank', - 2: 'normal', -}; export default function SeasonList() { const setModal = useSetRecoilState(modalState); - const [seasonList, setSeasonList] = useState({ - mode: '', + const [useSeasonList, setUseSeasonList] = useState({ seasonList: [], }); - const [tabVal, setTabVal] = useState(0); - const [selectedSeasonMode, setSelectedSeasonMode] = useState( - VAL_SEASON_MODE[tabVal] - ); + const setSnackBar = useSetRecoilState(toastState); - const getSeasonList = async (mode: string) => { + const getSeasonList = async () => { try { - const res = await instanceInManage.get(`/season/${mode}`); - setSeasonList({ ...res.data }); + const res = await instanceInManage.get(`/seasons`); + setUseSeasonList({ ...res.data }); } catch (e: any) { setSnackBar({ toastName: 'Get Error', @@ -65,12 +39,12 @@ export default function SeasonList() { }; useEffect(() => { - getSeasonList(selectedSeasonMode); - }, [selectedSeasonMode]); + getSeasonList(); + }, []); const deleteHandler = async (deleteId: number) => { try { - await instanceInManage.delete(`/season/${deleteId}`); + await instanceInManage.delete(`/seasons/${deleteId}`); setSnackBar({ toastName: 'Season Delete', severity: 'success', @@ -89,18 +63,6 @@ export default function SeasonList() { return (
- { - setTabVal(newVal); - setSelectedSeasonMode(VAL_SEASON_MODE[newVal]); - }} - > - - - - - @@ -113,20 +75,23 @@ export default function SeasonList() { - {seasonList.seasonList.map((seasonL: ISeason, index: number) => ( + {useSeasonList.seasonList.map((seasonL: ISeason, index: number) => ( {tableFormat['season'].columns.map( - (columnName, index: number) => ( - + (columnName, innerIndex: number) => ( + {columnName === 'startTime' || columnName === 'endTime' ? ( seasonL[columnName as keyof ISeason] ?.toString() .replace('T', ' ') ) : columnName === 'edit' ? ( - seasonL['status'] === 0 ? ( + seasonL['status'] === 'PAST' ? (
과거 시즌입니다 !
- ) : seasonL['status'] === 1 ? ( + ) : seasonL['status'] === 'CURRENT' ? (
- ) : seasonL['status'] === 2 ? ( + ) : seasonL['status'] === 'FUTURE' ? (
diff --git a/components/admin/users/UserManagementTable.tsx b/components/admin/users/UserManagementTable.tsx index dfa6b2ad1..dba277769 100644 --- a/components/admin/users/UserManagementTable.tsx +++ b/components/admin/users/UserManagementTable.tsx @@ -49,14 +49,10 @@ export default function UserManagementTable() { const buttonList: string[] = [styles.detail, styles.penalty]; - const handleButtonAction = ( - buttonName: string, - userId: number, - intraId: string - ) => { + const handleButtonAction = (buttonName: string, intraId: string) => { switch (buttonName) { case '자세히': - setModal({ modalName: 'ADMIN-PROFILE', userId }); + setModal({ modalName: 'ADMIN-PROFILE', intraId }); break; case '패널티 부여': setModal({ modalName: 'ADMIN-PENALTY', intraId }); @@ -75,7 +71,7 @@ export default function UserManagementTable() { setUserManagements({ userInfoList: res.data.userSearchAdminDtos, totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS06'); @@ -85,12 +81,12 @@ export default function UserManagementTable() { const getUserInfo = useCallback(async () => { try { const res = await instanceInManage.get( - `/users?q=${intraId}&page=${currentPage}` + `/users?intraId=${intraId}&page=${currentPage}` ); setUserManagements({ userInfoList: res.data.userSearchAdminDtos, totalPage: res.data.totalPage, - currentPage: res.data.currentPage, + currentPage: currentPage, }); } catch (e) { console.error('MS05'); @@ -143,7 +139,6 @@ export default function UserManagementTable() { onClick={() => handleButtonAction( buttonName, - userInfo.id, userInfo.intraId ) } diff --git a/components/error/Error.tsx b/components/error/Error.tsx index 0581a8d0d..0b991c7af 100644 --- a/components/error/Error.tsx +++ b/components/error/Error.tsx @@ -1,21 +1,10 @@ -import { useRouter } from 'next/router'; -import React, { useEffect } from 'react'; -import { useRecoilState } from 'recoil'; -import { errorState } from 'utils/recoil/error'; +import React from 'react'; +import useErrorPage from 'hooks/error/useErrorPage'; import styles from 'styles/Error.module.scss'; +import ErrorEmoji from 'public/image/noti_empty.svg'; export default function ErrorPage() { - const [error, setError] = useRecoilState(errorState); - const router = useRouter(); - - useEffect(() => { - router.replace(`/`); - }, []); - - const goHome = () => { - setError(''); - router.push('/'); - }; + const { error, goHome } = useErrorPage(); return (
@@ -26,6 +15,14 @@ export default function ErrorPage() { ? '잘못된 요청입니다!' : '데이터 요청에 실패하였습니다.'}
({error})
+
+
+
+
+
+
+ +
diff --git a/components/game/GameResult.tsx b/components/game/GameResult.tsx index fd804c79e..5d6593901 100644 --- a/components/game/GameResult.tsx +++ b/components/game/GameResult.tsx @@ -1,8 +1,7 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { QueryClient, QueryClientProvider } from 'react-query'; import { SeasonMode } from 'types/mainType'; +import useGameResult from 'hooks/game/useGameResult'; import GameResultList from 'components/game/GameResultList'; +import { QueryClient, QueryClientProvider } from 'react-query'; interface GameResultProps { mode?: SeasonMode; @@ -10,37 +9,13 @@ interface GameResultProps { } export default function GameResult({ mode, season }: GameResultProps) { - const [path, setPath] = useState(''); const queryClient = new QueryClient(); - const router = useRouter(); - const asPath = router.asPath; - const intraId = router.query.intraId; - - const makePath = () => { - if (asPath === '/' || asPath.includes('token')) { - setPath(`/pingpong/games?count=${3}&status=${'live'}&gameId=`); - return; - } - const userQuery = intraId ? `/users/${intraId}` : ''; - const seasonQuery = mode === 'rank' && `season=${season}`; - const modeQuery = mode && mode !== 'both' ? `mode=${mode}` : ''; - const countQuery = router.pathname === '/users/detail' && `count=${5}`; - const query = [modeQuery, seasonQuery, countQuery, 'gameId='] - .filter((item) => item) - .join('&'); - setPath(`/pingpong/games${userQuery}?${query}`); - return; - }; - - useEffect(() => { - makePath(); - }, [asPath, intraId, mode, season]); - + const path = useGameResult({ mode: mode, season: season }); return (
{path && ( - + )}
diff --git a/components/game/GameResultList.tsx b/components/game/GameResultList.tsx index 61edfa978..7883063a7 100644 --- a/components/game/GameResultList.tsx +++ b/components/game/GameResultList.tsx @@ -1,38 +1,26 @@ -import { useRouter } from 'next/router'; -import { useEffect, useState } from 'react'; -import { useRecoilState } from 'recoil'; +import React from 'react'; +import { FaChevronDown } from 'react-icons/fa'; import { Game } from 'types/gameTypes'; -import InfScroll from 'utils/infinityScroll'; -import { clickedGameItemState } from 'utils/recoil/game'; import GameResultEmptyItem from './GameResultEmptyItem'; import GameResultBigItem from './big/GameResultBigItem'; import GameResultSmallItem from './small/GameResultSmallItem'; import styles from 'styles/game/GameResultItem.module.scss'; +import useGameResultList from 'hooks/game/useGameResultList'; +import { SeasonMode } from 'types/mainType'; interface GameResultListProps { path: string; + radioMode?: SeasonMode; } -export default function GameResultList({ path }: GameResultListProps) { - const { data, fetchNextPage, status, remove, refetch } = InfScroll(path); - const [isLast, setIsLast] = useState(false); - const [clickedGameItem, setClickedGameItem] = - useRecoilState(clickedGameItemState); - const pathName = useRouter().pathname; +export default function GameResultList({ + path, + radioMode, +}: GameResultListProps) { + const { data, status, fetchNextPage, isLast, clickedGameItem, pathName } = + useGameResultList(path); - useEffect(() => { - remove(); - refetch(); - }, [path]); - - useEffect(() => { - if (status === 'success') { - const gameList = data?.pages; - if (gameList.length === 1 && gameList[0].games.length) - setClickedGameItem(gameList[0].games[0].gameId); - setIsLast(gameList[gameList.length - 1].isLast); - } - }, [data]); + const isGamePage = pathName === '/game'; if (status === 'loading') return ; @@ -40,28 +28,39 @@ export default function GameResultList({ path }: GameResultListProps) { return ; return ( -
+
{status === 'success' && ( <> - {data?.pages.map((gameList, index) => ( -
- {gameList.games.map((game: Game) => { + {data?.pages.map((gameList, pageIndex) => ( + + {gameList.games.map((game: Game, index) => { + const type = Number.isInteger(index / 2) ? 'LIGHT' : 'DARK'; return clickedGameItem === game.gameId ? ( - + ) : ( - + ); })} -
+ ))} - {pathName === '/game' && !isLast && ( -
- fetchNextPage()} - /> -
+ {isGamePage && !isLast && ( + )} )} diff --git a/components/game/GameScore.ts b/components/game/GameScore.ts new file mode 100644 index 000000000..69961147a --- /dev/null +++ b/components/game/GameScore.ts @@ -0,0 +1,18 @@ +import { GameStatus, GameMode } from 'types/gameTypes'; + +export default function gameScore( + type: 'BIG' | 'SMALL', + mode: GameMode, + status: GameStatus, + scoreTeam1?: number, + scoreTeam2?: number +) { + let score = ''; + if (mode === 'RANK') { + if (status === 'END') score = `${scoreTeam1} : ${scoreTeam2}`; + else score = 'VS'; + } else { + score = type === 'BIG' ? 'VS' : '일반전'; + } + return score; +} diff --git a/components/game/big/GameResultBigItem.tsx b/components/game/big/GameResultBigItem.tsx index 6958efbe8..dac02fda6 100644 --- a/components/game/big/GameResultBigItem.tsx +++ b/components/game/big/GameResultBigItem.tsx @@ -5,30 +5,39 @@ import { clickedGameItemState } from 'utils/recoil/game'; import GameResultBigScore from 'components/game/big/GameResultBigScore'; import GameResultBigTeam from 'components/game/big/GameResultBigTeam'; import styles from 'styles/game/GameResultItem.module.scss'; +import { SeasonMode } from 'types/mainType'; interface GameResultBigItemProps { game: Game; + radioMode?: SeasonMode; + zIndexList: boolean; } -function GameResultBigItem({ game }: GameResultBigItemProps) { +function GameResultBigItem({ + game, + zIndexList, + radioMode, +}: GameResultBigItemProps) { const { mode, team1, team2, status, time, gameId } = game; const setClickedItemId = useSetRecoilState(clickedGameItemState); return (
setClickedItemId(gameId)} id={String(gameId)} + className={`${styles['bigItemContainer']} ${ + zIndexList ? styles['zIndexList'] : '' + } ${radioMode ? styles[radioMode.toLowerCase()] : ''}`} > - + - +
); } diff --git a/components/game/big/GameResultBigScore.tsx b/components/game/big/GameResultBigScore.tsx index 3cd200db3..7c1d0e4df 100644 --- a/components/game/big/GameResultBigScore.tsx +++ b/components/game/big/GameResultBigScore.tsx @@ -1,12 +1,16 @@ import { getTimeAgo } from 'utils/handleTime'; +import { GameStatus, GameMode } from 'types/gameTypes'; +import { SeasonMode } from 'types/mainType'; import styles from 'styles/game/GameResultItem.module.scss'; +import gameScore from '../GameScore'; interface GameResultBigScoreProps { - mode: string; - status: string; + mode: GameMode; + status: GameStatus; time: string; scoreTeam1?: number; scoreTeam2?: number; + radioMode?: SeasonMode; } export default function GameResultBigScore({ @@ -15,22 +19,28 @@ export default function GameResultBigScore({ time, scoreTeam1, scoreTeam2, + radioMode, }: GameResultBigScoreProps) { + const score = gameScore('BIG', mode, status, scoreTeam1, scoreTeam2); return (
- {makeScoreStatus(status, time)} -
- {mode === 'normal' ? 'VS' : `${scoreTeam1} : ${scoreTeam2}`} -
+ +
{score}
); } -function makeScoreStatus(status: string, time: string) { +type scoreStatusProps = { + status: GameStatus; + time: string; + radioMode?: SeasonMode; +}; + +function ScoreStatus({ status, time, radioMode }: scoreStatusProps) { switch (status) { - case 'live': + case 'LIVE': return
Live
; - case 'wait': + case 'WAIT': return (
o @@ -38,8 +48,15 @@ function makeScoreStatus(status: string, time: string) { o
); - case 'end': - return
{getTimeAgo(time)}
; + case 'END': + return ( +
+ {getTimeAgo(time)} +
+ ); default: return null; } diff --git a/components/game/big/GameResultBigTeam.tsx b/components/game/big/GameResultBigTeam.tsx index dcb5f3f6e..ab7181c34 100644 --- a/components/game/big/GameResultBigTeam.tsx +++ b/components/game/big/GameResultBigTeam.tsx @@ -1,29 +1,21 @@ import Link from 'next/link'; -import { RankResult, RankPlayer, NormalPlayer } from 'types/gameTypes'; +import { Team, Player, RankPlayer } from 'types/gameTypes'; import PlayerImage from 'components/PlayerImage'; import styles from 'styles/game/GameResultItem.module.scss'; interface GameResultBigTeamProps { - team: RankResult; + team: Team; + zIndexList?: boolean; } -export function isRankPlayerType( - arg: RankPlayer | NormalPlayer -): arg is RankPlayer { +export function isRankPlayerType(arg: Player | RankPlayer): arg is RankPlayer { return 'wins' in arg; } -export default function GameResultBigTeam({ team }: GameResultBigTeamProps) { - const makeRate = (player: RankPlayer | NormalPlayer) => { - return ( - - {isRankPlayerType(player) - ? `${player.wins}승 ${player.losses}패` - : `Lv. ${player.level}`} - - ); - }; - +export default function GameResultBigTeam({ + team, + zIndexList, +}: GameResultBigTeamProps) { return (
{team.players.map((player, index) => ( @@ -38,7 +30,14 @@ export default function GameResultBigTeam({ team }: GameResultBigTeamProps) {
{player.intraId}
-
{makeRate(player)}
+
+ {isRankPlayerType(player) + ? `${player.wins}승 ${player.losses}패` + : `Lv. ${player.level}`} +
))}
diff --git a/components/game/small/GameResultSmallItem.tsx b/components/game/small/GameResultSmallItem.tsx index 376cc67ff..20b3dcb21 100644 --- a/components/game/small/GameResultSmallItem.tsx +++ b/components/game/small/GameResultSmallItem.tsx @@ -5,24 +5,37 @@ import { clickedGameItemState } from 'utils/recoil/game'; import GameResultSmallScore from 'components/game/small/GameResultSmallScore'; import GameResultSmallTeam from 'components/game/small/GameResultSmallTeam'; import styles from 'styles/game/GameResultItem.module.scss'; +import { SeasonMode } from 'types/mainType'; interface GameResultSmallItemProps { game: Game; + type: 'LIGHT' | 'DARK'; + zIndexList: boolean; + radioMode?: SeasonMode; } -function GameResultSmallItem({ game }: GameResultSmallItemProps) { +function GameResultSmallItem({ + game, + type, + zIndexList, + radioMode, +}: GameResultSmallItemProps) { const { mode, team1, team2, gameId } = game; const setClickedItemId = useSetRecoilState(clickedGameItemState); + return (
setClickedItemId(gameId)} id={String(gameId)} + className={`${styles['smallItemContainer']} + ${styles[type.toLowerCase()]} ${ + radioMode !== undefined ? styles[radioMode.toLowerCase()] : '' + } ${zIndexList ? styles['zIndexList'] : ''}`} + onClick={() => setClickedItemId(gameId)} > diff --git a/components/game/small/GameResultSmallScore.tsx b/components/game/small/GameResultSmallScore.tsx index 1052bd833..4d7b96017 100644 --- a/components/game/small/GameResultSmallScore.tsx +++ b/components/game/small/GameResultSmallScore.tsx @@ -1,19 +1,20 @@ +import { GameMode, GameStatus } from 'types/gameTypes'; import styles from 'styles/game/GameResultItem.module.scss'; +import gameScore from '../GameScore'; interface GameResultSmallScoreProps { - mode: string; + mode: GameMode; + status: GameStatus; scoreTeam1?: number; scoreTeam2?: number; } export default function GameResultSmallScore({ mode, + status, scoreTeam1, scoreTeam2, }: GameResultSmallScoreProps) { - return ( -
- {mode === 'normal' ? '일반전' : `${scoreTeam1} : ${scoreTeam2}`} -
- ); + const score = gameScore('SMALL', mode, status, scoreTeam1, scoreTeam2); + return
{score}
; } diff --git a/components/game/small/GameResultSmallTeam.tsx b/components/game/small/GameResultSmallTeam.tsx index b151ebf9f..d17cfd8a8 100644 --- a/components/game/small/GameResultSmallTeam.tsx +++ b/components/game/small/GameResultSmallTeam.tsx @@ -1,10 +1,10 @@ -import { RankResult } from 'types/gameTypes'; +import { Team } from 'types/gameTypes'; import PlayerImage from 'components/PlayerImage'; import styles from 'styles/game/GameResultItem.module.scss'; interface GameResultSmallTeamProps { - team: RankResult; - position: string; + team: Team; + position: 'Left' | 'Right'; } export default function GameResultSmallTeam({ @@ -12,22 +12,18 @@ export default function GameResultSmallTeam({ position, }: GameResultSmallTeamProps) { return ( -
-
- {team.players.map((player, index) => ( - - ))} - - {team.players.map((player) => ( -
{player.intraId}
- ))} -
-
+
+ {team.players.map((player, index) => ( + + ))} + {team.players.map((player) => ( + {player.intraId} + ))}
); } diff --git a/components/main/SearchBar.tsx b/components/main/SearchBar.tsx index f9e5bf0c0..c7519e677 100644 --- a/components/main/SearchBar.tsx +++ b/components/main/SearchBar.tsx @@ -1,64 +1,28 @@ import Link from 'next/link'; -import { useEffect, useState, useRef } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { instance } from 'utils/axios'; -import { errorState } from 'utils/recoil/error'; import { GoSearch } from 'react-icons/go'; import { IoIosCloseCircle } from 'react-icons/io'; import styles from 'styles/main/SearchBar.module.scss'; -let timer: ReturnType; +import useSearchBar from 'hooks/useSearchBar'; export default function SearchBar() { - const [keyword, setKeyword] = useState(''); - const [showDropDown, setShowDropDown] = useState(false); - const [searchResult, setSearchResult] = useState([]); - const setError = useSetRecoilState(errorState); - const searchBarRef = useRef(null); - - useEffect(() => { - const checkId = /^[a-z|A-Z|0-9|-]+$/; - if (keyword === '' || (keyword.length && !checkId.test(keyword))) { - clearTimeout(timer); - setSearchResult([]); - } else if (checkId.test(keyword)) { - debounce(getSearchResultHandler, 500)(); - } - }, [keyword]); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, []); - - const getSearchResultHandler = async () => { - try { - const res = await instance.get(`/pingpong/users/searches?q=${keyword}`); - setSearchResult(res?.data.users); - } catch (e) { - setError('JB06'); - } - }; - - const handleClickOutside = (event: MouseEvent) => { - if ( - searchBarRef.current && - !searchBarRef.current.contains(event.target as Node) - ) - setShowDropDown(false); - }; - - const keywordHandler = (event: React.ChangeEvent) => { - setKeyword(event.target.value); - }; + const { + keyword, + setKeyword, + keywordHandler, + showDropDown, + setShowDropDown, + searchResult, + searchBarRef, + handleKeyDown, + } = useSearchBar(); return (
setShowDropDown(true)} placeholder='유저 검색하기' maxLength={15} @@ -91,12 +55,3 @@ export default function SearchBar() {
); } - -function debounce(callback: () => void, timeout: number) { - return () => { - clearTimeout(timer); - timer = setTimeout(() => { - callback(); - }, timeout); - }; -} diff --git a/components/main/Section.tsx b/components/main/Section.tsx index ee6ee6ff6..82c223511 100644 --- a/components/main/Section.tsx +++ b/components/main/Section.tsx @@ -1,26 +1,32 @@ -import Link from 'next/link'; import React from 'react'; +import { useRouter } from 'next/router'; +import { FaChevronRight } from 'react-icons/fa'; import GameResult from 'components/game/GameResult'; import RankList from 'components/rank/RankList'; import styles from 'styles/main/Section.module.scss'; type SectionProps = { + sectionTitle: string; path: string; }; -type pathType = { - [key: string]: JSX.Element; -}; +type pathType = Record; -export default function Section({ path }: SectionProps) { +export default function Section({ sectionTitle, path }: SectionProps) { + const router = useRouter(); const pathCheck: pathType = { game: , - rank: , + rank: , }; return ( -
- 더보기 ▹ +
+
+ {sectionTitle} + +
{pathCheck[path]}
); diff --git a/components/match/MatchBoard.tsx b/components/match/MatchBoard.tsx index 5687102f9..c6692d26a 100644 --- a/components/match/MatchBoard.tsx +++ b/components/match/MatchBoard.tsx @@ -1,31 +1,23 @@ -import { useEffect, useRef, useState } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; -import { Match } from 'types/matchTypes'; -import { MatchMode } from 'types/mainType'; -import { instance } from 'utils/axios'; -import { errorState } from 'utils/recoil/error'; +import { useEffect, useMemo, useRef } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { CurrentMatchList, Match, Slot } from 'types/matchTypes'; +import { Live } from 'types/mainType'; import { modalState } from 'utils/recoil/modal'; -import { reloadMatchState } from 'utils/recoil/match'; -import MatchSlotList from './MatchSlotList'; +import { stringToHourMin } from 'utils/handleTime'; +import { liveState } from 'utils/recoil/layout'; +import { Modal } from 'types/modalTypes'; +import { MatchMode } from 'types/mainType'; +import { currentMatchState } from 'utils/recoil/match'; import styles from 'styles/match/MatchBoard.module.scss'; interface MatchBoardProps { - type: string; - toggleMode: MatchMode; + radioMode: MatchMode; + match: Match | null; } -export default function MatchBoard({ type, toggleMode }: MatchBoardProps) { - const [match, setMatch] = useState(null); - const [spinReloadButton, setSpinReloadButton] = useState(false); - const [reloadMatch, setReloadMatch] = useRecoilState(reloadMatchState); - const setError = useSetRecoilState(errorState); - const setModal = useSetRecoilState(modalState); +export default function MatchBoard({ radioMode, match }: MatchBoardProps) { const currentRef = useRef(null); - useEffect(() => { - setReloadMatch(true); - }, [toggleMode]); - useEffect(() => { currentRef.current?.scrollIntoView({ behavior: 'smooth', @@ -33,21 +25,6 @@ export default function MatchBoard({ type, toggleMode }: MatchBoardProps) { }); }, [match]); - useEffect(() => { - if (reloadMatch) getMatchHandler(); - }, [reloadMatch]); - - const getMatchHandler = async () => { - try { - const res = await instance.get( - `/pingpong/match/tables/${1}/${toggleMode}/${type}` - ); - setMatch(res?.data); - } catch (e) { - setError('SJ01'); - } - }; - if (!match) return null; const { matchBoards } = match; @@ -55,71 +32,174 @@ export default function MatchBoard({ type, toggleMode }: MatchBoardProps) { if (matchBoards.length === 0) return
❌ 열린 슬롯이 없습니다 😵‍💫 ❌
; - const openManual = () => { - setModal({ modalName: 'MATCH-MANUAL', manual: { toggleMode: toggleMode } }); - }; - const getFirstOpenSlot = () => { for (let i = 0; i < matchBoards.length; i++) { - for (let j = 0; j < matchBoards[i].length; j++) { - const match = matchBoards[i][j]; - if (match.status === 'open') { - return new Date(match.time).getHours(); - } + const matchSlot = matchBoards[i]; + if (matchSlot[0].status === 'open') { + return stringToHourMin(matchSlot[0].startTime).nHour; } } return null; }; - const reloadMatchHandler = () => { - setSpinReloadButton(true); - setTimeout(() => { - setSpinReloadButton(false); - }, 1000); - setReloadMatch(true); - }; - const getScrollCurrentRef = (slotsHour: number) => { - if (getFirstOpenSlot() === slotsHour) return currentRef; + if (getFirstOpenSlot() === slotsHour) { + return currentRef; + } return null; }; return ( <> -
-
- {getFirstOpenSlot() === null && ( -
❌ 열린 슬롯이 없습니다 😵‍💫 ❌
- )} - - -
-
- {matchBoards.map((matchSlots, index) => { - const slotTime = new Date(matchSlots[0].time); - return ( -
- +
+ {matchBoards.map((slot, index) => { + return ( +
+ {stringToHourMin(slot[0].startTime).sMin === '00' && ( + + )} +
+ {slot.map((minSlots, minIndex) => ( + + ))}
- ); - })} -
+
+ ); + })}
); } + +function ChangeHourFrom24To12(hour: number) { + return `${hour < 12 ? '오전 ' : '오후 '} ${ + hour % 12 === 0 ? 12 : hour % 12 + }시`; +} + +interface MatchTimeProps { + startTime: string; +} + +export const MatchTime = ({ startTime }: MatchTimeProps) => { + const slotTime = new Date(startTime); + const slotHourIn12 = ChangeHourFrom24To12(slotTime.getHours()); + + return ( +
+ {slotHourIn12} + {slotHourIn12 === '오전 12시' && ( +
{`${slotTime.getMonth() + 1}월 ${slotTime.getDate()}일`}
+ )} +
+ ); +}; + +interface MatchSlotProps { + radioMode: MatchMode; + slot: Slot; +} + +export const MatchSlot = ({ radioMode, slot }: MatchSlotProps) => { + const setModal = useSetRecoilState(modalState); + const { event } = useRecoilValue(liveState); + const { match } = useRecoilValue(currentMatchState); + const { startTime, endTime, status } = slot; + const slotData = `${stringToHourMin(startTime).sMin} - ${ + stringToHourMin(endTime).sMin + }`; + + const enrollhandler = async () => { + if (status === 'mytable') { + setModal({ + modalName: 'MATCH-CANCEL', + cancel: { + startTime: startTime, + }, + }); + } else if (event === 'match' && match.length === 3) { + setModal({ modalName: 'MATCH-REJECT' }); + } else { + setModal({ + modalName: 'MATCH-ENROLL', + enroll: { + startTime: startTime, + endTime: endTime, + mode: radioMode, + }, + }); + } + }; + + const buttonStyle: { [key: string]: string } = useMemo( + () => ({ + mytable: status === 'mytable' ? styles.mytable : styles.disabled, + close: + event === 'match' && match.some((m) => m.startTime === startTime) + ? styles.mytableDisabled + : styles.disabled, + open: + radioMode === 'BOTH' + ? styles.both + : radioMode === 'RANK' + ? styles.rank + : styles.normal, + match: + radioMode === 'BOTH' + ? styles.both + : radioMode === 'RANK' + ? styles.rank + : styles.normal, + }), + [slot, match] + ); + + const isAfterSlot: boolean = + new Date(startTime).getTime() - new Date().getTime() >= 0; + + const headCount = + status === 'close' ? 2 : status === 'mytable' || status === 'match' ? 1 : 0; + + return ( + <> + + + ); +}; diff --git a/components/match/MatchSlot.tsx b/components/match/MatchSlot.tsx deleted file mode 100644 index 8c40c501e..000000000 --- a/components/match/MatchSlot.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React, { useMemo } from 'react'; -import { useRecoilValue, useRecoilState, useSetRecoilState } from 'recoil'; -import { MatchMode } from 'types/mainType'; -import { Slot } from 'types/matchTypes'; -import { liveState } from 'utils/recoil/layout'; -import { modalState } from 'utils/recoil/modal'; -import { fillZero } from 'utils/handleTime'; -import { currentMatchState } from 'utils/recoil/match'; -import styles from 'styles/match/MatchSlot.module.scss'; - -interface MatchSlotProps { - type: string; - slot: Slot; - toggleMode?: MatchMode; -} - -function MatchSlot({ type, slot, toggleMode }: MatchSlotProps) { - const setModal = useSetRecoilState(modalState); - const [currentMatch] = useRecoilState(currentMatchState); - const { event } = useRecoilValue(liveState); - const { headCount, slotId, status, time, endTime, mode } = slot; - const headMax = type === 'single' ? 2 : 4; - const slotStartTime = new Date(time); - const slotEndTime = new Date(endTime); - const slotData = `${minuiteToStr( - slotStartTime.getMinutes() - )} - ${minuiteToStr(slotEndTime.getMinutes())}`; - - const isAfterSlot: boolean = - slotStartTime.getTime() - new Date().getTime() >= 0; - const buttonStyle: { [key: string]: string } = useMemo( - () => ({ - mytable: toggleMode === mode ? styles.my : styles.disabled, - close: styles.disabled, - open: toggleMode === 'rank' ? styles.rank : styles.normal, - }), - [slot] - ); - - const enrollHandler = async () => { - if (status === 'mytable') { - setModal({ - modalName: 'MATCH-CANCEL', - cancel: { - isMatched: currentMatch.isMatched, - slotId: currentMatch.slotId, - time: currentMatch.time, - }, - }); - } else if (event === 'match') { - setModal({ modalName: 'MATCH-REJECT' }); - } else { - setModal({ - modalName: 'MATCH-ENROLL', - enroll: { - slotId, - type, - mode: toggleMode, - slotStartTime, - slotEndTime, - }, - }); - } - }; - - return ( - - ); -} - -function minuiteToStr(min: number) { - return fillZero(min.toString(), 2); -} - -export default React.memo(MatchSlot); diff --git a/components/match/MatchSlotList.tsx b/components/match/MatchSlotList.tsx deleted file mode 100644 index 124a2ccd9..000000000 --- a/components/match/MatchSlotList.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { MatchMode } from 'types/mainType'; -import { Slot } from 'types/matchTypes'; -import MatchSlot from './MatchSlot'; -import styles from 'styles/match/MatchSlotList.module.scss'; - -interface MatchSlotListProps { - type: string; - toggleMode?: MatchMode; - matchSlots: Slot[]; -} - -export default function MatchSlotList({ - type, - toggleMode, - matchSlots, -}: MatchSlotListProps) { - const slotTime = new Date(matchSlots[0].time); - const slotHour = slotTime.getHours(); - const slotHourIn12 = ChangeHourFrom24To12(slotHour); - - return ( - <> -
- {slotHourIn12} - {slotHourIn12 === '오전 12시' && ( -
{`${slotTime.getMonth() + 1}월 ${slotTime.getDate()}일`}
- )} -
-
- {matchSlots.map((slot) => ( - - ))} -
- - ); -} - -function ChangeHourFrom24To12(hour: number) { - return `${hour < 12 ? '오전 ' : '오후 '} ${ - hour % 12 === 0 ? 12 : hour % 12 - }시`; -} diff --git a/components/modal/ModalProvider.tsx b/components/modal/ModalProvider.tsx index d41ea9748..f23960c10 100644 --- a/components/modal/ModalProvider.tsx +++ b/components/modal/ModalProvider.tsx @@ -3,6 +3,7 @@ import { useRecoilState, useSetRecoilState } from 'recoil'; import { modalState } from 'utils/recoil/modal'; import { reloadMatchState } from 'utils/recoil/match'; import EditProfileModal from './profile/EditProfileModal'; +import KakaoEditModal from './profile/KakaoEditModal'; import LogoutModal from './menu/LogoutModal'; import MatchCancelModal from './match/MatchCancelModal'; import MatchEnrollModal from './match/MatchEnrollModal'; @@ -14,12 +15,12 @@ import AfterGameModal from './afterGame/AfterGameModal'; import StatChangeModal from './statChange/StatChangeModal'; import AdminProfileModal from './admin/AdminProfileModal'; import AdminPenaltyModal from './admin/AdminPenaltyModal'; -import AdminNotiAllModal from './admin/AdminNotiAllModal'; import AdminNotiUserModal from './admin/AdminNotiUserModal'; import AdminCheckFeedback from './admin/AdminFeedbackCheckModal'; import AdminSeasonEdit from './admin/SeasonEdit'; import FeedbackDetailModal from './admin/FeedbackDetailModal'; import DeletePenaltyModal from './admin/DeletePenaltyModal'; +import AdminModifyScoreModal from './admin/AdminModifyScoreModal'; import styles from 'styles/modal/Modal.module.scss'; export default function ModalProvider() { @@ -34,8 +35,9 @@ export default function ModalProvider() { intraId, detailContent, feedback, - userId, + penaltyId, ISeason, + ModifyScore, }, setModal, ] = useRecoilState(modalState); @@ -53,12 +55,12 @@ export default function ModalProvider() { 'USER-PROFILE_EDIT': , 'FIXED-AFTER_GAME': , 'FIXED-STAT': , - 'ADMIN-PROFILE': userId ? : null, - 'ADMIN-PENALTY': intraId ? : null, - 'ADMIN-PENALTY_DELETE': intraId ? ( - - ) : null, - 'ADMIN-NOTI_ALL': , + 'ADMIN-PROFILE': intraId ? : null, + 'ADMIN-PENALTY': intraId ? : null, + 'ADMIN-PENALTY_DELETE': + penaltyId && intraId ? ( + + ) : null, 'ADMIN-NOTI_USER': , 'ADMIN-SEASON_EDIT': ISeason ? : null, 'ADMIN-CHECK_FEEDBACK': feedback ? ( @@ -68,6 +70,10 @@ export default function ModalProvider() { intraId && detailContent ? ( ) : null, + 'ADMIN-MODIFY_SCORE': ModifyScore ? ( + + ) : null, + 'USER-KAKAO_EDIT': , }; useEffect(() => { @@ -92,7 +98,7 @@ export default function ModalProvider() { id='modalOutside' onClick={closeModalHandler} > -
{content[modalName]}
+ {content[modalName]}
) ); diff --git a/components/modal/admin/AdminFeedbackCheckModal.tsx b/components/modal/admin/AdminFeedbackCheckModal.tsx index fbfb239ec..8890b9cbd 100644 --- a/components/modal/admin/AdminFeedbackCheckModal.tsx +++ b/components/modal/admin/AdminFeedbackCheckModal.tsx @@ -14,15 +14,15 @@ export default function AdminFeedbackCheck({ const sendNotificationHandler = async (isSend: boolean) => { try { - await instanceInManage.put(`/feedback/is-solved`, { - feedbackId: id, + await instanceInManage.patch(`/feedback/${id}`, { + isSolved: isSolved, }); - await instanceInManage.post(`/notifications/${intraId}`, { + await instanceInManage.post(`/notifications`, { intraId, message: isSolved ? '피드백을 검토중입니다.' : '피드백이 반영되었습니다.', - sendMail: isSend, + // sendMail: isSend, todo: 슬랙으로 보내는 것으로 변경 }); setModal({ modalName: null }); } catch (e) { diff --git a/components/modal/admin/AdminModifyScoreModal.tsx b/components/modal/admin/AdminModifyScoreModal.tsx new file mode 100644 index 000000000..fdd26c857 --- /dev/null +++ b/components/modal/admin/AdminModifyScoreModal.tsx @@ -0,0 +1,95 @@ +import { useSetRecoilState } from 'recoil'; +import { modalState } from 'utils/recoil/modal'; +import { toastState } from 'utils/recoil/toast'; +import { instanceInManage } from 'utils/axios'; +import { ModifyScoreType } from 'types/admin/gameLogTypes'; +import { HiCheckCircle } from 'react-icons/hi'; +import styles from 'styles/admin/modal/AdminModifyScoreModal.module.scss'; + +type ModifyScoreBodyType = { + team1Score: number; + team2Score: number; + team1Id: number; + team2Id: number; +}; + +export default function AdminModifyScoreModal({ + gameId, + team1, + team2, +}: ModifyScoreType) { + const setModal = useSetRecoilState(modalState); + const setSnackbar = useSetRecoilState(toastState); + + const handleModifyScore = async () => { + const requestBody: ModifyScoreBodyType = { + team1Score: team1.score, + team2Score: team2.score, + team1Id: team1.teamId, + team2Id: team2.teamId, + }; + try { + await instanceInManage.put(`/games/${gameId}`, requestBody); + setSnackbar({ + toastName: `put request`, + severity: 'success', + message: `🔥 스코어가 수정되었습니다. 🔥`, + clicked: true, + }); + } catch (e: any) { + setSnackbar({ + toastName: `bad request`, + severity: 'error', + message: `🔥 ${e.response.data.message} 🔥`, + clicked: true, + }); + } + setModal({ modalName: null }); + }; + + return ( +
+ +
수정하시겠습니까?
+
+
Team 1
+
Team 2
+
+ {team1.intraId1} {team1.intraId2} +
+
+ {team2.intraId1} {team2.intraId2} +
+
+ {team1.score} +
+
+ {team2.score} +
+
+
+ + +
+
+ ); +} diff --git a/components/modal/admin/AdminNotiAllModal.tsx b/components/modal/admin/AdminNotiAllModal.tsx deleted file mode 100644 index 13efe5912..000000000 --- a/components/modal/admin/AdminNotiAllModal.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useRef } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { modalState } from 'utils/recoil/modal'; -import { toastState } from 'utils/recoil/toast'; -import { instanceInManage } from 'utils/axios'; -import styles from 'styles/admin/modal/AdminNoti.module.scss'; - -const STAT_MSG_LIMIT = 25; - -export default function AdminNotiAllModal() { - const setModal = useSetRecoilState(modalState); - const setSnackBar = useSetRecoilState(toastState); - const notiContent = useRef(null); - - const inputHandler = ({ - target: { name, value }, - }: React.ChangeEvent) => { - if (name === 'notification' && value.length > STAT_MSG_LIMIT) - setSnackBar({ - toastName: 'noti all', - severity: 'warning', - message: `${STAT_MSG_LIMIT}자 이내로 입력하세요`, - clicked: true, - }); - return; - }; - - const sendNotificationHandler = async () => { - if (notiContent.current?.value === '') { - setSnackBar({ - toastName: 'noti all', - severity: 'warning', - message: `알림 내용을 입력해주세요.`, - clicked: true, - }); - return; - } - try { - const res = await instanceInManage.post(`/notifications/`, { - message: notiContent.current?.value - ? notiContent.current?.value - : '알림 전송 실패', - }); - if (res.status === 200) { - setSnackBar({ - toastName: 'noti all', - severity: 'success', - message: `성공적으로 전송되었습니다!`, - clicked: true, - }); - setModal({ modalName: null }); - } else { - setSnackBar({ - toastName: 'noti all', - severity: 'error', - message: `전송에 실패하였습니다!`, - clicked: true, - }); - } - } catch (e) { - setSnackBar({ - toastName: 'noti all', - severity: 'error', - message: `API 요청에 문제가 발생했습니다.`, - clicked: true, - }); - } - }; - - return ( -
-
-
전체 알림
-
-
-