From 07a29b6d2341371d5ebb0e454823aeeabc49365d Mon Sep 17 00:00:00 2001 From: Jincheol Park <67998022+Clearsu@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:41:41 +0900 Subject: [PATCH 1/5] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B0=B0=ED=8F=AC=20?= =?UTF-8?q?(#1116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * Update README.md * [Feat] [GGFE-273] 게임 관리 상태 타입 추가 * [Fix] [GGFE-273] 빌드 에러 수정 * [Feat] [GGFE-273] 게임 종료 전 관리자 점수 입력 방지 * [Style] [GGFE-285] 클릭 가능한 부분 curosr:pointer 추가 * 매치 [GGFE-286] 매뉴얼 노쇼 패널티 문구 추가 * [] * [] * [] * .husky 전체 주석처리 * [] * feat:토너먼트 테이블 타입 추가#1075 * [] * [] * [Feat] 토너먼트 참가용 모달 샘플 생성 * Feat/유저 토너먼트 전적 페이지 레이아웃 #1077 * [Chore] add tournament-record.tsx * [Feat] 토너먼트 전적 페이지 레이아웃 #1077 * [Style] 토너먼트 전적 페이지 레이아웃 스타일 #1077 * [Style] 토너먼트 페이지 스타일 * [Feat] 대기중인 토너먼트를 보여주는 Card 컴포넌트 생성 * [Style] 대기중인 토너먼트 Card 스타일 생성 * [Feat] 테스트용 데이터 생성 * [Stylle] 대기중인 토너먼트 css 수정#1073 * [FIX] 토너먼트 모달에 사용되는 인터페이스 멤버 네이밍을 API에서 사용되는 이름으로 변경 #1073 * [FIX] 토너먼트 Card의 Props들을 API에 맞춰서 변경#1073 * feat: 리스트에 버튼 추가#1075 * [FIX] ismainn에 따라서 bangContainer의 변화 * [Style] 랭킹화면 margin변화 * feat:토너먼트 모달타입추가 * feat:버튼 클릭 모달열기 추가#1080 * [Feat] API 받아오는거 추가#1083 * fix: ITournament TournamentInfo 필드 이름 변경 * [FIX] 토너먼트 타입 변경#1084 * [FIX] admin 토너먼트 타입 변경#1084 * [FIX] ITournamentInfo 사용하는곳 들의 Sample Data 변경 * [FIX] 타입들을 사용하는 곳에서 변경된 내용들을 수정 #1084 * [FIX] 사용할수 없는 api주석 처리#1083 * [Style] 토너먼트 모달 스타일 수정#1083 * [S[Style] 모달 X 버튼클릭으로 나가기#1083 * [Style] 모달 참가인원 추가 #1083 * [Style] 모달 내부 텍스트 중앙정렬 #1083 * Feat/유저 토너먼트 전적 페이지 우승자 스와이프 뷰 #1070 (#1086) * [Feat] winner images swipe ui #1070 * [Style] add border and brightness #1070 * [Refactor] choose style in function #1070 * [Chore] install Swiper #1070 * [Feat] swiper UI #1070 * [Feat] 토너먼트 전체조회 API mock #1089 (#1091) Co-authored-by: joonho0410 <76806109+joonho0410@users.noreply.github.com> * [Feat] 무한스크롤 제네릭 함수 #1092 (#1093) * [Feat] TournamentData 추가 #1094 (#1095) * [Fix] 토너먼트전체조회 Mock API 페이지네이션 로직 수정 #1096 (#1097) * [Feat] tournament 스타일 추가 * [Feat] 토너먼트 전적 수정 모달 작성 * [Feat] 토너먼트 브래킷뷰 구현 * [Feat] 토너먼트 전적 수정 버튼 추가 * Feat/유저 토너먼트 전적 페이지 우승자 스와이퍼 슬라이드 UI 및 mock API #1088 (#1099) * [Feat] 슬라이드 구현 #1088 * [Feat] prop 추가 #1088 * [Feat] mock 데이터 수정 #1088 * [Refactor] optional chaining #1088 * [Feat] 토너먼트 전체조회 API 명세에 맞춤 #1088 * [Feat] 토너먼트전체조회 API 확정 #1088 * [Feat] 토너먼트전체조회 API 확정 #1088 * [Feat] 토너먼트전체조회 API 확정 #1088 * [Refactor] 삼항연산자 제거 #1088 * [Chore] 파일명 변경 #1088 * [Refactor] 컴포넌트명 변경 #1088 * [Fix] 토너먼트 전체조회 엔드포인트 수정 #1088 * [Fix] 토너먼트 전체조회 엔드포인트 수정 #1088 * [Fix] TournamentInfo 타입 변경에 따른 수정 #1088 * [Refactor] 토너먼트 타입 임시로 대문자로 통일 #1088 * [Refactor] 컴포넌트 분리 #1088 * [Refactor] WinnerSwiper 컴포넌트 #1088 * [Feat] 브래킷뷰 스타일 작성 #1080 * [Refactor] 스타일 분리 #1080 * [Fix] 수정버튼 위치 수정 * [Fix] AdminEditTournamentBraket props 제거 #1080 * [Fix] dynamic import 타입 명시 #1080 * [Fix] 구버전 TournamentInfo 삭제 #1080 * [Style] 주석제거 * [Fix] 구버전 TournamentInfo 삭제 * [Feat] 토너먼트 페이지 인피니티 스크롤#1090 (#1100) * [FIX] a mock API page문제 수정#1090 * [Feat] 대기중인 토너먼트 인피니트 스크롤 #1090 * [Feat] 메인페이지 토너먼트 안내 메가폰 설치 #1090 * [FIX] 메가폰 클릭시 토너먼트 페이지로 이동#1090 * [Feat] 대토너먼트 인피니티 스크롤 #1080 * [Fix] ] 오타수정 #1090 * [Fix] mock api 주소 수정 및 기존에 쓰던 mock api 삭제 #1090 * [Chore] test용 콘솔로그 제거 #1090 * [Fix] api 루키와 마스터리그 모두다 받아오도록 수정#1090 * [Fix] fetch async 함수로 변환#1090 --------- Co-authored-by: Junho Jeon * Feat/유저 토너먼트 전적 페이지 토너먼트 정보 표시 #1104 (#1105) * [Fix] TournamentInfo 날짜 타입 수정 #1104 * [Feat] 토너먼트 정보 표시 #1104 * [Refactor] 사용하지 않는 더미데이터 삭제 및 에러코드 수정 #1104 * Feat/유저 토너먼트 전적 페이지 리그 선택 버튼 #1079 (#1107) * [Fix] TournamentInfo 날짜 타입 수정 #1104 * [Feat] 토너먼트 정보 표시 #1104 * [Feat] 리그선택버튼 기능구현 #1079 * [Style] 활성 버튼 디자인 #1079 * [Feat] 활성 버튼 스타일 변경 #1079 * [Fix] 버튼 스타일 변경 시 위치 이동되지 않도록 고정크기 적용 #1079 * Feat/메뉴바에 tournament record(명예의전당) 바로가기 추가 #1108 (#1109) * [Feat] 명예의전당 링크 추가 #1108 * [Chore] 명예의 전당 아이콘 추가 #1108 * [Feat] 토너먼트 게임 타입 추가 #1102 * [Feat] 토너먼트게임 브래킷뷰 데이터 컨버터 구현 #1102 * [Fix] props 변경 #1102 * [Feat] mockApi 추가 #1102 * Feat/우승자 슬라이드 애니메이션 및 이미지 로드 실패 시 fall back 이미지 #1098 (#1110) * [Refactor] 불필요한 리턴값 제거 #1098 * [Feat] fade in 애니메이션 추가 #1098 * [Feat] 이미지 로드 에러 시 fallBack 이미지 표시 #1098 * [Docs] 주석 제거 #1098 * [Fix] 잘못된 setState 호출 삭제 #1098 * [Chore] edit test-deploy action #1114 (#1115) --------- Co-authored-by: PARK <100325940+PHJoon@users.noreply.github.com> Co-authored-by: Yoon Jeongyeon Co-authored-by: hyobicho Co-authored-by: hyobb109 <105159293+hyobb109@users.noreply.github.com> Co-authored-by: PHJoon Co-authored-by: Junho Jeon Co-authored-by: kimjaehyuk Co-authored-by: joonho0410 <76806109+joonho0410@users.noreply.github.com> Co-authored-by: greatSweetMango <93255519+greatSweetMango@users.noreply.github.com> Co-authored-by: Junho jeon --- .github/workflows/test-deploy.yml | 55 +++-- .husky/prepare-commit-msg | 108 ++++----- README.md | 3 +- components/Layout/MenuBar/MenuBarElement.tsx | 10 + components/admin/SideNav.tsx | 10 +- components/admin/games/GamesTable.tsx | 3 +- components/admin/games/ModifyScoreForm.tsx | 4 + .../admin/tournament/TournamentEdit.tsx | 193 ++++++++++++++++ .../admin/tournament/TournamentList.tsx | 138 ++++++++++++ components/main/Section.tsx | 2 + components/modal/ModalButton.tsx | 2 +- components/modal/ModalProvider.tsx | 3 + .../modal/admin/AdminEditTournamentBraket.tsx | 37 +++ components/modal/match/MatchManualModal.tsx | 10 +- components/modal/modalType/AdminModal.tsx | 5 + .../modal/modalType/TournamentModal.tsx | 16 ++ .../tournament/TournamentRegistryModal.tsx | 71 ++++++ components/rank/topRank/RankListMain.tsx | 8 +- .../tournament-record/LeagueButtonGroup.tsx | 40 ++++ .../tournament-record/WinnerProfileImage.tsx | 63 ++++++ components/tournament-record/WinnerSwiper.tsx | 96 ++++++++ components/tournament/TournamentBraket.tsx | 76 +++++++ components/tournament/TournamentCard.tsx | 47 ++++ components/tournament/TournamentMatch.tsx | 75 +++++++ components/tournament/TournamentMegaphone.tsx | 108 +++++++++ constants/admin/table.ts | 8 + package-lock.json | 210 +++++++++++++++++- package.json | 2 + pages/admin/tournament.tsx | 12 + pages/api/pingpong/admin/games/index.ts | 3 + .../games/dummyTournamentGame.ts | 102 +++++++++ .../tournament/[tournamentId]/games/index.ts | 21 ++ .../tournament/dummyTournamentData.ts | 64 ++++++ pages/api/pingpong/tournament/index.ts | 51 +++++ pages/index.tsx | 3 + pages/tournament-record.tsx | 41 ++++ pages/tournament.tsx | 70 ++++++ public/image/match_qustion.png | Bin 0 -> 15750 bytes public/image/menu_halloffame.svg | 7 + styles/Layout/CurrentMatchInfo.module.scss | 2 + styles/PlayerImage.module.scss | 8 + .../AdminEditTournamentBraket.module.scss | 10 + styles/common.scss | 15 ++ styles/game/GameResultItem.module.scss | 1 + styles/main/Section.module.scss | 2 +- .../modal/event/AnnouncementModal.module.scss | 2 + .../event/TournamentRegistryModal.module.scss | 68 ++++++ styles/mode/SeasonDropDown.module.scss | 1 + styles/rank/RankListMain.module.scss | 8 +- styles/store/ItemCard.module.scss | 2 + .../LeagueButtonGroup.module.scss | 24 ++ .../TournamentRecord.module.scss | 53 +++++ .../WinnerProfileImage.module.scss | 31 +++ .../WinnerSwiper.module.scss | 6 + styles/tournament/TournamentCard.module.scss | 16 ++ .../TournamentContainer.module.scss | 65 ++++++ styles/tournament/TournamentMatch.module.scss | 29 +++ types/admin/adminTournamentTypes.ts | 18 ++ types/admin/gameLogTypes.ts | 4 +- types/admin/tableTypes.ts | 4 +- types/gameTypes.ts | 2 +- types/modalTypes.ts | 13 +- types/tournamentTypes.ts | 26 +++ utils/handleTournamentGame.ts | 87 ++++++++ utils/infinityScroll.ts | 37 +++ utils/recoil/modal.ts | 3 + 66 files changed, 2213 insertions(+), 101 deletions(-) create mode 100644 components/admin/tournament/TournamentEdit.tsx create mode 100644 components/admin/tournament/TournamentList.tsx create mode 100644 components/modal/admin/AdminEditTournamentBraket.tsx create mode 100644 components/modal/modalType/TournamentModal.tsx create mode 100644 components/modal/tournament/TournamentRegistryModal.tsx create mode 100644 components/tournament-record/LeagueButtonGroup.tsx create mode 100644 components/tournament-record/WinnerProfileImage.tsx create mode 100644 components/tournament-record/WinnerSwiper.tsx create mode 100644 components/tournament/TournamentBraket.tsx create mode 100644 components/tournament/TournamentCard.tsx create mode 100644 components/tournament/TournamentMatch.tsx create mode 100644 components/tournament/TournamentMegaphone.tsx create mode 100644 pages/admin/tournament.tsx create mode 100644 pages/api/pingpong/tournament/[tournamentId]/games/dummyTournamentGame.ts create mode 100644 pages/api/pingpong/tournament/[tournamentId]/games/index.ts create mode 100644 pages/api/pingpong/tournament/dummyTournamentData.ts create mode 100644 pages/api/pingpong/tournament/index.ts create mode 100644 pages/tournament-record.tsx create mode 100644 pages/tournament.tsx create mode 100644 public/image/match_qustion.png create mode 100644 public/image/menu_halloffame.svg create mode 100644 styles/admin/modal/AdminEditTournamentBraket.module.scss create mode 100644 styles/modal/event/TournamentRegistryModal.module.scss create mode 100644 styles/tournament-record/LeagueButtonGroup.module.scss create mode 100644 styles/tournament-record/TournamentRecord.module.scss create mode 100644 styles/tournament-record/WinnerProfileImage.module.scss create mode 100644 styles/tournament-record/WinnerSwiper.module.scss create mode 100644 styles/tournament/TournamentCard.module.scss create mode 100644 styles/tournament/TournamentContainer.module.scss create mode 100644 styles/tournament/TournamentMatch.module.scss create mode 100644 types/admin/adminTournamentTypes.ts create mode 100644 types/tournamentTypes.ts create mode 100644 utils/handleTournamentGame.ts diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index 2ef3ee076..9239ca06e 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -1,8 +1,4 @@ -name: Test Deploy to Vercel - -# action을 돌리기 전에 secret을 설정해야 한다. -# - TEST_DEPLOY_REPO_OWNER : vercel과 연결된 repository의 owner -# - TEST_DEPLOY_REPO_OWNER_TOKEN : vercel과 연결된 repository의 owner의 token (repo 권한 필요) +name: Test deploy to 42ggDevS3 on: push: @@ -10,31 +6,34 @@ on: - test-deploy jobs: - sync: - runs-on: ubuntu-latest - + continuous-deployment: + runs-on: macos-12 steps: - - uses: actions/checkout@v2 - - name: create build.sh - run: | - touch build.sh - echo "#!/bin/sh" >> build.sh - echo "cd ../" >> build.sh - echo "mkdir output" >> build.sh - echo "cp -R ./42gg.client/* ./output" >> build.sh - echo "cp -R ./output ./42gg.client/" >> build.sh - chmod +x build.sh + - name: Git Checkout + uses: actions/checkout@v2 - - name: run build.sh - run: sh ./build.sh + - name: Use Node.js version 16.x + uses: actions/setup-node@v1 + with: + node-version: 16.x - - name: Pushes to another repository - uses: cpina/github-action-push-to-another-repository@main + - name: Build env: - API_TOKEN_GITHUB: ${{ secrets.TEST_DEPLOY_REPO_OWNER_TOKEN }} + NEXT_PUBLIC_SERVER_ENDPOINT: ${{ secrets.NEXT_DEV_PUBLIC_SERVER_ENDPOINT }} + NEXT_PUBLIC_MANAGE_SERVER_ENDPOINT: ${{ secrets.NEXT_DEV_PUBLIC_MANAGE_SERVER_ENDPOINT }} + GENERATE_SOURCEMAP: ${{ secrets.GENERATE_SOURCEMAP }} + run: | + npm install + npm run build + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 with: - source-directory: 'output' - destination-github-username: ${{ secrets.TEST_DEPLOY_REPO_OWNER }} - destination-repository-name: '42gg.client' - target-branch: 'test-deploy' - commit-message: '[Test-Deploy] Update from 42gg.client' + aws-access-key-id: ${{ secrets.AWS_DEV_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_DEV_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_BUCKET_REGION }} + + - name: Deploy to S3 + run: aws s3 sync ./${{ secrets.BUILD_DIRECTORY }} ${{ secrets.AWS_DEV_BUCKET_NAME }} --acl public-read --delete + + - name: CloudFront Invalidate Cache + run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DEV_CLOUDFRONT_DISTRIBUTION_ID }} --paths '/*' diff --git a/.husky/prepare-commit-msg b/.husky/prepare-commit-msg index 692f3bfe7..a5ced03a8 100755 --- a/.husky/prepare-commit-msg +++ b/.husky/prepare-commit-msg @@ -1,61 +1,61 @@ #!/bin/bash -# get current branch name -branch_name=$(git symbolic-ref --short HEAD) -exclude_branch_list=("main" "dev" "deploy") - -# if current branch name is in exclude_branch_list, exit -for exclude_branch in "${exclude_branch_list[@]}"; do - if [[ "$branch_name" == "$exclude_branch" ]]; then - exit 0 - fi -done - -# if branch name is not valid, exit -contains_issue_key=$(echo $branch_name | grep -c "GGFE-") -if [[ $contains_issue_key -eq 0 ]]; then - echo "브랜치명에 이슈 키를 포함해주세요." - exit 1 -fi - -# get issue key from branch name -issue_key=$(echo $branch_name | grep -o "GGFE-[0-9]*") - -# if issue key is not valid, exit -if ! [[ $issue_key =~ ^GGFE-[0-9]+$ ]]; then - echo "브랜치명의 이슈 키가 올바른 형식이 아닙니다." - exit 1 -fi - -# get commit message -commit_msg_title=$(head -n 1 $1) -commit_msg_body=$(tail -n +2 $1) -# get issue key from commit message -issue_key_from_commit_msg=$(echo $commit_msg_title | grep -o "\[GGFE-[0-9]*\]") # [GGFE-1234] - -# if this commit is merge commit, exit 0 -if [[ $commit_msg_title =~ ^Merge ]]; then - exit 0 -fi - -# if there is issue key in commit message but not equal to issue key from branch name, exit -if [[ -n $issue_key_from_commit_msg ]] && [[ "$issue_key_from_commit_msg" != "[$issue_key]" ]]; then - echo "커밋 메시지의 이슈 키가 브랜치명의 이슈 키와 일치하지 않습니다." - exit 1 -fi +# # get current branch name +# branch_name=$(git symbolic-ref --short HEAD) +# exclude_branch_list=("main" "dev" "deploy") + +# # if current branch name is in exclude_branch_list, exit +# for exclude_branch in "${exclude_branch_list[@]}"; do +# if [[ "$branch_name" == "$exclude_branch" ]]; then +# exit 0 +# fi +# done + +# # if branch name is not valid, exit +# contains_issue_key=$(echo $branch_name | grep -c "GGFE-") +# if [[ $contains_issue_key -eq 0 ]]; then +# echo "브랜치명에 이슈 키를 포함해주세요." +# exit 1 +# fi + +# # get issue key from branch name +# issue_key=$(echo $branch_name | grep -o "GGFE-[0-9]*") + +# # if issue key is not valid, exit +# if ! [[ $issue_key =~ ^GGFE-[0-9]+$ ]]; then +# echo "브랜치명의 이슈 키가 올바른 형식이 아닙니다." +# exit 1 +# fi + +# # get commit message +# commit_msg_title=$(head -n 1 $1) +# commit_msg_body=$(tail -n +2 $1) +# # get issue key from commit message +# issue_key_from_commit_msg=$(echo $commit_msg_title | grep -o "\[GGFE-[0-9]*\]") # [GGFE-1234] + +# # if this commit is merge commit, exit 0 +# if [[ $commit_msg_title =~ ^Merge ]]; then +# exit 0 +# fi + +# # if there is issue key in commit message but not equal to issue key from branch name, exit +# if [[ -n $issue_key_from_commit_msg ]] && [[ "$issue_key_from_commit_msg" != "[$issue_key]" ]]; then +# echo "커밋 메시지의 이슈 키가 브랜치명의 이슈 키와 일치하지 않습니다." +# exit 1 +# fi # if issue key from commit message is equal to issue key from branch name, exit -if [[ -n $issue_key_from_commit_msg ]]; then - exit 0 -fi +# if [[ -n $issue_key_from_commit_msg ]]; then +# exit 0 +# fi -# make commit message [{commit_action}] [{issue_key}] {commit_title_msg} -commit_title_action=$(echo $commit_msg_title | awk '{ print $1 }') # [Feat] -commit_title_msg=${commit_msg_title#"$commit_title_action "} # commit message +# # make commit message [{commit_action}] [{issue_key}] {commit_title_msg} +# commit_title_action=$(echo $commit_msg_title | awk '{ print $1 }') # [Feat] +# commit_title_msg=${commit_msg_title#"$commit_title_action "} # commit message -echo "$commit_title_action [$issue_key] $commit_title_msg" > $1 -if [[ -n $commit_msg_body ]]; then - echo "$commit_msg_body" >> $1 -fi +# echo "$commit_title_action [$issue_key] $commit_title_msg" > $1 +# if [[ -n $commit_msg_body ]]; then +# echo "$commit_msg_body" >> $1 +# fi -exit 0 +# exit 0 diff --git a/README.md b/README.md index 27980623e..bee9e6bdd 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ - + + diff --git a/components/Layout/MenuBar/MenuBarElement.tsx b/components/Layout/MenuBar/MenuBarElement.tsx index bc0b40062..8f6cba703 100644 --- a/components/Layout/MenuBar/MenuBarElement.tsx +++ b/components/Layout/MenuBar/MenuBarElement.tsx @@ -11,6 +11,7 @@ import { import AdminEmoji from 'public/image/menu_admin.svg'; import AnnouncementEmoji from 'public/image/menu_announcement.svg'; import CurrentMatchEmoji from 'public/image/menu_currentMatch.svg'; +import HallOfFameEmoji from 'public/image/menu_halloffame.svg'; import ManualEmoji from 'public/image/menu_manual.svg'; import RankingEmoji from 'public/image/menu_ranking.svg'; import ReportEmoji from 'public/image/menu_report.svg'; @@ -44,6 +45,10 @@ const MenuItem = ({ itemName, onClick }: menuItemProps) => { name: '최근 경기', svg: , }, + HallOfFame: { + name: '명예의 전당', + svg: , + }, Announcement: { name: '공지사항', svg: , @@ -116,6 +121,11 @@ export const MainMenu = () => { itemName='CurrentMatch' onClick={HeaderState?.resetOpenMenuBarState} /> + getAnnouncementHandler()} diff --git a/components/admin/SideNav.tsx b/components/admin/SideNav.tsx index e4d91804f..3d01b48fb 100644 --- a/components/admin/SideNav.tsx +++ b/components/admin/SideNav.tsx @@ -9,7 +9,7 @@ import { } from 'react-icons/gr'; import { IoGameControllerOutline, IoReceiptOutline } from 'react-icons/io5'; import { MdOutlineMessage } from 'react-icons/md'; -import { TbCalendarTime, TbCoin, TbPaperBag } from 'react-icons/tb'; +import { TbCalendarTime, TbCoin, TbPaperBag, TbTrophy } from 'react-icons/tb'; import SideNavContent from 'components/admin/SideNavContent'; import styles from 'styles/admin/SideNav.module.scss'; @@ -113,6 +113,14 @@ export default function SideNav() { > + + + + ); } diff --git a/components/admin/games/GamesTable.tsx b/components/admin/games/GamesTable.tsx index 73a66c999..fa208b83b 100644 --- a/components/admin/games/GamesTable.tsx +++ b/components/admin/games/GamesTable.tsx @@ -88,7 +88,7 @@ export default function GamesTable() { 시작 날짜: {game.startAt.toLocaleString().split(' ')[0]}
시작 시간: {gameTimeToString(game.startAt)}
-
게임 모드: {mode}
+
게임 모드: {`${mode}(${game.status})`}
슬롯 시간: {game.slotTime}분
{mode === 'RANK' && ( @@ -96,6 +96,7 @@ export default function GamesTable() { gameId={game.gameId} team1={team1} team2={team2} + status={game.status} /> )}
diff --git a/components/admin/games/ModifyScoreForm.tsx b/components/admin/games/ModifyScoreForm.tsx index b66c4cf62..9e9400b1b 100644 --- a/components/admin/games/ModifyScoreForm.tsx +++ b/components/admin/games/ModifyScoreForm.tsx @@ -7,6 +7,7 @@ export default function ModifyScoreForm({ gameId, team1, team2, + status, }: ModifyScoreType) { const setModal = useSetRecoilState(modalState); @@ -22,6 +23,7 @@ export default function ModifyScoreForm({ gameId: gameId, team1: { ...team1, score: team1Score, win: team1Score > team2Score }, team2: { ...team2, score: team2Score, win: team2Score > team1Score }, + status: status, }, }); } @@ -38,6 +40,7 @@ export default function ModifyScoreForm({ name='team1Score' min='0' max='2' + disabled={status !== 'END'} defaultValue={team1.score} required /> @@ -47,6 +50,7 @@ export default function ModifyScoreForm({ name='team2Score' min='0' max='2' + disabled={status !== 'END'} defaultValue={team2.score} required /> diff --git a/components/admin/tournament/TournamentEdit.tsx b/components/admin/tournament/TournamentEdit.tsx new file mode 100644 index 000000000..136ee855b --- /dev/null +++ b/components/admin/tournament/TournamentEdit.tsx @@ -0,0 +1,193 @@ +import dynamic from 'next/dynamic'; +import { useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, +} from '@mui/material'; +import { QUILL_EDIT_MODULES, QUILL_FORMATS } from 'types/quillTypes'; +import { toastState } from 'utils/recoil/toast'; +import { useUser } from 'hooks/Layout/useUser'; +import styles from 'styles/admin/announcement/AnnounceEdit.module.scss'; +import 'react-quill/dist/quill.snow.css'; +import 'react-quill/dist/quill.bubble.css'; +import { AdminTableHead } from '../common/AdminTable'; + +const Quill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

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

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

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

Loading ...

, +}); + +const tableTitle: { [key: string]: string } = { + title: '토너먼트 이름', + contents: '토너먼트 내용', + startTime: '시작 시간', + endTime: '종료 시간', + type: '토너먼트 타입', + edit: '수정하기', +}; + +export default function TournamentList() { + const setModal = useSetRecoilState(modalState); + const [tournamentInfo, setTournamentInfo] = useState({ + tournamentList: [], + totalPage: 0, + currentPage: 0, + }); + + const [currentPage, setCurrentPage] = useState(1); + + const fetchTournaments = useCallback(async () => { + try { + const res = await mockInstance.get(`/tournament?page=${currentPage}`); + setTournamentInfo({ + tournamentList: res.data.tournaments, + totalPage: res.data.totalPage, + currentPage: currentPage, + }); + } catch (e) { + console.error('MS01'); + } + }, [currentPage]); + + useEffect(() => { + fetchTournaments(); + }, [fetchTournaments]); + + return ( +
+
+ 토너먼트 리스트 +
+ + + + + {tournamentInfo.tournamentList.length > 0 ? ( + tournamentInfo.tournamentList.map( + (tournament: ITournament, index: number) => ( + + {tableFormat['tournament'].columns.map( + (columnName: string, index: number) => { + return ( + + {columnName === 'startTime' || + columnName === 'endTime' ? ( + tournament[ + columnName as keyof ITournament + ]?.toLocaleString() + ) : columnName === 'edit' ? ( +
+ +
+ ) : ( + tournament[ + columnName as keyof ITournament + ]?.toString() + )} +
+ ); + } + )} +
+ ) + ) + ) : ( + + )} +
+
+
+
+ { + setCurrentPage(pageNumber); + }} + /> +
+
+ ); +} diff --git a/components/main/Section.tsx b/components/main/Section.tsx index e4d0e1c83..3f2fd3769 100644 --- a/components/main/Section.tsx +++ b/components/main/Section.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { FaChevronRight } from 'react-icons/fa'; import GameResult from 'components/game/GameResult'; import RankListMain from 'components/rank/topRank/RankListMain'; +import TournamentMegaphone from 'components/tournament/TournamentMegaphone'; import styles from 'styles/main/Section.module.scss'; type SectionProps = { @@ -17,6 +18,7 @@ export default function Section({ sectionTitle, path }: SectionProps) { const pathCheck: pathType = { game: , rank: , + tournament: , }; return ( diff --git a/components/modal/ModalButton.tsx b/components/modal/ModalButton.tsx index c248c8e0f..39a6a0250 100644 --- a/components/modal/ModalButton.tsx +++ b/components/modal/ModalButton.tsx @@ -2,7 +2,7 @@ import LoadingButton from 'components/modal/LoadingButton'; import styles from 'styles/modal/Modal.module.scss'; type ButtonProps = { - style: 'positive' | 'negative'; + style: 'positive' | 'negative' | 'close'; value: string; form?: string; isLoading?: boolean; diff --git a/components/modal/ModalProvider.tsx b/components/modal/ModalProvider.tsx index 1b6f6d4d6..abbf1aa7f 100644 --- a/components/modal/ModalProvider.tsx +++ b/components/modal/ModalProvider.tsx @@ -6,6 +6,7 @@ import AdminModal from 'components/modal/modalType/AdminModal'; import NormalModal from 'components/modal/modalType/NormalModal'; import StoreModal from 'components/modal/modalType/StoreModal'; import styles from 'styles/modal/Modal.module.scss'; +import TournamentModal from './modalType/TournamentModal'; export default function ModalProvider() { const [{ modalName }, setModal] = useRecoilState(modalState); @@ -43,6 +44,8 @@ export default function ModalProvider() { ) : modalType === 'ADMIN' ? ( + ) : modalType === 'TOURNAMENT' ? ( + ) : null} ) diff --git a/components/modal/admin/AdminEditTournamentBraket.tsx b/components/modal/admin/AdminEditTournamentBraket.tsx new file mode 100644 index 000000000..1a86d325a --- /dev/null +++ b/components/modal/admin/AdminEditTournamentBraket.tsx @@ -0,0 +1,37 @@ +import { Match } from '@g-loot/react-tournament-brackets/dist/src/types'; +import { useCallback, useEffect, useState } from 'react'; +import { ITournament } from 'types/admin/adminTournamentTypes'; +import { TournamentGame, TournamentInfo } from 'types/tournamentTypes'; +import { convertTournamentGamesToBracketMatchs } from 'utils/handleTournamentGame'; +import { mockInstance } from 'utils/mockAxios'; +import TournamentBraket from 'components/tournament/TournamentBraket'; +import styles from 'styles/admin/modal/AdminEditTournamentBraket.module.scss'; + +export default function AdminEditTournamentBraket({ + tournamentId, +}: ITournament) { + const [bracketMatchs, setBracketMatchs] = useState([]); + + const fetchTournamentGames = useCallback(async () => { + console.log('Fetching more data...'); + try { + const res = await mockInstance.get(`/tournament/${tournamentId}/games`); + const data: TournamentGame[] = res.data.games; + const bracketMatchs = convertTournamentGamesToBracketMatchs(data); + setBracketMatchs(bracketMatchs); + return data; + } catch (error) { + console.error('Error fetching data:', error); + } + }, []); + + useEffect(() => { + fetchTournamentGames(); + }, [fetchTournamentGames]); + + return ( +
+ +
+ ); +} diff --git a/components/modal/match/MatchManualModal.tsx b/components/modal/match/MatchManualModal.tsx index 7a61bf551..84f7b77b7 100644 --- a/components/modal/match/MatchManualModal.tsx +++ b/components/modal/match/MatchManualModal.tsx @@ -55,7 +55,10 @@ const modalContents: contentsType = { }, { title: , - description: [`노쇼는 건의사항 기능 이용해서 신고`], + description: [ + '노쇼는 건의사항 기능 이용해서 신고', + '노쇼 시 관리자 확인 후 24시간 패널티 적용', + ], }, ], NORMAL: [ @@ -80,7 +83,10 @@ const modalContents: contentsType = { }, { title: , - description: [`노쇼는 건의사항 기능 이용해서 신고`], + description: [ + '노쇼는 건의사항 기능 이용해서 신고', + '노쇼 시 관리자 확인 후 24시간 패널티 적용', + ], }, ], RANK: [ diff --git a/components/modal/modalType/AdminModal.tsx b/components/modal/modalType/AdminModal.tsx index 148590a3a..569710ca8 100644 --- a/components/modal/modalType/AdminModal.tsx +++ b/components/modal/modalType/AdminModal.tsx @@ -15,6 +15,7 @@ import AdminUserCoinModal from 'components/modal/admin/AdminUserCoinModal'; import DeletePenaltyModal from 'components/modal/admin/DeletePenaltyModal'; import DetailModal from 'components/modal/admin/DetailModal'; import AdminSeasonEdit from 'components/modal/admin/SeasonEdit'; +import AdminEditTournamentBraket from '../admin/AdminEditTournamentBraket'; export default function AdminModal() { const { @@ -30,6 +31,7 @@ export default function AdminModal() { profile, item, coinPolicy, + tournament, } = useRecoilValue(modalState); const content: { [key: string]: JSX.Element | null } = { @@ -76,6 +78,9 @@ export default function AdminModal() { 'ADMIN-COINPOLICY_EDIT': coinPolicy ? ( ) : null, + 'ADMIN-TOURNAMENT_BRAKET_EDIT': tournament ? ( + + ) : null, }; if (!modalName) return null; diff --git a/components/modal/modalType/TournamentModal.tsx b/components/modal/modalType/TournamentModal.tsx new file mode 100644 index 000000000..aea7d3053 --- /dev/null +++ b/components/modal/modalType/TournamentModal.tsx @@ -0,0 +1,16 @@ +import { useRecoilValue } from 'recoil'; +import { modalState } from 'utils/recoil/modal'; +import TournamentRegistryModal from '../tournament/TournamentRegistryModal'; + +export default function TournamentModal() { + const { modalName, tournamentInfo } = useRecoilValue(modalState); + + const content: { [key: string]: JSX.Element | null } = { + 'TOURNAMENT-REGISTRY': tournamentInfo ? ( + + ) : null, + }; + + if (!modalName) return null; + return content[modalName]; +} diff --git a/components/modal/tournament/TournamentRegistryModal.tsx b/components/modal/tournament/TournamentRegistryModal.tsx new file mode 100644 index 000000000..50595047c --- /dev/null +++ b/components/modal/tournament/TournamentRegistryModal.tsx @@ -0,0 +1,71 @@ +import dynamic from 'next/dynamic'; +import { useSetRecoilState } from 'recoil'; +import { QUILL_FORMATS } from 'types/quillTypes'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { modalState } from 'utils/recoil/modal'; +import { + ModalButtonContainer, + ModalButton, +} from 'components/modal/ModalButton'; +import styles from 'styles/modal/event/TournamentRegistryModal.module.scss'; +import 'react-quill/dist/quill.bubble.css'; + +const Quill = dynamic(() => import('react-quill'), { + ssr: false, + loading: () =>

Loading ...

, +}); + +export default function TournamentRegistryModal({ + title, + contents, + startTime, + status, + type, + endTime, +}: TournamentInfo) { + const setModal = useSetRecoilState(modalState); + const Date = startTime.toString().split(':').slice(0, 2).join(':'); + + const registTournament = () => { + console.log('토너먼트에 등록하시겠습니까.'); + }; + + const closeModalButtonHandler = () => { + setModal({ modalName: null }); + }; + + return ( +
+
+ + + +
+
{title}
+
+
{Date}
+
현재인원 / 최대인원
+
+ +
+ + + +
+
+ ); +} diff --git a/components/rank/topRank/RankListMain.tsx b/components/rank/topRank/RankListMain.tsx index cecf97d2a..4b3160d7d 100644 --- a/components/rank/topRank/RankListMain.tsx +++ b/components/rank/topRank/RankListMain.tsx @@ -53,8 +53,10 @@ export default function RankListMain({ isMain, season }: RankListMainProps) { )); return ( -
-
{bangElements}
+ <> + {!isMain && ( +
{bangElements}
+ )}
{rank !== undefined && rank.map((item: userImages, index: number) => ( @@ -65,7 +67,7 @@ export default function RankListMain({ isMain, season }: RankListMainProps) { /> ))}
-
+ ); } diff --git a/components/tournament-record/LeagueButtonGroup.tsx b/components/tournament-record/LeagueButtonGroup.tsx new file mode 100644 index 000000000..6fdfca914 --- /dev/null +++ b/components/tournament-record/LeagueButtonGroup.tsx @@ -0,0 +1,40 @@ +import React, { SetStateAction, useState } from 'react'; +import styles from 'styles/tournament-record/LeagueButtonGroup.module.scss'; + +interface LeagueButtonGroupProps { + onSelect: React.Dispatch>; +} + +export default function LeagueButtonGroup({ + onSelect, +}: LeagueButtonGroupProps) { + const [activeButton, setActiveButton] = useState('ROOKIE'); + + const handleClick = (league: string) => { + onSelect(league); + setActiveButton(league); + }; + + return ( +
+ + + +
+ ); +} diff --git a/components/tournament-record/WinnerProfileImage.tsx b/components/tournament-record/WinnerProfileImage.tsx new file mode 100644 index 000000000..c64617ac4 --- /dev/null +++ b/components/tournament-record/WinnerProfileImage.tsx @@ -0,0 +1,63 @@ +import Image from 'next/image'; +import React, { useState, useEffect, SetStateAction } from 'react'; +import { useSwiper } from 'swiper/react'; +import { TournamentInfo } from 'types/tournamentTypes'; +import styles from 'styles/tournament-record/WinnerProfileImage.module.scss'; + +interface WinnerProfileImageProps { + tournament: TournamentInfo; + slideIndex: number; + setTournamentInfo: React.Dispatch>; +} + +export default function WinnerProfileImage({ + tournament, + slideIndex, + setTournamentInfo, +}: WinnerProfileImageProps) { + const swiper = useSwiper(); + const [indexDiff, setIndexDiff] = useState(swiper.activeIndex - slideIndex); + const [imageUrl, setImageUrl] = useState(tournament.winnerImageUrl); + + useEffect(() => { + const swiperUpdate = () => { + setIndexDiff(swiper.activeIndex - slideIndex); + }; + swiper.on('slideChange', swiperUpdate); + return () => { + swiper.off('slideChange', swiperUpdate); + }; + }, [swiper, slideIndex]); + + useEffect(() => { + if (indexDiff === 0) { + setTournamentInfo(tournament); + } + }, [indexDiff, tournament, setTournamentInfo]); + + function applyStyle() { + if (indexDiff === 0) { + return styles.firstLayer; + } + return styles.secondLayer; + } + + return ( +
-2 && indexDiff < 2 && applyStyle() + }`} + > + {tournament.winnerIntraId} { + setImageUrl('/image/fallBackSrc.jpeg'); + }} + /> +
+ ); +} diff --git a/components/tournament-record/WinnerSwiper.tsx b/components/tournament-record/WinnerSwiper.tsx new file mode 100644 index 000000000..39d159517 --- /dev/null +++ b/components/tournament-record/WinnerSwiper.tsx @@ -0,0 +1,96 @@ +import React, { + useMemo, + useCallback, + SetStateAction, + forwardRef, + Ref, +} from 'react'; +import { EffectCoverflow } from 'swiper/modules'; +import { Swiper, SwiperSlide, SwiperClass, SwiperRef } from 'swiper/react'; +import { TournamentData, TournamentInfo } from 'types/tournamentTypes'; +import { InfiniteScroll } from 'utils/infinityScroll'; +import { mockInstance } from 'utils/mockAxios'; +import styles from 'styles/tournament-record/WinnerSwiper.module.scss'; +import 'swiper/css'; +import 'swiper/css/effect-coverflow'; +import WinnerProfileImage from './WinnerProfileImage'; + +interface WinnerSwiperProps { + type: string; + size: number; + setTournamentInfo: React.Dispatch>; +} + +const WinnerSwiper = forwardRef( + (props: WinnerSwiperProps, ref: Ref | undefined) => { + const fetchTournamentData = useCallback( + async (page: number) => { + console.log('Fetching more data...'); + const res = await mockInstance.get( + `/tournament?page=${page}&type=${props.type}&size=${props.size}` + ); + return res.data; + }, + [props.type, props.size] + ); + + const { data, hasNextPage, fetchNextPage } = InfiniteScroll( + ['tournamentData', props.type], + fetchTournamentData, + 'JC01' + ); + + const coverflowEffect = useMemo( + () => ({ + rotate: 35, + stretch: 0, + depth: 500, + slideShadows: true, + }), + [] + ); + + const indexChangeHandler = useCallback( + (swiper: SwiperClass) => { + const slidesLength = swiper.slides.length; + if (hasNextPage && swiper.activeIndex >= slidesLength - 3) { + fetchNextPage(); + } + }, + [hasNextPage, fetchNextPage] + ); + + return ( + + {data?.pages.map((page, pageIndex) => ( + + {page.tournaments.length > 0 && + page.tournaments.map((tournament, index) => ( + + + + ))} + + ))} + + ); + } +); + +// forwardRef에 들어가는 익명함수에 대한 name +WinnerSwiper.displayName = 'WinnerSwiper'; + +export default WinnerSwiper; \ No newline at end of file diff --git a/components/tournament/TournamentBraket.tsx b/components/tournament/TournamentBraket.tsx new file mode 100644 index 000000000..fbc617a3b --- /dev/null +++ b/components/tournament/TournamentBraket.tsx @@ -0,0 +1,76 @@ +import dynamic from 'next/dynamic'; +import { + SVGViewer as StaticSVGViewer, + SingleEliminationBracket as StaticSingleEliminationBracket, +} from '@g-loot/react-tournament-brackets'; +import { Match } from '@g-loot/react-tournament-brackets/dist/src/types'; +import React from 'react'; +import TournamentMatch from 'components/tournament/TournamentMatch'; + +if (typeof window !== 'undefined' && typeof window.navigator !== 'undefined') { + import('@g-loot/react-tournament-brackets'); +} + +const SingleEliminationBracket = dynamic< + React.ComponentProps +>( + () => { + return import('@g-loot/react-tournament-brackets').then( + (mod) => mod.SingleEliminationBracket + ); + }, + { ssr: false } +); + +const SVGViewer = dynamic>( + () => { + return import('@g-loot/react-tournament-brackets').then( + (mod) => mod.SVGViewer + ); + }, + { ssr: false } +); + +interface TournamentBraketProps { + singleEliminationBracketMatchs: Match[]; +} + +export default function TournamentBraket({ + singleEliminationBracketMatchs, +}: TournamentBraketProps) { + if (singleEliminationBracketMatchs.length === 0) + return

Loading...

; + // const [width, height] = useWindowSize(); + const finalWidth = 500; //Math.max(width - 50, 500); + const finalHeight = 500; //Math.max(height - 100, 500); + + return ( + ( + + {children} + + )} + /> + ); +} diff --git a/components/tournament/TournamentCard.tsx b/components/tournament/TournamentCard.tsx new file mode 100644 index 000000000..10ba841eb --- /dev/null +++ b/components/tournament/TournamentCard.tsx @@ -0,0 +1,47 @@ +import { useSetRecoilState } from 'recoil'; +import { Modal } from 'types/modalTypes'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { modalState } from 'utils/recoil/modal'; +import styles from 'styles/tournament/TournamentCard.module.scss'; + +export default function TournamentCard({ + tournamentId, + title, + contents, + status, + type, + startTime, + endTime, + winnerIntraId, + winnerImageUrl, + player_cnt, +}: TournamentInfo) { + const setModal = useSetRecoilState(modalState); + + const openTournamentInfoModal = () => { + setModal({ + modalName: 'TOURNAMENT-REGISTRY', + tournamentInfo: { + tournamentId: tournamentId, + title: title, + contents: contents, + startTime: startTime, + status: status, + type: type, + endTime: endTime, + winnerIntraId: winnerIntraId, + winnerImageUrl: winnerImageUrl, + player_cnt: player_cnt, + }, + }); + }; + + return ( +
+
{title}
+
+ ); +} diff --git a/components/tournament/TournamentMatch.tsx b/components/tournament/TournamentMatch.tsx new file mode 100644 index 000000000..df3fe886e --- /dev/null +++ b/components/tournament/TournamentMatch.tsx @@ -0,0 +1,75 @@ +import { + MatchComponentProps, + Participant, +} from '@g-loot/react-tournament-brackets/dist/src/types'; +import PlayerImage from 'components/PlayerImage'; +import styles from 'styles/tournament/TournamentMatch.module.scss'; + +interface TournamentMatchPartyProps { + party: Participant; + teamNameFallback: string; + resultFallback: (participant: Participant) => string; + onMouseEnter: (partyId: string | number) => void; +} + +function TournamentMatchParty({ + party, + teamNameFallback, + onMouseEnter, + resultFallback, +}: TournamentMatchPartyProps) { + return ( +
onMouseEnter(party.id)} + > + + +
{party.name || teamNameFallback}
+
+ {party.resultText ?? resultFallback(party)} +
+
+ ); +} + +export default function TournamentMatch({ + match, + onMatchClick, + onPartyClick, + onMouseEnter, + onMouseLeave, + topParty, + bottomParty, + topWon, + bottomWon, + topHovered, + bottomHovered, + topText, + bottomText, + connectorColor, + computedStyles, + teamNameFallback, + resultFallback, +}: MatchComponentProps) { + return ( +
+ + +
+ ); +} diff --git a/components/tournament/TournamentMegaphone.tsx b/components/tournament/TournamentMegaphone.tsx new file mode 100644 index 000000000..0bad4aa61 --- /dev/null +++ b/components/tournament/TournamentMegaphone.tsx @@ -0,0 +1,108 @@ +import { useRouter } from 'next/router'; +import { useState, useEffect, useRef } from 'react'; +import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { instance } from 'utils/axios'; +import { mockInstance } from 'utils/mockAxios'; +import useInterval from 'hooks/useInterval'; +import styles from 'styles/Layout/MegaPhone.module.scss'; + +interface IMegaphoneContent { + megaphoneId?: number; + content: string; + intraId: string; + onClick: (event: React.MouseEvent) => void; +} + +type MegaphoneList = Array; + +export const MegaphoneItem = ({ + content, + intraId, + onClick, +}: IMegaphoneContent) => { + return ( +
+
{intraId}
+
{content}
+
+ ); +}; + +const TournamentMegaphone = () => { + const [contents, setContents] = useState([]); + const [TournamentList, setTournamentList] = useState([]); + const [megaphoneData, setMegaphoneData] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); + const virtuoso = useRef(null); + const router = useRouter(); + + function getTournamentListHandler() { + return mockInstance + .get(`tournament?page=1&status=예정&size=5`) + .then((res) => { + setTournamentList(res.data.tournaments); + }); + } + + const goTournamentPage = (event: React.MouseEvent) => { + router.push('tournament'); + }; + + useEffect(() => { + if (contents.length === 0) getTournamentListHandler(); + }, [contents]); + + useEffect(() => { + if (contents.length > 0) setMegaphoneData([...contents]); + else { + setMegaphoneData([ + ...TournamentList.map( + (item): IMegaphoneContent => ({ + megaphoneId: item.tournamentId, + content: item.title, + intraId: item.startTime.toString().split(':').slice(0, 2).join(':'), + onClick: goTournamentPage, + }) + ), + ]); + } + }, [contents, TournamentList]); + + useInterval(() => { + if (!megaphoneData) return; + const nextIndex = (selectedIndex + 1) % megaphoneData.length; + setSelectedIndex(nextIndex); + if (virtuoso.current !== null) + virtuoso.current.scrollToIndex({ + index: nextIndex, + align: 'start', + behavior: 'smooth', + }); + }, 4000); + + return ( +
+
+ {!!megaphoneData && megaphoneData.length > 0 && ( + ( + + )} + style={{ height: '100%' }} + /> + )} +
+
+ ); +}; + +export default TournamentMegaphone; diff --git a/constants/admin/table.ts b/constants/admin/table.ts index c95b6ff5d..05dbcd2e4 100644 --- a/constants/admin/table.ts +++ b/constants/admin/table.ts @@ -150,4 +150,12 @@ export const tableFormat: TableFormat = { 'rankLose', ], }, + tournament: { + name: '토너먼트', + columns: ['title', 'contents', 'startTime', 'endTime', 'type', 'edit'], + }, + tournamentCreate: { + name: '토너먼트 생성', + columns: ['tournamentName', 'startTime', 'endTime', 'tournamentType'], + }, }; diff --git a/package-lock.json b/package-lock.json index 3ca4dcd4d..627a3ff87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", + "@g-loot/react-tournament-brackets": "^1.0.30", "@mui/material": "^5.11.7", "@react-icons/all-files": "^4.1.0", "@types/react-js-pagination": "^3.0.4", @@ -34,6 +35,7 @@ "react-virtuoso": "^4.5.1", "recoil": "^0.7.3", "recoil-persist": "^4.2.0", + "swiper": "^11.0.4", "uuid": "^8.3.2", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" @@ -196,7 +198,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -3260,6 +3261,17 @@ "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", "dev": true }, + "node_modules/@g-loot/react-tournament-brackets": { + "version": "1.0.30", + "resolved": "https://registry.npmjs.org/@g-loot/react-tournament-brackets/-/react-tournament-brackets-1.0.30.tgz", + "integrity": "sha512-vbLjBtoOaZzxGpGM7J/XVHV8XY+1oktxgVTDv1orL+GolfIH8nueuEBZ/bt/m5wSZSFCFypmzVpVDPhmJGjKgA==", + "peerDependencies": { + "react": "^18.1.0", + "react-dom": "^18.1.0", + "react-svg-pan-zoom": "^3.10.0", + "styled-components": "^4.1.3" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -8357,6 +8369,22 @@ "react-docgen": "^5.0.0" } }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -8961,6 +8989,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001449", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz", @@ -9682,6 +9719,15 @@ "node": ">=8" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/css-functions-list": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.0.tgz", @@ -9733,6 +9779,23 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-to-react-native": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.3.2.tgz", + "integrity": "sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw==", + "peer": true, + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^3.3.0" + } + }, + "node_modules/css-to-react-native/node_modules/postcss-value-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "peer": true + }, "node_modules/css-tree": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", @@ -13935,6 +13998,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "peer": true + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -15011,6 +15080,12 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "peer": true + }, "node_modules/memoizerific": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", @@ -15120,6 +15195,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-anything": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.4.tgz", + "integrity": "sha512-l5XlriUDJKQT12bH+rVhAHjwIuXWdAIecGwsYjv2LJo+dA1AeRTmeQS+3QBpO6lEthBMDi2IUMpLC1yyRvGlwQ==", + "peer": true, + "dependencies": { + "is-what": "^3.3.1" + } + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -16221,7 +16305,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, "engines": { "node": ">=8.6" }, @@ -17284,6 +17367,19 @@ "react-dom": ">=16.8" } }, + "node_modules/react-svg-pan-zoom": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/react-svg-pan-zoom/-/react-svg-pan-zoom-3.12.1.tgz", + "integrity": "sha512-ug1LHCN5qed56C64xFypr/ClajuMFkig1OKvwJrIgGeSyHOjWM7XGgSgeP3IfHAkNw8QEc6a31ggZRpTijWYRw==", + "peer": true, + "dependencies": { + "prop-types": "^15.8.1", + "transformation-matrix": "^2.14.0" + }, + "funding": { + "url": "https://github.com/sponsors/chrvadala" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -18846,6 +18942,89 @@ "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", "dev": true }, + "node_modules/styled-components": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", + "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", + "hasInstallScript": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.0.0", + "@emotion/is-prop-valid": "^0.8.1", + "@emotion/unitless": "^0.7.0", + "babel-plugin-styled-components": ">= 1", + "css-to-react-native": "^2.2.2", + "memoize-one": "^5.0.0", + "merge-anything": "^2.2.4", + "prop-types": "^15.5.4", + "react-is": "^16.6.0", + "stylis": "^3.5.0", + "stylis-rule-sheet": "^0.0.10", + "supports-color": "^5.5.0" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "peer": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/styled-components/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "peer": true + }, + "node_modules/styled-components/node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "peer": true + }, + "node_modules/styled-components/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/styled-components/node_modules/stylis": { + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "peer": true + }, + "node_modules/styled-components/node_modules/stylis-rule-sheet": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", + "peer": true, + "peerDependencies": { + "stylis": "^3.5.0" + } + }, + "node_modules/styled-components/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -19196,6 +19375,24 @@ "node": ">= 10" } }, + "node_modules/swiper": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-11.0.4.tgz", + "integrity": "sha512-qtUxILrD4aD++rpKzGrkz3IAWL92f9uTrDwjb6HaNLmPvJhZCE/83DL+9w4kIgDDJeF6QKalV47rMBN77UOVYQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/synchronous-promise": { "version": "2.0.17", "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.17.tgz", @@ -19652,6 +19849,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "dev": true }, + "node_modules/transformation-matrix": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.15.0.tgz", + "integrity": "sha512-HN3kCvvH4ug3Xm/ycOfCFQOOktg5htxlC4Ih1Z7Wb6BMtQho+q+irOdGo10ARRKpqkRBXgBzQFw/AVmR0oIf0g==", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/chrvadala" + } + }, "node_modules/trim-newlines": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", diff --git a/package.json b/package.json index a3d4a175f..8f605391b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", + "@g-loot/react-tournament-brackets": "^1.0.30", "@mui/material": "^5.11.7", "@react-icons/all-files": "^4.1.0", "@types/react-js-pagination": "^3.0.4", @@ -40,6 +41,7 @@ "react-virtuoso": "^4.5.1", "recoil": "^0.7.3", "recoil-persist": "^4.2.0", + "swiper": "^11.0.4", "uuid": "^8.3.2", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" diff --git a/pages/admin/tournament.tsx b/pages/admin/tournament.tsx new file mode 100644 index 000000000..9bc472b01 --- /dev/null +++ b/pages/admin/tournament.tsx @@ -0,0 +1,12 @@ +import TournamentEdit from 'components/admin/tournament/TournamentEdit'; +import TournamentList from 'components/admin/tournament/TournamentList'; +import styles from 'styles/admin/announcement/Announcement.module.scss'; + +export default function Tournament() { + return ( +
+ + +
+ ); +} diff --git a/pages/api/pingpong/admin/games/index.ts b/pages/api/pingpong/admin/games/index.ts index fc3d6e983..efda697d8 100644 --- a/pages/api/pingpong/admin/games/index.ts +++ b/pages/api/pingpong/admin/games/index.ts @@ -48,6 +48,7 @@ function generateGamelog(): IGames { startAt: new Date(), slotTime: '15분', mode: 'RANK', + status: 'WAIT', team1: teams[0], team2: teams[1], }, @@ -56,6 +57,7 @@ function generateGamelog(): IGames { startAt: new Date(), slotTime: '15분', mode: 'RANK', + status: 'BEFORE', team1: teams[1], team2: teams[0], }, @@ -64,6 +66,7 @@ function generateGamelog(): IGames { startAt: new Date(), slotTime: '30분', mode: 'RANK', + status: 'LIVE', team1: teams[2], team2: teams[3], }, diff --git a/pages/api/pingpong/tournament/[tournamentId]/games/dummyTournamentGame.ts b/pages/api/pingpong/tournament/[tournamentId]/games/dummyTournamentGame.ts new file mode 100644 index 000000000..be63104d0 --- /dev/null +++ b/pages/api/pingpong/tournament/[tournamentId]/games/dummyTournamentGame.ts @@ -0,0 +1,102 @@ +import { TournamentGame } from 'types/tournamentTypes'; + +export const dummyLiveTournamentGames = { + games: [ + { + tournamentGameId: 19753, + game: null, + status: 'SCHEDULED', + nextTournamentGameId: null, + }, + { + tournamentGameId: 19754, + game: null, + status: 'SCHEDULED', + nextTournamentGameId: 19753, + }, + { + tournamentGameId: 19755, + game: { + gameId: 1, + status: 'END', + mode: 'TOURNAMENT', + time: '13', + team1: { + players: [ + { + intraId: 'CoKe BoYz', + userImageUri: + 'https://avatars.githubusercontent.com/u/93255519?v=4', + level: 3, + }, + ], + isWin: true, + score: 2, + }, + team2: { + players: [ + { + intraId: 'Aids Team', + userImageUri: + 'https://avatars.githubusercontent.com/u/93255519?v=4', + level: 3, + }, + ], + isWin: false, + score: 1, + }, + }, + status: 'SCORE_DONE', + nextTournamentGameId: 19754, + }, + { + tournamentGameId: 19756, + nextTournamentGameId: 19754, + status: 'RUNNING', + game: null, + }, + { + tournamentGameId: 19757, + nextTournamentGameId: 19753, + status: 'SCHEDULED', + game: null, + }, + { + tournamentGameId: 19758, + nextTournamentGameId: 19757, + status: 'SCHEDULED', + game: null, + }, + { + tournamentGameId: 19759, + nextTournamentGameId: 19757, + status: 'SCHEDULED', + game: { + gameId: 1, + status: 'LIVE', + mode: 'TOURNAMENT', + time: '13', + team1: { + players: [ + { + intraId: 'BLUEJAYS', + userImageUri: + 'https://avatars.githubusercontent.com/u/93255519?v=4', + level: 3, + }, + ], + }, + team2: { + players: [ + { + intraId: 'Bosphorus', + userImageUri: + 'https://avatars.githubusercontent.com/u/93255519?v=4', + level: 3, + }, + ], + }, + }, + }, + ], +}; diff --git a/pages/api/pingpong/tournament/[tournamentId]/games/index.ts b/pages/api/pingpong/tournament/[tournamentId]/games/index.ts new file mode 100644 index 000000000..b2b954f40 --- /dev/null +++ b/pages/api/pingpong/tournament/[tournamentId]/games/index.ts @@ -0,0 +1,21 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { dummyLiveTournamentGames } from './dummyTournamentGame'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { method, query } = req; + const { tournamentId } = query as { + tournamentId: string; + page: string; + type: string; + status: string; + size: string; + }; + + if (method !== 'GET') { + res.status(404).end('You must put page!!'); + return; + } + + res.status(200).json(dummyLiveTournamentGames); + return; +} diff --git a/pages/api/pingpong/tournament/dummyTournamentData.ts b/pages/api/pingpong/tournament/dummyTournamentData.ts new file mode 100644 index 000000000..e96a4443b --- /dev/null +++ b/pages/api/pingpong/tournament/dummyTournamentData.ts @@ -0,0 +1,64 @@ +import { TournamentInfo } from 'types/tournamentTypes'; + +const jincpark = { + intraId: 'jincpark', + imageUrl: + 'https://42gg-public-image.s3.ap-northeast-2.amazonaws.com/images/jincpark.jpeg', +}; + +const jaehyuki = { + intraId: 'jaehyuki', + imageUrl: + 'https://42gg-public-image.s3.ap-northeast-2.amazonaws.com/images/jaehyuki-7f6689c3-bf24-4e87-a04b-de9d61f1bef8.jpeg', +}; + +const junhjeon = { + intraId: 'junhjeon', + imageUrl: + 'https://42gg-public-image.s3.ap-northeast-2.amazonaws.com/images/junhjeon.jpeg', +}; + +const users = [jincpark, jaehyuki, junhjeon]; + +const dummyTournaments: TournamentInfo[] = []; + +for (let i = 28; i >= 1; i--) { + let status; + if (i === 28) { + status = '예정'; + } else if (i === 27) { + status = '진행중'; + } else { + status = '종료'; + } + + const rookieTournament: TournamentInfo = { + tournamentId: i * 2, + title: `${i}회 루키 토너먼트`, + contents: '블라블라', + startTime: new Date().toString(), + endTime: new Date().toString(), + status: status, + type: 'ROOKIE', + winnerIntraId: users[i % 3].intraId, + winnerImageUrl: users[i % 3].imageUrl, + player_cnt: 8, + }; + + const masterTournament: TournamentInfo = { + tournamentId: i * 2 - 1, + title: `${i}회 마스터 토너먼트`, + contents: '블라블라', + startTime: new Date().toString(), + endTime: new Date().toString(), + status: status, + type: 'MASTER', + winnerIntraId: users[i % 3].intraId, + winnerImageUrl: users[i % 3].imageUrl, + player_cnt: 8, + }; + + dummyTournaments.push(masterTournament, rookieTournament); +} + +export default dummyTournaments; diff --git a/pages/api/pingpong/tournament/index.ts b/pages/api/pingpong/tournament/index.ts new file mode 100644 index 000000000..3281e873e --- /dev/null +++ b/pages/api/pingpong/tournament/index.ts @@ -0,0 +1,51 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import dummyTournaments from './dummyTournamentData'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + const { page, type, status, size } = req.query as { + page: string; + type: string; + status: string; + size: string; + }; + + if (!page) { + res.status(404).end('You must put page!!'); + return; + } + + let filteredTournaments = dummyTournaments; + if (type || status) { + filteredTournaments = dummyTournaments.filter((tournament) => { + return ( + (!type || tournament.type === type) && + (!status || tournament.status === status) + ); + }); + } + + let sizeInt = 20; + if (size) { + sizeInt = parseInt(size); + } + + // 소수점이 있을 경우 올림 + const totalPage = Math.ceil(filteredTournaments.length / sizeInt); + + if (parseInt(page) > totalPage) { + res.status(404).end('Page number exceeded'); + return; + } + + // page와 size에 맞게 slice + const startIndex = (parseInt(page) - 1) * sizeInt; + filteredTournaments = filteredTournaments.slice( + startIndex, + startIndex + sizeInt + ); + + res.status(200).json({ + tournaments: filteredTournaments, + totalPage: totalPage, + }); +} diff --git a/pages/index.tsx b/pages/index.tsx index 14a26465b..9ceb0d2c3 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -9,6 +9,9 @@ const Home: NextPage = () => {
+
+
+
diff --git a/pages/tournament-record.tsx b/pages/tournament-record.tsx new file mode 100644 index 000000000..122866800 --- /dev/null +++ b/pages/tournament-record.tsx @@ -0,0 +1,41 @@ +import { useState, useRef, useEffect } from 'react'; +import { SwiperRef } from 'swiper/react'; +import { TournamentInfo } from 'types/tournamentTypes'; +import LeagueButtonGroup from 'components/tournament-record/LeagueButtonGroup'; +import WinnerSwiper from 'components/tournament-record/WinnerSwiper'; +import styles from 'styles/tournament-record/TournamentRecord.module.scss'; + +export default function TournamentRecord() { + const [tournamentInfo, setTournamentInfo] = useState(); + const [selectedType, setSelectedType] = useState('ROOKIE'); + const swiperRef = useRef(null); + const endTime = tournamentInfo ? new Date(tournamentInfo.endTime) : null; + + useEffect(() => { + if (swiperRef) { + swiperRef.current?.swiper.slideTo(0, 0); // index, speed + } + }, [selectedType]); + + return ( +
+

명예의 전당

+ + +
+

{tournamentInfo?.winnerIntraId}

+

+ {tournamentInfo?.title}{' '} + 우승자 +

+

{endTime?.toLocaleDateString()}

+
+
+
+ ); +} diff --git a/pages/tournament.tsx b/pages/tournament.tsx new file mode 100644 index 000000000..117e27e2e --- /dev/null +++ b/pages/tournament.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { TournamentInfo } from 'types/tournamentTypes'; +import { instance } from 'utils/axios'; +import { InfiniteScroll } from 'utils/infinityScroll'; +import { mockInstance } from 'utils/mockAxios'; +import { errorState } from 'utils/recoil/error'; +import { InfiniteScrollComponent } from 'components/store/InfiniteScrollComponent'; +import TournamentCard from 'components/tournament/TournamentCard'; +import styles from 'styles/tournament/TournamentContainer.module.scss'; + +export default function Tournament() { + const setError = useSetRecoilState(errorState); + + // const Info = useQuery( + // 'openTorunamentInfo', + // () => + // mockInstance + // .get('tournament?page=1&status=진행중') + // .then((res) => res.data), + // { retry: 1, staleTime: 60000 /* 60초 */ } + // ); + + async function fetchWaitTournamentData(page: number) { + return await mockInstance + .get(`tournament?page=${page}&status=예정&size=4`) + .then((res) => { + return res.data; + }); + } + + const { data, error, isLoading, hasNextPage, fetchNextPage } = InfiniteScroll( + 'waitTournament', + fetchWaitTournamentData, + 'JJH1' + ); + + return ( +
+

Tournament

+
+
대기중인 토너먼트
+
+ {data?.pages.map((page, pageIndex) => ( +
+ {page.tournaments.map( + (tournament: TournamentInfo, tournamentIndex: number) => ( + + ) + )} + {/* 실제로는 tournamnetId 를 key값으로 사용하는게 좋음, 현재는 mockdata에서 id 값들이 겹치기 때문에 Index로 사용 + {page.tournaments.map((tournament: TournamentInfo, tournamentIndex: number) => ( + < key={tournament.tournamentId} {...tournament} /> + ))} + */} +
+ ))} + +
+
진행중인 토너먼트
+
+
무언가 토너먼트의 사진
+
+
+
+ ); +} diff --git a/public/image/match_qustion.png b/public/image/match_qustion.png new file mode 100644 index 0000000000000000000000000000000000000000..44b2cec2906856082a0f2d964b37b4527683916f GIT binary patch literal 15750 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 L*qW9Z6E6K9qNFl+ literal 0 HcmV?d00001 diff --git a/public/image/menu_halloffame.svg b/public/image/menu_halloffame.svg new file mode 100644 index 000000000..8fafa1f4e --- /dev/null +++ b/public/image/menu_halloffame.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/styles/Layout/CurrentMatchInfo.module.scss b/styles/Layout/CurrentMatchInfo.module.scss index 79077f03a..06610ec88 100644 --- a/styles/Layout/CurrentMatchInfo.module.scss +++ b/styles/Layout/CurrentMatchInfo.module.scss @@ -112,9 +112,11 @@ $message-color: rgba(39, 10, 70, 1); border-radius: 0.375rem; &.nonBlock { color: white; + cursor: pointer; } &.block { color: red; + cursor: not-allowed; } } diff --git a/styles/PlayerImage.module.scss b/styles/PlayerImage.module.scss index 820a3d4dd..28722f17c 100644 --- a/styles/PlayerImage.module.scss +++ b/styles/PlayerImage.module.scss @@ -129,3 +129,11 @@ } } } + +.tournament { + @include imageWrap; + div { + @include userImage(2.2rem); + cursor: pointer; + } +} diff --git a/styles/admin/modal/AdminEditTournamentBraket.module.scss b/styles/admin/modal/AdminEditTournamentBraket.module.scss new file mode 100644 index 000000000..364003780 --- /dev/null +++ b/styles/admin/modal/AdminEditTournamentBraket.module.scss @@ -0,0 +1,10 @@ +@import 'styles/common.scss'; + +.whole { + display: flex; + width: 510px; + height: 569px; + flex-direction: column; + background: rgb(38, 38, 38); + border-radius: 9px; +} diff --git a/styles/common.scss b/styles/common.scss index f2f27944d..ee80234fd 100644 --- a/styles/common.scss +++ b/styles/common.scss @@ -331,6 +331,7 @@ $vip-background: linear-gradient( justify-content: center; align-items: center; } + .negative { input { margin-right: 1rem; @@ -352,6 +353,20 @@ $vip-background: linear-gradient( ); } } + .close { + input { + padding: 0.5rem 1rem; + color: rgba(255, 255, 255, 1); + cursor: pointer; + background: linear-gradient( + 180deg, + #c66bf2 0%, + rgba(96, 6, 138, 0.52) 100% + ); + border: 0; + border-radius: 0.625rem; + } + } } @mixin txtColor($color: black) { diff --git a/styles/game/GameResultItem.module.scss b/styles/game/GameResultItem.module.scss index 7d49610ef..f65ed7b2b 100644 --- a/styles/game/GameResultItem.module.scss +++ b/styles/game/GameResultItem.module.scss @@ -238,6 +238,7 @@ $gameItemThemes: ( color: #ffffff; text-align: center; } + cursor: pointer; } // ANCHOR : big item diff --git a/styles/main/Section.module.scss b/styles/main/Section.module.scss index a220211e3..eecec2c95 100644 --- a/styles/main/Section.module.scss +++ b/styles/main/Section.module.scss @@ -2,7 +2,6 @@ .sectionWrap { position: relative; - z-index: 1; margin-top: 2.5rem; &.mainRank { margin-top: 1rem; @@ -23,6 +22,7 @@ align-items: center; } > button { + z-index: 10; width: 1.563rem; height: 1.563rem; color: #ffffff; diff --git a/styles/modal/event/AnnouncementModal.module.scss b/styles/modal/event/AnnouncementModal.module.scss index c9901c02e..c2d7869cb 100644 --- a/styles/modal/event/AnnouncementModal.module.scss +++ b/styles/modal/event/AnnouncementModal.module.scss @@ -43,9 +43,11 @@ width: 1rem; height: 1rem; margin: 0 0.5rem 0 0; + cursor: pointer; } label { display: inline-block; color: white; + cursor: pointer; } } diff --git a/styles/modal/event/TournamentRegistryModal.module.scss b/styles/modal/event/TournamentRegistryModal.module.scss new file mode 100644 index 000000000..92f9fe936 --- /dev/null +++ b/styles/modal/event/TournamentRegistryModal.module.scss @@ -0,0 +1,68 @@ +@import 'styles/common.scss'; + +.container { + @include modalContainer('SKYPINK'); + width: 80vw; + max-width: 22rem; +} + +.title { + @include modalTitle; + padding-bottom: 1rem; + color: rgba(18, 23, 37, 1); +} + +.startTime { + font-size: 0.7rem; + color: rgba(18, 23, 37, 1); +} + +.quillViewer { + width: 100%; + min-width: 10rem; + max-width: 15rem; + height: 100%; + max-height: 19rem; + padding-bottom: 1rem; + overflow-y: scroll; + color: rgba(18, 23, 37, 1); +} + +.quillViewer::-webkit-scrollbar { + display: inherit; + width: 5px; + height: 1rem; +} + +.quillViewer::-webkit-scrollbar-thumb { + background: white; + border-radius: 10px; +} + +.checkBox { + margin-bottom: 0.5rem; + input { + position: relative; + top: 2px; + display: inline-block; + width: 1rem; + height: 1rem; + margin: 0 0.5rem 0 0; + cursor: pointer; + } + label { + display: inline-block; + color: white; + cursor: pointer; + } +} + +.closeButtonContainer { + display: flex; + margin-left: auto; +} + +.tournamentInfo { + justify-content: center; + align-items: center; +} diff --git a/styles/mode/SeasonDropDown.module.scss b/styles/mode/SeasonDropDown.module.scss index eb2cd5d7d..fa5fb18b2 100644 --- a/styles/mode/SeasonDropDown.module.scss +++ b/styles/mode/SeasonDropDown.module.scss @@ -16,6 +16,7 @@ width: 4rem; height: inherit; margin: 0 0.5rem; + cursor: pointer; background: transparent; border: 0 none; outline: 0 none; diff --git a/styles/rank/RankListMain.module.scss b/styles/rank/RankListMain.module.scss index a0b1f3b78..161f037ed 100644 --- a/styles/rank/RankListMain.module.scss +++ b/styles/rank/RankListMain.module.scss @@ -1,5 +1,9 @@ @import 'styles/common.scss'; +.RankContainer { + z-index: 0; +} + .mainContainer { position: relative; top: 1.5rem; @@ -11,7 +15,6 @@ align-items: end; &.isMain { height: 15.5rem; - margin-top: 7.5rem; } } @@ -82,8 +85,9 @@ .bangContainer { display: flex; - justify-content: space-around; margin-top: -7.5rem; + pointer-events: none; + justify-content: space-around; } .bang { diff --git a/styles/store/ItemCard.module.scss b/styles/store/ItemCard.module.scss index 1db0c11eb..e70438c9f 100644 --- a/styles/store/ItemCard.module.scss +++ b/styles/store/ItemCard.module.scss @@ -85,6 +85,7 @@ min-width: 4rem; height: 1.5rem; color: white; + cursor: pointer; background: linear-gradient( 180deg, #ea80ea 0%, @@ -98,6 +99,7 @@ min-width: 4rem; height: 1.5rem; color: white; + cursor: pointer; background: $btn-purple; border-style: none; border-radius: 0.3rem; diff --git a/styles/tournament-record/LeagueButtonGroup.module.scss b/styles/tournament-record/LeagueButtonGroup.module.scss new file mode 100644 index 000000000..97aefe009 --- /dev/null +++ b/styles/tournament-record/LeagueButtonGroup.module.scss @@ -0,0 +1,24 @@ +.leagueButtonWrapper { + display: flex; + padding-right: 2rem; + padding-left: 2rem; + margin-top: 1.4rem; + justify-content: space-between; + + button { + width: 5rem; + height: 2rem; + font-size: 1rem; + font-weight: 400; + color: #cbcbcb; + background-color: transparent; + border-color: transparent; + + &.active { + font-weight: 700; + color: #ffffff; + border: 2px solid #9e00ff; + border-radius: 0.6rem; + } + } +} diff --git a/styles/tournament-record/TournamentRecord.module.scss b/styles/tournament-record/TournamentRecord.module.scss new file mode 100644 index 000000000..aef840cf7 --- /dev/null +++ b/styles/tournament-record/TournamentRecord.module.scss @@ -0,0 +1,53 @@ +@import 'styles/common.scss'; + +.pageWrap { + @include pageWrap; +} + +.title { + @include pageTitle; + display: flex; + justify-content: center; +} + +.winnerImageContainer { + height: 10rem; + margin-top: 1.8rem; + background-color: rgb(88, 88, 88); +} + +.winnerInfoContainer { + font-weight: 500; + color: #d591ff; + text-align: center; + + p { + margin: 0.4rem 0 0; + } + + .userId { + font-weight: 700; + color: white; + } + + .gameInfo { + font-size: 1rem; + + .highlighted { + color: #ffc700; + } + } + + .date { + font-size: 0.8rem; + } +} + +.bracketContainer { + width: 100%; + height: 21.5rem; + margin-top: 1rem; + background-color: rgba(112, 0, 255, 0.17); + border: 1px solid #c67dff; + border-radius: 26px; +} diff --git a/styles/tournament-record/WinnerProfileImage.module.scss b/styles/tournament-record/WinnerProfileImage.module.scss new file mode 100644 index 000000000..6ac302fd4 --- /dev/null +++ b/styles/tournament-record/WinnerProfileImage.module.scss @@ -0,0 +1,31 @@ +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.winnerProfileImage { + position: absolute; + top: 50%; + left: 50%; + width: 8rem; + height: 100%; + overflow: hidden; + filter: brightness(50%); + border: 4px solid #e2be00; + border-radius: 1.8rem; + transition: filter 0.4s ease; + transform: translate(-50%, -50%); + animation: fadeIn 0.3s ease-in-out; + + &.firstLayer { + filter: brightness(100%); + } + + &.secondLayer { + filter: brightness(75%); + } +} diff --git a/styles/tournament-record/WinnerSwiper.module.scss b/styles/tournament-record/WinnerSwiper.module.scss new file mode 100644 index 000000000..3be4e9a3f --- /dev/null +++ b/styles/tournament-record/WinnerSwiper.module.scss @@ -0,0 +1,6 @@ +.swiper { + height: 10rem; + margin-top: 1.7rem; + margin-bottom: 1rem; + overflow: hidden; +} diff --git a/styles/tournament/TournamentCard.module.scss b/styles/tournament/TournamentCard.module.scss new file mode 100644 index 000000000..c175a9325 --- /dev/null +++ b/styles/tournament/TournamentCard.module.scss @@ -0,0 +1,16 @@ +.tournamentCardContainer { + display: flex; + width: 100%; + padding: 1rem; + margin-bottom: 1rem; + background-color: black; + border: 2px solid black; + border-radius: 0.3rem; + + .text { + overflow: hidden; + color: white; + text-overflow: ellipsis; + white-space: nowrap; + } +} diff --git a/styles/tournament/TournamentContainer.module.scss b/styles/tournament/TournamentContainer.module.scss new file mode 100644 index 000000000..566583c88 --- /dev/null +++ b/styles/tournament/TournamentContainer.module.scss @@ -0,0 +1,65 @@ +@import 'styles/common.scss'; + +.pageWrap { + @include pageWrap; +} + +.title { + @include pageTitle; + width: fit-content; + cursor: pointer; +} + +.tournamentContainer { + display: flex; + height: 60vh; + flex-direction: column; + padding: 1rem 1rem 0rem; + overflow-y: scroll; + background: #d4b8f2; + border-radius: $small-radius; +} + +.waitTournamentBox { + display: flex; + width: 100%; + flex: 5; + flex-direction: column; + padding: 1rem 1rem 0; + overflow-x: hidden; + overflow-y: scroll; + background: rgba(112, 0, 255, 0.17); + border: 2px solid #c67dff; + border-radius: 0.3rem; +} + +.openTournamentBox { + display: flex; + width: 100%; + flex: 15; + flex-direction: column; + padding: 0.2rem 0 0.2rem 0.2rem; + margin-bottom: 1rem; + overflow-x: scroll; + overflow-y: scroll; + background: rgba(112, 0, 255, 0.17); + border: 2px solid #c67dff; + border-radius: 0.3rem; +} + +.tournamentTextWait { + display: flex; + flex: 1; + font-weight: 700; + color: black; + text-align: center; /* 텍스트를 가로로 중앙에 정렬 */ +} + +.tournamentTextOpen { + display: flex; + flex: 1; + padding-top: 1rem; + font-weight: 700; + color: black; + text-align: center; /* 텍스트를 가로로 중앙에 정렬 */ +} diff --git a/styles/tournament/TournamentMatch.module.scss b/styles/tournament/TournamentMatch.module.scss new file mode 100644 index 000000000..5a897168d --- /dev/null +++ b/styles/tournament/TournamentMatch.module.scss @@ -0,0 +1,29 @@ +@import 'styles/common.scss'; + +.tournamentPartyWrapper { + display: flex; + height: 45%; + padding: 0.5rem; + color: white; + background-color: $rank-purple; + border: 0.1rem solid black; + border-radius: 0.6rem; + justify-content: space-between; + align-items: center; + .partyName { + margin-left: 1rem; + } + + .score { + margin-left: auto; + } +} + +.tournamentMatchContainer { + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + color: #000000; + justify-content: space-around; +} diff --git a/types/admin/adminTournamentTypes.ts b/types/admin/adminTournamentTypes.ts new file mode 100644 index 000000000..57683c7cd --- /dev/null +++ b/types/admin/adminTournamentTypes.ts @@ -0,0 +1,18 @@ +export interface ITournament { + tournamentId: number; + title: string; + contents: string; + status: string; // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' + type: 'CUSTOM' | 'MASTER' | 'ROOKIE'; + winnerIntraId: string; + winnerImageUrl: string; + startTime: Date; + endTime: Date; + player_cnt: number; +} + +export interface ITournamentTable { + tournamentList: ITournament[]; + totalPage: number; + currentPage: number; +} diff --git a/types/admin/gameLogTypes.ts b/types/admin/gameLogTypes.ts index 7cabdd63a..65424e637 100644 --- a/types/admin/gameLogTypes.ts +++ b/types/admin/gameLogTypes.ts @@ -1,5 +1,5 @@ export type GameType = 'NORMAL' | 'RANK'; - +export type GameStatus = 'WAIT' | 'BEFORE' | 'LIVE' | 'END'; export interface ITeam { intraId1: string; intraId2?: string; @@ -13,6 +13,7 @@ export interface IGameLog { startAt: Date; slotTime: string; mode: GameType; + status: GameStatus; team1: ITeam; team2: ITeam; } @@ -26,4 +27,5 @@ export type ModifyScoreType = { gameId: number; team1: ITeam; team2: ITeam; + status: GameStatus; }; diff --git a/types/admin/tableTypes.ts b/types/admin/tableTypes.ts index 056ba85e8..333b85d68 100644 --- a/types/admin/tableTypes.ts +++ b/types/admin/tableTypes.ts @@ -15,7 +15,9 @@ export type TableName = | 'itemList' | 'itemHistory' | 'coinPolicy' - | 'coinPolicyHistory'; + | 'coinPolicyHistory' + | 'tournament' + | 'tournamentCreate'; export type EtcType = 'button' | 'toggle'; diff --git a/types/gameTypes.ts b/types/gameTypes.ts index 0f6342eb4..1646c42a2 100644 --- a/types/gameTypes.ts +++ b/types/gameTypes.ts @@ -17,7 +17,7 @@ export type Team = { score?: number; }; -export type GameMode = 'NORMAL' | 'RANK'; +export type GameMode = 'NORMAL' | 'RANK' | 'TOURNAMENT'; export type GameStatus = 'LIVE' | 'WAIT' | 'END'; export type Game = { diff --git a/types/modalTypes.ts b/types/modalTypes.ts index 27906a532..1dc6fa74d 100644 --- a/types/modalTypes.ts +++ b/types/modalTypes.ts @@ -11,6 +11,8 @@ import { MatchMode } from 'types/mainType'; import { ISeason } from 'types/seasonTypes'; import { StoreManualMode } from 'types/storeTypes'; import { ICoin } from 'types/userTypes'; +import { ITournament } from './admin/adminTournamentTypes'; +import { TournamentInfo } from './tournamentTypes'; type EventModal = 'WELCOME' | 'ANNOUNCEMENT'; @@ -27,8 +29,11 @@ type PurchaseModal = 'BUY' | 'GIFT' | 'NO_COIN'; type UseItemModal = ItemType | 'GACHA'; type EditItemModal = 'MEGAPHONE'; + type StoreModal = 'MANUAL' | 'COIN_HISTORY'; +type TournamentModal = 'REGISTRY'; + type AdminModal = | 'PROFILE' | 'USER-COIN' @@ -44,7 +49,8 @@ type AdminModal = | 'ITEM_EDIT' | 'ITEM_DELETE' | 'COINPOLICY_EDIT' - | 'CHECK_SEND_NOTI'; + | 'CHECK_SEND_NOTI' + | 'TOURNAMENT_BRAKET_EDIT'; type ModalName = | null @@ -59,7 +65,8 @@ type ModalName = | `USE-ITEM-${UseItemModal}` | `EDIT-ITEM-${EditItemModal}` | `STORE-${StoreModal}` - | `PURCHASE-${PurchaseModal}`; + | `PURCHASE-${PurchaseModal}` + | `TOURNAMENT-${TournamentModal}`; export interface Cancel { startTime: string; @@ -129,4 +136,6 @@ export interface Modal { isAttended?: boolean; totalCoin?: ICoin; randomItem?: IRandomItem; + tournamentInfo?: TournamentInfo; + tournament?: ITournament; } diff --git a/types/tournamentTypes.ts b/types/tournamentTypes.ts new file mode 100644 index 000000000..0702c6d63 --- /dev/null +++ b/types/tournamentTypes.ts @@ -0,0 +1,26 @@ +import { Game } from './gameTypes'; + +export interface TournamentInfo { + tournamentId: number; + title: string; + contents: string; + status: string; // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' + type: 'CUSTOM' | 'MASTER' | 'ROOKIE'; + winnerIntraId: string; + winnerImageUrl: string; + startTime: string; + endTime: string; + player_cnt: number; +} + +export interface TournamentData { + tournaments: TournamentInfo[]; + totalPage: number; +} + +export interface TournamentGame { + tournamentGameId: number; + game: Game | null; + status: string; // 'NO_SHOW' | 'WALK_OVER' | 'NO_PARTY' | 'DONE' | 'SCORE_DONE' + nextTournamentGameId: number | null; +} diff --git a/utils/handleTournamentGame.ts b/utils/handleTournamentGame.ts new file mode 100644 index 000000000..8ac6a385f --- /dev/null +++ b/utils/handleTournamentGame.ts @@ -0,0 +1,87 @@ +import { + Match, + Participant, +} from '@g-loot/react-tournament-brackets/dist/src/types'; +import { TournamentGame } from 'types/tournamentTypes'; + +/** + * 이전게임의 우승자를 다음 예상 게임의 참가자로 추가 + * @param {Match[]} matches 토너먼트 게임 + * */ +export const addExpectedMatchParticipants = (matches: Match[]) => { + for (let i = 0; i < matches.length; i++) { + if (matches[i].participants.length === 0) { + const beforeMatch = matches.find( + (match) => match.nextMatchId === matches[i].id + ); + const winner = beforeMatch?.participants.find( + (participant) => participant.isWinner === true + ); + if (winner) { + const modifiedWinner = { ...winner, isWinner: true, resultText: null }; + matches[i].participants.push(modifiedWinner); + } + } + } +}; + +/** + * 토너먼트 게임데이터를 브래킷 게임 데이터로 변환 + * @param {TournamentGame} tournamentGame 토너먼트 게임 + * @return {Match} + * */ +export const convertTournamentGameToBracketMatch = ( + tournamentGame: TournamentGame +): Match => { + const { tournamentGameId, game, status, nextTournamentGameId } = + tournamentGame; + const { team1, team2 } = game ?? { team1: null, team2: null }; + + const participantsTeam1: Participant[] = team1 + ? [ + { + id: team1.players.map((player) => player.intraId).join(' '), + resultText: team1.score?.toString(), + isWinner: team1.isWin ?? false, + name: team1.players.map((player) => player.intraId).join(' '), + picture: team1.players[0].userImageUri, + }, + ] + : []; + + const participantsTeam2: Participant[] = team2 + ? [ + { + id: team2.players.map((player) => player.intraId).join(' '), + resultText: team2.score?.toString() ?? null, + isWinner: team2.isWin ?? false, + name: team2.players.map((player) => player.intraId).join(' '), + picture: team2.players[0].userImageUri, + }, + ] + : []; + + const participants = [...participantsTeam1, ...participantsTeam2]; + + return { + id: tournamentGameId, + nextMatchId: nextTournamentGameId, + startTime: tournamentGame.game ? tournamentGame.game.time.toString() : '', + state: status, + participants, + }; +}; + +/** + * 토너먼트 게임데이터 리스트를 브래킷 게임 데이터 리스트로 변환 + * @param {TournamentGame[]} tournamentGames 토너먼트 게임 + * @return {Match[]} + * */ +export const convertTournamentGamesToBracketMatchs = ( + tournamentGames: TournamentGame[] +): Match[] => { + const matchs = tournamentGames.map(convertTournamentGameToBracketMatch); + + addExpectedMatchParticipants(matchs); + return matchs; +}; diff --git a/utils/infinityScroll.ts b/utils/infinityScroll.ts index 694574e2e..89ddd168c 100644 --- a/utils/infinityScroll.ts +++ b/utils/infinityScroll.ts @@ -53,3 +53,40 @@ export function InfinityScroll( } ); } + +interface PagenatedResponse { + totalPage: number; +} + +// 무한스크롤 제네릭 함수 +// Todo: 이 함수로 프로젝트 내 무한스크롤 모두 대체 +export function InfiniteScroll( + queryKey: string | string[], + fetchFunction: (page: number) => Promise, + errorCode: string +) { + const setError = useSetRecoilState(errorState); + return useInfiniteQuery( + queryKey, + ({ pageParam = 1 }) => fetchFunction(pageParam), + { + getNextPageParam: (lastPage, currPages) => { + const nextPage = currPages.length + 1; + if (nextPage <= lastPage.totalPage) { + return nextPage; + } + return undefined; + }, + onError: (e: unknown) => { + if (axios.isAxiosError(e)) { + setError(errorCode); + } else { + // axios에서 발생한 에러가 아닌 경우 + setError('JY03'); + } + }, + retry: 0, + keepPreviousData: true, + } + ); +} diff --git a/utils/recoil/modal.ts b/utils/recoil/modal.ts index 5821a6ba7..f0f1911c8 100644 --- a/utils/recoil/modal.ts +++ b/utils/recoil/modal.ts @@ -13,12 +13,15 @@ export const modalTypeState = selector({ let modalType = ''; const normalPrefixes = ['EVENT', 'MENU', 'MATCH', 'USER', 'FIXED']; const storePrefixes = ['COIN', 'STORE', 'PURCHASE', 'USE', 'EDIT']; + const tournamentPrefixes = ['TOURNAMENT']; const prefix = get(modalState).modalName?.split('-')[0] || ''; if (normalPrefixes.includes(prefix)) { modalType = 'NORMAL'; } else if (storePrefixes.includes(prefix)) { modalType = 'STORE'; + } else if (tournamentPrefixes.includes(prefix)) { + modalType = 'TOURNAMENT'; } else if (prefix === 'ADMIN') { modalType = 'ADMIN'; } From c9f5ab58f5fc161fbd376f6d8e7eda9ad91f796f Mon Sep 17 00:00:00 2001 From: Jincheol Park <67998022+Clearsu@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:44:51 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[Fix]=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=88=98=EC=A0=95=20#1169=20(#1170)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/index.tsx b/pages/index.tsx index eabfdfd17..a90a509d6 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -10,7 +10,7 @@ const Home: NextPage = () => { return (
- {tournamentData && ( + {tournamentData?.length && (
)}
From 74ef36910b212991345ed5f477318c0351450cac Mon Sep 17 00:00:00 2001 From: joonho0410 <76806109+joonho0410@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:56:19 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Others/=ED=86=A0=EB=84=88=EB=A8=BC=ED=8A=B8?= =?UTF-8?q?=20card=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20style=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20api=20=EC=B5=9C=EC=A0=81=ED=99=94#1168?= =?UTF-8?q?=20(#1172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [fix] 예정된 토너먼트가 없을 시 안내문구 생성#1159 * [Style] 예정된 토너먼트 없을 시 텍스트 스타일 수정 #1159 * [Fix] Open된 토너먼트가 없을 때 조건문 수정 #1159 * [Fix] 진행중인 토너먼트 undefined 상태 추가 #1159 * [feat] 토너먼트 레지스트리 모달 시작: 종료 시간 추가 #1159 * [fix] api 최적화 #1159 * [Fix] 토너먼트 카드 모달 참여인원 표시 수정 #1159 * [Fix] 메인페이지에서 토너먼트 없을 시 수정 #1159 * [Fix] 토너먼트 신청 모달 map 재작성 #1159 * merge * [Fix] API 호출 최적화 #1168 * [Style] 시간을 좌측정렬으로 크기 수정 #1168 * [Fix] 등록/취소중 에러발생시 에러메세지 수정 #1168 * [others] 에러발생시키기 위한 코드 제거 #1168 * [fix] 토너먼트 진행되는게 없을 시 컨테이너 크기 없이 생성#1168 --------- Co-authored-by: Junho jeon Co-authored-by: Junho Jeon --- .../tournament/TournamentRegistryModal.tsx | 93 +++++++++++++------ components/tournament/TournamentCard.tsx | 8 +- pages/index.tsx | 1 - pages/tournament.tsx | 50 ++++++---- .../event/TournamentRegistryModal.module.scss | 1 + styles/tournament/TournamentCard.module.scss | 11 +-- .../TournamentContainer.module.scss | 7 ++ utils/handleBraketSize.ts | 7 ++ 8 files changed, 119 insertions(+), 59 deletions(-) create mode 100644 utils/handleBraketSize.ts diff --git a/components/modal/tournament/TournamentRegistryModal.tsx b/components/modal/tournament/TournamentRegistryModal.tsx index b47200b41..8c54dc2db 100644 --- a/components/modal/tournament/TournamentRegistryModal.tsx +++ b/components/modal/tournament/TournamentRegistryModal.tsx @@ -8,6 +8,7 @@ import { instance } from 'utils/axios'; import { dateToKRLocaleTimeString } from 'utils/handleTime'; import { errorState } from 'utils/recoil/error'; import { modalState } from 'utils/recoil/modal'; +import { toastState } from 'utils/recoil/toast'; import { ModalButtonContainer, ModalButton, @@ -30,25 +31,40 @@ export default function TournamentRegistryModal({ player_cnt, tournamentId, }: TournamentInfo) { + const setSnackbar = useSetRecoilState(toastState); const setModal = useSetRecoilState(modalState); const setError = useSetRecoilState(errorState); const [registState, setRegistState] = useState('LOADING'); const [openDate, setOpenDate] = useState('미정'); + const [closeDate, setCloseDate] = useState('미정'); const [loading, setLoading] = useState(false); const [playerCount, setPlayerCount] = useState(player_cnt); - const registTournament = useCallback(() => { setLoading(true); return instance .post(`/pingpong/tournaments/${tournamentId}/users`) .then((res) => { - // alert('토너먼트 신청이 완료됐습니다'); setLoading(false); + setSnackbar({ + toastName: `토너먼트 신청`, + severity: 'success', + message: `🔥 토너먼트 참가 신청이 완료 됐습니다 ! 🔥`, + clicked: true, + }); setRegistState(res.data.status); return res.data.status; }) .catch((error) => { - setError('토너먼트 신청 중 에러가 발생했습니다.'); + setSnackbar({ + toastName: `토너먼트 신청`, + severity: 'error', + message: `${ + error.response?.data?.message + ? error.response.data.message + : '예상치 못한 에러가 발생했습니다 다시 시도해 주세요 😢' + } `, + clicked: true, + }); setLoading(false); }); }, []); @@ -59,17 +75,31 @@ export default function TournamentRegistryModal({ .delete(`/pingpong/tournaments/${tournamentId}/users`) .then((res) => { if (registState === 'WAIT') { - // alert('토너먼트 대기가 취소 되었습니다'); + setSnackbar({ + toastName: `토너먼트 대기 취소`, + severity: 'success', + message: `토너먼트 대기 신청을 취소했습니다.`, + clicked: true, + }); } else { - // setPlayerCount(playerCount - 1); - // alert('토너먼트 등록이 취소 되었습니다'); + setSnackbar({ + toastName: `토너먼트 신청 취소 `, + severity: 'success', + message: `토너먼트 참가 신청을 취소했습니다.`, + clicked: true, + }); } setRegistState(res.data.status); setLoading(false); return res.data.status; }) .catch((error) => { - setError('토너먼트 등록취소 중 에러가 발생했습니다'); + setSnackbar({ + toastName: `토너먼트 신청 취소`, + severity: 'error', + message: `취소중 에러가 발생했습니다.`, + clicked: true, + }); setLoading(false); }); }, []); @@ -99,38 +129,42 @@ export default function TournamentRegistryModal({ }, [tournamentId]); useEffect(() => { - getTournamentInfo(); getStatus(); - const date = new Date(startTime); - setOpenDate(dateToKRLocaleTimeString(date)); + setOpenDate(dateToKRLocaleTimeString(new Date(startTime))); + setCloseDate(dateToKRLocaleTimeString(new Date(endTime))); }, []); useEffect(() => { - getTournamentInfo(); - }, [registState]); + if (registState !== 'LOADING') getTournamentInfo(); + }, [registState, getTournamentInfo]); const closeModalButtonHandler = () => { setModal({ modalName: null }); }; - const buttonContents: Record = { - LOADING: '로딩중...', - BEFORE: '등록', - WAIT: '대기 취소', - PLAYER: '등록 취소', - }; - - const buttonAction: Record = { - BEFORE: registTournament, - WAIT: unRegistTournament, - PLAYER: unRegistTournament, - LOADING: () => { - console.log('loading..'); + const buttonMappings: Record = { + LOADING: { + content: '로딩중...', + handler: () => { + console.log('loading...'); + }, + }, + BEFORE: { + content: '등록', + handler: registTournament, + }, + WAIT: { + content: '대기 취소', + handler: unRegistTournament, + }, + PLAYER: { + content: '등록 취소', + handler: unRegistTournament, }, }; - const buttonContent = buttonContents[registState]; - const buttonHandler = buttonAction[registState]; + const { content: buttonContent, handler: buttonHandler } = + buttonMappings[registState]; return (
@@ -145,7 +179,8 @@ export default function TournamentRegistryModal({
{title}
-
{openDate}
+
시작 : {openDate}
+
종료 : {closeDate}
{playerCount} / 8
@@ -163,7 +198,7 @@ export default function TournamentRegistryModal({ { - getTournamentInfo(); - getStatus(); + if (modal.modalName === null) { + getTournamentInfo(); + getStatus(); + } }, [modal]); const getStatus = useCallback(() => { diff --git a/pages/index.tsx b/pages/index.tsx index a90a509d6..39b0c30a7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,7 +6,6 @@ import styles from 'styles/main/Home.module.scss'; const Home: NextPage = () => { const tournamentData = useBeforeLiveTournamentData(); - return (
diff --git a/pages/tournament.tsx b/pages/tournament.tsx index 3ea344ea9..d38ca460d 100644 --- a/pages/tournament.tsx +++ b/pages/tournament.tsx @@ -12,7 +12,9 @@ import styles from 'styles/tournament/TournamentContainer.module.scss'; export default function Tournament() { const setError = useSetRecoilState(errorState); - const [openTournamentId, setOpenTournamentId] = useState(0); + const [openTournamentId, setOpenTournamentId] = useState( + undefined + ); const [openTournament, setOpenTournament] = useState([]); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const containerRef = useRef(null); // useRef를 사용하여 Ref 생성 @@ -23,12 +25,15 @@ export default function Tournament() { instance .get('/pingpong/tournaments?size=20&page=1&status=LIVE') .then((res) => { - setOpenTournamentId(res.data.tournaments[0].tournamentId); + if (res.data.tournamets?.length === 1) { + console.log('openInfo'); + setOpenTournamentId(res.data.tournaments[0].tournamentId); + } return res.data; }), { onError: (error) => { - setError('JHH02'); + setError('JJH02'); }, retry: 1, staleTime: 60000 /* 60초 */, @@ -45,7 +50,7 @@ export default function Tournament() { }), { onError: (error) => { - setError('JHH02'); + setError('JJH03'); }, } ); @@ -67,36 +72,45 @@ export default function Tournament() { useEffect(() => { if (openTournamentId !== undefined) fetchTournamentGames(); + }, [openTournamentId, fetchTournamentGames]); + + useEffect(() => { if (containerRef.current) { const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; setContainerSize({ width, height }); } - }, [openTournamentId]); + }, []); return (

Tournament

예정된 토너먼트
- {waitInfo.data?.tournaments.map((tournament) => ( -
- -
- ))} -
진행중인 토너먼트
-
- {openInfo.data && openInfo.data.tournaments?.length === 0 ? ( -
- 진행중인 토너먼트가 없습니다 + {waitInfo.data?.tournaments.length === 0 ? ( +

+ 예정된 토너먼트가 없습니다. +

+ ) : ( + waitInfo.data?.tournaments.map((tournament) => ( +
+
- ) : ( + )) + )} +
진행중인 토너먼트
+ {openInfo.data && openInfo.data.tournaments?.length === 0 ? ( +
+ 진행중인 토너먼트가 없습니다 +
+ ) : ( +
- )} -
+
+ )}
); diff --git a/styles/modal/event/TournamentRegistryModal.module.scss b/styles/modal/event/TournamentRegistryModal.module.scss index 2cd411b88..17a37ab20 100644 --- a/styles/modal/event/TournamentRegistryModal.module.scss +++ b/styles/modal/event/TournamentRegistryModal.module.scss @@ -30,6 +30,7 @@ } .startTime { + margin-bottom: 0.2rem; font-size: 0.7rem; color: rgba(18, 23, 37, 1); } diff --git a/styles/tournament/TournamentCard.module.scss b/styles/tournament/TournamentCard.module.scss index d6278a796..4c2c8c9f8 100644 --- a/styles/tournament/TournamentCard.module.scss +++ b/styles/tournament/TournamentCard.module.scss @@ -66,14 +66,9 @@ .time { display: flex; padding-bottom: 0.5rem; - font-size: 0.8rem; - justify-content: space-evenly; - .start { - color: green; - } - .end { - color: red; - } + font-size: 0.7rem; + justify-content: flex-start; + gap: 0.5rem; } } } diff --git a/styles/tournament/TournamentContainer.module.scss b/styles/tournament/TournamentContainer.module.scss index 07cee703e..5ab0ee163 100644 --- a/styles/tournament/TournamentContainer.module.scss +++ b/styles/tournament/TournamentContainer.module.scss @@ -40,3 +40,10 @@ margin-bottom: 0rem; } } + +.noTournamentText { + display: flex; + color: white; + justify-content: center; + align-content: center; +} diff --git a/utils/handleBraketSize.ts b/utils/handleBraketSize.ts new file mode 100644 index 000000000..c576e0b66 --- /dev/null +++ b/utils/handleBraketSize.ts @@ -0,0 +1,7 @@ +export const useContainerSize = ( + containerRef: React.RefObject +) => { + const width = containerRef.current?.clientWidth; + const height = containerRef.current?.clientHeight; + return { width, height }; +}; From d3719b1c7560b3ff3b479a17ab5df5bf30b01b18 Mon Sep 17 00:00:00 2001 From: joonho0410 <76806109+joonho0410@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:00:45 +0900 Subject: [PATCH 4/5] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=94=94?= =?UTF-8?q?=ED=94=8C=EB=A1=9C=EC=9D=B4.=20(#1174)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Fix] 유효성 검사 수정 #1169 (#1170) * Others/토너먼트 card 모바일 style 수정 및 api 최적화#1168 (#1172) * [fix] 예정된 토너먼트가 없을 시 안내문구 생성#1159 * [Style] 예정된 토너먼트 없을 시 텍스트 스타일 수정 #1159 * [Fix] Open된 토너먼트가 없을 때 조건문 수정 #1159 * [Fix] 진행중인 토너먼트 undefined 상태 추가 #1159 * [feat] 토너먼트 레지스트리 모달 시작: 종료 시간 추가 #1159 * [fix] api 최적화 #1159 * [Fix] 토너먼트 카드 모달 참여인원 표시 수정 #1159 * [Fix] 메인페이지에서 토너먼트 없을 시 수정 #1159 * [Fix] 토너먼트 신청 모달 map 재작성 #1159 * merge * [Fix] API 호출 최적화 #1168 * [Style] 시간을 좌측정렬으로 크기 수정 #1168 * [Fix] 등록/취소중 에러발생시 에러메세지 수정 #1168 * [others] 에러발생시키기 위한 코드 제거 #1168 * [fix] 토너먼트 진행되는게 없을 시 컨테이너 크기 없이 생성#1168 --------- Co-authored-by: Junho jeon Co-authored-by: Junho Jeon --------- Co-authored-by: Jincheol Park <67998022+Clearsu@users.noreply.github.com> Co-authored-by: Junho jeon Co-authored-by: Junho Jeon --- .../tournament/TournamentRegistryModal.tsx | 93 +++++++++++++------ components/tournament/TournamentCard.tsx | 8 +- pages/index.tsx | 3 +- pages/tournament.tsx | 50 ++++++---- .../event/TournamentRegistryModal.module.scss | 1 + styles/tournament/TournamentCard.module.scss | 11 +-- .../TournamentContainer.module.scss | 7 ++ utils/handleBraketSize.ts | 7 ++ 8 files changed, 120 insertions(+), 60 deletions(-) create mode 100644 utils/handleBraketSize.ts diff --git a/components/modal/tournament/TournamentRegistryModal.tsx b/components/modal/tournament/TournamentRegistryModal.tsx index b47200b41..8c54dc2db 100644 --- a/components/modal/tournament/TournamentRegistryModal.tsx +++ b/components/modal/tournament/TournamentRegistryModal.tsx @@ -8,6 +8,7 @@ import { instance } from 'utils/axios'; import { dateToKRLocaleTimeString } from 'utils/handleTime'; import { errorState } from 'utils/recoil/error'; import { modalState } from 'utils/recoil/modal'; +import { toastState } from 'utils/recoil/toast'; import { ModalButtonContainer, ModalButton, @@ -30,25 +31,40 @@ export default function TournamentRegistryModal({ player_cnt, tournamentId, }: TournamentInfo) { + const setSnackbar = useSetRecoilState(toastState); const setModal = useSetRecoilState(modalState); const setError = useSetRecoilState(errorState); const [registState, setRegistState] = useState('LOADING'); const [openDate, setOpenDate] = useState('미정'); + const [closeDate, setCloseDate] = useState('미정'); const [loading, setLoading] = useState(false); const [playerCount, setPlayerCount] = useState(player_cnt); - const registTournament = useCallback(() => { setLoading(true); return instance .post(`/pingpong/tournaments/${tournamentId}/users`) .then((res) => { - // alert('토너먼트 신청이 완료됐습니다'); setLoading(false); + setSnackbar({ + toastName: `토너먼트 신청`, + severity: 'success', + message: `🔥 토너먼트 참가 신청이 완료 됐습니다 ! 🔥`, + clicked: true, + }); setRegistState(res.data.status); return res.data.status; }) .catch((error) => { - setError('토너먼트 신청 중 에러가 발생했습니다.'); + setSnackbar({ + toastName: `토너먼트 신청`, + severity: 'error', + message: `${ + error.response?.data?.message + ? error.response.data.message + : '예상치 못한 에러가 발생했습니다 다시 시도해 주세요 😢' + } `, + clicked: true, + }); setLoading(false); }); }, []); @@ -59,17 +75,31 @@ export default function TournamentRegistryModal({ .delete(`/pingpong/tournaments/${tournamentId}/users`) .then((res) => { if (registState === 'WAIT') { - // alert('토너먼트 대기가 취소 되었습니다'); + setSnackbar({ + toastName: `토너먼트 대기 취소`, + severity: 'success', + message: `토너먼트 대기 신청을 취소했습니다.`, + clicked: true, + }); } else { - // setPlayerCount(playerCount - 1); - // alert('토너먼트 등록이 취소 되었습니다'); + setSnackbar({ + toastName: `토너먼트 신청 취소 `, + severity: 'success', + message: `토너먼트 참가 신청을 취소했습니다.`, + clicked: true, + }); } setRegistState(res.data.status); setLoading(false); return res.data.status; }) .catch((error) => { - setError('토너먼트 등록취소 중 에러가 발생했습니다'); + setSnackbar({ + toastName: `토너먼트 신청 취소`, + severity: 'error', + message: `취소중 에러가 발생했습니다.`, + clicked: true, + }); setLoading(false); }); }, []); @@ -99,38 +129,42 @@ export default function TournamentRegistryModal({ }, [tournamentId]); useEffect(() => { - getTournamentInfo(); getStatus(); - const date = new Date(startTime); - setOpenDate(dateToKRLocaleTimeString(date)); + setOpenDate(dateToKRLocaleTimeString(new Date(startTime))); + setCloseDate(dateToKRLocaleTimeString(new Date(endTime))); }, []); useEffect(() => { - getTournamentInfo(); - }, [registState]); + if (registState !== 'LOADING') getTournamentInfo(); + }, [registState, getTournamentInfo]); const closeModalButtonHandler = () => { setModal({ modalName: null }); }; - const buttonContents: Record = { - LOADING: '로딩중...', - BEFORE: '등록', - WAIT: '대기 취소', - PLAYER: '등록 취소', - }; - - const buttonAction: Record = { - BEFORE: registTournament, - WAIT: unRegistTournament, - PLAYER: unRegistTournament, - LOADING: () => { - console.log('loading..'); + const buttonMappings: Record = { + LOADING: { + content: '로딩중...', + handler: () => { + console.log('loading...'); + }, + }, + BEFORE: { + content: '등록', + handler: registTournament, + }, + WAIT: { + content: '대기 취소', + handler: unRegistTournament, + }, + PLAYER: { + content: '등록 취소', + handler: unRegistTournament, }, }; - const buttonContent = buttonContents[registState]; - const buttonHandler = buttonAction[registState]; + const { content: buttonContent, handler: buttonHandler } = + buttonMappings[registState]; return (
@@ -145,7 +179,8 @@ export default function TournamentRegistryModal({
{title}
-
{openDate}
+
시작 : {openDate}
+
종료 : {closeDate}
{playerCount} / 8
@@ -163,7 +198,7 @@ export default function TournamentRegistryModal({ { - getTournamentInfo(); - getStatus(); + if (modal.modalName === null) { + getTournamentInfo(); + getStatus(); + } }, [modal]); const getStatus = useCallback(() => { diff --git a/pages/index.tsx b/pages/index.tsx index eabfdfd17..39b0c30a7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -6,11 +6,10 @@ import styles from 'styles/main/Home.module.scss'; const Home: NextPage = () => { const tournamentData = useBeforeLiveTournamentData(); - return (
- {tournamentData && ( + {tournamentData?.length && (
)}
diff --git a/pages/tournament.tsx b/pages/tournament.tsx index 3ea344ea9..d38ca460d 100644 --- a/pages/tournament.tsx +++ b/pages/tournament.tsx @@ -12,7 +12,9 @@ import styles from 'styles/tournament/TournamentContainer.module.scss'; export default function Tournament() { const setError = useSetRecoilState(errorState); - const [openTournamentId, setOpenTournamentId] = useState(0); + const [openTournamentId, setOpenTournamentId] = useState( + undefined + ); const [openTournament, setOpenTournament] = useState([]); const [containerSize, setContainerSize] = useState({ width: 0, height: 0 }); const containerRef = useRef(null); // useRef를 사용하여 Ref 생성 @@ -23,12 +25,15 @@ export default function Tournament() { instance .get('/pingpong/tournaments?size=20&page=1&status=LIVE') .then((res) => { - setOpenTournamentId(res.data.tournaments[0].tournamentId); + if (res.data.tournamets?.length === 1) { + console.log('openInfo'); + setOpenTournamentId(res.data.tournaments[0].tournamentId); + } return res.data; }), { onError: (error) => { - setError('JHH02'); + setError('JJH02'); }, retry: 1, staleTime: 60000 /* 60초 */, @@ -45,7 +50,7 @@ export default function Tournament() { }), { onError: (error) => { - setError('JHH02'); + setError('JJH03'); }, } ); @@ -67,36 +72,45 @@ export default function Tournament() { useEffect(() => { if (openTournamentId !== undefined) fetchTournamentGames(); + }, [openTournamentId, fetchTournamentGames]); + + useEffect(() => { if (containerRef.current) { const width = containerRef.current.clientWidth; const height = containerRef.current.clientHeight; setContainerSize({ width, height }); } - }, [openTournamentId]); + }, []); return (

Tournament

예정된 토너먼트
- {waitInfo.data?.tournaments.map((tournament) => ( -
- -
- ))} -
진행중인 토너먼트
-
- {openInfo.data && openInfo.data.tournaments?.length === 0 ? ( -
- 진행중인 토너먼트가 없습니다 + {waitInfo.data?.tournaments.length === 0 ? ( +

+ 예정된 토너먼트가 없습니다. +

+ ) : ( + waitInfo.data?.tournaments.map((tournament) => ( +
+
- ) : ( + )) + )} +
진행중인 토너먼트
+ {openInfo.data && openInfo.data.tournaments?.length === 0 ? ( +
+ 진행중인 토너먼트가 없습니다 +
+ ) : ( +
- )} -
+
+ )}
); diff --git a/styles/modal/event/TournamentRegistryModal.module.scss b/styles/modal/event/TournamentRegistryModal.module.scss index 2cd411b88..17a37ab20 100644 --- a/styles/modal/event/TournamentRegistryModal.module.scss +++ b/styles/modal/event/TournamentRegistryModal.module.scss @@ -30,6 +30,7 @@ } .startTime { + margin-bottom: 0.2rem; font-size: 0.7rem; color: rgba(18, 23, 37, 1); } diff --git a/styles/tournament/TournamentCard.module.scss b/styles/tournament/TournamentCard.module.scss index d6278a796..4c2c8c9f8 100644 --- a/styles/tournament/TournamentCard.module.scss +++ b/styles/tournament/TournamentCard.module.scss @@ -66,14 +66,9 @@ .time { display: flex; padding-bottom: 0.5rem; - font-size: 0.8rem; - justify-content: space-evenly; - .start { - color: green; - } - .end { - color: red; - } + font-size: 0.7rem; + justify-content: flex-start; + gap: 0.5rem; } } } diff --git a/styles/tournament/TournamentContainer.module.scss b/styles/tournament/TournamentContainer.module.scss index 07cee703e..5ab0ee163 100644 --- a/styles/tournament/TournamentContainer.module.scss +++ b/styles/tournament/TournamentContainer.module.scss @@ -40,3 +40,10 @@ margin-bottom: 0rem; } } + +.noTournamentText { + display: flex; + color: white; + justify-content: center; + align-content: center; +} diff --git a/utils/handleBraketSize.ts b/utils/handleBraketSize.ts new file mode 100644 index 000000000..c576e0b66 --- /dev/null +++ b/utils/handleBraketSize.ts @@ -0,0 +1,7 @@ +export const useContainerSize = ( + containerRef: React.RefObject +) => { + const width = containerRef.current?.clientWidth; + const height = containerRef.current?.clientHeight; + return { width, height }; +}; From af9e28a3e92f143e686fac331af40049b810b0ce Mon Sep 17 00:00:00 2001 From: joonho0410 <76806109+joonho0410@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:29:35 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[fix]=20=EC=98=A4=ED=83=80=EC=88=98?= =?UTF-8?q?=EC=A0=95=20#1176=20(#1177)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Junho jeon --- pages/tournament.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/tournament.tsx b/pages/tournament.tsx index d38ca460d..6eccaa455 100644 --- a/pages/tournament.tsx +++ b/pages/tournament.tsx @@ -25,7 +25,7 @@ export default function Tournament() { instance .get('/pingpong/tournaments?size=20&page=1&status=LIVE') .then((res) => { - if (res.data.tournamets?.length === 1) { + if (res.data.tournaments?.length === 1) { console.log('openInfo'); setOpenTournamentId(res.data.tournaments[0].tournamentId); }