diff --git a/.commitlintrc.json b/.commitlintrc.json index 5002c4f..1d11eb9 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -16,6 +16,7 @@ "style", "chore" ] - ] + ], + "subject-case": [0] } } diff --git a/src/api/admin/league.ts b/src/api/admin/league.ts new file mode 100644 index 0000000..5355da2 --- /dev/null +++ b/src/api/admin/league.ts @@ -0,0 +1,48 @@ +import { adminInstance } from '@/api'; +import { + DeleteLeaguePayload, + LeagueIdType, + LeagueType, + NewLeaguePayload, + PutLeaguePayload, + SportsCategoriesType, + SportsQuarterType, +} from '@/types/admin/league'; + +export const getAllLeaguesWithAuth = async () => { + const { data } = await adminInstance.get('/league/all/'); + + return data; +}; + +export const postNewLeagueWithAuth = async (body: NewLeaguePayload) => { + const { data } = await adminInstance.post('/league/', body); + + return data; +}; + +export const deleteLeagueByIdWithAuth = async (body: DeleteLeaguePayload) => { + const { status } = await adminInstance.delete('/league/', { data: body }); + + return status; +}; + +export const putLeagueWithAuth = async (data: PutLeaguePayload) => { + await adminInstance.put('/league/', data); + + return data.leagueId; +}; + +export const getSportsCategoriesWithAuth = async () => { + const { data } = await adminInstance.get('/sport/'); + + return data; +}; + +export const getSportsQuarterByIdWithAuth = async (sportId: string) => { + const { data } = await adminInstance.get( + `/sport/${sportId}/quarter/`, + ); + + return data; +}; diff --git a/src/api/admin/team.ts b/src/api/admin/team.ts new file mode 100644 index 0000000..c3d8a03 --- /dev/null +++ b/src/api/admin/team.ts @@ -0,0 +1,46 @@ +import { isAxiosError } from 'axios'; + +import { adminInstance } from '@/api'; +import { TeamErrorType, TeamType } from '@/types/admin/team'; + +/* 리그 내 팀 관리 API */ + +export const getTeamListByLeagueIdWithAuth = async (leagueId: string) => { + try { + const { data } = await adminInstance.get(`/team/${leagueId}/`); + + return data; + } catch (error) { + if (isAxiosError(error)) { + return error.response?.data.detail; + } else { + throw new Error('리그 내 팀을 조회하는데 실패했습니다.'); + } + } +}; + +export const postTeamByLeagueIdWithAuth = async (payload: { + leagueId: string; + body: FormData; +}) => { + await adminInstance.post( + `/team/register/${payload.leagueId}/`, + payload.body, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); +}; + +export const putTeamByIdWithAuth = async (payload: { + teamId: string; + body: FormData; +}) => { + await adminInstance.put(`/team/${payload.teamId}/change/`, payload.body, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); +}; diff --git a/src/app/admin/league/page.tsx b/src/app/admin/league/page.tsx new file mode 100644 index 0000000..cf37d21 --- /dev/null +++ b/src/app/admin/league/page.tsx @@ -0,0 +1,26 @@ +'use client'; + +import Link from 'next/link'; +import { Suspense } from 'react'; + +import LeagueList from '@/components/admin/league/LeagueList'; +import Button from '@/components/common/Button'; +import LeagueListFetcher from '@/queries/admin/useLeagueList/Fetcher'; + +export default function LeaguePage() { + return ( +
+
전체 리그
+ 리그 로딩중...
}> + + {data => } + + + + + ); +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx deleted file mode 100644 index 2a736a1..0000000 --- a/src/app/admin/page.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import { ChangeEvent, FormEvent, useState } from 'react'; - -import { createNewGame } from '@/api/admin'; -import Input from '@/components/common/Input/Input'; -import useDate from '@/hooks/useDate'; -import useValidate from '@/hooks/useValidate'; - -export default function Admin() { - const router = useRouter(); - - const { month, day } = useDate(new Date().toString()); - - const [gameData, setGameData] = useState({ - name: '삼건물대회', - sportsName: '축구', - firstTeam: 0, - secondTeam: 0, - date: '', - time: '', - }); - - const { isError: isDateError } = useValidate( - gameData.date, - dateValue => new Date(dateValue) < new Date(`2023-${month}-${day}`), - ); - const { isError: isTimeError } = useValidate(gameData.time, timeValue => { - const [hour] = (timeValue as string).split(':').map(Number); - - return hour < 8 || hour > 18; - }); - const { isError: isTeamError } = useValidate( - gameData.secondTeam, - teamValue => teamValue === gameData.firstTeam, - ); - - const handleInput = ( - e: ChangeEvent, - ) => { - const { name, value } = e.target; - - setGameData(prev => ({ ...prev, [name]: value })); - }; - - const submitHandler = (e: FormEvent) => { - e.preventDefault(); - - if (isDateError || isTeamError || isTimeError) return; - - createNewGame({ - name: gameData.name, - sportsName: gameData.sportsName, - firstTeam: Number(gameData.firstTeam), - secondTeam: Number(gameData.secondTeam), - startTime: new Date(`${gameData.date}T${gameData.time}:00`), - }).then(() => router.push('/')); - }; - - return ( -
- -

팀 선택

- {isTeamError && ( -
팀을 다시 선택해주세요!
- )} - - - - - -
- ); -} diff --git a/src/app/admin/register/[leagueId]/page.tsx b/src/app/admin/register/[leagueId]/page.tsx new file mode 100644 index 0000000..be8ad23 --- /dev/null +++ b/src/app/admin/register/[leagueId]/page.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { Suspense } from 'react'; + +import RegisterLeague from '@/components/admin/register/League'; +import LeagueRegisterFetcher from '@/queries/admin/useLeagueRegister/Fetcher'; +import useSportsListByLeagueId from '@/queries/useSportsListByLeagueId/query'; + +export default function EditLeague({ + params, +}: { + params: { leagueId: string }; +}) { + const { leagueId } = params; + const { sportsList: leagueSportsData } = useSportsListByLeagueId(leagueId); + return ( +
+ 리그 정보 로딩중...
}> + + {data => ( + + )} + + + + ); +} diff --git a/src/app/admin/register/page.tsx b/src/app/admin/register/page.tsx new file mode 100644 index 0000000..a01eabb --- /dev/null +++ b/src/app/admin/register/page.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Suspense } from 'react'; + +import RegisterWrapper from '@/components/admin/register/context/RegisterWrapper'; +import RegisterLeague from '@/components/admin/register/League'; +import RegisterTeam from '@/components/admin/register/Team'; +import { useFunnel } from '@/hooks/useFunnel'; +import LeagueRegisterFetcher from '@/queries/admin/useLeagueRegister/Fetcher'; +import TeamRegisterFetcher from '@/queries/admin/useTeamRegister/Fetcher'; + +export default function Register() { + const [Funnel, setStep] = useFunnel(['league', 'team', 'player'], 'league'); + + return ( + + + + 리그 정보 로딩중...}> + + {data => ( + setStep('team')} /> + )} + + + + + 팀 정보 로딩중...}> + + {data => } + + + + + 선수 정보 로딩중...}> + {/* + {data => } + */} + + + + + ); +} diff --git a/src/components/admin/league/LeagueList/index.tsx b/src/components/admin/league/LeagueList/index.tsx new file mode 100644 index 0000000..824f997 --- /dev/null +++ b/src/components/admin/league/LeagueList/index.tsx @@ -0,0 +1,91 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect, useState } from 'react'; + +import Button from '@/components/common/Button'; +import Card from '@/components/common/Card'; +import LeagueContent from '@/components/common/Card/league/Content'; +import { DELETE_DESCRIPTION } from '@/constants/adminDescription'; +import { useScrollLock } from '@/hooks/useScrollLock'; +import { useDeleteLeagueMutation } from '@/queries/admin/useLeagueList/query'; +import { LeagueType } from '@/types/admin/league'; + +export default function LeagueList({ data }: { data: LeagueType[] }) { + const [isModalOpen, setIsModalOpen] = useState(false); + const { disableScroll, enableScroll } = useScrollLock(); + + const { mutate } = useDeleteLeagueMutation(); + + const toggleModal = () => { + setIsModalOpen(!isModalOpen); + }; + const deleteLeague = async (leagueId: number) => { + mutate({ leagueId }); + setIsModalOpen(false); + }; + + useEffect(() => { + isModalOpen && disableScroll(); + return () => enableScroll(); + }, [isModalOpen]); + + return ( +
    + {data.map(league => ( +
  • + + +
    + + +
    +
    + {isModalOpen && ( +
    +
    + +
    + 리그 삭제 + {DELETE_DESCRIPTION} +
    +
    + + +
    +
    +
    + )} +
  • + ))} +
+ ); +} diff --git a/src/components/admin/register/League/index.tsx b/src/components/admin/register/League/index.tsx new file mode 100644 index 0000000..653ef63 --- /dev/null +++ b/src/components/admin/register/League/index.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { ChangeEvent, FormEvent, useEffect, useState } from 'react'; + +import Button from '@/components/common/Button'; +import CheckboxItem from '@/components/common/Checkbox/Item'; +import Input from '@/components/common/Input/Input'; +import useValidate from '@/hooks/useValidate'; +import { + usePostLeagueMutation, + usePutLeagueMutation, +} from '@/queries/admin/useLeagueRegister/query'; +import { + LeagueDataType, + LeagueRegisterDataType, + SportsDataType, +} from '@/types/admin/league'; +import { updateSet } from '@/utils/set'; +import { parseTimeString } from '@/utils/time'; + +export default function RegisterLeague({ + data, + leagueId, + onNext, +}: { + data: LeagueRegisterDataType; + leagueId?: number; + onNext?: () => void; +}) { + const [newLeagueData, setNewLeagueData] = useState( + {} as LeagueDataType, + ); + const [newSportsData, setNewSportsData] = useState>(new Set()); + + const { leagueData, leagueSportsData, sportsListData } = data; + const currentLeague = leagueData.find(e => e.leagueId === Number(leagueId)); + + const router = useRouter(); + + const { mutate: postLeague } = usePostLeagueMutation(); + const { mutate: putLeague } = usePutLeagueMutation(); + + useEffect(() => { + if (currentLeague) { + const parseDate = (dateString: string) => dateString.split('T')[0]; + + setNewLeagueData({ + name: currentLeague.name, + startAt: parseDate(currentLeague.startAt), + endAt: parseDate(currentLeague.endAt), + }); + } + if (leagueSportsData) { + const reducedSportsData = leagueSportsData.reduce( + (acc, cur) => [...acc, cur.sportId], + [] as SportsDataType, + ); + + setNewSportsData(new Set(reducedSportsData)); + } + }, []); + + const { month, date } = parseTimeString(new Date().toString()); + + const isDateError = + new Date(newLeagueData.endAt) < new Date(newLeagueData.startAt); + const { isError: isStartAtEmpty } = useValidate( + newLeagueData.startAt, + dateValue => !dateValue, + ); + const { isError: isEndAtEmpty } = useValidate( + newLeagueData.endAt, + dateValue => !dateValue, + ); + const { isError: isNameEmpty } = useValidate( + newLeagueData.name, + nameValue => String(nameValue).length < 1, + ); + const { isError: isSportsEmpty } = useValidate( + Array.from(newSportsData).length, + length => length === 0, + ); + const isAnyInvalid = + isNameEmpty || + isDateError || + isSportsEmpty || + isStartAtEmpty || + isEndAtEmpty; + + const updateCheckbox = (id: number) => { + setNewSportsData(prevSet => updateSet(prevSet, id)); + }; + + const handleInput = ( + e: ChangeEvent, + ) => { + const { name, value } = e.target; + + setNewLeagueData(prev => ({ ...prev, [name]: value })); + }; + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + + const payload = { + leagueData: newLeagueData, + sportData: Array.from(newSportsData), + }; + + if (leagueId) { + putLeague({ + leagueId, + ...payload, + }); + } else { + postLeague(payload); + } + + if (onNext) { + onNext(); + } else { + router.push('/admin/league/'); + } + }; + + return ( +
+
+ {currentLeague ? '리그 수정' : '새 리그 등록'} +
+
+ + + + +
+
+ ); +} diff --git a/src/components/admin/register/Team/index.tsx b/src/components/admin/register/Team/index.tsx new file mode 100644 index 0000000..9573cf3 --- /dev/null +++ b/src/components/admin/register/Team/index.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { ChangeEvent, FormEvent, useRef, useState } from 'react'; + +import Button from '@/components/common/Button'; +import { Icon } from '@/components/common/Icon'; +import Input from '@/components/common/Input/Input'; +import { useLeagueIdContext } from '@/hooks/useLeagueIdContext'; +import useValidate from '@/hooks/useValidate'; +import { usePostTeamMutation } from '@/queries/admin/useTeamRegister/query'; +import { TeamType } from '@/types/admin/team'; + +export default function RegisterTeam({ + data, + onNext, +}: { + data: TeamType[] | string | undefined; + onNext?: () => void; +}) { + const { leagueId } = useLeagueIdContext(); + + const router = useRouter(); + const { mutate } = usePostTeamMutation(leagueId); + + const inputRef = useRef(null); + const [teamName, setTeamName] = useState(''); + const [teamLogo, setTeamLogo] = useState(null); + + const { isError: isNameEmpty } = useValidate( + teamName, + nameValue => !nameValue, + ); + const { isError: isImageEmpty } = useValidate( + teamLogo as File, + file => !file, + ); + const isAnyInvaild = isNameEmpty || isImageEmpty; + + const handleInput = (e: ChangeEvent) => { + const { name, value, files } = e.target; + + if (name === 'logo') { + if (!files) return; + + setTeamLogo(files[0]); + } else { + setTeamName(value); + } + }; + + const triggerUploadImage = () => { + if (!inputRef.current) return; + + inputRef.current.click(); + }; + + const resetTeamData = () => { + setTeamName(''); + setTeamLogo(null); + }; + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + const formData = new FormData(); + if (teamName && teamLogo) { + formData.append('name', teamName); + formData.append('logo', teamLogo); + + mutate({ leagueId, body: formData }); + resetTeamData(); + } + }; + + const handleNextStep = () => { + if (onNext) { + onNext(); + } else { + router.push('/admin/league'); + } + }; + + return ( +
+
새 팀 등록
+
+ +
+
+ 팀 로고 + {isImageEmpty && ( + 필수 항목입니다. + )} +
+ + + +
+ {!isAnyInvaild && ( + <> +
등록될 팀
+
+
+ 팀명: + {teamName} +
+
+
로고:
+
+ + {teamLogo?.name} +
+
+ +
+ + )} + +
+ +
+ 팀 목록 +
+
+ {data && typeof data !== 'string' ? ( + data.map(team => ( +
+
+ 팀명: + {team.name} +
+
+
로고:
+
+ + + {team.logoImageUrl.split('/').slice(-1)[0]} + +
+
+
+ )) + ) : ( +
아직 등록된 팀이 없습니다.
+ )} +
+ + +
+ ); +} diff --git a/src/components/admin/register/context/RegisterWrapper.tsx b/src/components/admin/register/context/RegisterWrapper.tsx new file mode 100644 index 0000000..6b1bf67 --- /dev/null +++ b/src/components/admin/register/context/RegisterWrapper.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { createContext, ReactNode, useState } from 'react'; + +import { $ } from '@/utils/core'; + +import { LeagueIdContextType } from './wrapper.type'; + +type RegisterProps = { + children: ReactNode; + className?: string; +}; + +export const LeagueIdContext = createContext>( + {} as LeagueIdContextType, +); + +export default function RegisterWrapper({ + className, + children, +}: RegisterProps) { + const [leagueId, setLeagueId] = useState(''); + + return ( + +
+ {children} +
+
+ ); +} diff --git a/src/components/admin/register/context/wrapper.type.ts b/src/components/admin/register/context/wrapper.type.ts new file mode 100644 index 0000000..5e45c79 --- /dev/null +++ b/src/components/admin/register/context/wrapper.type.ts @@ -0,0 +1,6 @@ +import { Dispatch, SetStateAction } from 'react'; + +export type LeagueIdContextType = { + leagueId: T; + setLeagueId: Dispatch>; +}; diff --git a/src/components/common/Button/index.tsx b/src/components/common/Button/index.tsx new file mode 100644 index 0000000..8223401 --- /dev/null +++ b/src/components/common/Button/index.tsx @@ -0,0 +1,16 @@ +import { ButtonHTMLAttributes } from 'react'; + +import { $ } from '@/utils/core'; + +interface ButtonProps extends ButtonHTMLAttributes { + className?: string; +} + +export default function Button({ className, ...props }: ButtonProps) { + return ( + + + + + ); +} diff --git a/src/components/common/Icon/IconMap.ts b/src/components/common/Icon/IconMap.ts deleted file mode 100644 index 9ad7269..0000000 --- a/src/components/common/Icon/IconMap.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BackgroundLogo } from './svg/BackgroundLogo'; -import { Calendar } from './svg/Calendar'; -import { CaretDown } from './svg/CaretDown'; -import { CaretUp } from './svg/CaretUp'; -import { Clip } from './svg/Clip'; -import { Cross } from './svg/Cross'; -import { HamburgerMenu } from './svg/HamburgerMenu'; -import { Hcc } from './svg/Hcc'; -import { Image } from './svg/Image'; -import { PaperPlane } from './svg/PaperPlane'; -import { Pencil } from './svg/Pencil'; -import { PlusCircled } from './svg/PlusCircled'; -import { Symbol } from './svg/Symbol'; -import { ThumbsUp } from './svg/Thumbsup'; -import { Trash } from './svg/Trash'; -import { Write } from './svg/Write'; - -export const iconMap = { - backgroundLogo: BackgroundLogo, - calendar: Calendar, - caretDown: CaretDown, - caretUp: CaretUp, - clip: Clip, - cross: Cross, - hamburgerMenu: HamburgerMenu, - hcc: Hcc, - image: Image, - paperPlane: PaperPlane, - pencil: Pencil, - plusCircled: PlusCircled, - symbol: Symbol, - thumbsUp: ThumbsUp, - trash: Trash, - write: Write, -}; diff --git a/src/components/common/Icon/svg/CheckCircled.tsx b/src/components/common/Icon/svg/CheckCircled.tsx new file mode 100644 index 0000000..bfac368 --- /dev/null +++ b/src/components/common/Icon/svg/CheckCircled.tsx @@ -0,0 +1,22 @@ +import { ComponentProps } from 'react'; + +export const CheckCircled = ({ + viewBox = '0 0 16 19', + ...props +}: ComponentProps<'svg'>) => { + return ( + + + + ); +}; diff --git a/src/constants/adminDescription.ts b/src/constants/adminDescription.ts new file mode 100644 index 0000000..585c304 --- /dev/null +++ b/src/constants/adminDescription.ts @@ -0,0 +1,2 @@ +export const DELETE_DESCRIPTION = + '정말 리그를 삭제하시겠습니까? 삭제할 경우 해당 리그에 더이상 접근할 수 없으며 한 번 삭제된 리그는 복구할 수 없습니다.'; diff --git a/src/hooks/useDate.ts b/src/hooks/useDate.ts deleted file mode 100644 index 2298699..0000000 --- a/src/hooks/useDate.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function useDate(date: string) { - const currentDate = new Date(date); - return { - month: currentDate.getMonth() + 1, - day: currentDate.getDate(), - hour: currentDate.getHours() + 9, - minute: currentDate.getMinutes(), - }; -} diff --git a/src/hooks/useFunnel.tsx b/src/hooks/useFunnel.tsx index 95b0516..ca0b61a 100644 --- a/src/hooks/useFunnel.tsx +++ b/src/hooks/useFunnel.tsx @@ -1,5 +1,4 @@ -import { useSearchParams } from 'next/navigation'; -import { useRouter } from 'next/router'; +import { useRouter } from 'next/navigation'; import { Children, isValidElement, @@ -9,6 +8,8 @@ import { useMemo, } from 'react'; +import useQueryParams from '@/hooks/useQueryParams'; + export interface FunnelProps { steps: T; step: T[number]; @@ -45,11 +46,11 @@ export const useFunnel = ( defaultStep: T[number], ) => { const router = useRouter(); - const searchParams = useSearchParams(); + const { params, setInParams } = useQueryParams(); const setStep = useCallback( (step: T[number]) => { - router.push({ query: { step } }); + setInParams('step', step); }, [router], ); @@ -57,13 +58,13 @@ export const useFunnel = ( const FunnelComponent = useMemo(() => { return Object.assign( (props: Omit, 'step' | 'steps'>) => { - const step = searchParams.get('step') ?? defaultStep; + const step = params.get('step') ?? defaultStep; return steps={steps} step={step} {...props} />; }, { Step }, ); - }, [defaultStep, searchParams, steps]); + }, [defaultStep, params, steps]); return [FunnelComponent, setStep] as const; }; diff --git a/src/hooks/useLeagueIdContext.ts b/src/hooks/useLeagueIdContext.ts new file mode 100644 index 0000000..8f2eb7c --- /dev/null +++ b/src/hooks/useLeagueIdContext.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; + +import { LeagueIdContext } from '@/components/admin/register/context/RegisterWrapper'; + +export const useLeagueIdContext = () => { + const leagueIdContext = useContext(LeagueIdContext); + + if (!leagueIdContext) throw new Error('Context가 비었습니다.'); + + return leagueIdContext; +}; diff --git a/src/hooks/useScrollLock.ts b/src/hooks/useScrollLock.ts new file mode 100644 index 0000000..222e92c --- /dev/null +++ b/src/hooks/useScrollLock.ts @@ -0,0 +1,20 @@ +export function useScrollLock() { + const scrollPosition = window.scrollY; + + const disableScroll = () => { + document.body.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.top = `-${scrollPosition}px`; + document.body.style.width = '100%'; + }; + + const enableScroll = () => { + document.body.style.removeProperty('overflow'); + document.body.style.removeProperty('position'); + document.body.style.removeProperty('top'); + document.body.style.removeProperty('width'); + window.scrollTo(0, scrollPosition); + }; + + return { disableScroll, enableScroll }; +} diff --git a/src/hooks/useValidate.ts b/src/hooks/useValidate.ts index eeaba59..223689f 100644 --- a/src/hooks/useValidate.ts +++ b/src/hooks/useValidate.ts @@ -1,8 +1,10 @@ import { useEffect, useState } from 'react'; +type DataType = string | number | Date | File; + export default function useValidate( - data: string | number | Date, - cb: (value: string | number | Date) => boolean, + data: DataType, + cb: (value: DataType) => boolean, ) { const [isError, setIsError] = useState(false); diff --git a/src/queries/admin/useLeagueList/Fetcher.tsx b/src/queries/admin/useLeagueList/Fetcher.tsx new file mode 100644 index 0000000..6972ab2 --- /dev/null +++ b/src/queries/admin/useLeagueList/Fetcher.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from 'react'; + +import { LeagueType } from '@/types/admin/league'; + +import { useLeagueList } from './query'; + +type LeagueListFetcherProps = { + children: (data: LeagueType[]) => ReactNode; +}; + +export default function LeagueListFetcher({ + children, +}: LeagueListFetcherProps) { + const { data: leagueDetail, error } = useLeagueList(); + + if (error) throw error; + + return children(leagueDetail); +} diff --git a/src/queries/admin/useLeagueList/query.ts b/src/queries/admin/useLeagueList/query.ts new file mode 100644 index 0000000..a50f0bf --- /dev/null +++ b/src/queries/admin/useLeagueList/query.ts @@ -0,0 +1,39 @@ +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; + +import { + deleteLeagueByIdWithAuth, + getAllLeaguesWithAuth, +} from '@/api/admin/league'; + +export const QUERY_KEY = { + LEAGUE_LIST: 'league-list', +}; + +export function useLeagueList() { + const { data, error } = useSuspenseQuery({ + queryKey: [QUERY_KEY.LEAGUE_LIST], + queryFn: () => getAllLeaguesWithAuth(), + }); + + return { + data, + error, + }; +} + +export function useDeleteLeagueMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteLeagueByIdWithAuth, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.LEAGUE_LIST], + }); + }, + }); +} diff --git a/src/queries/admin/useLeagueRegister/Fetcher.tsx b/src/queries/admin/useLeagueRegister/Fetcher.tsx new file mode 100644 index 0000000..5e906a6 --- /dev/null +++ b/src/queries/admin/useLeagueRegister/Fetcher.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from 'react'; + +import { useLeagueList } from '@/queries/admin/useLeagueList/query'; +import { LeagueRegisterDataType } from '@/types/admin/league'; + +import { useSportsList } from './query'; + +type LeagueRegisterFetcherProps = { + children: ({ + leagueData, + sportsListData, + }: LeagueRegisterDataType) => ReactNode; +}; + +export default function LeagueRegisterFetcher({ + children, +}: LeagueRegisterFetcherProps) { + const { data: leagueData, error: leagueDataError } = useLeagueList(); + const { sportsListData, sportsListError } = useSportsList(); + + if (leagueDataError) throw leagueDataError; + if (sportsListError) throw sportsListError; + + return children({ + leagueData, + sportsListData, + }); +} diff --git a/src/queries/admin/useLeagueRegister/query.ts b/src/queries/admin/useLeagueRegister/query.ts new file mode 100644 index 0000000..ef52f0e --- /dev/null +++ b/src/queries/admin/useLeagueRegister/query.ts @@ -0,0 +1,63 @@ +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; + +import { + getSportsCategoriesWithAuth, + postNewLeagueWithAuth, + putLeagueWithAuth, +} from '@/api/admin/league'; +import { useLeagueIdContext } from '@/hooks/useLeagueIdContext'; + +export const QUERY_KEY = { + LEAGUE_LIST: 'league-list', + LEAGUE_SPORTS: 'league-sports', + SPORTS_LIST: 'sports-list', +}; + +export function useSportsList() { + const { data: sportsListData, error: sportsListError } = useSuspenseQuery({ + queryKey: [QUERY_KEY.SPORTS_LIST], + queryFn: () => getSportsCategoriesWithAuth(), + }); + + return { + sportsListData, + sportsListError, + }; +} + +export function usePostLeagueMutation() { + const queryClient = useQueryClient(); + const { setLeagueId } = useLeagueIdContext(); + + return useMutation({ + mutationKey: ['post-new-league'], + mutationFn: postNewLeagueWithAuth, + onSuccess: data => { + const { leagueId } = data; + setLeagueId(leagueId.toString()); + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.LEAGUE_LIST] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.LEAGUE_SPORTS, leagueId], + }); + }, + }); +} + +export function usePutLeagueMutation() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['put-league'], + mutationFn: putLeagueWithAuth, + onSuccess: leagueId => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEY.LEAGUE_LIST] }); + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.LEAGUE_SPORTS, leagueId], + }); + }, + }); +} diff --git a/src/queries/admin/useTeamRegister/Fetcher.tsx b/src/queries/admin/useTeamRegister/Fetcher.tsx new file mode 100644 index 0000000..48f78d8 --- /dev/null +++ b/src/queries/admin/useTeamRegister/Fetcher.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from 'react'; + +import { useLeagueIdContext } from '@/hooks/useLeagueIdContext'; +import { TeamType } from '@/types/admin/team'; + +import { useTeamListByLeagueId } from './query'; + +type TeamRegisterFetcherProps = { + children: (data: TeamType[] | string | undefined) => ReactNode; +}; + +export default function TeamRegisterFetcher({ + children, +}: TeamRegisterFetcherProps) { + const { leagueId } = useLeagueIdContext(); + + const { teamList, error } = useTeamListByLeagueId(leagueId); + + if (error) throw Error; + + return children(teamList); +} diff --git a/src/queries/admin/useTeamRegister/query.ts b/src/queries/admin/useTeamRegister/query.ts new file mode 100644 index 0000000..11e8f9c --- /dev/null +++ b/src/queries/admin/useTeamRegister/query.ts @@ -0,0 +1,40 @@ +import { + useMutation, + useQueryClient, + useSuspenseQuery, +} from '@tanstack/react-query'; + +import { + getTeamListByLeagueIdWithAuth, + postTeamByLeagueIdWithAuth, +} from '@/api/admin/team'; + +const QUERY_KEY = { + TEAM_LIST: 'team-list', +}; + +export function useTeamListByLeagueId(leagueId: string) { + const { data, error } = useSuspenseQuery({ + queryKey: [QUERY_KEY.TEAM_LIST, leagueId], + queryFn: () => getTeamListByLeagueIdWithAuth(leagueId), + }); + + return { + teamList: data, + error, + }; +} + +export function usePostTeamMutation(leagueId: string) { + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['post-team'], + mutationFn: postTeamByLeagueIdWithAuth, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: [QUERY_KEY.TEAM_LIST, leagueId], + }); + }, + }); +} diff --git a/src/types/admin/league.ts b/src/types/admin/league.ts new file mode 100644 index 0000000..74312ef --- /dev/null +++ b/src/types/admin/league.ts @@ -0,0 +1,38 @@ +export type LeagueIdType = { + leagueId: number; +}; + +export type LeagueDataType = { + name: string; + startAt: string; + endAt: string; +}; + +export type SportIdType = { + sportId: number; +}; + +export type SportsQuarterType = { + name: string; +}; + +export type SportsDataType = number[]; + +export type SportsCategoriesType = SportIdType & SportsQuarterType; + +export type LeagueType = LeagueIdType & LeagueDataType; + +export type NewLeaguePayload = { + leagueData: LeagueDataType; + sportData: SportsDataType; +}; + +export type PutLeaguePayload = LeagueIdType & NewLeaguePayload; + +export type DeleteLeaguePayload = LeagueIdType; + +export type LeagueRegisterDataType = { + leagueData: LeagueType[]; + leagueSportsData?: SportsCategoriesType[]; + sportsListData: SportsCategoriesType[]; +}; diff --git a/src/types/admin/team.ts b/src/types/admin/team.ts new file mode 100644 index 0000000..81dab18 --- /dev/null +++ b/src/types/admin/team.ts @@ -0,0 +1,9 @@ +export type TeamType = { + id: number; + name: string; + logoImageUrl: string; +}; + +export type TeamErrorType = { + detail: string; +}; diff --git a/src/utils/set.ts b/src/utils/set.ts new file mode 100644 index 0000000..773c260 --- /dev/null +++ b/src/utils/set.ts @@ -0,0 +1,11 @@ +export const updateSet = (set: Set, value: T) => { + const target = new Set(set); + + if (target.has(value)) { + target.delete(value); + } else { + target.add(value); + } + + return target; +};