Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT]: 관리자 매치 정보 수정 구현 #126

Merged
merged 8 commits into from
Jan 23, 2024
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-select": "^2.0.0",
"@sentry/nextjs": "^7.73.0",
"@stomp/stompjs": "^7.0.0",
"@tanstack/react-query": "^5.8.2",
Expand Down
9 changes: 0 additions & 9 deletions src/api/admin/league.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
NewLeaguePayload,
PutLeaguePayload,
SportsCategoriesType,
SportsQuarterType,
} from '@/types/admin/league';

export const getAllLeaguesWithAuth = async () => {
Expand Down Expand Up @@ -38,11 +37,3 @@ export const getSportsCategoriesWithAuth = async () => {

return data;
};

export const getSportsQuarterByIdWithAuth = async (sportId: string) => {
const { data } = await adminInstance.get<SportsQuarterType[]>(
`/sport/${sportId}/quarter/`,
);

return data;
};
32 changes: 32 additions & 0 deletions src/api/admin/match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {
MatchInfoType,
PutMatchInfoPayload,
SportsQuarterType,
} from '@/types/admin/match';

import { adminInstance } from '..';

export const getMatchInfoByIdWithAuth = async (matchId: string) => {
const { data } = await adminInstance.get<MatchInfoType>(
`/game/info/${matchId}/`,
);

return data;
};

export const putMatchInfoWithAuth = async (payload: {
matchId: string;
data: PutMatchInfoPayload;
}) => {
await adminInstance.put(`/game/change/${payload.matchId}/`, payload.data);

return payload.matchId;
};

export const getSportsQuarterByIdWithAuth = async (sportId: number) => {
const { data } = await adminInstance.get<SportsQuarterType[]>(
`/sport/${sportId}/quarter/`,
);

return data;
};
8 changes: 8 additions & 0 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ export const postLogin = async (body: AuthPayload) => {
export const postGameStatus = async (id: number, gameStatus: string) => {
adminInstance.post(`/manage/game/statustype/${id}/`, { gameStatus });
};

export const checkPermission = async () => {
const { data } = await adminInstance.get<number>('/accounts/permission/');
if (data === 400) {
throw new Error('해당 페이지에 접근할 권한이 없습니다');
}
return;
};
77 changes: 75 additions & 2 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios from 'axios';
import axios, { AxiosError } from 'axios';

import LocalStorage from '@/utils/LocalStorage';

Expand All @@ -17,10 +17,83 @@ export const adminInstance = axios.create({
},
});

const getAccessToken = () => {
const tokenInLocalStorage = LocalStorage.getItem('token');

if (tokenInLocalStorage) return tokenInLocalStorage;
else return null;
};

const onError = async (message: string) => {
alert(message);
return;
};

adminInstance.interceptors.request.use(config => {
config.headers.Authorization = `Bearer ${LocalStorage.getItem('token')}`;
const existedToken = getAccessToken();
config.headers.Authorization = `Bearer ${existedToken}`;

return config;
});

let retryCounter = 0;

adminInstance.interceptors.response.use(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

궁금한 게 있는데, tanstack query의 retry 옵션으로 재요청을 보내는 것과 어떤 차이가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 tanstack query는 그 성격이 react hook이다보니까 jsx(tsx)내부에서밖에 사용하지 못합니다. 그래서 해당 파일은 일반 타입스크립트 파일이기 때문에 해당 기능을 사용하지 못했습니다!

res => {
retryCounter = 0;
return res;
},
async (error: AxiosError) => {
if (error.code === 'ECONNABORTED') {
alert('서버에 문제가 발생했습니다.');
}

switch (error.response?.status) {
case 400: {
onError('400: 잘못된 요청입니다.');
break;
}
case 401: {
if (retryCounter < 3) {
retryCounter++;
try {
const existedToken = getAccessToken();
if (!existedToken) throw new Error('no accessible token');
return await adminInstance({
...error.config,
headers: { Authorization: `Bearer ${existedToken}` },
});
} catch (e) {
const message =
e instanceof Error ? e.message : '401: 로그인이 필요합니다.';
onError(message);
return (window.location.href = '/login');
}
} else {
onError('재시도 횟수를 초과하였습니다. 로그인 페이지로 이동합니다.');
return (window.location.href = '/login');
}
}
case 403: {
onError('403: 권한이 필요합니다.');
break;
}
case 404: {
onError('404: 잘못된 요청입니다.');
break;
}
case 500: {
onError('500: 서버에 문제가 발생했습니다.');
break;
}
default: {
onError('알 수 없는 오류입니다.');
break;
}
}

return Promise.reject(error);
},
);

export default instance;
15 changes: 15 additions & 0 deletions src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import LeagueIdWrapper from '@/components/admin/context/LeagueIdWrapper';

export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<LeagueIdWrapper>
<section className="space-y-8">{children}</section>
</LeagueIdWrapper>
);
}
28 changes: 28 additions & 0 deletions src/app/admin/league/[leagueId]/match/[matchId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use client';

import MatchInfoDetail from '@/components/admin/match/detail/Info';
import AsyncBoundary from '@/components/common/AsyncBoundary';
import AuthMatchInfoFetcher from '@/queries/admin/match/useMatchInfoByIdWithAuth/Fetcher';

export default function Page({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

컴포넌트명을 단순히 Page로 작성한 이유가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이것또한 레포가 분리돼있지 않아서 페이지 컴포넌트명을 작명하는데 좀 어려움이 많아 그냥 Page로 통일시켜버렸습니다. 저희 프로젝트 구조상 페이지 컴포넌트 자체에 기능들이 곧바로 작성돼있는게 아니라 기능별 서브 컴포넌트로 분리되어 호출하고 있는 방식이기 때문에 페이지 컴포넌트 자체에 이름을 붙이는게 큰 의미가 없다고 판단했습니다.

추후 레포를 분리하게 되면 이 부분에 있어 제약이 줄어듦에 따라 다른 컨벤션을 적용할 수도 있을 것 같습니다.

params,
}: {
params: { matchId: string; leagueId: string };
}) {
const { matchId } = params;

return (
<section>
<AsyncBoundary
errorFallback={props => <MatchInfoDetail.ErrorFallback {...props} />}
loadingFallback={<div>로딩 중입니다...</div>}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 loading fallback도 추가해주시면 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 확인했습니다. 에러/로딩 fallback도 레포 분리 이후에 제대로 작성하는 시간을 가져야할 것 같습니다.

>
<AuthMatchInfoFetcher matchId={matchId}>
{data => {
return <MatchInfoDetail {...{ ...data, params }} />;
}}
</AuthMatchInfoFetcher>
</AsyncBoundary>
</section>
);
}
25 changes: 25 additions & 0 deletions src/app/admin/league/[leagueId]/match/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use client';

import { useEffect } from 'react';

import { checkPermission } from '@/api/auth';
import Home from '@/app/page';
import { QUERY_PARAMS } from '@/constants/queryParams';
import useQueryParams from '@/hooks/useQueryParams';

export default function Page({ params }: { params: { leagueId: string } }) {
const { leagueId } = params;
const { setInParams } = useQueryParams();

useEffect(() => {
setInParams(QUERY_PARAMS.league, leagueId);
checkPermission();
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의존성 배열을 잘 작성해주는 것이 좋을 것 같아요!

Copy link
Contributor Author

@HiimKwak HiimKwak Jan 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 useEffect는 컴포넌트 최초 마운트시에만 작동하도록 의도해서 의존성 배열을 추가한다하더라도 빈 배열을 추가하기 때문에 딱히 의미가 없긴 한데 그래도 추가하는게 좋을까요? 왜냐하면 빈 배열을 추가하는게 에러는 아니지만 lint에도 Warning : React Hook useEffect has missing dependencies: 'leagueId' and 'setInParams'. Either include them or remove the dependency array. 이란 경고가 매번 뜨는데 되도록이면 최초 마운트 시에만 동작하는 useEffect들은 의존성 배열을 경고처럼 제거해서 경고가 안뜨게 하는게 맞지 않나란 생각이 듭니다.


return (
<>
<div className="text-2xl font-medium">리그 내 매치 </div>
<Home />
</>
);
}
31 changes: 31 additions & 0 deletions src/app/admin/league/[leagueId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use client';

import { Suspense, useEffect } from 'react';

import LeagueDetail from '@/components/admin/league/detail';
import { useLeagueIdContext } from '@/hooks/useLeagueIdContext';
import LeagueRegisterFetcher from '@/queries/admin/league/useLeagueRegister/Fetcher';
import useSportsListByLeagueId from '@/queries/useSportsListByLeagueId/query';

export default function Page({ params }: { params: { leagueId: string } }) {
const { leagueId } = params;
const { setLeagueId } = useLeagueIdContext();
const { sportsList: leagueSportsData } = useSportsListByLeagueId(leagueId);

useEffect(() => {
setLeagueId(leagueId);
}, []);

return (
<Suspense fallback={<div>리그 정보 로딩중...</div>}>
<LeagueRegisterFetcher>
{data => (
<LeagueDetail
data={{ ...data, leagueSportsData }}
leagueId={leagueId}
/>
)}
</LeagueRegisterFetcher>
</Suspense>
);
}
10 changes: 4 additions & 6 deletions src/app/admin/league/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ const LeagueListFetcher = dynamic(
() => import('@/queries/admin/league/useLeagueList/Fetcher'),
{ ssr: false },
);
const LeagueList = dynamic(
() => import('@/components/admin/league/LeagueList'),
);
const LeagueList = dynamic(() => import('@/components/admin/league/list'));

export default function LeaguePage() {
export default function Page() {
return (
<div className="space-y-8">
<>
<div className="text-2xl font-medium">전체 리그</div>
<Suspense fallback={<div>리그 로딩중...</div>}>
<LeagueListFetcher>
Expand All @@ -28,6 +26,6 @@ export default function LeaguePage() {
새 리그 등록
</Link>
</Button>
</div>
</>
);
}
26 changes: 0 additions & 26 deletions src/app/admin/register/[leagueId]/page.tsx

This file was deleted.

51 changes: 24 additions & 27 deletions src/app/admin/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,40 @@

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/league/useLeagueRegister/Fetcher';
import TeamRegisterFetcher from '@/queries/admin/team/useTeamRegister/Fetcher';

export default function Register() {
export default function Page() {
const [Funnel, setStep] = useFunnel(['league', 'team', 'player'], 'league');

return (
<RegisterWrapper className="space-y-8">
<Funnel>
<Funnel.Step name="league">
<Suspense fallback={<div>리그 정보 로딩중...</div>}>
<LeagueRegisterFetcher>
{data => (
<RegisterLeague data={data} onNext={() => setStep('team')} />
)}
</LeagueRegisterFetcher>
</Suspense>
</Funnel.Step>
<Funnel.Step name="team">
<Suspense fallback={<div>팀 정보 로딩중...</div>}>
<TeamRegisterFetcher>
{data => <RegisterTeam data={data} />}
</TeamRegisterFetcher>
</Suspense>
</Funnel.Step>
<Funnel.Step name="player">
<Suspense fallback={<div>선수 정보 로딩중...</div>}>
{/* <LeagueRegisterFetcher leagueId={leagueId}>
<Funnel>
<Funnel.Step name="league">
<Suspense fallback={<div>리그 정보 로딩중...</div>}>
<LeagueRegisterFetcher>
{data => (
<RegisterLeague data={data} onNext={() => setStep('team')} />
)}
</LeagueRegisterFetcher>
</Suspense>
</Funnel.Step>
<Funnel.Step name="team">
<Suspense fallback={<div>팀 정보 로딩중...</div>}>
<TeamRegisterFetcher>
{data => <RegisterTeam data={data} />}
</TeamRegisterFetcher>
</Suspense>
</Funnel.Step>
<Funnel.Step name="player">
<Suspense fallback={<div>선수 정보 로딩중...</div>}>
{/* <LeagueRegisterFetcher leagueId={leagueId}>
{data => <RegisterLeague data={data} leagueId={leagueId} />}
</LeagueRegisterFetcher> */}
</Suspense>
</Funnel.Step>
</Funnel>
</RegisterWrapper>
</Suspense>
</Funnel.Step>
</Funnel>
);
}
Loading