From 08a0699c4ed7d785293f5360ff31f50660214bc9 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 00:32:35 +0900 Subject: [PATCH 01/19] =?UTF-8?q?feat:=20Button,=20Card=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Button/index.tsx | 16 ++++++++++ src/components/common/Card/index.tsx | 20 ++++++++++++ src/components/common/Card/league/Content.tsx | 31 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/components/common/Button/index.tsx create mode 100644 src/components/common/Card/index.tsx create mode 100644 src/components/common/Card/league/Content.tsx 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/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 ( + + ); +} 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 = + '정말 리그를 삭제하시겠습니까? 삭제할 경우 해당 리그에 더이상 접근할 수 없으며 한 번 삭제된 리그는 복구할 수 없습니다.'; From 93cf3cedaee98e92694d596c13c498039657dc38 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:22:59 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20=EB=A6=AC=EA=B7=B8=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D/=EC=88=98=EC=A0=95=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BF=BC=EB=A6=AC,=20=ED=8C=A8=EC=B3=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/useLeagueRegister/Fetcher.tsx | 28 +++++++++ src/queries/admin/useLeagueRegister/query.ts | 63 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/queries/admin/useLeagueRegister/Fetcher.tsx create mode 100644 src/queries/admin/useLeagueRegister/query.ts 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], + }); + }, + }); +} From a45960db5bdf86fb269c4b591e86c831a60cfb27 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:25:58 +0900 Subject: [PATCH 06/19] =?UTF-8?q?refactor:=20useFunnel=20=EB=82=B4=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B8=B0=EB=8A=A5=20setInParams?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFunnel.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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; }; From 74c161f6f49e579315c3b15f6c603e188028a217 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:27:48 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat:=20=EB=A6=AC=EA=B7=B8=20ID=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20API=20Wrapper=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../register/context/RegisterWrapper.tsx | 31 +++++++++++++++++++ .../admin/register/context/wrapper.type.ts | 6 ++++ src/hooks/useLeagueIdContext.ts | 11 +++++++ 3 files changed, 48 insertions(+) create mode 100644 src/components/admin/register/context/RegisterWrapper.tsx create mode 100644 src/components/admin/register/context/wrapper.type.ts create mode 100644 src/hooks/useLeagueIdContext.ts 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/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; +}; From 21185e423d2b7abb7545548f7b9083f60aa723ba Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:29:51 +0900 Subject: [PATCH 08/19] =?UTF-8?q?chore:=20data=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=97=90=20File=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useValidate.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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); From d3de514af35d2f3bee30d70c8868e2a139de4e8b Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:31:04 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20Set=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=9C=A0=ED=8B=B8?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/set.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/utils/set.ts 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; +}; From b37ebb9ca95d5210617fc02a80bf910cd7b17332 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:33:32 +0900 Subject: [PATCH 10/19] =?UTF-8?q?feat:=20=EB=A6=AC=EA=B7=B8=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D/=EC=88=98=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8,=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/register/League/index.tsx | 212 ++++++++++++++++++ src/components/common/Checkbox/Item.tsx | 52 +++++ 2 files changed, 264 insertions(+) create mode 100644 src/components/admin/register/League/index.tsx create mode 100644 src/components/common/Checkbox/Item.tsx 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/common/Checkbox/Item.tsx b/src/components/common/Checkbox/Item.tsx new file mode 100644 index 0000000..748db4a --- /dev/null +++ b/src/components/common/Checkbox/Item.tsx @@ -0,0 +1,52 @@ +'use client'; + +import { ComponentProps, useRef } from 'react'; + +import { $ } from '@/utils/core'; + +import Button from '../Button'; +import { Icon } from '../Icon'; + +export default function CheckboxItem({ + id, + name, + value, + children, + checked, + onChange, +}: ComponentProps<'input'>) { + const inputRef = useRef(null); + + const onClick = () => { + if (!inputRef.current) return; + + inputRef.current.click(); + }; + + return ( + <> + + + + + ); +} From 10efc0c898f624cf6d6858ea26fae691587e272e Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:43:24 +0900 Subject: [PATCH 11/19] =?UTF-8?q?chore:=20putLeagueWithAuth=20=EB=A6=AC?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95=20=ED=9B=84=20=ED=95=B4=EB=8B=B9?= =?UTF-8?q?=20=EB=A6=AC=EA=B7=B8=20ID=20=EB=B0=98=ED=99=98=ED=86=A0?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/league.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/admin/league.ts b/src/api/admin/league.ts index 3d3919f..5355da2 100644 --- a/src/api/admin/league.ts +++ b/src/api/admin/league.ts @@ -28,9 +28,9 @@ export const deleteLeagueByIdWithAuth = async (body: DeleteLeaguePayload) => { }; export const putLeagueWithAuth = async (data: PutLeaguePayload) => { - const { status } = await adminInstance.put('/league/', data); + await adminInstance.put('/league/', data); - return status; + return data.leagueId; }; export const getSportsCategoriesWithAuth = async () => { From 7ef5be179f05588afa0ab20f7a5ec318b5b2c1b8 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:45:56 +0900 Subject: [PATCH 12/19] =?UTF-8?q?feat:=20=EB=A6=AC=EA=B7=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/register/[leagueId]/page.tsx | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/app/admin/register/[leagueId]/page.tsx 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 => ( + + )} + + + + ); +} From 08fe356e9f9fe35092ff27b4ddf7ef249a2f0a27 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 01:50:26 +0900 Subject: [PATCH 13/19] =?UTF-8?q?feat:=20=ED=8C=80=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20API,=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC,=20=ED=8C=A8=EC=B3=90,=20=ED=83=80=EC=9E=85=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin/team.ts | 46 +++++++++++++++++++ src/queries/admin/useTeamRegister/Fetcher.tsx | 22 +++++++++ src/queries/admin/useTeamRegister/query.ts | 40 ++++++++++++++++ src/types/admin/team.ts | 9 ++++ 4 files changed, 117 insertions(+) create mode 100644 src/api/admin/team.ts create mode 100644 src/queries/admin/useTeamRegister/Fetcher.tsx create mode 100644 src/queries/admin/useTeamRegister/query.ts create mode 100644 src/types/admin/team.ts 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/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/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; +}; From 6b363ee1227db28b5f54cc41190de99b641137eb Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 02:04:52 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20CheckCircled=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Icon/iconMap.ts | 2 ++ .../common/Icon/svg/CheckCircled.tsx | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 src/components/common/Icon/svg/CheckCircled.tsx diff --git a/src/components/common/Icon/iconMap.ts b/src/components/common/Icon/iconMap.ts index 9ad7269..094b1fe 100644 --- a/src/components/common/Icon/iconMap.ts +++ b/src/components/common/Icon/iconMap.ts @@ -2,6 +2,7 @@ import { BackgroundLogo } from './svg/BackgroundLogo'; import { Calendar } from './svg/Calendar'; import { CaretDown } from './svg/CaretDown'; import { CaretUp } from './svg/CaretUp'; +import { CheckCircled } from './svg/CheckCircled'; import { Clip } from './svg/Clip'; import { Cross } from './svg/Cross'; import { HamburgerMenu } from './svg/HamburgerMenu'; @@ -20,6 +21,7 @@ export const iconMap = { calendar: Calendar, caretDown: CaretDown, caretUp: CaretUp, + checkCircled: CheckCircled, clip: Clip, cross: Cross, hamburgerMenu: HamburgerMenu, 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 ( + + + + ); +}; From 431748175a902a63f458c868106c4e2e55dd6682 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 02:06:02 +0900 Subject: [PATCH 15/19] =?UTF-8?q?feat:=20=ED=8C=80=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=8A=B8,=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/register/page.tsx | 44 ++++ src/components/admin/register/Team/index.tsx | 212 +++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 src/app/admin/register/page.tsx create mode 100644 src/components/admin/register/Team/index.tsx 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/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]} + +
+
+
+ )) + ) : ( +
아직 등록된 팀이 없습니다.
+ )} +
+ + +
+ ); +} From fb179d1bf1918a3aa41659cc8a17c9e246fa24f7 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 02:06:49 +0900 Subject: [PATCH 16/19] =?UTF-8?q?chore:=20useDate=20=ED=9B=85=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useDate.ts | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/hooks/useDate.ts 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(), - }; -} From 696fd0ec946844517c4eb7c9fcd3a45d7e241a54 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 02:07:14 +0900 Subject: [PATCH 17/19] =?UTF-8?q?chore:=20admin/page.tsx=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/admin/page.tsx | 126 ----------------------------------------- 1 file changed, 126 deletions(-) delete mode 100644 src/app/admin/page.tsx 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 && ( -
팀을 다시 선택해주세요!
- )} - - - - - -
- ); -} From 1f3b1d771382a8c53b588ca99abe670b9546e1c4 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 02:13:27 +0900 Subject: [PATCH 18/19] =?UTF-8?q?chore:=20IconMap.ts=20->=20iconMap.ts=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/common/Icon/IconMap.ts | 35 --------------------------- src/components/common/Icon/iconMap.ts | 2 -- 2 files changed, 37 deletions(-) delete mode 100644 src/components/common/Icon/IconMap.ts 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/iconMap.ts b/src/components/common/Icon/iconMap.ts index 094b1fe..9ad7269 100644 --- a/src/components/common/Icon/iconMap.ts +++ b/src/components/common/Icon/iconMap.ts @@ -2,7 +2,6 @@ import { BackgroundLogo } from './svg/BackgroundLogo'; import { Calendar } from './svg/Calendar'; import { CaretDown } from './svg/CaretDown'; import { CaretUp } from './svg/CaretUp'; -import { CheckCircled } from './svg/CheckCircled'; import { Clip } from './svg/Clip'; import { Cross } from './svg/Cross'; import { HamburgerMenu } from './svg/HamburgerMenu'; @@ -21,7 +20,6 @@ export const iconMap = { calendar: Calendar, caretDown: CaretDown, caretUp: CaretUp, - checkCircled: CheckCircled, clip: Clip, cross: Cross, hamburgerMenu: HamburgerMenu, From e6560ebbfb75ad3c46e2bc374cd99b089ad66797 Mon Sep 17 00:00:00 2001 From: HiImConan Date: Wed, 29 Nov 2023 02:14:41 +0900 Subject: [PATCH 19/19] =?UTF-8?q?chore:=20commitlint=20=EC=BB=A4=EB=B0=8B?= =?UTF-8?q?=20subject=20case=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .commitlintrc.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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] } }