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] 라우트 경로 정리 #232

Merged
merged 14 commits into from
Oct 13, 2024
1 change: 1 addition & 0 deletions src/lib/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const ENV = {
IS_PRODUCTION: import.meta.env.PROD,
jhsung23 marked this conversation as resolved.
Show resolved Hide resolved
API_BASE_URL: `${import.meta.env.VITE_API_BASE_URL}`,
KAKAO_REST_API_KEY: `${import.meta.env.VITE_KAKAO_REST_API_KEY}`,
KAKAO_LOGIN_REDIRECT_URI: `${import.meta.env.VITE_KAKAO_LOGIN_REDIRECT_URI}`,
Expand Down
14 changes: 14 additions & 0 deletions src/pages/Landing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Navigate } from 'react-router-dom';

import { useAuth } from '@/hooks/useAuth';

// NOTE: 랜딩 페이지는 로그인하지 않은 사용자에게만 보여짐
export const Landing = () => {
const { isAuthenticated } = useAuth();

if (isAuthenticated) {
return <Navigate to="/meeting" />;
}

return <div>랜딩페이지</div>;
};
39 changes: 39 additions & 0 deletions src/pages/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useNavigate, useSearchParams } from 'react-router-dom';

import { Button } from '@/components/common/Button';
import { FlexBox } from '@/components/common/FlexBox';
import { Icon } from '@/components/common/Icon';
import { Head3 } from '@/components/common/Typography';
import { KakaoLoginButton } from '@/components/features/KakaoLoginButton';
import { ENV } from '@/lib/env';
import { isUnauthorizedUserAccessibleUrlRegExp } from '@/utils/regex';

export const Login = () => {
const [searchParams] = useSearchParams();
const redirect = searchParams.get('redirect') ?? '/meeting'; // NOTE: 로그인 완료 후 redirect할 url이 없다면, /meeting (모임 목록)으로 이동
const navigate = useNavigate();

return (
<FlexBox width="100%" height="100vh" padding="0 20px 82px">
<FlexBox height="100%" gap={56}>
<Icon name="jjakkakLogo1" size={136} />
<Head3>{`모임의 시작을\n더욱 간편하게`}</Head3>
</FlexBox>
<FlexBox width="100%" gap={12}>
{isUnauthorizedUserAccessibleUrl(redirect) && (
<Button
variant="tertiary"
height="small"
onClick={() => navigate(redirect, { state: 'allow' })}
>
비회원으로 시작하기
</Button>
)}
<KakaoLoginButton href={`${ENV.API_BASE_URL}/auth/oauth2/kakao?redirect=${redirect}`} />
</FlexBox>
</FlexBox>
);
};

const isUnauthorizedUserAccessibleUrl = (url: string) =>
isUnauthorizedUserAccessibleUrlRegExp.some((regex) => regex.test(url));
4 changes: 2 additions & 2 deletions src/pages/LoginSuccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { queries } from '@/apis';
export const LoginSuccess = () => {
const { isLoading, isError } = useQuery({ ...queries.member.reissue });
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get('redirect');
const redirect = searchParams.get('redirect');

if (isLoading) {
return <div>로그인중</div>; // TODO Loading Indicator
Expand All @@ -17,5 +17,5 @@ export const LoginSuccess = () => {
return <Navigate to="/" replace />;
}

return <Navigate to={redirectTo ?? '/meeting'} replace />;
return <Navigate to={redirect ?? '/meeting'} replace />;
};
8 changes: 7 additions & 1 deletion src/pages/NewMeetingShare.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ import { FlexBox } from '@/components/common/FlexBox';
import { FormLayout } from '@/components/common/FormLayout';
import { Icon } from '@/components/common/Icon';
import { IconButton } from '@/components/common/IconButton';
// import { ENV } from '@/lib/env'; // TODO 주석 해제

export const NewMeetingShare = () => {
const navigate = useNavigate();
const { state } = useLocation();
const meetingUuid = state?.meetingUuid;

if (Boolean(meetingUuid) === false) {
return <Navigate to="/" />; // TODO 랜딩 페이지로 이동 (현재 /는 로그인페이지)
return <Navigate to="/" />;
}

// TODO 주석 해제 (@typescript-eslint/no-unused-vars 경고때문에 임시 주석처리)
// const shareUrl = ENV.IS_PRODUCTION
// ? `https://jjakkak.com/${meetingUuid}`
// : `http://localhost:5173/${meetingUuid}`;

return (
<>
<FormLayout
Expand Down
52 changes: 0 additions & 52 deletions src/pages/Onboarding.tsx

This file was deleted.

18 changes: 0 additions & 18 deletions src/routes/PrivateRoute.tsx

This file was deleted.

28 changes: 28 additions & 0 deletions src/routes/RequireAuthRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Navigate, useLocation } from 'react-router-dom';

import { useAuth } from '@/hooks/useAuth';

type Props = {
children: React.ReactNode;
};

/**
* @description 로그인이 필요한 페이지를 래핑하는 컴포넌트.
* 로그인 여부 확인 후, 로그인했다면 래핑한 페이지를 렌더하고 로그인하지 않았다면 로그인 페이지로 리다이렉트함
*/
export const RequireAuthRoute = ({ children }: Props) => {
const { pathname, state } = useLocation();
const { isAuthenticated } = useAuth();
const isAccessAllowed = state === 'allow'; // NOTE: accessToken이 없어도 예외적으로 접근을 허용할 때 사용 (e.g. 비회원으로 일정 입력하기)

if (!isAuthenticated && !isAccessAllowed) {
/**
* 로그인하지 않았을 때 권한을 필요로 하는 페이지(children으로 렌더링할 페이지)에 접근하면,
* 로그인을 마치고 다시 권한을 필요로 하는 페이지로 이동하도록 현재 페이지(children으로 렌더링할 페이지)의 pathname을 전달
*/
console.error('접근할 수 없는 페이지입니다.');
return <Navigate to={`/login?redirect=${pathname}`} />;
}

return <>{children}</>;
};
34 changes: 23 additions & 11 deletions src/routes/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { createBrowserRouter } from 'react-router-dom';

import { App } from '@/App';
import { EditSchedule } from '@/pages/EditSchedule';
import { meetingLoader } from '@/pages/loaders/meetingLoader';
import { Landing } from '@/pages/Landing';
import { Login } from '@/pages/Login';
import { LoginFailure } from '@/pages/LoginFailure';
import { LoginSuccess } from '@/pages/LoginSuccess';
import { Meeting } from '@/pages/Meeting';
Expand All @@ -11,12 +12,11 @@ import { NewMeeting } from '@/pages/NewMeeting';
import { NewMeetingShare } from '@/pages/NewMeetingShare';
import { NewSchedule } from '@/pages/NewSchedule';
import { NotFound } from '@/pages/NotFound';
import { Onboarding } from '@/pages/Onboarding';
import { PinRelease } from '@/pages/PinRelease';
import { TimeCollection } from '@/pages/TimeCollection';
import { TotalSchedule } from '@/pages/TotalSchedule';

import { PrivateRoute } from './PrivateRoute';
import { RequireAuthRoute } from './RequireAuthRoute';

export const router = createBrowserRouter([
{
Expand All @@ -25,11 +25,12 @@ export const router = createBrowserRouter([
children: [
{
index: true,
element: <Onboarding />,
element: <Landing />,
},
{
path: 'login',
children: [
{ index: true, element: <Login /> },
{ path: 'success', element: <LoginSuccess /> },
{ path: 'failure', element: <LoginFailure /> },
],
Expand All @@ -39,19 +40,19 @@ export const router = createBrowserRouter([
children: [
{
index: true,
loader: meetingLoader,
// loader: meetingLoader, FIXME: 로그인하지 않은 경우 권한을 필요로하는 로더 동작 불가능하므로 제거하기
element: (
<PrivateRoute>
<RequireAuthRoute>
<Meeting />
</PrivateRoute>
</RequireAuthRoute>
),
},
{
path: 'new',
element: (
<PrivateRoute>
<RequireAuthRoute>
<NewMeeting />
</PrivateRoute>
</RequireAuthRoute>
),
},
{
Expand All @@ -70,7 +71,11 @@ export const router = createBrowserRouter([
},
{
path: 'new',
element: <NewSchedule />,
element: (
jhsung23 marked this conversation as resolved.
Show resolved Hide resolved
<RequireAuthRoute>
<NewSchedule />
</RequireAuthRoute>
),
},
{
path: 'edit',
Expand All @@ -84,9 +89,16 @@ export const router = createBrowserRouter([
path: 'schedules',
element: <TotalSchedule />,
},
{ path: 'schedules/overview', element: <TimeCollection /> },
{
path: 'schedules/overview',
element: <TimeCollection />,
},
],
},
],
},
{
path: '*',
element: <NotFound />,
},
]);
9 changes: 9 additions & 0 deletions src/utils/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,12 @@
export const englishAndNumberRegex = (str: string): boolean => {
return /^[a-zA-Z0-9]+$/.test(str);
};

/**
* NOTE: 비회원도 접근 가능한 페이지(isUnauthorizedUserAccessibleUrl)지만 로그인 유도를 위해 Login 페이지로 리다이렉트됐을 때,
* `비회원으로 시작하기` 버튼 노출이 필요함
*
* 로그인 완료 후 리다이렉트할 url이 비회원 허용 페이지인지 확인하기 위해 url을 정규식으로 검사함
* 비회원 허용 페이지에는 [/:uuid/new (일정 입력), ...]가 있음
*/
export const isUnauthorizedUserAccessibleUrlRegExp = [/^\/[A-Za-z\d]{8}\/new$/];
Loading